diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts index 17468b791f..faa273ee32 100644 --- a/src/execution/collectFields.ts +++ b/src/execution/collectFields.ts @@ -26,13 +26,17 @@ import { typeFromAST } from '../utilities/typeFromAST.js'; import { getDirectiveValues } from './values.js'; +export type FieldGroup = ReadonlyArray; + +export type GroupedFieldSet = Map; + export interface PatchFields { label: string | undefined; - fields: Map>; + fields: GroupedFieldSet; } export interface FieldsAndPatches { - fields: Map>; + fields: GroupedFieldSet; patches: Array; } @@ -85,7 +89,7 @@ export function collectSubfields( variableValues: { [variable: string]: unknown }, operation: OperationDefinitionNode, returnType: GraphQLObjectType, - fieldNodes: ReadonlyArray, + fieldGroup: FieldGroup, ): FieldsAndPatches { const subFieldNodes = new AccumulatorMap(); const visitedFragmentNames = new Set(); @@ -96,7 +100,7 @@ export function collectSubfields( patches: subPatches, }; - for (const node of fieldNodes) { + for (const node of fieldGroup) { if (node.selectionSet) { collectFieldsImpl( schema, diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 1bc6c4267b..2485ead567 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -19,7 +19,6 @@ import { locatedError } from '../error/locatedError.js'; import type { DocumentNode, - FieldNode, FragmentDefinitionNode, OperationDefinitionNode, } from '../language/ast.js'; @@ -48,6 +47,7 @@ import { GraphQLStreamDirective } from '../type/directives.js'; import type { GraphQLSchema } from '../type/schema.js'; import { assertValidSchema } from '../type/validate.js'; +import type { FieldGroup, GroupedFieldSet } from './collectFields.js'; import { collectFields, collectSubfields as _collectSubfields, @@ -72,7 +72,7 @@ const collectSubfields = memoize3( ( exeContext: ExecutionContext, returnType: GraphQLObjectType, - fieldNodes: ReadonlyArray, + fieldGroup: FieldGroup, ) => _collectSubfields( exeContext.schema, @@ -80,7 +80,7 @@ const collectSubfields = memoize3( exeContext.variableValues, exeContext.operation, returnType, - fieldNodes, + fieldGroup, ), ); @@ -536,7 +536,7 @@ function executeOperation( ); } - const { fields: rootFields, patches } = collectFields( + const { fields: groupedFieldSet, patches } = collectFields( schema, fragments, variableValues, @@ -548,7 +548,13 @@ function executeOperation( switch (operation.operation) { case OperationTypeNode.QUERY: - result = executeFields(exeContext, rootType, rootValue, path, rootFields); + result = executeFields( + exeContext, + rootType, + rootValue, + path, + groupedFieldSet, + ); break; case OperationTypeNode.MUTATION: result = executeFieldsSerially( @@ -556,13 +562,19 @@ function executeOperation( rootType, rootValue, path, - rootFields, + groupedFieldSet, ); break; case OperationTypeNode.SUBSCRIPTION: // TODO: deprecate `subscribe` and move all logic here // Temporary solution until we finish merging execute and subscribe together - result = executeFields(exeContext, rootType, rootValue, path, rootFields); + result = executeFields( + exeContext, + rootType, + rootValue, + path, + groupedFieldSet, + ); } for (const patch of patches) { @@ -589,17 +601,17 @@ function executeFieldsSerially( parentType: GraphQLObjectType, sourceValue: unknown, path: Path | undefined, - fields: Map>, + groupedFieldSet: GroupedFieldSet, ): PromiseOrValue> { return promiseReduce( - fields, - (results, [responseName, fieldNodes]) => { + groupedFieldSet, + (results, [responseName, fieldGroup]) => { const fieldPath = addPath(path, responseName, parentType.name); const result = executeField( exeContext, parentType, sourceValue, - fieldNodes, + fieldGroup, fieldPath, ); if (result === undefined) { @@ -627,20 +639,20 @@ function executeFields( parentType: GraphQLObjectType, sourceValue: unknown, path: Path | undefined, - fields: Map>, + groupedFieldSet: GroupedFieldSet, asyncPayloadRecord?: AsyncPayloadRecord, ): PromiseOrValue> { const results = Object.create(null); let containsPromise = false; try { - for (const [responseName, fieldNodes] of fields) { + for (const [responseName, fieldGroup] of groupedFieldSet) { const fieldPath = addPath(path, responseName, parentType.name); const result = executeField( exeContext, parentType, sourceValue, - fieldNodes, + fieldGroup, fieldPath, asyncPayloadRecord, ); @@ -683,12 +695,12 @@ function executeField( exeContext: ExecutionContext, parentType: GraphQLObjectType, source: unknown, - fieldNodes: ReadonlyArray, + fieldGroup: FieldGroup, path: Path, asyncPayloadRecord?: AsyncPayloadRecord, ): PromiseOrValue { const errors = asyncPayloadRecord?.errors ?? exeContext.errors; - const fieldName = fieldNodes[0].name.value; + const fieldName = fieldGroup[0].name.value; const fieldDef = exeContext.schema.getField(parentType, fieldName); if (!fieldDef) { return; @@ -700,7 +712,7 @@ function executeField( const info = buildResolveInfo( exeContext, fieldDef, - fieldNodes, + fieldGroup, parentType, path, ); @@ -712,7 +724,7 @@ function executeField( // TODO: find a way to memoize, in case this field is within a List type. const args = getArgumentValues( fieldDef, - fieldNodes[0], + fieldGroup[0], exeContext.variableValues, ); @@ -727,7 +739,7 @@ function executeField( return completePromisedValue( exeContext, returnType, - fieldNodes, + fieldGroup, info, path, result, @@ -738,7 +750,7 @@ function executeField( const completed = completeValue( exeContext, returnType, - fieldNodes, + fieldGroup, info, path, result, @@ -749,7 +761,7 @@ function executeField( // Note: we don't rely on a `catch` method, but we do expect "thenable" // to take a second callback for the error case. return completed.then(undefined, (rawError) => { - const error = locatedError(rawError, fieldNodes, pathToArray(path)); + const error = locatedError(rawError, fieldGroup, pathToArray(path)); const handledError = handleFieldError(error, returnType, errors); filterSubsequentPayloads(exeContext, path, asyncPayloadRecord); return handledError; @@ -757,7 +769,7 @@ function executeField( } return completed; } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(path)); + const error = locatedError(rawError, fieldGroup, pathToArray(path)); const handledError = handleFieldError(error, returnType, errors); filterSubsequentPayloads(exeContext, path, asyncPayloadRecord); return handledError; @@ -771,7 +783,7 @@ function executeField( export function buildResolveInfo( exeContext: ExecutionContext, fieldDef: GraphQLField, - fieldNodes: ReadonlyArray, + fieldGroup: FieldGroup, parentType: GraphQLObjectType, path: Path, ): GraphQLResolveInfo { @@ -779,7 +791,7 @@ export function buildResolveInfo( // information about the current execution state. return { fieldName: fieldDef.name, - fieldNodes, + fieldNodes: fieldGroup, returnType: fieldDef.type, parentType, path, @@ -832,7 +844,7 @@ function handleFieldError( function completeValue( exeContext: ExecutionContext, returnType: GraphQLOutputType, - fieldNodes: ReadonlyArray, + fieldGroup: FieldGroup, info: GraphQLResolveInfo, path: Path, result: unknown, @@ -849,7 +861,7 @@ function completeValue( const completed = completeValue( exeContext, returnType.ofType, - fieldNodes, + fieldGroup, info, path, result, @@ -873,7 +885,7 @@ function completeValue( return completeListValue( exeContext, returnType, - fieldNodes, + fieldGroup, info, path, result, @@ -893,7 +905,7 @@ function completeValue( return completeAbstractValue( exeContext, returnType, - fieldNodes, + fieldGroup, info, path, result, @@ -906,7 +918,7 @@ function completeValue( return completeObjectValue( exeContext, returnType, - fieldNodes, + fieldGroup, info, path, result, @@ -924,7 +936,7 @@ function completeValue( async function completePromisedValue( exeContext: ExecutionContext, returnType: GraphQLOutputType, - fieldNodes: ReadonlyArray, + fieldGroup: FieldGroup, info: GraphQLResolveInfo, path: Path, result: Promise, @@ -935,7 +947,7 @@ async function completePromisedValue( let completed = completeValue( exeContext, returnType, - fieldNodes, + fieldGroup, info, path, resolved, @@ -947,7 +959,7 @@ async function completePromisedValue( return completed; } catch (rawError) { const errors = asyncPayloadRecord?.errors ?? exeContext.errors; - const error = locatedError(rawError, fieldNodes, pathToArray(path)); + const error = locatedError(rawError, fieldGroup, pathToArray(path)); const handledError = handleFieldError(error, returnType, errors); filterSubsequentPayloads(exeContext, path, asyncPayloadRecord); return handledError; @@ -961,7 +973,7 @@ async function completePromisedValue( */ function getStreamValues( exeContext: ExecutionContext, - fieldNodes: ReadonlyArray, + fieldGroup: FieldGroup, path: Path, ): | undefined @@ -978,7 +990,7 @@ function getStreamValues( // safe to only check the first fieldNode for the stream directive const stream = getDirectiveValues( GraphQLStreamDirective, - fieldNodes[0], + fieldGroup[0], exeContext.variableValues, ); @@ -1018,14 +1030,14 @@ function getStreamValues( async function completeAsyncIteratorValue( exeContext: ExecutionContext, itemType: GraphQLOutputType, - fieldNodes: ReadonlyArray, + fieldGroup: FieldGroup, info: GraphQLResolveInfo, path: Path, iterator: AsyncIterator, asyncPayloadRecord?: AsyncPayloadRecord, ): Promise> { const errors = asyncPayloadRecord?.errors ?? exeContext.errors; - const stream = getStreamValues(exeContext, fieldNodes, path); + const stream = getStreamValues(exeContext, fieldGroup, path); let containsPromise = false; const completedResults: Array = []; let index = 0; @@ -1041,7 +1053,7 @@ async function completeAsyncIteratorValue( index, iterator, exeContext, - fieldNodes, + fieldGroup, info, itemType, path, @@ -1060,7 +1072,7 @@ async function completeAsyncIteratorValue( break; } } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); + const error = locatedError(rawError, fieldGroup, pathToArray(itemPath)); completedResults.push(handleFieldError(error, itemType, errors)); break; } @@ -1072,7 +1084,7 @@ async function completeAsyncIteratorValue( errors, exeContext, itemType, - fieldNodes, + fieldGroup, info, itemPath, asyncPayloadRecord, @@ -1092,7 +1104,7 @@ async function completeAsyncIteratorValue( function completeListValue( exeContext: ExecutionContext, returnType: GraphQLList, - fieldNodes: ReadonlyArray, + fieldGroup: FieldGroup, info: GraphQLResolveInfo, path: Path, result: unknown, @@ -1107,7 +1119,7 @@ function completeListValue( return completeAsyncIteratorValue( exeContext, itemType, - fieldNodes, + fieldGroup, info, path, iterator, @@ -1121,7 +1133,7 @@ function completeListValue( ); } - const stream = getStreamValues(exeContext, fieldNodes, path); + const stream = getStreamValues(exeContext, fieldGroup, path); // This is specified as a simple map, however we're optimizing the path // where the list contains no Promises by avoiding creating another Promise. @@ -1144,7 +1156,7 @@ function completeListValue( itemPath, item, exeContext, - fieldNodes, + fieldGroup, info, itemType, stream.label, @@ -1161,7 +1173,7 @@ function completeListValue( errors, exeContext, itemType, - fieldNodes, + fieldGroup, info, itemPath, asyncPayloadRecord, @@ -1187,7 +1199,7 @@ function completeListItemValue( errors: Array, exeContext: ExecutionContext, itemType: GraphQLOutputType, - fieldNodes: ReadonlyArray, + fieldGroup: FieldGroup, info: GraphQLResolveInfo, itemPath: Path, asyncPayloadRecord?: AsyncPayloadRecord, @@ -1197,7 +1209,7 @@ function completeListItemValue( completePromisedValue( exeContext, itemType, - fieldNodes, + fieldGroup, info, itemPath, item, @@ -1212,7 +1224,7 @@ function completeListItemValue( const completedItem = completeValue( exeContext, itemType, - fieldNodes, + fieldGroup, info, itemPath, item, @@ -1226,7 +1238,7 @@ function completeListItemValue( completedItem.then(undefined, (rawError) => { const error = locatedError( rawError, - fieldNodes, + fieldGroup, pathToArray(itemPath), ); const handledError = handleFieldError(error, itemType, errors); @@ -1240,7 +1252,7 @@ function completeListItemValue( completedResults.push(completedItem); } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); + const error = locatedError(rawError, fieldGroup, pathToArray(itemPath)); const handledError = handleFieldError(error, itemType, errors); filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord); completedResults.push(handledError); @@ -1274,7 +1286,7 @@ function completeLeafValue( function completeAbstractValue( exeContext: ExecutionContext, returnType: GraphQLAbstractType, - fieldNodes: ReadonlyArray, + fieldGroup: FieldGroup, info: GraphQLResolveInfo, path: Path, result: unknown, @@ -1292,11 +1304,11 @@ function completeAbstractValue( resolvedRuntimeType, exeContext, returnType, - fieldNodes, + fieldGroup, info, result, ), - fieldNodes, + fieldGroup, info, path, result, @@ -1311,11 +1323,11 @@ function completeAbstractValue( runtimeType, exeContext, returnType, - fieldNodes, + fieldGroup, info, result, ), - fieldNodes, + fieldGroup, info, path, result, @@ -1327,14 +1339,14 @@ function ensureValidRuntimeType( runtimeTypeName: unknown, exeContext: ExecutionContext, returnType: GraphQLAbstractType, - fieldNodes: ReadonlyArray, + fieldGroup: FieldGroup, info: GraphQLResolveInfo, result: unknown, ): GraphQLObjectType { if (runtimeTypeName == null) { throw new GraphQLError( `Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}". Either the "${returnType.name}" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`, - { nodes: fieldNodes }, + { nodes: fieldGroup }, ); } @@ -1357,21 +1369,21 @@ function ensureValidRuntimeType( if (runtimeType == null) { throw new GraphQLError( `Abstract type "${returnType.name}" was resolved to a type "${runtimeTypeName}" that does not exist inside the schema.`, - { nodes: fieldNodes }, + { nodes: fieldGroup }, ); } if (!isObjectType(runtimeType)) { throw new GraphQLError( `Abstract type "${returnType.name}" was resolved to a non-object type "${runtimeTypeName}".`, - { nodes: fieldNodes }, + { nodes: fieldGroup }, ); } if (!exeContext.schema.isSubType(returnType, runtimeType)) { throw new GraphQLError( `Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`, - { nodes: fieldNodes }, + { nodes: fieldGroup }, ); } @@ -1384,7 +1396,7 @@ function ensureValidRuntimeType( function completeObjectValue( exeContext: ExecutionContext, returnType: GraphQLObjectType, - fieldNodes: ReadonlyArray, + fieldGroup: FieldGroup, info: GraphQLResolveInfo, path: Path, result: unknown, @@ -1399,12 +1411,12 @@ function completeObjectValue( if (isPromise(isTypeOf)) { return isTypeOf.then((resolvedIsTypeOf) => { if (!resolvedIsTypeOf) { - throw invalidReturnTypeError(returnType, result, fieldNodes); + throw invalidReturnTypeError(returnType, result, fieldGroup); } return collectAndExecuteSubfields( exeContext, returnType, - fieldNodes, + fieldGroup, path, result, asyncPayloadRecord, @@ -1413,14 +1425,14 @@ function completeObjectValue( } if (!isTypeOf) { - throw invalidReturnTypeError(returnType, result, fieldNodes); + throw invalidReturnTypeError(returnType, result, fieldGroup); } } return collectAndExecuteSubfields( exeContext, returnType, - fieldNodes, + fieldGroup, path, result, asyncPayloadRecord, @@ -1430,18 +1442,18 @@ function completeObjectValue( function invalidReturnTypeError( returnType: GraphQLObjectType, result: unknown, - fieldNodes: ReadonlyArray, + fieldGroup: FieldGroup, ): GraphQLError { return new GraphQLError( `Expected value of type "${returnType.name}" but got: ${inspect(result)}.`, - { nodes: fieldNodes }, + { nodes: fieldGroup }, ); } function collectAndExecuteSubfields( exeContext: ExecutionContext, returnType: GraphQLObjectType, - fieldNodes: ReadonlyArray, + fieldGroup: FieldGroup, path: Path, result: unknown, asyncPayloadRecord?: AsyncPayloadRecord, @@ -1450,7 +1462,7 @@ function collectAndExecuteSubfields( const { fields: subFieldNodes, patches: subPatches } = collectSubfields( exeContext, returnType, - fieldNodes, + fieldGroup, ); const subFields = executeFields( @@ -1691,7 +1703,7 @@ function executeSubscription( ); } - const { fields: rootFields } = collectFields( + const { fields: groupedFieldSet } = collectFields( schema, fragments, variableValues, @@ -1699,15 +1711,15 @@ function executeSubscription( operation, ); - const firstRootField = rootFields.entries().next().value; - const [responseName, fieldNodes] = firstRootField; - const fieldName = fieldNodes[0].name.value; + const firstRootField = groupedFieldSet.entries().next().value; + const [responseName, fieldGroup] = firstRootField; + const fieldName = fieldGroup[0].name.value; const fieldDef = schema.getField(rootType, fieldName); if (!fieldDef) { throw new GraphQLError( `The subscription field "${fieldName}" is not defined.`, - { nodes: fieldNodes }, + { nodes: fieldGroup }, ); } @@ -1715,7 +1727,7 @@ function executeSubscription( const info = buildResolveInfo( exeContext, fieldDef, - fieldNodes, + fieldGroup, rootType, path, ); @@ -1726,7 +1738,7 @@ function executeSubscription( // Build a JS object of arguments from the field.arguments AST, using the // variables scope to fulfill any variable references. - const args = getArgumentValues(fieldDef, fieldNodes[0], variableValues); + const args = getArgumentValues(fieldDef, fieldGroup[0], variableValues); // The resolve function's optional third argument is a context value that // is provided to every resolve function within an execution. It is commonly @@ -1740,13 +1752,13 @@ function executeSubscription( if (isPromise(result)) { return result.then(assertEventStream).then(undefined, (error) => { - throw locatedError(error, fieldNodes, pathToArray(path)); + throw locatedError(error, fieldGroup, pathToArray(path)); }); } return assertEventStream(result); } catch (error) { - throw locatedError(error, fieldNodes, pathToArray(path)); + throw locatedError(error, fieldGroup, pathToArray(path)); } } @@ -1770,7 +1782,7 @@ function executeDeferredFragment( exeContext: ExecutionContext, parentType: GraphQLObjectType, sourceValue: unknown, - fields: Map>, + groupedFieldSet: GroupedFieldSet, label?: string, path?: Path, parentContext?: AsyncPayloadRecord, @@ -1788,7 +1800,7 @@ function executeDeferredFragment( parentType, sourceValue, path, - fields, + groupedFieldSet, asyncPayloadRecord, ); @@ -1810,7 +1822,7 @@ function executeStreamField( itemPath: Path, item: PromiseOrValue, exeContext: ExecutionContext, - fieldNodes: ReadonlyArray, + fieldGroup: FieldGroup, info: GraphQLResolveInfo, itemType: GraphQLOutputType, label?: string, @@ -1826,7 +1838,7 @@ function executeStreamField( const completedItems = completePromisedValue( exeContext, itemType, - fieldNodes, + fieldGroup, info, itemPath, item, @@ -1850,14 +1862,14 @@ function executeStreamField( completedItem = completeValue( exeContext, itemType, - fieldNodes, + fieldGroup, info, itemPath, item, asyncPayloadRecord, ); } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); + const error = locatedError(rawError, fieldGroup, pathToArray(itemPath)); completedItem = handleFieldError( error, itemType, @@ -1875,7 +1887,7 @@ function executeStreamField( if (isPromise(completedItem)) { const completedItems = completedItem .then(undefined, (rawError) => { - const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); + const error = locatedError(rawError, fieldGroup, pathToArray(itemPath)); const handledError = handleFieldError( error, itemType, @@ -1904,7 +1916,7 @@ function executeStreamField( async function executeStreamIteratorItem( iterator: AsyncIterator, exeContext: ExecutionContext, - fieldNodes: ReadonlyArray, + fieldGroup: FieldGroup, info: GraphQLResolveInfo, itemType: GraphQLOutputType, asyncPayloadRecord: StreamRecord, @@ -1919,7 +1931,7 @@ async function executeStreamIteratorItem( } item = value; } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); + const error = locatedError(rawError, fieldGroup, pathToArray(itemPath)); const value = handleFieldError(error, itemType, asyncPayloadRecord.errors); // don't continue if iterator throws return { done: true, value }; @@ -1929,7 +1941,7 @@ async function executeStreamIteratorItem( completedItem = completeValue( exeContext, itemType, - fieldNodes, + fieldGroup, info, itemPath, item, @@ -1938,7 +1950,7 @@ async function executeStreamIteratorItem( if (isPromise(completedItem)) { completedItem = completedItem.then(undefined, (rawError) => { - const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); + const error = locatedError(rawError, fieldGroup, pathToArray(itemPath)); const handledError = handleFieldError( error, itemType, @@ -1950,7 +1962,7 @@ async function executeStreamIteratorItem( } return { done: false, value: completedItem }; } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); + const error = locatedError(rawError, fieldGroup, pathToArray(itemPath)); const value = handleFieldError(error, itemType, asyncPayloadRecord.errors); filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord); return { done: false, value }; @@ -1961,7 +1973,7 @@ async function executeStreamIterator( initialIndex: number, iterator: AsyncIterator, exeContext: ExecutionContext, - fieldNodes: ReadonlyArray, + fieldGroup: FieldGroup, info: GraphQLResolveInfo, itemType: GraphQLOutputType, path: Path, @@ -1987,7 +1999,7 @@ async function executeStreamIterator( iteration = await executeStreamIteratorItem( iterator, exeContext, - fieldNodes, + fieldGroup, info, itemType, asyncPayloadRecord, diff --git a/src/validation/rules/SingleFieldSubscriptionsRule.ts b/src/validation/rules/SingleFieldSubscriptionsRule.ts index 4a3d834124..bc68586e01 100644 --- a/src/validation/rules/SingleFieldSubscriptionsRule.ts +++ b/src/validation/rules/SingleFieldSubscriptionsRule.ts @@ -49,15 +49,15 @@ export function SingleFieldSubscriptionsRule( node, ); if (fields.size > 1) { - const fieldSelectionLists = [...fields.values()]; - const extraFieldSelectionLists = fieldSelectionLists.slice(1); - const extraFieldSelections = extraFieldSelectionLists.flat(); + const fieldGroups = [...fields.values()]; + const extraFieldGroups = fieldGroups.slice(1); + const extraFields = extraFieldGroups.flat(); context.reportError( new GraphQLError( operationName != null ? `Subscription "${operationName}" must select only one top level field.` : 'Anonymous Subscription must select only one top level field.', - { nodes: extraFieldSelections }, + { nodes: extraFields }, ), ); }