diff --git a/src/execution/buildFieldPlan.ts b/src/execution/buildFieldPlan.ts index 970b8d5c46..d29ae94cde 100644 --- a/src/execution/buildFieldPlan.ts +++ b/src/execution/buildFieldPlan.ts @@ -1,55 +1,39 @@ import { getBySet } from '../jsutils/getBySet.js'; import { isSameSet } from '../jsutils/isSameSet.js'; -import type { DeferUsage, FieldDetails } from './collectFields.js'; +import type { + DeferUsage, + FieldGroup, + GroupedFieldSet, +} from './collectFields.js'; export type DeferUsageSet = ReadonlySet; -export interface FieldGroup { - fields: ReadonlyArray; - deferUsages?: DeferUsageSet | undefined; +export interface FieldPlan { + groupedFieldSet: GroupedFieldSet; + newGroupedFieldSets: Map; } -export type GroupedFieldSet = Map; - export function buildFieldPlan( - fields: Map>, + originalGroupedFieldSet: GroupedFieldSet, parentDeferUsages: DeferUsageSet = new Set(), -): { - groupedFieldSet: GroupedFieldSet; - newGroupedFieldSets: Map; -} { - const groupedFieldSet = new Map< - string, - { - fields: Array; - deferUsages: DeferUsageSet; - } - >(); +): FieldPlan { + const groupedFieldSet = new Map(); - const newGroupedFieldSets = new Map< - DeferUsageSet, - Map< - string, - { - fields: Array; - deferUsages: DeferUsageSet; - } - > - >(); + const newGroupedFieldSets = new Map>(); const map = new Map< string, { deferUsageSet: DeferUsageSet; - fieldDetailsList: ReadonlyArray; + fieldGroup: FieldGroup; } >(); - for (const [responseKey, fieldDetailsList] of fields) { + for (const [responseKey, fieldGroup] of originalGroupedFieldSet) { const deferUsageSet = new Set(); let inOriginalResult = false; - for (const fieldDetails of fieldDetailsList) { + for (const fieldDetails of fieldGroup) { const deferUsage = fieldDetails.deferUsage; if (deferUsage === undefined) { inOriginalResult = true; @@ -69,44 +53,21 @@ export function buildFieldPlan( } }); } - map.set(responseKey, { deferUsageSet, fieldDetailsList }); + map.set(responseKey, { deferUsageSet, fieldGroup }); } - for (const [responseKey, { deferUsageSet, fieldDetailsList }] of map) { + for (const [responseKey, { deferUsageSet, fieldGroup }] of map) { if (isSameSet(deferUsageSet, parentDeferUsages)) { - let fieldGroup = groupedFieldSet.get(responseKey); - if (fieldGroup === undefined) { - fieldGroup = { - fields: [], - deferUsages: deferUsageSet, - }; - groupedFieldSet.set(responseKey, fieldGroup); - } - fieldGroup.fields.push(...fieldDetailsList); + groupedFieldSet.set(responseKey, fieldGroup); continue; } let newGroupedFieldSet = getBySet(newGroupedFieldSets, deferUsageSet); if (newGroupedFieldSet === undefined) { - newGroupedFieldSet = new Map< - string, - { - fields: Array; - deferUsages: DeferUsageSet; - knownDeferUsages: DeferUsageSet; - } - >(); + newGroupedFieldSet = new Map(); newGroupedFieldSets.set(deferUsageSet, newGroupedFieldSet); } - let fieldGroup = newGroupedFieldSet.get(responseKey); - if (fieldGroup === undefined) { - fieldGroup = { - fields: [], - deferUsages: deferUsageSet, - }; - newGroupedFieldSet.set(responseKey, fieldGroup); - } - fieldGroup.fields.push(...fieldDetailsList); + newGroupedFieldSet.set(responseKey, fieldGroup); } return { diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts index 03ba5efde6..d411ff3f77 100644 --- a/src/execution/collectFields.ts +++ b/src/execution/collectFields.ts @@ -36,6 +36,10 @@ export interface FieldDetails { deferUsage: DeferUsage | undefined; } +export type FieldGroup = ReadonlyArray; + +export type GroupedFieldSet = ReadonlyMap; + interface CollectFieldsContext { schema: GraphQLSchema; fragments: ObjMap; @@ -61,7 +65,7 @@ export function collectFields( runtimeType: GraphQLObjectType, operation: OperationDefinitionNode, ): { - fields: Map>; + groupedFieldSet: GroupedFieldSet; newDeferUsages: ReadonlyArray; } { const groupedFieldSet = new AccumulatorMap(); @@ -81,7 +85,7 @@ export function collectFields( groupedFieldSet, newDeferUsages, ); - return { fields: groupedFieldSet, newDeferUsages }; + return { groupedFieldSet, newDeferUsages }; } /** @@ -101,9 +105,9 @@ export function collectSubfields( variableValues: { [variable: string]: unknown }, operation: OperationDefinitionNode, returnType: GraphQLObjectType, - fieldDetails: ReadonlyArray, + fieldGroup: FieldGroup, ): { - fields: Map>; + groupedFieldSet: GroupedFieldSet; newDeferUsages: ReadonlyArray; } { const context: CollectFieldsContext = { @@ -117,7 +121,7 @@ export function collectSubfields( const subGroupedFieldSet = new AccumulatorMap(); const newDeferUsages: Array = []; - for (const fieldDetail of fieldDetails) { + for (const fieldDetail of fieldGroup) { const node = fieldDetail.node; if (node.selectionSet) { collectFieldsImpl( @@ -131,7 +135,7 @@ export function collectSubfields( } return { - fields: subGroupedFieldSet, + groupedFieldSet: subGroupedFieldSet, newDeferUsages, }; } diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 68037516e1..23bfa50702 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -47,14 +47,17 @@ import { GraphQLStreamDirective } from '../type/directives.js'; import type { GraphQLSchema } from '../type/schema.js'; import { assertValidSchema } from '../type/validate.js'; +import type { DeferUsageSet, FieldPlan } from './buildFieldPlan.js'; +import { buildFieldPlan } from './buildFieldPlan.js'; import type { - DeferUsageSet, + DeferUsage, FieldGroup, GroupedFieldSet, -} from './buildFieldPlan.js'; -import { buildFieldPlan } from './buildFieldPlan.js'; -import type { DeferUsage, FieldDetails } from './collectFields.js'; -import { collectFields, collectSubfields } from './collectFields.js'; +} from './collectFields.js'; +import { + collectFields, + collectSubfields as _collectSubfields, +} from './collectFields.js'; import type { CancellableStreamRecord, DeferredGroupedFieldSetRecord, @@ -83,29 +86,24 @@ import { // so just disable it for entire file. /** - * A memoized function for building subfield plans with regard to the return - * type. Memoizing ensures the subfield plans are not repeatedly calculated, which + * A memoized collection of relevant subfields with regard to the return + * type. Memoizing ensures the subfields are not repeatedly calculated, which * saves overhead when resolving lists of values. */ -const buildSubFieldPlan = memoize3( +const collectSubfields = memoize3( ( exeContext: ExecutionContext, returnType: GraphQLObjectType, fieldGroup: FieldGroup, - ) => { - const { fields: subFields, newDeferUsages } = collectSubfields( + ) => + _collectSubfields( exeContext.schema, exeContext.fragments, exeContext.variableValues, exeContext.operation, returnType, - fieldGroup.fields, - ); - return { - ...buildFieldPlan(subFields, fieldGroup.deferUsages), - newDeferUsages, - }; - }, + fieldGroup, + ), ); /** @@ -144,9 +142,15 @@ export interface ExecutionContext { fieldResolver: GraphQLFieldResolver; typeResolver: GraphQLTypeResolver; subscribeFieldResolver: GraphQLFieldResolver; + errors: Array; cancellableStreams: Set; } +interface IncrementalContext { + errors: Array; + deferUsageSet?: DeferUsageSet | undefined; +} + export interface ExecutionArgs { schema: GraphQLSchema; document: DocumentNode; @@ -259,7 +263,6 @@ export function experimentalExecuteIncrementally( function executeOperation( exeContext: ExecutionContext, ): PromiseOrValue { - const errors: Array = []; try { const { operation, schema, fragments, variableValues, rootValue } = exeContext; @@ -271,59 +274,76 @@ function executeOperation( ); } - const { fields, newDeferUsages } = collectFields( + const collectedFields = collectFields( schema, fragments, variableValues, rootType, operation, ); - const { groupedFieldSet, newGroupedFieldSets } = buildFieldPlan(fields); - - const newDeferMap = addNewDeferredFragments(newDeferUsages, new Map()); - - let graphqlWrappedResult = executeRootGroupedFieldSet( - exeContext, - operation.operation, - rootType, - rootValue, - groupedFieldSet, - errors, - newDeferMap, - ); + let groupedFieldSet = collectedFields.groupedFieldSet; + const newDeferUsages = collectedFields.newDeferUsages; + let graphqlWrappedResult: PromiseOrValue< + GraphQLWrappedResult> + >; + if (newDeferUsages.length === 0) { + graphqlWrappedResult = executeRootGroupedFieldSet( + exeContext, + operation.operation, + rootType, + rootValue, + groupedFieldSet, + undefined, + ); + } else { + const fieldPLan = buildFieldPlan(groupedFieldSet); + groupedFieldSet = fieldPLan.groupedFieldSet; + const newGroupedFieldSets = fieldPLan.newGroupedFieldSets; + const newDeferMap = addNewDeferredFragments(newDeferUsages, new Map()); - const newDeferredGroupedFieldSetRecords = executeDeferredGroupedFieldSets( - exeContext, - rootType, - rootValue, - undefined, - undefined, - newGroupedFieldSets, - newDeferMap, - ); + graphqlWrappedResult = executeRootGroupedFieldSet( + exeContext, + operation.operation, + rootType, + rootValue, + groupedFieldSet, + newDeferMap, + ); - graphqlWrappedResult = withNewDeferredGroupedFieldSets( - graphqlWrappedResult, - newDeferredGroupedFieldSetRecords, - ); + if (newGroupedFieldSets.size > 0) { + const newDeferredGroupedFieldSetRecords = + executeDeferredGroupedFieldSets( + exeContext, + rootType, + rootValue, + undefined, + undefined, + newGroupedFieldSets, + newDeferMap, + ); + + graphqlWrappedResult = withNewDeferredGroupedFieldSets( + graphqlWrappedResult, + newDeferredGroupedFieldSetRecords, + ); + } + } if (isPromise(graphqlWrappedResult)) { return graphqlWrappedResult.then( - (resolved) => - buildDataResponse(exeContext, resolved[0], errors, resolved[1]), + (resolved) => buildDataResponse(exeContext, resolved[0], resolved[1]), (error) => ({ data: null, - errors: withError(errors, error), + errors: withError(exeContext.errors, error), }), ); } return buildDataResponse( exeContext, graphqlWrappedResult[0], - errors, graphqlWrappedResult[1], ); } catch (error) { - return { data: null, errors: withError(errors, error) }; + return { data: null, errors: withError(exeContext.errors, error) }; } } @@ -352,9 +372,9 @@ function withError( function buildDataResponse( exeContext: ExecutionContext, data: ObjMap, - errors: ReadonlyArray, incrementalDataRecords: ReadonlyArray, ): ExecutionResult | ExperimentalIncrementalExecutionResults { + const errors = exeContext.errors; if (incrementalDataRecords.length === 0) { return errors.length > 0 ? { errors, data } : { data }; } @@ -468,6 +488,7 @@ export function buildExecutionContext( fieldResolver: fieldResolver ?? defaultFieldResolver, typeResolver: typeResolver ?? defaultTypeResolver, subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, + errors: [], cancellableStreams: new Set(), }; } @@ -478,6 +499,7 @@ function buildPerEventExecutionContext( ): ExecutionContext { return { ...exeContext, + errors: [], rootValue: payload, }; } @@ -488,7 +510,6 @@ function executeRootGroupedFieldSet( rootType: GraphQLObjectType, rootValue: unknown, groupedFieldSet: GroupedFieldSet, - errors: Array, deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { switch (operation) { @@ -499,7 +520,7 @@ function executeRootGroupedFieldSet( rootValue, undefined, groupedFieldSet, - errors, + undefined, deferMap, ); case OperationTypeNode.MUTATION: @@ -509,7 +530,7 @@ function executeRootGroupedFieldSet( rootValue, undefined, groupedFieldSet, - errors, + undefined, deferMap, ); case OperationTypeNode.SUBSCRIPTION: @@ -521,7 +542,7 @@ function executeRootGroupedFieldSet( rootValue, undefined, groupedFieldSet, - errors, + undefined, deferMap, ); } @@ -537,7 +558,7 @@ function executeFieldsSerially( sourceValue: unknown, path: Path | undefined, groupedFieldSet: GroupedFieldSet, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { return promiseReduce( @@ -550,7 +571,7 @@ function executeFieldsSerially( sourceValue, fieldGroup, fieldPath, - errors, + incrementalContext, deferMap, ); if (result === undefined) { @@ -581,7 +602,7 @@ function executeFields( sourceValue: unknown, path: Path | undefined, groupedFieldSet: GroupedFieldSet, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { const results = Object.create(null); @@ -600,7 +621,7 @@ function executeFields( sourceValue, fieldGroup, fieldPath, - errors, + incrementalContext, deferMap, ); @@ -644,7 +665,7 @@ function executeFields( } function toNodes(fieldGroup: FieldGroup): ReadonlyArray { - return fieldGroup.fields.map((fieldDetails) => fieldDetails.node); + return fieldGroup.map((fieldDetails) => fieldDetails.node); } /** @@ -659,10 +680,10 @@ function executeField( source: unknown, fieldGroup: FieldGroup, path: Path, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): PromiseOrValue> | undefined { - const fieldName = fieldGroup.fields[0].node.name.value; + const fieldName = fieldGroup[0].node.name.value; const fieldDef = exeContext.schema.getField(parentType, fieldName); if (!fieldDef) { return; @@ -686,7 +707,7 @@ function executeField( // TODO: find a way to memoize, in case this field is within a List type. const args = getArgumentValues( fieldDef, - fieldGroup.fields[0].node, + fieldGroup[0].node, exeContext.variableValues, ); @@ -705,7 +726,7 @@ function executeField( info, path, result, - errors, + incrementalContext, deferMap, ); } @@ -717,7 +738,7 @@ function executeField( info, path, result, - errors, + incrementalContext, deferMap, ); @@ -725,12 +746,14 @@ 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 errors = (incrementalContext ?? exeContext).errors; handleFieldError(rawError, returnType, fieldGroup, path, errors); return [null, []]; }); } return completed; } catch (rawError) { + const errors = (incrementalContext ?? exeContext).errors; handleFieldError(rawError, returnType, fieldGroup, path, errors); return [null, []]; } @@ -811,7 +834,7 @@ function completeValue( info: GraphQLResolveInfo, path: Path, result: unknown, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): PromiseOrValue> { // If result is an Error, throw a located error. @@ -829,7 +852,7 @@ function completeValue( info, path, result, - errors, + incrementalContext, deferMap, ); if ((completed as GraphQLWrappedResult)[0] === null) { @@ -854,7 +877,7 @@ function completeValue( info, path, result, - errors, + incrementalContext, deferMap, ); } @@ -875,7 +898,7 @@ function completeValue( info, path, result, - errors, + incrementalContext, deferMap, ); } @@ -889,7 +912,7 @@ function completeValue( info, path, result, - errors, + incrementalContext, deferMap, ); } @@ -908,7 +931,7 @@ async function completePromisedValue( info: GraphQLResolveInfo, path: Path, result: Promise, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): Promise> { try { @@ -920,7 +943,7 @@ async function completePromisedValue( info, path, resolved, - errors, + incrementalContext, deferMap, ); @@ -929,6 +952,7 @@ async function completePromisedValue( } return completed; } catch (rawError) { + const errors = (incrementalContext ?? exeContext).errors; handleFieldError(rawError, returnType, fieldGroup, path, errors); return [null, []]; } @@ -963,7 +987,7 @@ function getStreamUsage( // safe to only check the first fieldNode for the stream directive const stream = getDirectiveValues( GraphQLStreamDirective, - fieldGroup.fields[0].node, + fieldGroup[0].node, exeContext.variableValues, ); @@ -990,12 +1014,10 @@ function getStreamUsage( '`@stream` directive not supported on subscription operations. Disable `@stream` by setting the `if` argument to `false`.', ); - const streamedFieldGroup: FieldGroup = { - fields: fieldGroup.fields.map((fieldDetails) => ({ - node: fieldDetails.node, - deferUsage: undefined, - })), - }; + const streamedFieldGroup: FieldGroup = fieldGroup.map((fieldDetails) => ({ + node: fieldDetails.node, + deferUsage: undefined, + })); const streamUsage = { initialCount: stream.initialCount, @@ -1020,7 +1042,7 @@ async function completeAsyncIteratorValue( info: GraphQLResolveInfo, path: Path, asyncIterator: AsyncIterator, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): Promise>> { let containsPromise = false; @@ -1092,7 +1114,7 @@ async function completeAsyncIteratorValue( info, itemPath, item, - errors, + incrementalContext, deferMap, ).then((resolved) => { graphqlWrappedResult[1].push(...resolved[1]); @@ -1111,7 +1133,7 @@ async function completeAsyncIteratorValue( fieldGroup, info, itemPath, - errors, + incrementalContext, deferMap, ) // TODO: add tests for stream backed by asyncIterator that completes to a promise @@ -1142,7 +1164,7 @@ function completeListValue( info: GraphQLResolveInfo, path: Path, result: unknown, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { const itemType = returnType.ofType; @@ -1157,7 +1179,7 @@ function completeListValue( info, path, asyncIterator, - errors, + incrementalContext, deferMap, ); } @@ -1217,7 +1239,7 @@ function completeListValue( info, itemPath, item, - errors, + incrementalContext, deferMap, ).then((resolved) => { graphqlWrappedResult[1].push(...resolved[1]); @@ -1235,7 +1257,7 @@ function completeListValue( fieldGroup, info, itemPath, - errors, + incrementalContext, deferMap, ) ) { @@ -1268,7 +1290,7 @@ function completeListItemValue( fieldGroup: FieldGroup, info: GraphQLResolveInfo, itemPath: Path, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): boolean { try { @@ -1279,7 +1301,7 @@ function completeListItemValue( info, itemPath, item, - errors, + incrementalContext, deferMap, ); @@ -1293,6 +1315,7 @@ function completeListItemValue( return resolved[0]; }, (rawError) => { + const errors = (incrementalContext ?? exeContext).errors; handleFieldError(rawError, itemType, fieldGroup, itemPath, errors); return null; }, @@ -1304,6 +1327,7 @@ function completeListItemValue( completedResults.push(completedItem[0]); parent[1].push(...completedItem[1]); } catch (rawError) { + const errors = (incrementalContext ?? exeContext).errors; handleFieldError(rawError, itemType, fieldGroup, itemPath, errors); completedResults.push(null); } @@ -1339,7 +1363,7 @@ function completeAbstractValue( info: GraphQLResolveInfo, path: Path, result: unknown, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { const resolveTypeFn = returnType.resolveType ?? exeContext.typeResolver; @@ -1362,7 +1386,7 @@ function completeAbstractValue( info, path, result, - errors, + incrementalContext, deferMap, ), ); @@ -1382,7 +1406,7 @@ function completeAbstractValue( info, path, result, - errors, + incrementalContext, deferMap, ); } @@ -1452,7 +1476,7 @@ function completeObjectValue( info: GraphQLResolveInfo, path: Path, result: unknown, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { // If there is an isTypeOf predicate function, call it with the @@ -1472,7 +1496,7 @@ function completeObjectValue( fieldGroup, path, result, - errors, + incrementalContext, deferMap, ); }); @@ -1489,7 +1513,7 @@ function completeObjectValue( fieldGroup, path, result, - errors, + incrementalContext, deferMap, ); } @@ -1564,13 +1588,35 @@ function collectAndExecuteSubfields( fieldGroup: FieldGroup, path: Path, result: unknown, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { // Collect sub-fields to execute to complete this value. - const { groupedFieldSet, newGroupedFieldSets, newDeferUsages } = - buildSubFieldPlan(exeContext, returnType, fieldGroup); + const collectedSubfields = collectSubfields( + exeContext, + returnType, + fieldGroup, + ); + let groupedFieldSet = collectedSubfields.groupedFieldSet; + const newDeferUsages = collectedSubfields.newDeferUsages; + if (deferMap === undefined && newDeferUsages.length === 0) { + return executeFields( + exeContext, + returnType, + result, + path, + groupedFieldSet, + incrementalContext, + undefined, + ); + } + const subFieldPlan = buildSubFieldPlan( + groupedFieldSet, + incrementalContext?.deferUsageSet, + ); + groupedFieldSet = subFieldPlan.groupedFieldSet; + const newGroupedFieldSets = subFieldPlan.newGroupedFieldSets; const newDeferMap = addNewDeferredFragments( newDeferUsages, new Map(deferMap), @@ -1583,24 +1629,43 @@ function collectAndExecuteSubfields( result, path, groupedFieldSet, - errors, + incrementalContext, newDeferMap, ); - const newDeferredGroupedFieldSetRecords = executeDeferredGroupedFieldSets( - exeContext, - returnType, - result, - path, - fieldGroup.deferUsages, - newGroupedFieldSets, - newDeferMap, - ); + if (newGroupedFieldSets.size > 0) { + const newDeferredGroupedFieldSetRecords = executeDeferredGroupedFieldSets( + exeContext, + returnType, + result, + path, + incrementalContext?.deferUsageSet, + newGroupedFieldSets, + newDeferMap, + ); - return withNewDeferredGroupedFieldSets( - subFields, - newDeferredGroupedFieldSetRecords, - ); + return withNewDeferredGroupedFieldSets( + subFields, + newDeferredGroupedFieldSetRecords, + ); + } + return subFields; +} + +function buildSubFieldPlan( + originalGroupedFieldSet: GroupedFieldSet, + deferUsageSet: DeferUsageSet | undefined, +): FieldPlan { + let fieldPlan = ( + originalGroupedFieldSet as unknown as { _fieldPlan: FieldPlan } + )._fieldPlan; + if (fieldPlan !== undefined) { + return fieldPlan; + } + fieldPlan = buildFieldPlan(originalGroupedFieldSet, deferUsageSet); + (originalGroupedFieldSet as unknown as { _fieldPlan: FieldPlan })._fieldPlan = + fieldPlan; + return fieldPlan; } /** @@ -1816,7 +1881,7 @@ function executeSubscription( ); } - const { fields } = collectFields( + const { groupedFieldSet } = collectFields( schema, fragments, variableValues, @@ -1824,15 +1889,15 @@ function executeSubscription( operation, ); - const firstRootField = fields.entries().next().value as [ + const firstRootField = groupedFieldSet.entries().next().value as [ string, - ReadonlyArray, + FieldGroup, ]; - const [responseName, fieldDetailsList] = firstRootField; - const fieldName = fieldDetailsList[0].node.name.value; + const [responseName, fieldGroup] = firstRootField; + const fieldName = fieldGroup[0].node.name.value; const fieldDef = schema.getField(rootType, fieldName); - const fieldNodes = fieldDetailsList.map((fieldDetails) => fieldDetails.node); + const fieldNodes = fieldGroup.map((fieldDetails) => fieldDetails.node); if (!fieldDef) { throw new GraphQLError( `The subscription field "${fieldName}" is not defined.`, @@ -1921,7 +1986,10 @@ function executeDeferredGroupedFieldSets( sourceValue, path, groupedFieldSet, - [], + { + errors: [], + deferUsageSet, + }, deferMap, ); @@ -1961,7 +2029,7 @@ function executeDeferredGroupedFieldSet( sourceValue: unknown, path: Path | undefined, groupedFieldSet: GroupedFieldSet, - errors: Array, + incrementalContext: IncrementalContext, deferMap: ReadonlyMap, ): PromiseOrValue { let result; @@ -1972,14 +2040,14 @@ function executeDeferredGroupedFieldSet( sourceValue, path, groupedFieldSet, - errors, + incrementalContext, deferMap, ); } catch (error) { return { deferredFragmentRecords, path: pathToArray(path), - errors: withError(errors, error), + errors: withError(incrementalContext.errors, error), }; } @@ -1987,7 +2055,7 @@ function executeDeferredGroupedFieldSet( return result.then( (resolved) => buildDeferredGroupedFieldSetResult( - errors, + incrementalContext.errors, deferredFragmentRecords, path, resolved, @@ -1995,13 +2063,13 @@ function executeDeferredGroupedFieldSet( (error) => ({ deferredFragmentRecords, path: pathToArray(path), - errors: withError(errors, error), + errors: withError(incrementalContext.errors, error), }), ); } return buildDeferredGroupedFieldSetResult( - errors, + incrementalContext.errors, deferredFragmentRecords, path, result, @@ -2053,7 +2121,7 @@ function firstSyncStreamItems( initialPath, initialItem, exeContext, - [], + { errors: [] }, fieldGroup, info, itemType, @@ -2075,7 +2143,7 @@ function firstSyncStreamItems( currentPath, item, exeContext, - [], + { errors: [] }, fieldGroup, info, itemType, @@ -2191,7 +2259,7 @@ async function getNextAsyncStreamItemsResult( itemPath, iteration.value, exeContext, - [], + { errors: [] }, fieldGroup, info, itemType, @@ -2219,7 +2287,7 @@ function completeStreamItems( itemPath: Path, item: unknown, exeContext: ExecutionContext, - errors: Array, + incrementalContext: IncrementalContext, fieldGroup: FieldGroup, info: GraphQLResolveInfo, itemType: GraphQLOutputType, @@ -2232,14 +2300,18 @@ function completeStreamItems( info, itemPath, item, - errors, + incrementalContext, new Map(), ).then( (resolvedItem) => - buildStreamItemsResult(errors, streamRecord, resolvedItem), + buildStreamItemsResult( + incrementalContext.errors, + streamRecord, + resolvedItem, + ), (error) => ({ streamRecord, - errors: withError(errors, error), + errors: withError(incrementalContext.errors, error), }), ); } @@ -2254,37 +2326,57 @@ function completeStreamItems( info, itemPath, item, - errors, + incrementalContext, new Map(), ); } catch (rawError) { - handleFieldError(rawError, itemType, fieldGroup, itemPath, errors); + handleFieldError( + rawError, + itemType, + fieldGroup, + itemPath, + incrementalContext.errors, + ); result = [null, []]; } } catch (error) { return { streamRecord, - errors: withError(errors, error), + errors: withError(incrementalContext.errors, error), }; } if (isPromise(result)) { return result .then(undefined, (rawError) => { - handleFieldError(rawError, itemType, fieldGroup, itemPath, errors); + handleFieldError( + rawError, + itemType, + fieldGroup, + itemPath, + incrementalContext.errors, + ); return [null, []] as GraphQLWrappedResult; }) .then( (resolvedItem) => - buildStreamItemsResult(errors, streamRecord, resolvedItem), + buildStreamItemsResult( + incrementalContext.errors, + streamRecord, + resolvedItem, + ), (error) => ({ streamRecord, - errors: withError(errors, error), + errors: withError(incrementalContext.errors, error), }), ); } - return buildStreamItemsResult(errors, streamRecord, result); + return buildStreamItemsResult( + incrementalContext.errors, + streamRecord, + result, + ); } function buildStreamItemsResult( diff --git a/src/validation/rules/SingleFieldSubscriptionsRule.ts b/src/validation/rules/SingleFieldSubscriptionsRule.ts index 06d9545fbc..700bc0bda7 100644 --- a/src/validation/rules/SingleFieldSubscriptionsRule.ts +++ b/src/validation/rules/SingleFieldSubscriptionsRule.ts @@ -10,15 +10,13 @@ import type { import { Kind } from '../../language/kinds.js'; import type { ASTVisitor } from '../../language/visitor.js'; -import type { FieldDetails } from '../../execution/collectFields.js'; +import type { FieldGroup } from '../../execution/collectFields.js'; import { collectFields } from '../../execution/collectFields.js'; import type { ValidationContext } from '../ValidationContext.js'; -function toNodes( - fieldDetailsList: ReadonlyArray, -): ReadonlyArray { - return fieldDetailsList.map((fieldDetails) => fieldDetails.node); +function toNodes(fieldGroup: FieldGroup): ReadonlyArray { + return fieldGroup.map((fieldDetails) => fieldDetails.node); } /** @@ -49,15 +47,15 @@ export function SingleFieldSubscriptionsRule( fragments[definition.name.value] = definition; } } - const { fields } = collectFields( + const { groupedFieldSet } = collectFields( schema, fragments, variableValues, subscriptionType, node, ); - if (fields.size > 1) { - const fieldGroups = [...fields.values()]; + if (groupedFieldSet.size > 1) { + const fieldGroups = [...groupedFieldSet.values()]; const extraFieldGroups = fieldGroups.slice(1); const extraFieldSelections = extraFieldGroups.flatMap( (fieldGroup) => toNodes(fieldGroup), @@ -71,7 +69,7 @@ export function SingleFieldSubscriptionsRule( ), ); } - for (const fieldGroup of fields.values()) { + for (const fieldGroup of groupedFieldSet.values()) { const fieldName = toNodes(fieldGroup)[0].name.value; if (fieldName.startsWith('__')) { context.reportError(