@graphql-tools/utils
Version:
Common package containing utils and types for GraphQL tools
228 lines (227 loc) • 9.07 kB
JavaScript
import { GraphQLIncludeDirective, GraphQLSkipDirective, isAbstractType, Kind, typeFromAST, } from 'graphql';
import { AccumulatorMap } from './AccumulatorMap.js';
import { GraphQLDeferDirective } from './directives.js';
import { getDirectiveValues } from './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.
*/
export function collectFieldsDependsOnVariables(document) {
for (const definition of document.definitions) {
if (definition.kind === Kind.OPERATION_DEFINITION ||
definition.kind === 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 Kind.FIELD:
if (selection.selectionSet && selectionSetDependsOnVariables(selection.selectionSet)) {
return true;
}
break;
case Kind.INLINE_FRAGMENT:
if (selectionSetDependsOnVariables(selection.selectionSet)) {
return true;
}
break;
case 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 !== 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 Kind.FIELD: {
if (!shouldIncludeNode(variableValues, selection)) {
continue;
}
fields.add(getFieldEntryKey(selection), selection);
break;
}
case Kind.INLINE_FRAGMENT: {
if (!shouldIncludeNode(variableValues, selection) ||
!doesFragmentConditionMatch(schema, selection, runtimeType)) {
continue;
}
const defer = getDeferValues(variableValues, selection);
if (defer) {
const patchFields = new 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 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();
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.
*
*/
export function collectFields(schema, fragments, variableValues, runtimeType, selectionSet) {
const fields = new 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`.
*/
export 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.
*/
export 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)) {
const possibleTypes = schema.getPossibleTypes(conditionalType);
return possibleTypes.includes(type);
}
return false;
}
/**
* Implements the logic to compute the key of a given field's entry
*/
export 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.
*/
export function getDeferValues(variableValues, node) {
const defer = getDirectiveValues(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.
*/
export function collectSubFields(schema, fragments, variableValues, returnType, fieldNodes) {
const subFieldNodes = new 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,
};
}