Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changeset/brown-books-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@graphql-codegen/visitor-plugin-common': patch
'@graphql-codegen/typescript-operations': patch
---

handles conditional spread of a fragment whose top-level selections are fragment spreads or contain
inline fragments
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,125 @@ 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');
});

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');
});
});

describe('TypeScript Operations Plugin - @skip directive', () => {
Expand Down