From d9e35480853bf35e3dccad1cf4dee1623b329d6f Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 23 Mar 2025 12:42:17 +0200 Subject: [PATCH] use closure instead of CollectFieldsContext --- src/execution/collectFields.ts | 472 +++++++++++++++------------------ 1 file changed, 210 insertions(+), 262 deletions(-) diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts index bc00b413d8..38b7f37c2d 100644 --- a/src/execution/collectFields.ts +++ b/src/execution/collectFields.ts @@ -50,17 +50,6 @@ export interface FragmentDetails { variableSignatures?: ObjMap | undefined; } -interface CollectFieldsContext { - schema: GraphQLSchema; - fragments: ObjMap; - variableValues: VariableValues; - runtimeType: GraphQLObjectType; - visitedFragmentNames: Set; - hideSuggestions: boolean; - forbiddenDirectiveInstances: Array; - forbidSkipAndInclude: boolean; -} - /** * Given a selectionSet, collects all of the fields and returns them. * @@ -86,22 +75,26 @@ export function collectFields( } { const groupedFieldSet = new AccumulatorMap(); const newDeferUsages: Array = []; - const context: CollectFieldsContext = { + const forbiddenDirectiveInstances: Array = []; + + const selectionSetVisitor = buildSelectionSetVisitor( schema, fragments, variableValues, runtimeType, - visitedFragmentNames: new Set(), hideSuggestions, - forbiddenDirectiveInstances: [], forbidSkipAndInclude, - }; + groupedFieldSet, + newDeferUsages, + forbiddenDirectiveInstances, + ); + + selectionSetVisitor(selectionSet); - collectFieldsImpl(context, selectionSet, groupedFieldSet, newDeferUsages); return { groupedFieldSet, newDeferUsages, - forbiddenDirectiveInstances: context.forbiddenDirectiveInstances, + forbiddenDirectiveInstances, }; } @@ -127,31 +120,26 @@ export function collectSubfields( groupedFieldSet: GroupedFieldSet; newDeferUsages: ReadonlyArray; } { - const context: CollectFieldsContext = { + const subGroupedFieldSet = new AccumulatorMap(); + const newDeferUsages: Array = []; + + const selectionSetVisitor = buildSelectionSetVisitor( schema, fragments, variableValues, - runtimeType: returnType, - visitedFragmentNames: new Set(), + returnType, hideSuggestions, - forbiddenDirectiveInstances: [], - forbidSkipAndInclude: false, - }; - const subGroupedFieldSet = new AccumulatorMap(); - const newDeferUsages: Array = []; + false, + subGroupedFieldSet, + newDeferUsages, + [], + ); for (const fieldDetail of fieldDetailsList) { const selectionSet = fieldDetail.node.selectionSet; if (selectionSet) { const { deferUsage, fragmentVariableValues } = fieldDetail; - collectFieldsImpl( - context, - selectionSet, - subGroupedFieldSet, - newDeferUsages, - deferUsage, - fragmentVariableValues, - ); + selectionSetVisitor(selectionSet, deferUsage, fragmentVariableValues); } } @@ -162,259 +150,219 @@ export function collectSubfields( } // eslint-disable-next-line @typescript-eslint/max-params -function collectFieldsImpl( - context: CollectFieldsContext, - selectionSet: SelectionSetNode, +function buildSelectionSetVisitor( + schema: GraphQLSchema, + fragments: ObjMap, + variableValues: VariableValues, + runtimeType: GraphQLObjectType, + hideSuggestions: boolean, + forbidSkipAndInclude: boolean, groupedFieldSet: AccumulatorMap, newDeferUsages: Array, + forbiddenDirectiveInstances: Array, +): ( + node: SelectionSetNode, deferUsage?: DeferUsage, fragmentVariableValues?: VariableValues, -): void { - const { - schema, - fragments, - variableValues, - runtimeType, - visitedFragmentNames, - hideSuggestions, - } = context; - - for (const selection of selectionSet.selections) { - switch (selection.kind) { - case Kind.FIELD: { - if ( - !shouldIncludeNode( - context, - selection, - variableValues, +) => void { + const visitedFragmentNames = new Set(); + + function selectionSetVisitor( + selectionSet: SelectionSetNode, + deferUsage?: DeferUsage, + fragmentVariableValues?: VariableValues, + ): void { + for (const selection of selectionSet.selections) { + switch (selection.kind) { + case Kind.FIELD: { + if (!shouldIncludeNode(selection)) { + continue; + } + groupedFieldSet.add(getFieldEntryKey(selection), { + node: selection, + deferUsage, fragmentVariableValues, - ) - ) { - continue; + }); + break; } - groupedFieldSet.add(getFieldEntryKey(selection), { - node: selection, - deferUsage, - fragmentVariableValues, - }); - break; - } - case Kind.INLINE_FRAGMENT: { - if ( - !shouldIncludeNode( - context, - selection, - variableValues, - fragmentVariableValues, - ) || - !doesFragmentConditionMatch(schema, selection, runtimeType) - ) { - continue; + case Kind.INLINE_FRAGMENT: { + if ( + !shouldIncludeNode(selection) || + !doesFragmentConditionMatch(selection) + ) { + continue; + } + + const newDeferUsage = getDeferUsage(selection); + + if (!newDeferUsage) { + selectionSetVisitor( + selection.selectionSet, + deferUsage, + fragmentVariableValues, + ); + } else { + newDeferUsages.push(newDeferUsage); + selectionSetVisitor( + selection.selectionSet, + newDeferUsage, + fragmentVariableValues, + ); + } + break; } - - const newDeferUsage = getDeferUsage( - variableValues, - fragmentVariableValues, - selection, - deferUsage, - ); - - if (!newDeferUsage) { - collectFieldsImpl( - context, - selection.selectionSet, - groupedFieldSet, - newDeferUsages, - deferUsage, - fragmentVariableValues, - ); - } else { - newDeferUsages.push(newDeferUsage); - collectFieldsImpl( - context, - selection.selectionSet, - groupedFieldSet, - newDeferUsages, - newDeferUsage, - fragmentVariableValues, - ); + case Kind.FRAGMENT_SPREAD: { + const fragName = selection.name.value; + + if ( + visitedFragmentNames.has(fragName) || + !shouldIncludeNode(selection) + ) { + continue; + } + + const fragment = fragments[fragName]; + if ( + fragment == null || + !doesFragmentConditionMatch(fragment.definition) + ) { + continue; + } + + const newDeferUsage = getDeferUsage(selection); + + const fragmentVariableSignatures = fragment.variableSignatures; + let newFragmentVariableValues: VariableValues | undefined; + if (fragmentVariableSignatures) { + newFragmentVariableValues = getFragmentVariableValues( + selection, + fragmentVariableSignatures, + variableValues, + fragmentVariableValues, + hideSuggestions, + ); + } + + if (!newDeferUsage) { + visitedFragmentNames.add(fragName); + selectionSetVisitor( + fragment.definition.selectionSet, + deferUsage, + newFragmentVariableValues, + ); + } else { + newDeferUsages.push(newDeferUsage); + selectionSetVisitor( + fragment.definition.selectionSet, + newDeferUsage, + newFragmentVariableValues, + ); + } + break; } + } + } + + /** + * Returns an object containing the `@defer` arguments if a field should be + * deferred based on the experimental flag, defer directive present and + * not disabled by the "if" argument. + */ + function getDeferUsage( + node: FragmentSpreadNode | InlineFragmentNode, + ): DeferUsage | undefined { + const defer = getDirectiveValues( + GraphQLDeferDirective, + node, + variableValues, + fragmentVariableValues, + ); + + if (!defer) { + return; + } - break; + if (defer.if === false) { + return; } - case Kind.FRAGMENT_SPREAD: { - const fragName = selection.name.value; - - if ( - visitedFragmentNames.has(fragName) || - !shouldIncludeNode( - context, - selection, + + return { + label: typeof defer.label === 'string' ? defer.label : undefined, + parentDeferUsage: deferUsage, + }; + } + + /** + * Determines if a field should be included based on the `@include` and `@skip` + * directives, where `@skip` has higher precedence than `@include`. + */ + function shouldIncludeNode( + node: FragmentSpreadNode | FieldNode | InlineFragmentNode, + ): boolean { + const skipDirectiveNode = node.directives?.find( + (directive) => directive.name.value === GraphQLSkipDirective.name, + ); + if (skipDirectiveNode && forbidSkipAndInclude) { + forbiddenDirectiveInstances.push(skipDirectiveNode); + return false; + } + const skip = skipDirectiveNode + ? experimentalGetArgumentValues( + skipDirectiveNode, + GraphQLSkipDirective.args, variableValues, fragmentVariableValues, + hideSuggestions, ) - ) { - continue; - } - - const fragment = fragments[fragName]; - if ( - fragment == null || - !doesFragmentConditionMatch(schema, fragment.definition, runtimeType) - ) { - continue; - } + : undefined; + if (skip?.if === true) { + return false; + } - const newDeferUsage = getDeferUsage( - variableValues, - fragmentVariableValues, - selection, - deferUsage, - ); - - const fragmentVariableSignatures = fragment.variableSignatures; - let newFragmentVariableValues: VariableValues | undefined; - if (fragmentVariableSignatures) { - newFragmentVariableValues = getFragmentVariableValues( - selection, - fragmentVariableSignatures, + const includeDirectiveNode = node.directives?.find( + (directive) => directive.name.value === GraphQLIncludeDirective.name, + ); + if (includeDirectiveNode && forbidSkipAndInclude) { + forbiddenDirectiveInstances.push(includeDirectiveNode); + return false; + } + const include = includeDirectiveNode + ? experimentalGetArgumentValues( + includeDirectiveNode, + GraphQLIncludeDirective.args, variableValues, fragmentVariableValues, hideSuggestions, - ); - } - - if (!newDeferUsage) { - visitedFragmentNames.add(fragName); - collectFieldsImpl( - context, - fragment.definition.selectionSet, - groupedFieldSet, - newDeferUsages, - deferUsage, - newFragmentVariableValues, - ); - } else { - newDeferUsages.push(newDeferUsage); - collectFieldsImpl( - context, - fragment.definition.selectionSet, - groupedFieldSet, - newDeferUsages, - newDeferUsage, - newFragmentVariableValues, - ); - } - break; + ) + : undefined; + if (include?.if === false) { + return false; } + return true; } } -} - -/** - * Returns an object containing the `@defer` arguments if a field should be - * deferred based on the experimental flag, defer directive present and - * not disabled by the "if" argument. - */ -function getDeferUsage( - variableValues: VariableValues, - fragmentVariableValues: VariableValues | undefined, - node: FragmentSpreadNode | InlineFragmentNode, - parentDeferUsage: DeferUsage | undefined, -): DeferUsage | undefined { - const defer = getDirectiveValues( - GraphQLDeferDirective, - node, - variableValues, - fragmentVariableValues, - ); - - if (!defer) { - return; - } - - if (defer.if === false) { - return; - } - - return { - label: typeof defer.label === 'string' ? defer.label : undefined, - parentDeferUsage, - }; -} - -/** - * Determines if a field should be included based on the `@include` and `@skip` - * directives, where `@skip` has higher precedence than `@include`. - */ -function shouldIncludeNode( - context: CollectFieldsContext, - node: FragmentSpreadNode | FieldNode | InlineFragmentNode, - variableValues: VariableValues, - fragmentVariableValues: VariableValues | undefined, -): boolean { - const skipDirectiveNode = node.directives?.find( - (directive) => directive.name.value === GraphQLSkipDirective.name, - ); - if (skipDirectiveNode && context.forbidSkipAndInclude) { - context.forbiddenDirectiveInstances.push(skipDirectiveNode); - return false; - } - const skip = skipDirectiveNode - ? experimentalGetArgumentValues( - skipDirectiveNode, - GraphQLSkipDirective.args, - variableValues, - fragmentVariableValues, - context.hideSuggestions, - ) - : undefined; - if (skip?.if === true) { - return false; - } - const includeDirectiveNode = node.directives?.find( - (directive) => directive.name.value === GraphQLIncludeDirective.name, - ); - if (includeDirectiveNode && context.forbidSkipAndInclude) { - context.forbiddenDirectiveInstances.push(includeDirectiveNode); - return false; - } - const include = includeDirectiveNode - ? experimentalGetArgumentValues( - includeDirectiveNode, - GraphQLIncludeDirective.args, - variableValues, - fragmentVariableValues, - context.hideSuggestions, - ) - : undefined; - if (include?.if === false) { + /** + * Determines if a fragment is applicable to the given type. + */ + function doesFragmentConditionMatch( + fragment: FragmentDefinitionNode | InlineFragmentNode, + ): boolean { + const typeConditionNode = fragment.typeCondition; + if (!typeConditionNode) { + return true; + } + const conditionalType = typeFromAST(schema, typeConditionNode); + if (conditionalType === runtimeType) { + return true; + } + if (isAbstractType(conditionalType)) { + return schema.isSubType(conditionalType, runtimeType); + } return false; } - return true; -} -/** - * Determines if a fragment is applicable to the given type. - */ -function doesFragmentConditionMatch( - schema: GraphQLSchema, - fragment: FragmentDefinitionNode | InlineFragmentNode, - type: GraphQLObjectType, -): boolean { - const typeConditionNode = fragment.typeCondition; - if (!typeConditionNode) { - return true; - } - const conditionalType = typeFromAST(schema, typeConditionNode); - if (conditionalType === type) { - return true; - } - if (isAbstractType(conditionalType)) { - return schema.isSubType(conditionalType, type); - } - return false; + return selectionSetVisitor; } /**