Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ The main `graphQlQueryToJson` function:
### Testing
- Comprehensive Jest test suite in `src/test/full_functionality.spec.ts`
- README examples validation tests in `src/test/readme_examples.spec.ts`
- Tests cover queries, mutations, subscriptions, aliases, enums, variables, scalar fields with arguments, float arguments (including edge cases), and error cases
- Tests cover queries, mutations, subscriptions, aliases, enums, variables, scalar fields with arguments, float arguments (including edge cases), inline fragments, and error cases
- Tests run against compiled dist/ files, so always build before testing
- To run a single test file: `npm test -- --testNamePattern="specific test name"`
- Current coverage: 99% statements, 98% branches (line 111 in index.ts is unreachable dead code)
Expand All @@ -48,6 +48,7 @@ The main `graphQlQueryToJson` function:
The library processes GraphQL Abstract Syntax Trees (AST) from the `graphql` library. Key AST node types handled:
- `OperationDefinition` - Query/mutation/subscription operations
- `Field` - Individual field selections with optional arguments and aliases
- `InlineFragment` - Inline fragments for conditional type-based field selection
- `Argument` - Field arguments with various value types (string, int, float, enum, object, list, variable)
- `SelectionSet` - Groups of field selections

Expand All @@ -58,6 +59,7 @@ The library processes GraphQL Abstract Syntax Trees (AST) from the `graphql` lib
- **Aliases**: Aliased fields get `__aliasFor` metadata: `alias: { __aliasFor: "originalField" }`
- **Variables**: Replaced with actual values using `replaceVariables()` function
- **Enums**: Wrapped in `EnumType` objects from json-to-graphql-query library (except in arrays where they remain strings)
- **Inline Fragments**: Single fragment becomes `__on: { __typeName: "TypeName", ...fields }`, multiple fragments become `__on: [{ __typeName: "Type1", ...}, ...]`

### Error Handling
- Validates that all variables referenced in query are provided
Expand Down
138 changes: 134 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ A TypeScript library that converts GraphQL query and mutation strings into struc
- ✅ **Variable Handling**: Complete variable substitution with validation
- ✅ **Arguments**: All argument types (strings, integers, floats, objects, arrays, enums)
- ✅ **Aliases**: Field aliasing with metadata preservation
- ✅ **Inline Fragments**: Complete support for conditional type-based field selection
- ✅ **Type Safety**: Full TypeScript support with comprehensive type definitions
- ✅ **Error Handling**: Descriptive error messages for malformed queries and missing variables
- ✅ **Framework Agnostic**: Works with any JavaScript/TypeScript environment
Expand Down Expand Up @@ -83,6 +84,7 @@ The library follows predictable transformation patterns:
| **Aliases** | Field renaming + `__aliasFor` | `renamed: user` → `renamed: { __aliasFor: "user" }` |
| **Variables** | Substituted values | `$userId` → actual variable value |
| **Enums** | `EnumType` wrapper | `status: ACTIVE` → `status: { "value": "ACTIVE" }` |
| **Inline Fragments** | `__on` property | `... on User { name }` → `__on: { __typeName: "User", name: true }` |

## Comprehensive Examples

Expand Down Expand Up @@ -568,6 +570,134 @@ const result = graphQlQueryToJson(query)
}
```

### Inline Fragments

```ts
// Single inline fragment
const query = `
query {
posts {
title
... on TextPost {
content
wordCount
}
}
}
`

const result = graphQlQueryToJson(query)

// Output:
{
query: {
posts: {
title: true,
__on: {
__typeName: "TextPost",
content: true,
wordCount: true
}
}
}
}
```

```ts
// Multiple inline fragments
const query = `
query {
media {
... on TextPost {
content
author {
name
}
}
... on ImagePost {
imageUrl
altText
}
... on VideoPost {
videoUrl
duration
}
}
}
`

const result = graphQlQueryToJson(query)

// Output:
{
query: {
media: {
__on: [
{
__typeName: "TextPost",
content: true,
author: {
name: true
}
},
{
__typeName: "ImagePost",
imageUrl: true,
altText: true
},
{
__typeName: "VideoPost",
videoUrl: true,
duration: true
}
]
}
}
}
```

```ts
// Inline fragments with arguments and variables
const query = `
query GetPosts($limit: Int!) {
posts {
title
... on TextPost {
comments(limit: $limit) {
text
author {
name
}
}
}
}
}
`

const result = graphQlQueryToJson(query, {
variables: { limit: 5 }
})

// Output:
{
query: {
posts: {
title: true,
__on: {
__typeName: "TextPost",
comments: {
__args: { limit: 5 },
text: true,
author: {
name: true
}
}
}
}
}
}
```

### Subscriptions

```ts
Expand Down Expand Up @@ -686,11 +816,11 @@ const result = graphQlQueryToJson(subscription)
While the library supports the core GraphQL features, there are some limitations:

### Fragment Support
- **Inline Fragments**: ✅ **Fully Supported** (e.g., `... on TypeName`)
- **Named Fragments**: Not supported due to multiple definition restriction
- **Inline Fragments**: Not supported (e.g., `... on TypeName`)

```ts
// ❌ This will throw an error
// ❌ Named fragments still throw an error
const queryWithFragment = `
query {
user {
Expand All @@ -705,7 +835,7 @@ fragment UserFields on User {
`
// Throws: "The parsed query has more than one set of definitions"

// ❌ This will cause a runtime error
// ✅ Inline fragments work perfectly
const queryWithInlineFragment = `
query {
search {
Expand All @@ -718,7 +848,7 @@ query {
}
}
`
// Causes: Cannot read properties of undefined
// Output: { query: { search: { __on: [...] } } }
```

### Directives
Expand Down
78 changes: 59 additions & 19 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,23 @@ interface Argument {

interface Selection {
kind: string
alias: {
alias?: {
kind: string
value: string
}
name: {
name?: {
kind: string
value: string
}
arguments?: Argument[]
selectionSet?: SelectionSet
typeCondition?: {
kind: string
name: {
kind: string
value: string
}
}
}

interface SelectionSet {
Expand Down Expand Up @@ -128,30 +135,63 @@ const getArguments = (args) => {
}

const getSelections = (selections: Selection[]) => {
const selObj = {}
const selObj: any = {}
const inlineFragments = []

selections.forEach((selection) => {
const selectionHasAlias = selection.alias
const selectionName = selectionHasAlias
? selection.alias.value
: selection.name.value
if (selection.selectionSet) {
selObj[selectionName] = getSelections(
if (selection.kind === "InlineFragment") {
// Handle inline fragments
const typeName = selection.typeCondition.name.value
const fragmentSelections = getSelections(
selection.selectionSet.selections,
)
if (selectionHasAlias) {
selObj[selection.alias.value].__aliasFor = selection.name.value
inlineFragments.push({
__typeName: typeName,
...fragmentSelections,
})
} else {
// Handle regular fields
const selectionHasAlias = selection.alias
const selectionName = selectionHasAlias
? selection.alias.value
: selection.name.value
if (selection.selectionSet) {
selObj[selectionName] = getSelections(
selection.selectionSet.selections,
)
if (selectionHasAlias) {
selObj[selection.alias.value].__aliasFor =
selection.name.value
}
}
}
if (selection.arguments.length > 0) {
if (!selObj[selectionName]) {
selObj[selectionName] = {}
if (selection.arguments && selection.arguments.length > 0) {
if (!selObj[selectionName]) {
selObj[selectionName] = {}
}
selObj[selectionName].__args = getArguments(selection.arguments)
}
if (
!selection.selectionSet &&
(!selection.arguments || selection.arguments.length === 0)
) {
if (selectionHasAlias) {
selObj[selectionName] = {__aliasFor: selection.name.value}
} else {
selObj[selectionName] = true
}
}
selObj[selectionName].__args = getArguments(selection.arguments)
}
if (!selection.selectionSet && selection.arguments.length === 0) {
selObj[selectionName] = true
}
})

// Add inline fragments to the result
if (inlineFragments.length > 0) {
if (inlineFragments.length === 1) {
selObj.__on = inlineFragments[0]
} else {
selObj.__on = inlineFragments
}
}

return selObj
}

Expand Down
Loading