diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts new file mode 100644 index 0000000000..2da9440969 --- /dev/null +++ b/src/execution/collectFields.ts @@ -0,0 +1,159 @@ +import type { ObjMap } from '../jsutils/ObjMap'; + +import type { + SelectionSetNode, + FieldNode, + FragmentSpreadNode, + InlineFragmentNode, + FragmentDefinitionNode, +} from '../language/ast'; +import { Kind } from '../language/kinds'; + +import type { GraphQLSchema } from '../type/schema'; +import type { GraphQLObjectType } from '../type/definition'; +import { + GraphQLIncludeDirective, + GraphQLSkipDirective, +} from '../type/directives'; +import { isAbstractType } from '../type/definition'; + +import { typeFromAST } from '../utilities/typeFromAST'; + +import { getDirectiveValues } from './values'; + +/** + * Given a selectionSet, adds all of the fields in that selection to + * the passed in map of fields, and returns it at the end. + * + * CollectFields requires the "runtime type" of an object. For a field which + * returns an Interface or Union type, the "runtime type" will be the actual + * Object type returned by that field. + * + * @internal + */ +export function collectFields( + schema: GraphQLSchema, + fragments: ObjMap, + variableValues: { [variable: string]: unknown }, + runtimeType: GraphQLObjectType, + selectionSet: SelectionSetNode, + fields: Map>, + visitedFragmentNames: Set, +): Map> { + for (const selection of selectionSet.selections) { + switch (selection.kind) { + case Kind.FIELD: { + if (!shouldIncludeNode(variableValues, selection)) { + continue; + } + const name = getFieldEntryKey(selection); + const fieldList = fields.get(name); + if (fieldList !== undefined) { + fieldList.push(selection); + } else { + fields.set(name, [selection]); + } + break; + } + case Kind.INLINE_FRAGMENT: { + if ( + !shouldIncludeNode(variableValues, selection) || + !doesFragmentConditionMatch(schema, selection, runtimeType) + ) { + continue; + } + collectFields( + schema, + fragments, + variableValues, + runtimeType, + selection.selectionSet, + fields, + visitedFragmentNames, + ); + break; + } + case Kind.FRAGMENT_SPREAD: { + const fragName = selection.name.value; + if ( + visitedFragmentNames.has(fragName) || + !shouldIncludeNode(variableValues, selection) + ) { + continue; + } + visitedFragmentNames.add(fragName); + const fragment = fragments[fragName]; + if ( + !fragment || + !doesFragmentConditionMatch(schema, fragment, runtimeType) + ) { + continue; + } + collectFields( + schema, + fragments, + variableValues, + runtimeType, + fragment.selectionSet, + fields, + visitedFragmentNames, + ); + break; + } + } + } + return fields; +} + +/** + * Determines if a field should be included based on the @include and @skip + * directives, where @skip has higher precedence than @include. + */ +function shouldIncludeNode( + variableValues: { [variable: string]: unknown }, + node: FragmentSpreadNode | FieldNode | InlineFragmentNode, +): boolean { + const skip = getDirectiveValues(GraphQLSkipDirective, node, variableValues); + if (skip?.if === true) { + return false; + } + + const include = getDirectiveValues( + GraphQLIncludeDirective, + node, + variableValues, + ); + if (include?.if === false) { + 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; +} + +/** + * Implements the logic to compute the key of a given field's entry + */ +function getFieldEntryKey(node: FieldNode): string { + return node.alias ? node.alias.value : node.name.value; +} diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 985d3173ed..4d7bc386db 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -20,10 +20,7 @@ import { locatedError } from '../error/locatedError'; import type { DocumentNode, OperationDefinitionNode, - SelectionSetNode, FieldNode, - FragmentSpreadNode, - InlineFragmentNode, FragmentDefinitionNode, } from '../language/ast'; import { Kind } from '../language/kinds'; @@ -46,10 +43,6 @@ import { TypeMetaFieldDef, TypeNameMetaFieldDef, } from '../type/introspection'; -import { - GraphQLIncludeDirective, - GraphQLSkipDirective, -} from '../type/directives'; import { isObjectType, isAbstractType, @@ -58,14 +51,10 @@ import { isNonNullType, } from '../type/definition'; -import { typeFromAST } from '../utilities/typeFromAST'; import { getOperationRootType } from '../utilities/getOperationRootType'; -import { - getVariableValues, - getArgumentValues, - getDirectiveValues, -} from './values'; +import { getVariableValues, getArgumentValues } from './values'; +import { collectFields } from './collectFields'; /** * Terminology @@ -336,7 +325,9 @@ function executeOperation( ): PromiseOrValue | null> { const type = getOperationRootType(exeContext.schema, operation); const fields = collectFields( - exeContext, + exeContext.schema, + exeContext.fragments, + exeContext.variableValues, type, operation.selectionSet, new Map(), @@ -447,141 +438,6 @@ function executeFields( return promiseForObject(results); } -/** - * Given a selectionSet, adds all of the fields in that selection to - * the passed in map of fields, and returns it at the end. - * - * CollectFields requires the "runtime type" of an object. For a field which - * returns an Interface or Union type, the "runtime type" will be the actual - * Object type returned by that field. - * - * @internal - */ -export function collectFields( - exeContext: ExecutionContext, - runtimeType: GraphQLObjectType, - selectionSet: SelectionSetNode, - fields: Map>, - visitedFragmentNames: Set, -): Map> { - for (const selection of selectionSet.selections) { - switch (selection.kind) { - case Kind.FIELD: { - if (!shouldIncludeNode(exeContext, selection)) { - continue; - } - const name = getFieldEntryKey(selection); - const fieldList = fields.get(name); - if (fieldList !== undefined) { - fieldList.push(selection); - } else { - fields.set(name, [selection]); - } - break; - } - case Kind.INLINE_FRAGMENT: { - if ( - !shouldIncludeNode(exeContext, selection) || - !doesFragmentConditionMatch(exeContext, selection, runtimeType) - ) { - continue; - } - collectFields( - exeContext, - runtimeType, - selection.selectionSet, - fields, - visitedFragmentNames, - ); - break; - } - case Kind.FRAGMENT_SPREAD: { - const fragName = selection.name.value; - if ( - visitedFragmentNames.has(fragName) || - !shouldIncludeNode(exeContext, selection) - ) { - continue; - } - visitedFragmentNames.add(fragName); - const fragment = exeContext.fragments[fragName]; - if ( - !fragment || - !doesFragmentConditionMatch(exeContext, fragment, runtimeType) - ) { - continue; - } - collectFields( - exeContext, - runtimeType, - fragment.selectionSet, - fields, - visitedFragmentNames, - ); - break; - } - } - } - return fields; -} - -/** - * Determines if a field should be included based on the @include and @skip - * directives, where @skip has higher precedence than @include. - */ -function shouldIncludeNode( - exeContext: ExecutionContext, - node: FragmentSpreadNode | FieldNode | InlineFragmentNode, -): boolean { - const skip = getDirectiveValues( - GraphQLSkipDirective, - node, - exeContext.variableValues, - ); - if (skip?.if === true) { - return false; - } - - const include = getDirectiveValues( - GraphQLIncludeDirective, - node, - exeContext.variableValues, - ); - if (include?.if === false) { - return false; - } - return true; -} - -/** - * Determines if a fragment is applicable to the given type. - */ -function doesFragmentConditionMatch( - exeContext: ExecutionContext, - fragment: FragmentDefinitionNode | InlineFragmentNode, - type: GraphQLObjectType, -): boolean { - const typeConditionNode = fragment.typeCondition; - if (!typeConditionNode) { - return true; - } - const conditionalType = typeFromAST(exeContext.schema, typeConditionNode); - if (conditionalType === type) { - return true; - } - if (isAbstractType(conditionalType)) { - return exeContext.schema.isSubType(conditionalType, type); - } - return false; -} - -/** - * Implements the logic to compute the key of a given field's entry - */ -function getFieldEntryKey(node: FieldNode): string { - return node.alias ? node.alias.value : node.name.value; -} - /** * Implements the "Executing field" section of the spec * In particular, this function figures out the value that the field returns by @@ -1081,7 +937,9 @@ function _collectSubfields( for (const node of fieldNodes) { if (node.selectionSet) { subFieldNodes = collectFields( - exeContext, + exeContext.schema, + exeContext.fragments, + exeContext.variableValues, returnType, node.selectionSet, subFieldNodes, diff --git a/src/subscription/subscribe.ts b/src/subscription/subscribe.ts index 6b4c6c13bf..6fbca779df 100644 --- a/src/subscription/subscribe.ts +++ b/src/subscription/subscribe.ts @@ -9,12 +9,12 @@ import { locatedError } from '../error/locatedError'; import type { DocumentNode } from '../language/ast'; import type { ExecutionResult, ExecutionContext } from '../execution/execute'; +import { collectFields } from '../execution/collectFields'; import { getArgumentValues } from '../execution/values'; import { assertValidExecutionArguments, buildExecutionContext, buildResolveInfo, - collectFields, execute, getFieldDef, } from '../execution/execute'; @@ -189,10 +189,13 @@ export async function createSourceEventStream( async function executeSubscription( exeContext: ExecutionContext, ): Promise { - const { schema, operation, variableValues, rootValue } = exeContext; + const { schema, fragments, operation, variableValues, rootValue } = + exeContext; const type = getOperationRootType(schema, operation); const fields = collectFields( - exeContext, + schema, + fragments, + variableValues, type, operation.selectionSet, new Map(), diff --git a/src/validation/rules/SingleFieldSubscriptionsRule.ts b/src/validation/rules/SingleFieldSubscriptionsRule.ts index 0098ddc3d7..736f0f006f 100644 --- a/src/validation/rules/SingleFieldSubscriptionsRule.ts +++ b/src/validation/rules/SingleFieldSubscriptionsRule.ts @@ -8,12 +8,7 @@ import type { } from '../../language/ast'; import { Kind } from '../../language/kinds'; -import type { ExecutionContext } from '../../execution/execute'; -import { - collectFields, - defaultFieldResolver, - defaultTypeResolver, -} from '../../execution/execute'; +import { collectFields } from '../../execution/collectFields'; import type { ValidationContext } from '../ValidationContext'; @@ -43,20 +38,10 @@ export function SingleFieldSubscriptionsRule( fragments[definition.name.value] = definition; } } - // FIXME: refactor out `collectFields` into utility function that doesn't need fake context. - const fakeExecutionContext: ExecutionContext = { + const fields = collectFields( schema, fragments, - rootValue: undefined, - contextValue: undefined, - operation: node, variableValues, - fieldResolver: defaultFieldResolver, - typeResolver: defaultTypeResolver, - errors: [], - }; - const fields = collectFields( - fakeExecutionContext, subscriptionType, node.selectionSet, new Map(),