UNPKG

@graphql-tools/executor

Version:

Fork of GraphQL.js' execute function

174 lines (173 loc) 6.93 kB
import { getDirectiveValues, GraphQLIncludeDirective, GraphQLSkipDirective, isAbstractType, Kind, typeFromAST, } from 'graphql'; import { GraphQLDeferDirective, pathToArray } from '@graphql-tools/utils'; import { AccumulatorMap } from './AccumulatorMap.js'; import { invariant } from './invariant.js'; /** * Given a selectionSet, collects all of the fields and returns them. * * CollectFields requires the "runtime type" of an object. For a field that * returns an Interface or Union type, the "runtime type" will be the actual * object type returned by that field. * * @internal */ export function collectFields(schema, fragments, variableValues, runtimeType, selectionSet, errorOnSubscriptionWithIncrementalDelivery) { const groupedFieldSet = new AccumulatorMap(); const context = { schema, fragments, variableValues, runtimeType, errorOnSubscriptionWithIncrementalDelivery, visitedFragmentNames: new Set(), encounteredDefer: false, }; collectFieldsImpl(context, selectionSet, groupedFieldSet); if (context.encounteredDefer) { groupedFieldSet.encounteredDefer = true; } return groupedFieldSet; } /** * Given an array of field nodes, collects all of the subfields of the passed * in fields, and returns them at the end. * * CollectSubFields requires the "return type" of an object. For a field that * returns an Interface or Union type, the "return type" will be the actual * object type returned by that field. * * @internal */ export function collectSubfields(schema, fragments, variableValues, errorOnSubscriptionWithIncrementalDelivery, returnType, fieldGroup, path) { const context = { schema, fragments, variableValues, runtimeType: returnType, errorOnSubscriptionWithIncrementalDelivery, visitedFragmentNames: new Set(), encounteredDefer: false, }; const subGroupedFieldSet = new AccumulatorMap(); for (const fieldDetail of fieldGroup) { const { node, deferUsage } = fieldDetail; if (node.selectionSet) { collectFieldsImpl(context, node.selectionSet, subGroupedFieldSet, path, deferUsage); } } if (context.encounteredDefer) { subGroupedFieldSet.encounteredDefer = true; } return subGroupedFieldSet; } function collectFieldsImpl(context, selectionSet, groupedFieldSet, path, deferUsage) { const { schema, fragments, variableValues, runtimeType, errorOnSubscriptionWithIncrementalDelivery, visitedFragmentNames, } = context; for (const selection of selectionSet.selections) { switch (selection.kind) { case Kind.FIELD: { if (!shouldIncludeNode(variableValues, selection)) { continue; } groupedFieldSet.add(getFieldEntryKey(selection), { node: selection, deferUsage, }); break; } case Kind.INLINE_FRAGMENT: { if (!shouldIncludeNode(variableValues, selection) || !doesFragmentConditionMatch(schema, selection, runtimeType)) { continue; } const newDeferUsage = getDeferUsage(errorOnSubscriptionWithIncrementalDelivery, variableValues, selection, path, deferUsage); if (!newDeferUsage) { collectFieldsImpl(context, selection.selectionSet, groupedFieldSet, path, deferUsage); } else { context.encounteredDefer = true; collectFieldsImpl(context, selection.selectionSet, groupedFieldSet, path, newDeferUsage); } break; } case Kind.FRAGMENT_SPREAD: { const fragName = selection.name.value; const newDeferUsage = getDeferUsage(errorOnSubscriptionWithIncrementalDelivery, variableValues, selection, path, deferUsage); if (!newDeferUsage && (visitedFragmentNames.has(fragName) || !shouldIncludeNode(variableValues, selection))) { continue; } const fragment = fragments[fragName]; if (fragment == null || !doesFragmentConditionMatch(schema, fragment, runtimeType)) { continue; } if (!newDeferUsage) { visitedFragmentNames.add(fragName); collectFieldsImpl(context, fragment.selectionSet, groupedFieldSet, path, deferUsage); } else { context.encounteredDefer = true; collectFieldsImpl(context, fragment.selectionSet, groupedFieldSet, path, newDeferUsage); } 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(errorOnSubscriptionWithIncrementalDelivery, variableValues, node, path, parentDeferUsage) { const defer = getDirectiveValues(GraphQLDeferDirective, node, variableValues); if (!defer) { return; } if (defer['if'] === false) { return; } invariant(!errorOnSubscriptionWithIncrementalDelivery, '`@defer` directive not supported on subscription operations. Disable `@defer` by setting the `if` argument to `false`.'); return { label: typeof defer['label'] === 'string' ? defer['label'] : undefined, parentDeferUsage, depth: pathToArray(path).length, }; } /** * Determines if a field should be included based on the `@include` and `@skip` * directives, where `@skip` has higher precedence than `@include`. */ function shouldIncludeNode(variableValues, node) { 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, fragment, type) { 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) { return node.alias ? node.alias.value : node.name.value; }