UNPKG

@graphql-tools/utils

Version:

Common package containing utils and types for GraphQL tools

237 lines (236 loc) • 9.72 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.collectFieldsDependsOnVariables = collectFieldsDependsOnVariables; exports.collectFields = collectFields; exports.shouldIncludeNode = shouldIncludeNode; exports.doesFragmentConditionMatch = doesFragmentConditionMatch; exports.getFieldEntryKey = getFieldEntryKey; exports.getDeferValues = getDeferValues; exports.collectSubFields = collectSubFields; const graphql_1 = require("graphql"); const AccumulatorMap_js_1 = require("./AccumulatorMap.js"); const directives_js_1 = require("./directives.js"); const getDirectiveValues_js_1 = require("./getDirectiveValues.js"); const VARIABLE_CONDITIONAL_DIRECTIVES = new Set(['skip', 'include', 'defer']); /** * True when `@skip` / `@include` / `@defer` use a variable argument that * affects collected fields (`if`, and for `@defer` also `label`) somewhere in * the document. In that case collected field sets depend on runtime variable * values; otherwise collectFields results are stable for a given schema, * fragments, type and selection set. */ function collectFieldsDependsOnVariables(document) { for (const definition of document.definitions) { if (definition.kind === graphql_1.Kind.OPERATION_DEFINITION || definition.kind === graphql_1.Kind.FRAGMENT_DEFINITION) { if (selectionSetDependsOnVariables(definition.selectionSet)) { return true; } } } return false; } function selectionSetDependsOnVariables(selectionSet) { for (const selection of selectionSet.selections) { if (directivesDependOnVariables(selection.directives)) { return true; } switch (selection.kind) { case graphql_1.Kind.FIELD: if (selection.selectionSet && selectionSetDependsOnVariables(selection.selectionSet)) { return true; } break; case graphql_1.Kind.INLINE_FRAGMENT: if (selectionSetDependsOnVariables(selection.selectionSet)) { return true; } break; case graphql_1.Kind.FRAGMENT_SPREAD: break; } } return false; } function directivesDependOnVariables(directives) { if (directives == null) { return false; } for (const directive of directives) { const name = directive.name.value; if (!VARIABLE_CONDITIONAL_DIRECTIVES.has(name)) { continue; } for (const arg of directive.arguments ?? []) { if (arg.value.kind !== graphql_1.Kind.VARIABLE) { continue; } // `@skip` / `@include` only expose `if`; `@defer` also resolves `label` // into PatchFields, so a variable label must disable the stable cache. if (arg.name.value === 'if' || (name === 'defer' && arg.name.value === 'label')) { return true; } } } return false; } function collectFieldsImpl(schema, fragments, variableValues, runtimeType, selectionSet, fields, patches, visitedFragmentNames) { for (const selection of selectionSet.selections) { switch (selection.kind) { case graphql_1.Kind.FIELD: { if (!shouldIncludeNode(variableValues, selection)) { continue; } fields.add(getFieldEntryKey(selection), selection); break; } case graphql_1.Kind.INLINE_FRAGMENT: { if (!shouldIncludeNode(variableValues, selection) || !doesFragmentConditionMatch(schema, selection, runtimeType)) { continue; } const defer = getDeferValues(variableValues, selection); if (defer) { const patchFields = new AccumulatorMap_js_1.AccumulatorMap(); const nestedPatches = []; collectFieldsImpl(schema, fragments, variableValues, runtimeType, selection.selectionSet, patchFields, nestedPatches, visitedFragmentNames); patches.push({ label: defer.label, fields: patchFields, patches: nestedPatches.length > 0 ? nestedPatches : undefined, }); } else { collectFieldsImpl(schema, fragments, variableValues, runtimeType, selection.selectionSet, fields, patches, visitedFragmentNames); } break; } case graphql_1.Kind.FRAGMENT_SPREAD: { const fragName = selection.name.value; if (!shouldIncludeNode(variableValues, selection)) { continue; } const defer = getDeferValues(variableValues, selection); if (visitedFragmentNames.has(fragName) && !defer) { continue; } const fragment = fragments[fragName]; if (!fragment || !doesFragmentConditionMatch(schema, fragment, runtimeType)) { continue; } if (!defer) { visitedFragmentNames.add(fragName); } if (defer) { const patchFields = new AccumulatorMap_js_1.AccumulatorMap(); const nestedPatches = []; collectFieldsImpl(schema, fragments, variableValues, runtimeType, fragment.selectionSet, patchFields, nestedPatches, visitedFragmentNames); patches.push({ label: defer.label, fields: patchFields, patches: nestedPatches.length > 0 ? nestedPatches : undefined, }); } else { collectFieldsImpl(schema, fragments, variableValues, runtimeType, fragment.selectionSet, fields, patches, visitedFragmentNames); } break; } } } } /** * 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. * */ function collectFields(schema, fragments, variableValues, runtimeType, selectionSet) { const fields = new AccumulatorMap_js_1.AccumulatorMap(); const patches = []; collectFieldsImpl(schema, fragments, variableValues, runtimeType, selectionSet, fields, patches, new Set()); return { fields, patches }; } /** * 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 = (0, getDirectiveValues_js_1.getDirectiveValues)(graphql_1.GraphQLSkipDirective, node, variableValues); if (skip?.['if'] === true) { return false; } const include = (0, getDirectiveValues_js_1.getDirectiveValues)(graphql_1.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 = (0, graphql_1.typeFromAST)(schema, typeConditionNode); if (conditionalType === type) { return true; } if ((0, graphql_1.isAbstractType)(conditionalType)) { const possibleTypes = schema.getPossibleTypes(conditionalType); return possibleTypes.includes(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; } /** * 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 getDeferValues(variableValues, node) { const defer = (0, getDirectiveValues_js_1.getDirectiveValues)(directives_js_1.GraphQLDeferDirective, node, variableValues); if (!defer) { return; } if (defer['if'] === false) { return; } return { label: typeof defer['label'] === 'string' ? defer['label'] : undefined, }; } /** * 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. * * Unmemoized on purpose: callers that hit this on a hot path (e.g. executor) * should wrap with a single lightweight memoize3 keyed by execution context. */ function collectSubFields(schema, fragments, variableValues, returnType, fieldNodes) { const subFieldNodes = new AccumulatorMap_js_1.AccumulatorMap(); const visitedFragmentNames = new Set(); const subPatches = []; for (const node of fieldNodes) { if (node.selectionSet) { collectFieldsImpl(schema, fragments, variableValues, returnType, node.selectionSet, subFieldNodes, subPatches, visitedFragmentNames); } } return { fields: subFieldNodes, patches: subPatches, }; }