[typescript-operations] Fix @skip and @include not applying conditional modifiers correctly when used on inline fragment#10645
Conversation
…ional when inline fragment is used
🦋 Changeset detectedLatest commit: f1be5d6 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@skip and @include not applying conditional modifiers correctly to inline fragment fields
@skip and @include not applying conditional modifiers correctly to inline fragment fields@skip and @include not applying conditional modifiers correctly when used on inline fragment
🚀 Snapshot Release (
|
| Package | Version | Info |
|---|---|---|
@graphql-codegen/cli |
7.0.0-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/introspection |
5.0.2-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/visitor-plugin-common |
7.0.0-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/typescript-document-nodes |
5.0.10-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/gql-tag-operations |
5.1.5-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/typescript-operations |
6.0.0-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/typescript-resolvers |
6.0.0-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/typed-document-node |
6.1.8-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/typescript |
6.0.0-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/client-preset |
6.0.0-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/graphql-modules-preset |
5.1.5-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 |
npm ↗︎ unpkg ↗︎ |
| type CollectedFragmentNode = (SelectionNode | FragmentSpreadUsage | DirectiveNode) & FragmentDirectives; | ||
| /** | ||
| * Each grouped TypeName has an array of nodes. | ||
| * These are collected when parsing the selection set, | ||
| * then turned into TypeScript strings | ||
| */ | ||
| type GroupedTypeNameNode = | ||
| | EnrichedFieldNode | ||
| | FragmentSpreadNode | ||
| | InlineFragmentNode | ||
| | FragmentSpreadUsage | ||
| | DirectiveNode; |
There was a problem hiding this comment.
GroupedTypeNameNode is a replacement for CollectedFragmentNode to be more semantically and type correct:
- Semantic improvement:
CollectedFragmentNodeis not the correct name since we collect certain nodes, not just Fragments. - Type improvement: previously,
DirectiveNodecan never havefragmentDirectivesfield, so that was misleading that it has& FragmentDirectives. Now, onlyFieldNode(becomesEnrichedFieldNode) andFragmentSpreadNodemay havefragmentDirectivesfield
| | EnrichedFieldNode | ||
| | FragmentSpreadNode | ||
| | InlineFragmentNode |
There was a problem hiding this comment.
The SelectionNode type is FieldNode | FragmentSpreadNode | InlineFragmentNode.
Since a FieldNode here could be enriched with fragmentDirectives (directives from the fragment the field is in), so I've just inlined and replaced FieldNode with EnrichedFieldNode
| protected _createInlineFragmentForFieldNodes( | ||
| parentType: GraphQLNamedType, | ||
| fieldNodes: FieldNode[] | ||
| ): InlineFragmentNode { | ||
| return { | ||
| kind: Kind.INLINE_FRAGMENT, | ||
| typeCondition: { | ||
| kind: Kind.NAMED_TYPE, | ||
| name: { | ||
| kind: Kind.NAME, | ||
| value: parentType.name, | ||
| }, | ||
| }, | ||
| directives: [], | ||
| selectionSet: { | ||
| kind: Kind.SELECTION_SET, | ||
| selections: fieldNodes, | ||
| }, | ||
| }; | ||
| } |
There was a problem hiding this comment.
This is just inlined below, since it's only used once
| return result; | ||
| } | ||
|
|
||
| private _appendToTypeMap<T = CollectedFragmentNode>( |
There was a problem hiding this comment.
We never use the generic, may as well inline it.
| const collectGrouped = (nodes: GroupedTypeNameNode[]): void => { | ||
| // incrementalNodes are the ones flagged with @defer, meaning they become nullable | ||
| const { selectionNodes, incrementalNodes, fragmentSpreads } = nodes.reduce<{ | ||
| selectionNodes: GroupedTypeNameNode[]; | ||
| incrementalNodes: FragmentSpreadUsage[]; | ||
| fragmentSpreads: string[]; | ||
| }>( | ||
| (acc, node) => { | ||
| if ('fragmentDirectives' in node && hasIncrementalDeliveryDirectives(node.fragmentDirectives)) { | ||
| acc.incrementalNodes.push(node as FragmentSpreadUsage); // FIXME: check whether @defer would pick up EnrichedFieldNode here too? | ||
| } else { | ||
| acc.selectionNodes.push(node); | ||
| } | ||
| return acc; | ||
| }, | ||
| { selectionNodes: [], incrementalNodes: [], fragmentSpreads: [] } | ||
| ); | ||
|
|
||
| const { fields, dependentTypes: subDependentTypes } = this.buildSelectionSet(schemaType, selectionNodes, { | ||
| parentFieldName: this.buildParentFieldName(typeName, parentName), | ||
| }); | ||
| const transformedSet = this.selectionSetStringFromFields(fields); | ||
| const { fields, dependentTypes: subDependentTypes } = this.buildSelectionSet(schemaType, selectionNodes, { | ||
| parentFieldName: this.buildParentFieldName(typeName, parentName), | ||
| }); | ||
| const transformedSet = this.selectionSetStringFromFields(fields); | ||
|
|
||
| if (transformedSet) { | ||
| prev[typeName].push(transformedSet); | ||
| } | ||
| dependentTypes.push(...subDependentTypes); | ||
| if (!transformedSet && !fragmentSpreads.length) { | ||
| mustAddEmptyObject = true; | ||
| } | ||
| if (transformedSet) { | ||
| prev[typeName].push(transformedSet); | ||
| } | ||
| dependentTypes.push(...subDependentTypes); | ||
| if (!transformedSet && !fragmentSpreads.length) { | ||
| mustAddEmptyObject = true; | ||
| } | ||
|
|
||
| for (const incrementalNode of incrementalNodes) { | ||
| // 1. fragment masking | ||
| if (this._config.inlineFragmentTypes === 'mask' && 'fragmentName' in incrementalNode) { | ||
| const { fields: incrementalFields, dependentTypes: incrementalDependentTypes } = this.buildSelectionSet( | ||
| for (const incrementalNode of incrementalNodes) { | ||
| // 1. fragment masking | ||
| if (this._config.inlineFragmentTypes === 'mask' && 'fragmentName' in incrementalNode) { | ||
| const { fields: incrementalFields, dependentTypes: incrementalDependentTypes } = this.buildSelectionSet( | ||
| schemaType, | ||
| [incrementalNode], | ||
| { unsetTypes: true, parentFieldName: parentName } | ||
| ); | ||
| const incrementalSet = this.selectionSetStringFromFields(incrementalFields); | ||
| prev[typeName].push(incrementalSet); | ||
| dependentTypes.push(...incrementalDependentTypes); | ||
|
|
||
| continue; | ||
| } | ||
|
|
||
| // 2. @defer | ||
| const { fields: initialFields, dependentTypes: initialDependentTypes } = this.buildSelectionSet( | ||
| schemaType, | ||
| [incrementalNode], | ||
| { parentFieldName: parentName } | ||
| ); | ||
|
|
||
| const { fields: subsequentFields, dependentTypes: subsequentDependentTypes } = this.buildSelectionSet( | ||
| schemaType, | ||
| [incrementalNode], | ||
| { unsetTypes: true, parentFieldName: parentName } | ||
| ); | ||
| const incrementalSet = this.selectionSetStringFromFields(incrementalFields); | ||
| prev[typeName].push(incrementalSet); | ||
| dependentTypes.push(...incrementalDependentTypes); | ||
|
|
||
| continue; | ||
| const initialSet = this.selectionSetStringFromFields(initialFields); | ||
| const subsequentSet = this.selectionSetStringFromFields(subsequentFields); | ||
| dependentTypes.push(...initialDependentTypes, ...subsequentDependentTypes); | ||
| prev[typeName].push({ union: [initialSet, subsequentSet] }); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Previously, this function only runs on the base selection set and inline fragments.
This is now extracted to run on conditional fragments as well.
| const primitiveFields = new Map<string, EnrichedFieldNode>(); | ||
| const primitiveAliasFields = new Map<string, EnrichedFieldNode>(); |
There was a problem hiding this comment.
Each FieldNode in this function may have fragmentDirectives in it. This makes it more type correct
| // 1. Handle Field or Directtives selection nodes | ||
| if ('kind' in selectionNode) { | ||
| if (selectionNode.kind === 'Field') { | ||
| if (selectionNode.kind === Kind.FIELD) { |
There was a problem hiding this comment.
Using Kind improves consistency here
| export type FragmentDirectives = { | ||
| fragmentDirectives?: Array<DirectiveNode>; | ||
| }; |
There was a problem hiding this comment.
Moved to selection-set-to-object.ts as it's only used there
| /** | ||
| * Check if any of the directives are conditional i.e. `@skip` and `@include` | ||
| */ | ||
| export function hasConditionalDirectives(directives: readonly DirectiveNode[] = []): boolean { | ||
| const CONDITIONAL_DIRECTIVES = ['skip', 'include']; | ||
| return field.directives?.some(directive => CONDITIONAL_DIRECTIVES.includes(directive.name.value)); | ||
| return directives.some(directive => CONDITIONAL_DIRECTIVES.includes(directive.name.value)); |
There was a problem hiding this comment.
hasConditionalDirectives is updated to handle an array of directives, instead of just FieldNode for more flexibilities.
Directives may appear on a more nodes than just fields.
There was a problem hiding this comment.
Moved tests related to @skip and @include to ts-documents.skip-include-directives.spec.ts as-is
…tional modifiers correctly when used on inline fragment (#10645) * Migrate skip and include directive tests to a new file * Add test for aliased field * Add test for include directive, inline fragment cases * Refactor selection set to object to reflect current usages * Standardise type, parsing, fragment directive handling and fix conditional when inline fragment is used * Add inline fragment skip directive test * Add changeset
…tional modifiers correctly when used on inline fragment (#10645) * Migrate skip and include directive tests to a new file * Add test for aliased field * Add test for include directive, inline fragment cases * Refactor selection set to object to reflect current usages * Standardise type, parsing, fragment directive handling and fix conditional when inline fragment is used * Add inline fragment skip directive test * Add changeset
…tional modifiers correctly when used on inline fragment (#10645) * Migrate skip and include directive tests to a new file * Add test for aliased field * Add test for include directive, inline fragment cases * Refactor selection set to object to reflect current usages * Standardise type, parsing, fragment directive handling and fix conditional when inline fragment is used * Add inline fragment skip directive test * Add changeset
…tional modifiers correctly when used on inline fragment (#10645) * Migrate skip and include directive tests to a new file * Add test for aliased field * Add test for include directive, inline fragment cases * Refactor selection set to object to reflect current usages * Standardise type, parsing, fragment directive handling and fix conditional when inline fragment is used * Add inline fragment skip directive test * Add changeset
Description
Previously, all inline fragment fields were merged with the base selection set, without considering if conditional directives (
@skipor@inlcude) are applied on at the inline fragment, all fields of said fragment are optional as wellNow, conditional inline fragments are separated from the base and other inline fragments so correct conditional modifiers can be applied to the fields.
For example, if we have the following query:
Then, the generated Result type would look like this:
Related: #10496
Issue: #9745
Type of change
How Has This Been Tested?