diff --git a/packages/plugins/other/visitor-plugin-common/CHANGELOG.md b/packages/plugins/other/visitor-plugin-common/CHANGELOG.md index f5c4448ec0d..8dd7fd18778 100644 --- a/packages/plugins/other/visitor-plugin-common/CHANGELOG.md +++ b/packages/plugins/other/visitor-plugin-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @graphql-codegen/visitor-plugin-common +## 7.1.2 + +### Patch Changes + +- [#10839](https://github.com/dotansimha/graphql-code-generator/pull/10839) + [`8a65a1b`](https://github.com/dotansimha/graphql-code-generator/commit/8a65a1b403e92eedf73c9b1bce8262a4dbc7df8f) + Thanks [@vkbansal-rubrik](https://github.com/vkbansal-rubrik)! - handles conditional spread of a + fragment whose top-level selections are fragment spreads or contain inline fragments + ## 7.1.1 ### Patch Changes diff --git a/packages/plugins/other/visitor-plugin-common/package.json b/packages/plugins/other/visitor-plugin-common/package.json index 563f5e46456..46c9ddff43c 100644 --- a/packages/plugins/other/visitor-plugin-common/package.json +++ b/packages/plugins/other/visitor-plugin-common/package.json @@ -1,6 +1,6 @@ { "name": "@graphql-codegen/visitor-plugin-common", - "version": "7.1.1", + "version": "7.1.2", "type": "module", "repository": { "type": "git", diff --git a/packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts b/packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts index c5d463b0e32..6ab411bb62b 100644 --- a/packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts +++ b/packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts @@ -571,7 +571,36 @@ export class SelectionSetToObject< [], ); - collectGrouped(flattenedSelectionNodes); + // Top-level INLINE_FRAGMENT / FRAGMENT_SPREAD nodes among the + // inlined selections cannot be consumed directly by + // `buildSelectionSet`, which only handles FIELD and DIRECTIVE + // kinds for AST nodes and throws "Unexpected type." otherwise. + // Route them through `flattenSelectionSet` — which already + // expands inline fragments and fragment spreads per type — and + // merge the FIELD-only result with the already-FIELD selections + // before handing off. + const directNodes: GroupedTypeNameNode[] = []; + const nestedSelections: SelectionNode[] = []; + for (const n of flattenedSelectionNodes) { + if ( + 'kind' in n && + (n.kind === Kind.INLINE_FRAGMENT || n.kind === Kind.FRAGMENT_SPREAD) + ) { + nestedSelections.push(n); + } else { + directNodes.push(n); + } + } + if (nestedSelections.length) { + const { selectionNodesByTypeName: nestedByType } = this.flattenSelectionSet( + nestedSelections, + schemaType, + ); + const nestedForThisType = nestedByType.get(typeName) ?? []; + directNodes.push(...nestedForThisType); + } + + collectGrouped(directNodes); } if (incrementalDirectivesFound) { diff --git a/packages/plugins/typescript/operations/CHANGELOG.md b/packages/plugins/typescript/operations/CHANGELOG.md index 10161e07836..9100ab5b29b 100644 --- a/packages/plugins/typescript/operations/CHANGELOG.md +++ b/packages/plugins/typescript/operations/CHANGELOG.md @@ -1,5 +1,17 @@ # @graphql-codegen/typescript-operations +## 6.0.5 + +### Patch Changes + +- [#10839](https://github.com/dotansimha/graphql-code-generator/pull/10839) + [`8a65a1b`](https://github.com/dotansimha/graphql-code-generator/commit/8a65a1b403e92eedf73c9b1bce8262a4dbc7df8f) + Thanks [@vkbansal-rubrik](https://github.com/vkbansal-rubrik)! - handles conditional spread of a + fragment whose top-level selections are fragment spreads or contain inline fragments +- Updated dependencies + [[`8a65a1b`](https://github.com/dotansimha/graphql-code-generator/commit/8a65a1b403e92eedf73c9b1bce8262a4dbc7df8f)]: + - @graphql-codegen/visitor-plugin-common@7.1.2 + ## 6.0.4 ### Patch Changes diff --git a/packages/plugins/typescript/operations/package.json b/packages/plugins/typescript/operations/package.json index d2b1941596c..01787bb2f24 100644 --- a/packages/plugins/typescript/operations/package.json +++ b/packages/plugins/typescript/operations/package.json @@ -1,6 +1,6 @@ { "name": "@graphql-codegen/typescript-operations", - "version": "6.0.4", + "version": "6.0.5", "type": "module", "description": "GraphQL Code Generator plugin for generating TypeScript types for GraphQL queries, mutations, subscriptions and fragments", "repository": { diff --git a/packages/plugins/typescript/operations/tests/ts-documents.skip-include-directives.spec.ts b/packages/plugins/typescript/operations/tests/ts-documents.skip-include-directives.spec.ts index 605758badf6..99fdaf6fa14 100644 --- a/packages/plugins/typescript/operations/tests/ts-documents.skip-include-directives.spec.ts +++ b/packages/plugins/typescript/operations/tests/ts-documents.skip-include-directives.spec.ts @@ -604,6 +604,173 @@ describe('TypeScript Operations Plugin - @include directives', () => { " `); }); + + it('handles conditional spread of a fragment whose top-level selections contain inline fragments', async () => { + // Regression: when the spread fragment's top-level selection set contains + // an INLINE_FRAGMENT (or nested FRAGMENT_SPREAD), the conditional path used + // to push those raw AST nodes into `buildSelectionSet`, which only accepts + // FIELD/DIRECTIVE for raw nodes and threw `TypeError: Unexpected type.`. + const testSchema = buildSchema(/* GraphQL */ ` + type Book { + id: ID! + title: String! + } + type Magazine { + id: ID! + issue: Int! + } + union Publication = Book | Magazine + type Library { + id: ID! + name: String! + featured: Publication + } + type Query { + library(id: ID!): Library + } + `); + + const document = parse(/* GraphQL */ ` + fragment PublicationFragment on Publication { + ... on Book { + id + title + } + ... on Magazine { + id + issue + } + } + + query Library($includeFeatured: Boolean!) { + library(id: "x") { + id + name + featured { + ...PublicationFragment @include(if: $includeFeatured) + } + } + } + `); + + const { content } = await plugin( + testSchema, + [{ location: '', document }], + {}, + { outputFile: 'graphql.ts' }, + ); + + // Should not throw, and should produce a usable type where the Publication + // fields appear when includeFeatured=true and an empty-object variant + // covers includeFeatured=false. + expect(content).toContain('LibraryQuery'); + expect(content).toContain('featured'); + expect(content).toMatchInlineSnapshot(` + "type PublicationFragment_Book_Fragment = { id: string, title: string }; + + type PublicationFragment_Magazine_Fragment = { id: string, issue: number }; + + export type PublicationFragmentFragment = + | PublicationFragment_Book_Fragment + | PublicationFragment_Magazine_Fragment + ; + + export type LibraryQueryVariables = Exact<{ + includeFeatured: boolean; + }>; + + + export type LibraryQuery = { library: { id: string, name: string, featured: + | { id: string, title: string } + | { id: string, issue: number } + | Record + | null } | null }; + " + `); + }); + + it('handles conditional spread of a fragment whose top-level selections are fragment spreads', async () => { + // Same regression, but the inline fragments inside the spread fragment have + // been refactored into named fragment spreads — also failed before the fix. + const testSchema = buildSchema(/* GraphQL */ ` + type Book { + id: ID! + title: String! + } + type Magazine { + id: ID! + issue: Int! + } + union Publication = Book | Magazine + type Library { + id: ID! + featured: Publication + } + type Query { + library(id: ID!): Library + } + `); + + const document = parse(/* GraphQL */ ` + fragment BookFragment on Book { + id + title + } + fragment MagazineFragment on Magazine { + id + issue + } + fragment PublicationFragment on Publication { + ...BookFragment + ...MagazineFragment + } + + query Library($includeFeatured: Boolean!) { + library(id: "x") { + id + featured { + ...PublicationFragment @include(if: $includeFeatured) + } + } + } + `); + + const { content } = await plugin( + testSchema, + [{ location: '', document }], + {}, + { outputFile: 'graphql.ts' }, + ); + + expect(content).toContain('LibraryQuery'); + expect(content).toContain('featured'); + expect(content).toMatchInlineSnapshot(` + "export type BookFragmentFragment = { id: string, title: string }; + + export type MagazineFragmentFragment = { id: string, issue: number }; + + type PublicationFragment_Book_Fragment = { id: string, title: string }; + + type PublicationFragment_Magazine_Fragment = { id: string, issue: number }; + + export type PublicationFragmentFragment = + | PublicationFragment_Book_Fragment + | PublicationFragment_Magazine_Fragment + ; + + export type LibraryQueryVariables = Exact<{ + includeFeatured: boolean; + }>; + + + export type LibraryQuery = { library: { id: string, featured: + | { id: string, title: string } + | { id: string, issue: number } + | Record + | null } | null }; + " + `); + }); }); describe('TypeScript Operations Plugin - @skip directive', () => {