Skip to content

Commit eec207d

Browse files
[EE-3024] support inline fragments (#56)
* support inline fragments * pr comments
1 parent 496f54d commit eec207d

File tree

5 files changed

+616
-28
lines changed

5 files changed

+616
-28
lines changed

CLAUDE.md

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

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

6264
### Error Handling
6365
- Validates that all variables referenced in query are provided

README.md

Lines changed: 134 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ A TypeScript library that converts GraphQL query and mutation strings into struc
1414
-**Variable Handling**: Complete variable substitution with validation
1515
-**Arguments**: All argument types (strings, integers, floats, objects, arrays, enums)
1616
-**Aliases**: Field aliasing with metadata preservation
17+
-**Inline Fragments**: Complete support for conditional type-based field selection
1718
-**Type Safety**: Full TypeScript support with comprehensive type definitions
1819
-**Error Handling**: Descriptive error messages for malformed queries and missing variables
1920
-**Framework Agnostic**: Works with any JavaScript/TypeScript environment
@@ -83,6 +84,7 @@ The library follows predictable transformation patterns:
8384
| **Aliases** | Field renaming + `__aliasFor` | `renamed: user``renamed: { __aliasFor: "user" }` |
8485
| **Variables** | Substituted values | `$userId` → actual variable value |
8586
| **Enums** | `EnumType` wrapper | `status: ACTIVE``status: { "value": "ACTIVE" }` |
87+
| **Inline Fragments** | `__on` property | `... on User { name }``__on: { __typeName: "User", name: true }` |
8688

8789
## Comprehensive Examples
8890

@@ -568,6 +570,134 @@ const result = graphQlQueryToJson(query)
568570
}
569571
```
570572

573+
### Inline Fragments
574+
575+
```ts
576+
// Single inline fragment
577+
const query = `
578+
query {
579+
posts {
580+
title
581+
... on TextPost {
582+
content
583+
wordCount
584+
}
585+
}
586+
}
587+
`
588+
589+
const result = graphQlQueryToJson(query)
590+
591+
// Output:
592+
{
593+
query: {
594+
posts: {
595+
title: true,
596+
__on: {
597+
__typeName: "TextPost",
598+
content: true,
599+
wordCount: true
600+
}
601+
}
602+
}
603+
}
604+
```
605+
606+
```ts
607+
// Multiple inline fragments
608+
const query = `
609+
query {
610+
media {
611+
... on TextPost {
612+
content
613+
author {
614+
name
615+
}
616+
}
617+
... on ImagePost {
618+
imageUrl
619+
altText
620+
}
621+
... on VideoPost {
622+
videoUrl
623+
duration
624+
}
625+
}
626+
}
627+
`
628+
629+
const result = graphQlQueryToJson(query)
630+
631+
// Output:
632+
{
633+
query: {
634+
media: {
635+
__on: [
636+
{
637+
__typeName: "TextPost",
638+
content: true,
639+
author: {
640+
name: true
641+
}
642+
},
643+
{
644+
__typeName: "ImagePost",
645+
imageUrl: true,
646+
altText: true
647+
},
648+
{
649+
__typeName: "VideoPost",
650+
videoUrl: true,
651+
duration: true
652+
}
653+
]
654+
}
655+
}
656+
}
657+
```
658+
659+
```ts
660+
// Inline fragments with arguments and variables
661+
const query = `
662+
query GetPosts($limit: Int!) {
663+
posts {
664+
title
665+
... on TextPost {
666+
comments(limit: $limit) {
667+
text
668+
author {
669+
name
670+
}
671+
}
672+
}
673+
}
674+
}
675+
`
676+
677+
const result = graphQlQueryToJson(query, {
678+
variables: { limit: 5 }
679+
})
680+
681+
// Output:
682+
{
683+
query: {
684+
posts: {
685+
title: true,
686+
__on: {
687+
__typeName: "TextPost",
688+
comments: {
689+
__args: { limit: 5 },
690+
text: true,
691+
author: {
692+
name: true
693+
}
694+
}
695+
}
696+
}
697+
}
698+
}
699+
```
700+
571701
### Subscriptions
572702

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

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

692822
```ts
693-
//This will throw an error
823+
//Named fragments still throw an error
694824
const queryWithFragment = `
695825
query {
696826
user {
@@ -705,7 +835,7 @@ fragment UserFields on User {
705835
`
706836
// Throws: "The parsed query has more than one set of definitions"
707837

708-
// ❌ This will cause a runtime error
838+
// ✅ Inline fragments work perfectly
709839
const queryWithInlineFragment = `
710840
query {
711841
search {
@@ -718,7 +848,7 @@ query {
718848
}
719849
}
720850
`
721-
// Causes: Cannot read properties of undefined
851+
// Output: { query: { search: { __on: [...] } } }
722852
```
723853

724854
### Directives

src/index.ts

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,23 @@ interface Argument {
2727

2828
interface Selection {
2929
kind: string
30-
alias: {
30+
alias?: {
3131
kind: string
3232
value: string
3333
}
34-
name: {
34+
name?: {
3535
kind: string
3636
value: string
3737
}
3838
arguments?: Argument[]
3939
selectionSet?: SelectionSet
40+
typeCondition?: {
41+
kind: string
42+
name: {
43+
kind: string
44+
value: string
45+
}
46+
}
4047
}
4148

4249
interface SelectionSet {
@@ -128,30 +135,63 @@ const getArguments = (args) => {
128135
}
129136

130137
const getSelections = (selections: Selection[]) => {
131-
const selObj = {}
138+
const selObj: any = {}
139+
const inlineFragments = []
140+
132141
selections.forEach((selection) => {
133-
const selectionHasAlias = selection.alias
134-
const selectionName = selectionHasAlias
135-
? selection.alias.value
136-
: selection.name.value
137-
if (selection.selectionSet) {
138-
selObj[selectionName] = getSelections(
142+
if (selection.kind === "InlineFragment") {
143+
// Handle inline fragments
144+
const typeName = selection.typeCondition.name.value
145+
const fragmentSelections = getSelections(
139146
selection.selectionSet.selections,
140147
)
141-
if (selectionHasAlias) {
142-
selObj[selection.alias.value].__aliasFor = selection.name.value
148+
inlineFragments.push({
149+
__typeName: typeName,
150+
...fragmentSelections,
151+
})
152+
} else {
153+
// Handle regular fields
154+
const selectionHasAlias = selection.alias
155+
const selectionName = selectionHasAlias
156+
? selection.alias.value
157+
: selection.name.value
158+
if (selection.selectionSet) {
159+
selObj[selectionName] = getSelections(
160+
selection.selectionSet.selections,
161+
)
162+
if (selectionHasAlias) {
163+
selObj[selection.alias.value].__aliasFor =
164+
selection.name.value
165+
}
143166
}
144-
}
145-
if (selection.arguments.length > 0) {
146-
if (!selObj[selectionName]) {
147-
selObj[selectionName] = {}
167+
if (selection.arguments && selection.arguments.length > 0) {
168+
if (!selObj[selectionName]) {
169+
selObj[selectionName] = {}
170+
}
171+
selObj[selectionName].__args = getArguments(selection.arguments)
172+
}
173+
if (
174+
!selection.selectionSet &&
175+
(!selection.arguments || selection.arguments.length === 0)
176+
) {
177+
if (selectionHasAlias) {
178+
selObj[selectionName] = {__aliasFor: selection.name.value}
179+
} else {
180+
selObj[selectionName] = true
181+
}
148182
}
149-
selObj[selectionName].__args = getArguments(selection.arguments)
150-
}
151-
if (!selection.selectionSet && selection.arguments.length === 0) {
152-
selObj[selectionName] = true
153183
}
154184
})
185+
186+
// Add inline fragments to the result
187+
if (inlineFragments.length > 0) {
188+
if (inlineFragments.length === 1) {
189+
selObj.__on = inlineFragments[0]
190+
} else {
191+
selObj.__on = inlineFragments
192+
}
193+
}
194+
155195
return selObj
156196
}
157197

0 commit comments

Comments
 (0)