diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts index d1916867d6..51f434f0b6 100644 --- a/src/execution/collectFields.ts +++ b/src/execution/collectFields.ts @@ -1,5 +1,6 @@ import { AccumulatorMap } from '../jsutils/AccumulatorMap.js'; import { invariant } from '../jsutils/invariant.js'; +import { mapValue } from '../jsutils/mapValue.js'; import type { ObjMap } from '../jsutils/ObjMap.js'; import type { @@ -25,6 +26,7 @@ import type { GraphQLSchema } from '../type/schema.js'; import { typeFromAST } from '../utilities/typeFromAST.js'; import type { GraphQLVariableSignature } from './getVariableSignature.js'; +import type { VariableValues } from './values.js'; import { experimentalGetArgumentValues, getDirectiveValues } from './values.js'; export interface DeferUsage { @@ -32,15 +34,10 @@ export interface DeferUsage { parentDeferUsage: DeferUsage | undefined; } -export interface FragmentVariables { - signatures: ObjMap; - values: ObjMap; -} - export interface FieldDetails { node: FieldNode; deferUsage?: DeferUsage | undefined; - fragmentVariables?: FragmentVariables | undefined; + fragmentVariableValues?: VariableValues | undefined; } export type FieldGroup = ReadonlyArray; @@ -55,7 +52,7 @@ export interface FragmentDetails { interface CollectFieldsContext { schema: GraphQLSchema; fragments: ObjMap; - variableValues: { [variable: string]: unknown }; + variableValues: VariableValues; operation: OperationDefinitionNode; runtimeType: GraphQLObjectType; visitedFragmentNames: Set; @@ -73,7 +70,7 @@ interface CollectFieldsContext { export function collectFields( schema: GraphQLSchema, fragments: ObjMap, - variableValues: { [variable: string]: unknown }, + variableValues: VariableValues, runtimeType: GraphQLObjectType, operation: OperationDefinitionNode, ): { @@ -114,7 +111,7 @@ export function collectFields( export function collectSubfields( schema: GraphQLSchema, fragments: ObjMap, - variableValues: { [variable: string]: unknown }, + variableValues: VariableValues, operation: OperationDefinitionNode, returnType: GraphQLObjectType, fieldGroup: FieldGroup, @@ -136,14 +133,14 @@ export function collectSubfields( for (const fieldDetail of fieldGroup) { const selectionSet = fieldDetail.node.selectionSet; if (selectionSet) { - const { deferUsage, fragmentVariables } = fieldDetail; + const { deferUsage, fragmentVariableValues } = fieldDetail; collectFieldsImpl( context, selectionSet, subGroupedFieldSet, newDeferUsages, deferUsage, - fragmentVariables, + fragmentVariableValues, ); } } @@ -161,7 +158,7 @@ function collectFieldsImpl( groupedFieldSet: AccumulatorMap, newDeferUsages: Array, deferUsage?: DeferUsage, - fragmentVariables?: FragmentVariables, + fragmentVariableValues?: VariableValues, ): void { const { schema, @@ -175,19 +172,25 @@ function collectFieldsImpl( for (const selection of selectionSet.selections) { switch (selection.kind) { case Kind.FIELD: { - if (!shouldIncludeNode(selection, variableValues, fragmentVariables)) { + if ( + !shouldIncludeNode(selection, variableValues, fragmentVariableValues) + ) { continue; } groupedFieldSet.add(getFieldEntryKey(selection), { node: selection, deferUsage, - fragmentVariables, + fragmentVariableValues, }); break; } case Kind.INLINE_FRAGMENT: { if ( - !shouldIncludeNode(selection, variableValues, fragmentVariables) || + !shouldIncludeNode( + selection, + variableValues, + fragmentVariableValues, + ) || !doesFragmentConditionMatch(schema, selection, runtimeType) ) { continue; @@ -196,7 +199,7 @@ function collectFieldsImpl( const newDeferUsage = getDeferUsage( operation, variableValues, - fragmentVariables, + fragmentVariableValues, selection, deferUsage, ); @@ -208,7 +211,7 @@ function collectFieldsImpl( groupedFieldSet, newDeferUsages, deferUsage, - fragmentVariables, + fragmentVariableValues, ); } else { newDeferUsages.push(newDeferUsage); @@ -218,7 +221,7 @@ function collectFieldsImpl( groupedFieldSet, newDeferUsages, newDeferUsage, - fragmentVariables, + fragmentVariableValues, ); } @@ -230,7 +233,7 @@ function collectFieldsImpl( const newDeferUsage = getDeferUsage( operation, variableValues, - fragmentVariables, + fragmentVariableValues, selection, deferUsage, ); @@ -238,7 +241,11 @@ function collectFieldsImpl( if ( !newDeferUsage && (visitedFragmentNames.has(fragName) || - !shouldIncludeNode(selection, variableValues, fragmentVariables)) + !shouldIncludeNode( + selection, + variableValues, + fragmentVariableValues, + )) ) { continue; } @@ -252,15 +259,20 @@ function collectFieldsImpl( } const fragmentVariableSignatures = fragment.variableSignatures; - let newFragmentVariables: FragmentVariables | undefined; + let newFragmentVariableValues: VariableValues | undefined; if (fragmentVariableSignatures) { - newFragmentVariables = { - signatures: fragmentVariableSignatures, - values: experimentalGetArgumentValues( + newFragmentVariableValues = { + sources: mapValue(fragmentVariableSignatures, (varSignature) => ({ + signature: varSignature, + value: fragmentVariableValues?.sources[varSignature.name] + ? fragmentVariableValues.coerced[varSignature.name] + : variableValues.coerced[varSignature.name], + })), + coerced: experimentalGetArgumentValues( selection, Object.values(fragmentVariableSignatures), variableValues, - fragmentVariables, + fragmentVariableValues, ), }; } @@ -273,7 +285,7 @@ function collectFieldsImpl( groupedFieldSet, newDeferUsages, deferUsage, - newFragmentVariables, + newFragmentVariableValues, ); } else { newDeferUsages.push(newDeferUsage); @@ -283,7 +295,7 @@ function collectFieldsImpl( groupedFieldSet, newDeferUsages, newDeferUsage, - newFragmentVariables, + newFragmentVariableValues, ); } break; @@ -299,8 +311,8 @@ function collectFieldsImpl( */ function getDeferUsage( operation: OperationDefinitionNode, - variableValues: { [variable: string]: unknown }, - fragmentVariables: FragmentVariables | undefined, + variableValues: VariableValues, + fragmentVariableValues: VariableValues | undefined, node: FragmentSpreadNode | InlineFragmentNode, parentDeferUsage: DeferUsage | undefined, ): DeferUsage | undefined { @@ -308,7 +320,7 @@ function getDeferUsage( GraphQLDeferDirective, node, variableValues, - fragmentVariables, + fragmentVariableValues, ); if (!defer) { @@ -336,14 +348,14 @@ function getDeferUsage( */ function shouldIncludeNode( node: FragmentSpreadNode | FieldNode | InlineFragmentNode, - variableValues: { [variable: string]: unknown }, - fragmentVariables: FragmentVariables | undefined, + variableValues: VariableValues, + fragmentVariableValues: VariableValues | undefined, ): boolean { const skip = getDirectiveValues( GraphQLSkipDirective, node, variableValues, - fragmentVariables, + fragmentVariableValues, ); if (skip?.if === true) { return false; @@ -353,7 +365,7 @@ function shouldIncludeNode( GraphQLIncludeDirective, node, variableValues, - fragmentVariables, + fragmentVariableValues, ); if (include?.if === false) { return false; diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 782fda7537..404d267411 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -75,6 +75,7 @@ import type { StreamRecord, } from './types.js'; import { DeferredFragmentRecord } from './types.js'; +import type { VariableValues } from './values.js'; import { experimentalGetArgumentValues, getArgumentValues, @@ -139,7 +140,7 @@ export interface ExecutionContext { rootValue: unknown; contextValue: unknown; operation: OperationDefinitionNode; - variableValues: { [variable: string]: unknown }; + variableValues: VariableValues; fieldResolver: GraphQLFieldResolver; typeResolver: GraphQLTypeResolver; subscribeFieldResolver: GraphQLFieldResolver; @@ -510,15 +511,15 @@ export function buildExecutionContext( /* c8 ignore next */ const variableDefinitions = operation.variableDefinitions ?? []; - const coercedVariableValues = getVariableValues( + const variableValuesOrErrors = getVariableValues( schema, variableDefinitions, rawVariableValues ?? {}, { maxErrors: 50 }, ); - if (coercedVariableValues.errors) { - return coercedVariableValues.errors; + if (variableValuesOrErrors.errors) { + return variableValuesOrErrors.errors; } return { @@ -527,7 +528,7 @@ export function buildExecutionContext( rootValue, contextValue, operation, - variableValues: coercedVariableValues.coerced, + variableValues: variableValuesOrErrors.variableValues, fieldResolver: fieldResolver ?? defaultFieldResolver, typeResolver: typeResolver ?? defaultTypeResolver, subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, @@ -753,7 +754,7 @@ function executeField( fieldGroup[0].node, fieldDef.args, exeContext.variableValues, - fieldGroup[0].fragmentVariables, + fieldGroup[0].fragmentVariableValues, ); // The resolve function's optional third argument is a context value that @@ -842,7 +843,7 @@ export function buildResolveInfo( ), rootValue: exeContext.rootValue, operation: exeContext.operation, - variableValues: exeContext.variableValues, + variableValues: exeContext.variableValues.coerced, }; } @@ -1062,7 +1063,7 @@ function getStreamUsage( GraphQLStreamDirective, fieldGroup[0].node, exeContext.variableValues, - fieldGroup[0].fragmentVariables, + fieldGroup[0].fragmentVariableValues, ); if (!stream) { @@ -1091,7 +1092,7 @@ function getStreamUsage( const streamedFieldGroup: FieldGroup = fieldGroup.map((fieldDetails) => ({ node: fieldDetails.node, deferUsage: undefined, - fragmentVariables: fieldDetails.fragmentVariables, + fragmentVariables: fieldDetails.fragmentVariableValues, })); const streamUsage = { diff --git a/src/execution/values.ts b/src/execution/values.ts index 5ac0c7656c..d078b9b71a 100644 --- a/src/execution/values.ts +++ b/src/execution/values.ts @@ -1,6 +1,6 @@ import { inspect } from '../jsutils/inspect.js'; import type { Maybe } from '../jsutils/Maybe.js'; -import type { ObjMap } from '../jsutils/ObjMap.js'; +import type { ObjMap, ReadOnlyObjMap } from '../jsutils/ObjMap.js'; import { printPathArray } from '../jsutils/printPathArray.js'; import { GraphQLError } from '../error/GraphQLError.js'; @@ -25,13 +25,22 @@ import { coerceInputValue, } from '../utilities/coerceInputValue.js'; -import type { FragmentVariables } from './collectFields.js'; import type { GraphQLVariableSignature } from './getVariableSignature.js'; import { getVariableSignature } from './getVariableSignature.js'; -type CoercedVariableValues = - | { errors: ReadonlyArray; coerced?: never } - | { coerced: { [variable: string]: unknown }; errors?: never }; +export interface VariableValues { + readonly sources: ReadOnlyObjMap; + readonly coerced: ReadOnlyObjMap; +} + +interface VariableValueSource { + readonly signature: GraphQLVariableSignature; + readonly value: unknown; +} + +type VariableValuesOrErrors = + | { variableValues: VariableValues; errors?: never } + | { errors: ReadonlyArray; variableValues?: never }; /** * Prepares an object map of variableValues of the correct type based on the @@ -47,11 +56,11 @@ export function getVariableValues( varDefNodes: ReadonlyArray, inputs: { readonly [variable: string]: unknown }, options?: { maxErrors?: number }, -): CoercedVariableValues { - const errors = []; +): VariableValuesOrErrors { + const errors: Array = []; const maxErrors = options?.maxErrors; try { - const coerced = coerceVariableValues( + const variableValues = coerceVariableValues( schema, varDefNodes, inputs, @@ -66,7 +75,7 @@ export function getVariableValues( ); if (errors.length === 0) { - return { coerced }; + return { variableValues }; } } catch (error) { errors.push(error); @@ -80,8 +89,9 @@ function coerceVariableValues( varDefNodes: ReadonlyArray, inputs: { readonly [variable: string]: unknown }, onError: (error: GraphQLError) => void, -): { [variable: string]: unknown } { - const coercedValues: { [variable: string]: unknown } = {}; +): VariableValues { + const sources: ObjMap = Object.create(null); + const coerced: ObjMap = Object.create(null); for (const varDefNode of varDefNodes) { const varSignature = getVariableSignature(schema, varDefNode); if (varSignature instanceof GraphQLError) { @@ -91,11 +101,13 @@ function coerceVariableValues( const { name: varName, type: varType } = varSignature; if (!Object.hasOwn(inputs, varName)) { - if (varSignature.defaultValue) { - coercedValues[varName] = coerceDefaultValue( - varSignature.defaultValue, - varType, - ); + const defaultValue = varSignature.defaultValue; + if (defaultValue) { + sources[varName] = { + signature: varSignature, + value: undefined, + }; + coerced[varName] = coerceDefaultValue(defaultValue, varType); } else if (isNonNullType(varType)) { const varTypeStr = inspect(varType); onError( @@ -120,7 +132,8 @@ function coerceVariableValues( continue; } - coercedValues[varName] = coerceInputValue( + sources[varName] = { signature: varSignature, value }; + coerced[varName] = coerceInputValue( value, varType, (path, invalidValue, error) => { @@ -139,7 +152,7 @@ function coerceVariableValues( ); } - return coercedValues; + return { sources, coerced }; } /** @@ -153,7 +166,7 @@ function coerceVariableValues( export function getArgumentValues( def: GraphQLField | GraphQLDirective, node: FieldNode | DirectiveNode, - variableValues?: Maybe>, + variableValues?: Maybe, ): { [argument: string]: unknown } { return experimentalGetArgumentValues(node, def.args, variableValues); } @@ -161,8 +174,8 @@ export function getArgumentValues( export function experimentalGetArgumentValues( node: FieldNode | DirectiveNode | FragmentSpreadNode, argDefs: ReadonlyArray, - variableValues: Maybe>, - fragmentVariables?: Maybe, + variableValues: Maybe, + fragmentVariables?: Maybe, ): { [argument: string]: unknown } { const coercedValues: { [argument: string]: unknown } = {}; @@ -197,12 +210,12 @@ export function experimentalGetArgumentValues( if (valueNode.kind === Kind.VARIABLE) { const variableName = valueNode.name.value; - const scopedVariableValues = fragmentVariables?.signatures[variableName] - ? fragmentVariables.values + const scopedVariableValues = fragmentVariables?.sources[variableName] + ? fragmentVariables : variableValues; if ( scopedVariableValues == null || - !Object.hasOwn(scopedVariableValues, variableName) + !Object.hasOwn(scopedVariableValues.coerced, variableName) ) { if (argDef.defaultValue) { coercedValues[name] = coerceDefaultValue( @@ -218,7 +231,7 @@ export function experimentalGetArgumentValues( } continue; } - isNull = scopedVariableValues[variableName] == null; + isNull = scopedVariableValues.coerced[variableName] == null; } if (isNull && isNonNullType(argType)) { @@ -265,8 +278,8 @@ export function experimentalGetArgumentValues( export function getDirectiveValues( directiveDef: GraphQLDirective, node: { readonly directives?: ReadonlyArray | undefined }, - variableValues?: Maybe>, - fragmentVariables?: Maybe, + variableValues?: Maybe, + fragmentVariableValues?: Maybe, ): undefined | { [argument: string]: unknown } { const directiveNode = node.directives?.find( (directive) => directive.name.value === directiveDef.name, @@ -277,7 +290,7 @@ export function getDirectiveValues( directiveNode, directiveDef.args, variableValues, - fragmentVariables, + fragmentVariableValues, ); } } diff --git a/src/utilities/__tests__/coerceInputValue-test.ts b/src/utilities/__tests__/coerceInputValue-test.ts index 78dede7b81..0a71e39cba 100644 --- a/src/utilities/__tests__/coerceInputValue-test.ts +++ b/src/utilities/__tests__/coerceInputValue-test.ts @@ -3,11 +3,12 @@ import { describe, it } from 'mocha'; import { identityFunc } from '../../jsutils/identityFunc.js'; import { invariant } from '../../jsutils/invariant.js'; -import type { ObjMap } from '../../jsutils/ObjMap.js'; +import type { ReadOnlyObjMap } from '../../jsutils/ObjMap.js'; import { Kind } from '../../language/kinds.js'; -import { parseValue } from '../../language/parser.js'; +import { Parser, parseValue } from '../../language/parser.js'; import { print } from '../../language/printer.js'; +import { TokenKind } from '../../language/tokenKind.js'; import type { GraphQLInputType } from '../../type/definition.js'; import { @@ -24,6 +25,10 @@ import { GraphQLInt, GraphQLString, } from '../../type/scalars.js'; +import { GraphQLSchema } from '../../type/schema.js'; + +import type { VariableValues } from '../../execution/values.js'; +import { getVariableValues } from '../../execution/values.js'; import { coerceDefaultValue, @@ -557,20 +562,29 @@ describe('coerceInputLiteral', () => { valueText: string, type: GraphQLInputType, expected: unknown, - variables?: ObjMap, + variableValues?: VariableValues, ) { const ast = parseValue(valueText); - const value = coerceInputLiteral(ast, type, variables); + const value = coerceInputLiteral(ast, type, variableValues); expect(value).to.deep.equal(expected); } function testWithVariables( - variables: ObjMap, + variableDefs: string, + inputs: ReadOnlyObjMap, valueText: string, type: GraphQLInputType, expected: unknown, ) { - test(valueText, type, expected, variables); + const parser = new Parser(variableDefs); + parser.expectToken(TokenKind.SOF); + const variableValuesOrErrors = getVariableValues( + new GraphQLSchema({}), + parser.parseVariableDefinitions(), + inputs, + ); + invariant(variableValuesOrErrors.variableValues !== undefined); + test(valueText, type, expected, variableValuesOrErrors.variableValues); } it('converts according to input coercion rules', () => { @@ -789,19 +803,55 @@ describe('coerceInputLiteral', () => { it('accepts variable values assuming already coerced', () => { test('$var', GraphQLBoolean, undefined); - testWithVariables({ var: true }, '$var', GraphQLBoolean, true); - testWithVariables({ var: null }, '$var', GraphQLBoolean, null); - testWithVariables({ var: null }, '$var', nonNullBool, undefined); + testWithVariables( + '($var: Boolean)', + { var: true }, + '$var', + GraphQLBoolean, + true, + ); + testWithVariables( + '($var: Boolean)', + { var: null }, + '$var', + GraphQLBoolean, + null, + ); + testWithVariables( + '($var: Boolean)', + { var: null }, + '$var', + nonNullBool, + undefined, + ); }); it('asserts variables are provided as items in lists', () => { test('[ $foo ]', listOfBool, [null]); test('[ $foo ]', listOfNonNullBool, undefined); - testWithVariables({ foo: true }, '[ $foo ]', listOfNonNullBool, [true]); + testWithVariables( + '($foo: Boolean)', + { foo: true }, + '[ $foo ]', + listOfNonNullBool, + [true], + ); // Note: variables are expected to have already been coerced, so we // do not expect the singleton wrapping behavior for variables. - testWithVariables({ foo: true }, '$foo', listOfNonNullBool, true); - testWithVariables({ foo: [true] }, '$foo', listOfNonNullBool, [true]); + testWithVariables( + '($foo: Boolean)', + { foo: true }, + '$foo', + listOfNonNullBool, + true, + ); + testWithVariables( + '($foo: [Boolean])', + { foo: [true] }, + '$foo', + listOfNonNullBool, + [true], + ); }); it('omits input object fields for unprovided variables', () => { @@ -810,10 +860,13 @@ describe('coerceInputLiteral', () => { requiredBool: true, }); test('{ requiredBool: $foo }', testInputObj, undefined); - testWithVariables({ foo: true }, '{ requiredBool: $foo }', testInputObj, { - int: 42, - requiredBool: true, - }); + testWithVariables( + '($foo: Boolean)', + { foo: true }, + '{ requiredBool: $foo }', + testInputObj, + { int: 42, requiredBool: true }, + ); }); }); diff --git a/src/utilities/coerceInputValue.ts b/src/utilities/coerceInputValue.ts index f912fea4e4..3122edb2ab 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -4,7 +4,6 @@ import { invariant } from '../jsutils/invariant.js'; import { isIterableObject } from '../jsutils/isIterableObject.js'; import { isObjectLike } from '../jsutils/isObjectLike.js'; import type { Maybe } from '../jsutils/Maybe.js'; -import type { ObjMap } from '../jsutils/ObjMap.js'; import type { Path } from '../jsutils/Path.js'; import { addPath, pathToArray } from '../jsutils/Path.js'; import { printPathArray } from '../jsutils/printPathArray.js'; @@ -28,7 +27,7 @@ import { isRequiredInputField, } from '../type/definition.js'; -import type { FragmentVariables } from '../execution/collectFields.js'; +import type { VariableValues } from '../execution/values.js'; type OnErrorCB = ( path: ReadonlyArray, @@ -229,21 +228,21 @@ function coerceInputValueImpl( export function coerceInputLiteral( valueNode: ValueNode, type: GraphQLInputType, - variableValues?: Maybe>, - fragmentVariableValues?: Maybe, + variableValues?: Maybe, + fragmentVariableValues?: Maybe, ): unknown { if (valueNode.kind === Kind.VARIABLE) { - const variableValue = getVariableValue( + const coercedVariableValue = getCoercedVariableValue( valueNode, variableValues, fragmentVariableValues, ); - if (variableValue == null && isNonNullType(type)) { + if (coercedVariableValue == null && isNonNullType(type)) { return; // Invalid: intentionally return no value. } // Note: This does no further checking that this variable is correct. // This assumes validated has checked this variable is of the correct type. - return variableValue; + return coercedVariableValue; } if (isNonNullType(type)) { @@ -287,8 +286,11 @@ export function coerceInputLiteral( if (itemValue === undefined) { if ( itemNode.kind === Kind.VARIABLE && - getVariableValue(itemNode, variableValues, fragmentVariableValues) == - null && + getCoercedVariableValue( + itemNode, + variableValues, + fragmentVariableValues, + ) == null && !isNonNullType(type.ofType) ) { // A missing variable within a list is coerced to null. @@ -323,7 +325,7 @@ export function coerceInputLiteral( if ( !fieldNode || (fieldNode.value.kind === Kind.VARIABLE && - getVariableValue( + getCoercedVariableValue( fieldNode.value, variableValues, fragmentVariableValues, @@ -369,24 +371,24 @@ export function coerceInputLiteral( const leafType = assertLeafType(type); try { - return leafType.parseLiteral(valueNode, variableValues); + return leafType.parseLiteral(valueNode, variableValues?.coerced); } catch (_error) { // Invalid: ignore error and intentionally return no value. } } // Retrieves the variable value for the given variable node. -function getVariableValue( +function getCoercedVariableValue( variableNode: VariableNode, - variableValues: Maybe>, - fragmentVariableValues: Maybe | undefined, + variableValues: Maybe, + fragmentVariableValues: Maybe, ): unknown { const varName = variableNode.name.value; - if (fragmentVariableValues?.signatures[varName]) { - return fragmentVariableValues.values[varName]; + if (fragmentVariableValues?.sources[varName] !== undefined) { + return fragmentVariableValues.coerced[varName]; } - return variableValues?.[varName]; + return variableValues?.coerced[varName]; } /** diff --git a/src/validation/rules/SingleFieldSubscriptionsRule.ts b/src/validation/rules/SingleFieldSubscriptionsRule.ts index dfefc6cdd8..b5cddef755 100644 --- a/src/validation/rules/SingleFieldSubscriptionsRule.ts +++ b/src/validation/rules/SingleFieldSubscriptionsRule.ts @@ -11,6 +11,7 @@ import type { FragmentDetails, } from '../../execution/collectFields.js'; import { collectFields } from '../../execution/collectFields.js'; +import type { VariableValues } from '../../execution/values.js'; import type { ValidationContext } from '../ValidationContext.js'; @@ -36,9 +37,7 @@ export function SingleFieldSubscriptionsRule( const subscriptionType = schema.getSubscriptionType(); if (subscriptionType) { const operationName = node.name ? node.name.value : null; - const variableValues: { - [variable: string]: any; - } = Object.create(null); + const variableValues: VariableValues = Object.create(null); const document = context.getDocument(); const fragments: ObjMap = Object.create(null); for (const definition of document.definitions) {