From 25a04255ffc578d8556445af65cce910d61f309e Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 5 Apr 2024 14:01:02 +0300 Subject: [PATCH] add filtering again --- src/execution/IncrementalPublisher.ts | 23 +- src/execution/__tests__/stream-test.ts | 13 +- src/execution/execute.ts | 410 ++++++++++++++----------- src/jsutils/promiseForObject.ts | 7 +- 4 files changed, 264 insertions(+), 189 deletions(-) diff --git a/src/execution/IncrementalPublisher.ts b/src/execution/IncrementalPublisher.ts index 233df79c8bb..c9d4d672420 100644 --- a/src/execution/IncrementalPublisher.ts +++ b/src/execution/IncrementalPublisher.ts @@ -520,7 +520,7 @@ class IncrementalPublisher { ); } - if (deferredGroupedFieldSetResult.incrementalDataRecords !== undefined) { + if (deferredGroupedFieldSetResult.incrementalDataRecords) { this._addIncrementalDataRecords( deferredGroupedFieldSetResult.incrementalDataRecords, ); @@ -616,7 +616,7 @@ class IncrementalPublisher { this._incremental.push(incrementalEntry); - if (streamItemsResult.incrementalDataRecords !== undefined) { + if (streamItemsResult.incrementalDataRecords) { this._addIncrementalDataRecords( streamItemsResult.incrementalDataRecords, ); @@ -658,13 +658,13 @@ class IncrementalPublisher { } } -function isDeferredFragmentRecord( +export function isDeferredFragmentRecord( subsequentResultRecord: SubsequentResultRecord, ): subsequentResultRecord is DeferredFragmentRecord { return subsequentResultRecord instanceof DeferredFragmentRecord; } -function isDeferredGroupedFieldSetRecord( +export function isDeferredGroupedFieldSetRecord( incrementalDataRecord: IncrementalDataRecord, ): incrementalDataRecord is DeferredGroupedFieldSetRecord { return incrementalDataRecord instanceof DeferredGroupedFieldSetRecord; @@ -673,7 +673,8 @@ function isDeferredGroupedFieldSetRecord( export interface IncrementalContext { deferUsageSet: DeferUsageSet | undefined; path: Path | undefined; - errors?: Array | undefined; + errors?: Map | undefined; + incrementalDataRecords?: Array | undefined; } export type DeferredGroupedFieldSetResult = @@ -690,7 +691,7 @@ interface ReconcilableDeferredGroupedFieldSetResult { deferredFragmentRecords: ReadonlyArray; path: Array; result: BareDeferredGroupedFieldSetResult; - incrementalDataRecords: ReadonlyArray | undefined; + incrementalDataRecords?: ReadonlyArray | undefined; sent?: true | undefined; } @@ -795,7 +796,7 @@ interface NonReconcilableStreamItemsResult { interface NonTerminatingStreamItemsResult { streamRecord: StreamRecord; result: BareStreamItemsResult; - incrementalDataRecords: ReadonlyArray | undefined; + incrementalDataRecords?: ReadonlyArray | undefined; } interface TerminatingStreamItemsResult { @@ -857,10 +858,10 @@ export class StreamItemsRecord { this.nextStreamItems !== undefined ? { ...result, - incrementalDataRecords: - result.incrementalDataRecords === undefined - ? [this.nextStreamItems] - : [this.nextStreamItems, ...result.incrementalDataRecords], + incrementalDataRecords: [ + this.nextStreamItems, + ...(result.incrementalDataRecords ?? []), + ], } : result; } diff --git a/src/execution/__tests__/stream-test.ts b/src/execution/__tests__/stream-test.ts index 9c0a7ed22b6..22be4e4d354 100644 --- a/src/execution/__tests__/stream-test.ts +++ b/src/execution/__tests__/stream-test.ts @@ -2096,14 +2096,23 @@ describe('Execute: stream directive', () => { id: '2', }, ], - completed: [{ id: '2' }, { id: '1' }], - hasNext: false, + completed: [{ id: '2' }], + hasNext: true, }, done: false, }); const result5 = await iterator.next(); expectJSON(result5).toDeepEqual({ + value: { + completed: [{ id: '1' }], + hasNext: false, + }, + done: false, + }); + + const result6 = await iterator.next(); + expectJSON(result6).toDeepEqual({ value: undefined, done: true, }); diff --git a/src/execution/execute.ts b/src/execution/execute.ts index c78734129de..2df10335591 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -70,6 +70,7 @@ import { buildIncrementalResponse, DeferredFragmentRecord, DeferredGroupedFieldSetRecord, + isDeferredGroupedFieldSetRecord, StreamItemsRecord, StreamRecord, } from './IncrementalPublisher.js'; @@ -141,8 +142,9 @@ export interface ExecutionContext { fieldResolver: GraphQLFieldResolver; typeResolver: GraphQLTypeResolver; subscribeFieldResolver: GraphQLFieldResolver; - errors: Array | undefined; + errors: Map | undefined; cancellableStreams: Set | undefined; + incrementalDataRecords: Array | undefined; } export interface ExecutionArgs { @@ -163,8 +165,6 @@ export interface StreamUsage { fieldGroup: FieldGroup; } -type GraphQLResult = [T, ReadonlyArray | undefined]; - const UNEXPECTED_EXPERIMENTAL_DIRECTIVES = 'The provided schema unexpectedly contains experimental directives (@defer or @stream). These directives may only be utilized if experimental execution features are explicitly enabled.'; @@ -270,9 +270,9 @@ function executeOperation( const { groupedFieldSet: nonPartitionedGroupedFieldSet, newDeferUsages } = collectFields(schema, fragments, variableValues, rootType, operation); - let acc: PromiseOrValue>>; + let data: PromiseOrValue>; if (newDeferUsages.length === 0) { - acc = executeRootGroupedFieldSet( + data = executeRootGroupedFieldSet( exeContext, operation.operation, rootType, @@ -287,7 +287,7 @@ function executeOperation( const newDeferMap = addNewDeferredFragments(newDeferUsages, new Map()); - acc = executeRootGroupedFieldSet( + data = executeRootGroupedFieldSet( exeContext, operation.operation, rootType, @@ -307,79 +307,133 @@ function executeOperation( newDeferMap, ); - acc = withNewDeferredGroupedFieldSets( - acc, + addIncrementalDataRecords( + exeContext, newDeferredGroupedFieldSetRecords, ); } } - if (isPromise(acc)) { - return acc.then( - (resolved) => buildDataResponse(exeContext, resolved[0], resolved[1]), + + if (isPromise(data)) { + return data.then( + (resolved) => buildDataResponse(exeContext, resolved), (error) => ({ data: null, errors: withError(exeContext.errors, error), }), ); } - return buildDataResponse(exeContext, acc[0], acc[1]); + return buildDataResponse(exeContext, data); } catch (error) { return { data: null, errors: withError(exeContext.errors, error) }; } } -function withNewDeferredGroupedFieldSets( - result: PromiseOrValue>>, - newDeferredGroupedFieldSetRecords: ReadonlyArray, -): PromiseOrValue>> { - if (isPromise(result)) { - return result.then((resolved) => { - appendNewIncrementalDataRecords( - resolved, - newDeferredGroupedFieldSetRecords, - ); - return resolved; - }); - } - - appendNewIncrementalDataRecords(result, newDeferredGroupedFieldSetRecords); - return result; -} - -function appendNewIncrementalDataRecords( - acc: GraphQLResult, - newRecords: ReadonlyArray | undefined, +function addIncrementalDataRecords( + context: ExecutionContext | IncrementalContext, + newIncrementalDataRecords: ReadonlyArray, ): void { - if (newRecords !== undefined) { - acc[1] = acc[1] === undefined ? newRecords : [...acc[1], ...newRecords]; + const incrementalDataRecords = context.incrementalDataRecords; + if (incrementalDataRecords === undefined) { + context.incrementalDataRecords = [...newIncrementalDataRecords]; + return; } + incrementalDataRecords.push(...newIncrementalDataRecords); } function withError( - errors: Array | undefined, + errors: ReadonlyMap | undefined, error: GraphQLError, ): ReadonlyArray { - return errors === undefined ? [error] : [...errors, error]; + return errors === undefined ? [error] : [...errors.values(), error]; } function buildDataResponse( exeContext: ExecutionContext, data: ObjMap, - incrementalDataRecords: ReadonlyArray | undefined, ): ExecutionResult | ExperimentalIncrementalExecutionResults { - const { errors } = exeContext; + const { errors, incrementalDataRecords } = exeContext; if (incrementalDataRecords === undefined) { - return errors !== undefined ? { errors, data } : { data }; + return buildSingleResult(data, errors); + } + + if (errors === undefined) { + return buildIncrementalResponse( + exeContext, + data, + undefined, + incrementalDataRecords, + ); + } + + const filteredIncrementalDataRecords = filterIncrementalDataRecords( + undefined, + errors, + incrementalDataRecords, + ); + + if (filteredIncrementalDataRecords.length === 0) { + return buildSingleResult(data, errors); } return buildIncrementalResponse( exeContext, data, - errors, - incrementalDataRecords, + Array.from(errors.values()), + filteredIncrementalDataRecords, ); } +function buildSingleResult( + data: ObjMap, + errors: ReadonlyMap | undefined, +): ExecutionResult { + return errors !== undefined + ? { errors: Array.from(errors.values()), data } + : { data }; +} + +function filterIncrementalDataRecords( + initialPath: Path | undefined, + errors: ReadonlyMap, + incrementalDataRecords: ReadonlyArray, +): ReadonlyArray { + const filteredIncrementalDataRecords: Array = []; + for (const incrementalDataRecord of incrementalDataRecords) { + let currentPath: Path | undefined = isDeferredGroupedFieldSetRecord( + incrementalDataRecord, + ) + ? incrementalDataRecord.path + : incrementalDataRecord.streamRecord.path; + + if (errors.has(currentPath)) { + continue; + } + + const paths: Array = [currentPath]; + let filtered = false; + while (currentPath !== initialPath) { + // Because currentPath leads to initialPath or is undefined, and the + // loop will exit if initialPath is undefined, currentPath must be + // defined. + // TODO: Consider, however, adding an invariant. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + currentPath = currentPath!.prev; + if (errors.has(currentPath)) { + filtered = true; + break; + } + paths.push(currentPath); + } + + if (!filtered) { + filteredIncrementalDataRecords.push(incrementalDataRecord); + } + } + + return filteredIncrementalDataRecords; +} + /** * Also implements the "Executing requests" section of the GraphQL specification. * However, it guarantees to complete synchronously (or throw an error) assuming @@ -483,6 +537,7 @@ export function buildExecutionContext( subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, errors: undefined, cancellableStreams: undefined, + incrementalDataRecords: undefined, }; } @@ -504,7 +559,7 @@ function executeRootGroupedFieldSet( rootValue: unknown, groupedFieldSet: GroupedFieldSet, deferMap: ReadonlyMap | undefined, -): PromiseOrValue>> { +): PromiseOrValue> { switch (operation) { case OperationTypeNode.QUERY: return executeFields( @@ -551,10 +606,10 @@ function executeFieldsSerially( path: Path | undefined, groupedFieldSet: GroupedFieldSet, deferMap: ReadonlyMap | undefined, -): PromiseOrValue>> { +): PromiseOrValue> { return promiseReduce( groupedFieldSet, - (acc, [responseName, fieldGroup]) => { + (results, [responseName, fieldGroup]) => { const fieldPath = addPath(path, responseName, parentType.name); const result = executeField( exeContext, @@ -566,20 +621,18 @@ function executeFieldsSerially( deferMap, ); if (result === undefined) { - return acc; + return results; } if (isPromise(result)) { return result.then((resolved) => { - acc[0][responseName] = resolved[0]; - appendNewIncrementalDataRecords(acc, resolved[1]); - return acc; + results[responseName] = resolved; + return results; }); } - acc[0][responseName] = result[0]; - appendNewIncrementalDataRecords(acc, result[1]); - return acc; + results[responseName] = result; + return results; }, - [Object.create(null), undefined] as GraphQLResult>, + Object.create(null), ); } @@ -595,9 +648,8 @@ function executeFields( groupedFieldSet: GroupedFieldSet, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): PromiseOrValue>> { +): PromiseOrValue> { const results = Object.create(null); - const acc: GraphQLResult> = [results, undefined]; let containsPromise = false; try { @@ -614,24 +666,16 @@ function executeFields( ); if (result !== undefined) { + results[responseName] = result; if (isPromise(result)) { - results[responseName] = result.then((resolved) => { - appendNewIncrementalDataRecords(acc, resolved[1]); - return resolved[0]; - }); containsPromise = true; - } else { - results[responseName] = result[0]; - appendNewIncrementalDataRecords(acc, result[1]); } } } } catch (error) { if (containsPromise) { // Ensure that any promises returned by other fields are handled, as they may also reject. - return promiseForObject(results, () => { - /* noop */ - }).finally(() => { + return promiseForObject(results).finally(() => { throw error; }) as never; } @@ -640,13 +684,13 @@ function executeFields( // If there are no promises, we can just return the object and any incrementalDataRecords if (!containsPromise) { - return acc; + return results; } // Otherwise, results is a map from field name to the result of resolving that // field, which is possibly a promise. Return a promise that will return this // same map, but with any promises replaced with the values they resolved to. - return promiseForObject(results, (resolved) => [resolved, acc[1]]); + return promiseForObject(results); } function toNodes(fieldGroup: FieldGroup): ReadonlyArray { @@ -667,7 +711,7 @@ function executeField( path: Path, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): PromiseOrValue> | undefined { +): PromiseOrValue { const fieldName = fieldGroup[0].node.name.value; const fieldDef = exeContext.schema.getField(parentType, fieldName); if (!fieldDef) { @@ -739,7 +783,7 @@ function executeField( path, incrementalContext, ); - return [null, undefined]; + return null; }); } return completed; @@ -752,7 +796,7 @@ function executeField( path, incrementalContext, ); - return [null, undefined]; + return null; } } @@ -804,10 +848,10 @@ function handleFieldError( const context = incrementalContext ?? exeContext; let errors = context.errors; if (errors === undefined) { - errors = []; + errors = new Map(); context.errors = errors; } - errors.push(error); + errors.set(path, error); } /** @@ -840,7 +884,7 @@ function completeValue( result: unknown, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): PromiseOrValue> { +): PromiseOrValue { // If result is an Error, throw a located error. if (result instanceof Error) { throw result; @@ -859,7 +903,7 @@ function completeValue( incrementalContext, deferMap, ); - if ((completed as GraphQLResult)[0] === null) { + if (completed === null) { throw new Error( `Cannot return null for non-nullable field ${info.parentType.name}.${info.fieldName}.`, ); @@ -869,7 +913,7 @@ function completeValue( // If result value is null or undefined then return null. if (result == null) { - return [null, undefined]; + return null; } // If field type is List, complete each item in the list with the inner type @@ -889,7 +933,7 @@ function completeValue( // If field type is a leaf type, Scalar or Enum, serialize to a valid value, // returning null if serialization is not possible. if (isLeafType(returnType)) { - return [completeLeafValue(returnType, result), undefined]; + return completeLeafValue(returnType, result); } // If field type is an abstract type, Interface or Union, determine the @@ -937,7 +981,7 @@ async function completePromisedValue( result: Promise, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): Promise> { +): Promise { try { const resolved = await result; let completed = completeValue( @@ -964,7 +1008,7 @@ async function completePromisedValue( path, incrementalContext, ); - return [null, undefined]; + return null; } } @@ -1053,10 +1097,9 @@ async function completeAsyncIteratorValue( asyncIterator: AsyncIterator, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): Promise>> { +): Promise> { let containsPromise = false; const completedResults: Array = []; - const acc: GraphQLResult> = [completedResults, undefined]; let index = 0; // eslint-disable-next-line no-constant-condition while (true) { @@ -1080,7 +1123,6 @@ async function completeAsyncIteratorValue( completedResults.push( completePromisedListItemValue( item, - acc, exeContext, itemType, fieldGroup, @@ -1096,7 +1138,6 @@ async function completeAsyncIteratorValue( completeListItemValue( item, completedResults, - acc, exeContext, itemType, fieldGroup, @@ -1108,12 +1149,11 @@ async function completeAsyncIteratorValue( ) { containsPromise = true; } + index++; } - return containsPromise - ? Promise.all(completedResults).then((resolved) => [resolved, acc[1]]) - : acc; + return containsPromise ? Promise.all(completedResults) : completedResults; } /** @@ -1130,10 +1170,9 @@ async function completeAsyncIteratorValueWithPossibleStream( streamUsage: StreamUsage, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): Promise>> { +): Promise> { let containsPromise = false; const completedResults: Array = []; - const acc: GraphQLResult> = [completedResults, undefined]; let index = 0; const initialCount = streamUsage.initialCount; // eslint-disable-next-line no-constant-condition @@ -1169,7 +1208,8 @@ async function completeAsyncIteratorValueWithPossibleStream( ), ); - appendNewIncrementalDataRecords(acc, [firstStreamItems]); + const context = incrementalContext ?? exeContext; + addIncrementalDataRecords(context, [firstStreamItems]); break; } @@ -1195,7 +1235,6 @@ async function completeAsyncIteratorValueWithPossibleStream( completedResults.push( completePromisedListItemValue( item, - acc, exeContext, itemType, fieldGroup, @@ -1211,7 +1250,6 @@ async function completeAsyncIteratorValueWithPossibleStream( completeListItemValue( item, completedResults, - acc, exeContext, itemType, fieldGroup, @@ -1230,11 +1268,8 @@ async function completeAsyncIteratorValueWithPossibleStream( } return containsPromise - ? /* c8 ignore start */ Promise.all(completedResults).then((resolved) => [ - resolved, - acc[1], - ]) - : /* c8 ignore stop */ acc; + ? /* c8 ignore start */ Promise.all(completedResults) + : /* c8 ignore stop */ completedResults; } /** @@ -1250,7 +1285,7 @@ function completeListValue( result: unknown, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): PromiseOrValue>> { +): PromiseOrValue> { const itemType = returnType.ofType; const streamUsage = getStreamUsage(exeContext, fieldGroup, path); @@ -1324,12 +1359,11 @@ function completeIterableValue( items: Iterable, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): PromiseOrValue>> { +): PromiseOrValue> { // This is specified as a simple map, however we're optimizing the path // where the list contains no Promises by avoiding creating another Promise. let containsPromise = false; const completedResults: Array = []; - const acc: GraphQLResult> = [completedResults, undefined]; let index = 0; for (const item of items) { // No need to modify the info object containing the path, @@ -1340,7 +1374,6 @@ function completeIterableValue( completedResults.push( completePromisedListItemValue( item, - acc, exeContext, itemType, fieldGroup, @@ -1355,7 +1388,6 @@ function completeIterableValue( completeListItemValue( item, completedResults, - acc, exeContext, itemType, fieldGroup, @@ -1367,12 +1399,11 @@ function completeIterableValue( ) { containsPromise = true; } + index++; } - return containsPromise - ? Promise.all(completedResults).then((resolved) => [resolved, acc[1]]) - : acc; + return containsPromise ? Promise.all(completedResults) : completedResults; } function completeIterableValueWithPossibleStream( @@ -1385,12 +1416,11 @@ function completeIterableValueWithPossibleStream( streamUsage: StreamUsage, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): PromiseOrValue>> { +): PromiseOrValue> { // This is specified as a simple map, however we're optimizing the path // where the list contains no Promises by avoiding creating another Promise. let containsPromise = false; const completedResults: Array = []; - const acc: GraphQLResult> = [completedResults, undefined]; let index = 0; const initialCount = streamUsage.initialCount; const iterator = items[Symbol.iterator](); @@ -1422,7 +1452,8 @@ function completeIterableValueWithPossibleStream( ), ); - appendNewIncrementalDataRecords(acc, [firstStreamItems]); + const context = incrementalContext ?? exeContext; + addIncrementalDataRecords(context, [firstStreamItems]); break; } @@ -1434,7 +1465,6 @@ function completeIterableValueWithPossibleStream( completedResults.push( completePromisedListItemValue( item, - acc, exeContext, itemType, fieldGroup, @@ -1449,7 +1479,6 @@ function completeIterableValueWithPossibleStream( completeListItemValue( item, completedResults, - acc, exeContext, itemType, fieldGroup, @@ -1461,14 +1490,13 @@ function completeIterableValueWithPossibleStream( ) { containsPromise = true; } + index++; iteration = iterator.next(); } - return containsPromise - ? Promise.all(completedResults).then((resolved) => [resolved, acc[1]]) - : acc; + return containsPromise ? Promise.all(completedResults) : completedResults; } /** @@ -1479,7 +1507,6 @@ function completeIterableValueWithPossibleStream( function completeListItemValue( item: unknown, completedResults: Array, - parent: GraphQLResult>, exeContext: ExecutionContext, itemType: GraphQLOutputType, fieldGroup: FieldGroup, @@ -1504,29 +1531,22 @@ function completeListItemValue( // Note: we don't rely on a `catch` method, but we do expect "thenable" // to take a second callback for the error case. completedResults.push( - completedItem.then( - (resolved) => { - appendNewIncrementalDataRecords(parent, resolved[1]); - return resolved[0]; - }, - (rawError) => { - handleFieldError( - rawError, - exeContext, - itemType, - fieldGroup, - itemPath, - incrementalContext, - ); - return null; - }, - ), + completedItem.then(undefined, (rawError) => { + handleFieldError( + rawError, + exeContext, + itemType, + fieldGroup, + itemPath, + incrementalContext, + ); + return null; + }), ); return true; } - completedResults.push(completedItem[0]); - appendNewIncrementalDataRecords(parent, completedItem[1]); + completedResults.push(completedItem); } catch (rawError) { handleFieldError( rawError, @@ -1543,7 +1563,6 @@ function completeListItemValue( async function completePromisedListItemValue( item: unknown, - parent: GraphQLResult>, exeContext: ExecutionContext, itemType: GraphQLOutputType, fieldGroup: FieldGroup, @@ -1567,8 +1586,7 @@ async function completePromisedListItemValue( if (isPromise(completed)) { completed = await completed; } - appendNewIncrementalDataRecords(parent, completed[1]); - return completed[0]; + return completed; } catch (rawError) { handleFieldError( rawError, @@ -1613,7 +1631,7 @@ function completeAbstractValue( result: unknown, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): PromiseOrValue>> { +): PromiseOrValue> { const resolveTypeFn = returnType.resolveType ?? exeContext.typeResolver; const contextValue = exeContext.contextValue; const runtimeType = resolveTypeFn(result, contextValue, info, returnType); @@ -1726,7 +1744,7 @@ function completeObjectValue( result: unknown, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): PromiseOrValue>> { +): PromiseOrValue> { // If there is an isTypeOf predicate function, call it with the // current result. If isTypeOf returns false, then raise an error rather // than continuing execution. @@ -1838,7 +1856,7 @@ function collectAndExecuteSubfields( result: unknown, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): PromiseOrValue>> { +): PromiseOrValue> { // Collect sub-fields to execute to complete this value. const { groupedFieldSet: nonPartitionedGroupedFieldSet, newDeferUsages } = collectSubfields(exeContext, returnType, fieldGroup); @@ -1881,10 +1899,8 @@ function collectAndExecuteSubfields( deferMap, ); - return withNewDeferredGroupedFieldSets( - subFields, - newDeferredGroupedFieldSetRecords, - ); + const context = incrementalContext ?? exeContext; + addIncrementalDataRecords(context, newDeferredGroupedFieldSetRecords); } return subFields; } @@ -1920,10 +1936,8 @@ function collectAndExecuteSubfields( newDeferMap, ); - return withNewDeferredGroupedFieldSets( - subFields, - newDeferredGroupedFieldSetRecords, - ); + const context = incrementalContext ?? exeContext; + addIncrementalDataRecords(context, newDeferredGroupedFieldSetRecords); } return subFields; } @@ -2286,9 +2300,9 @@ function executeDeferredGroupedFieldSet( incrementalContext: IncrementalContext, deferMap: ReadonlyMap, ): PromiseOrValue { - let result; + let data; try { - result = executeFields( + data = executeFields( exeContext, parentType, sourceValue, @@ -2306,8 +2320,8 @@ function executeDeferredGroupedFieldSet( }; } - if (isPromise(result)) { - return result.then( + if (isPromise(data)) { + return data.then( (resolved) => buildDeferredGroupedFieldSetResult( incrementalContext, @@ -2328,7 +2342,7 @@ function executeDeferredGroupedFieldSet( incrementalContext, deferredFragmentRecords, path, - result, + data, ); } @@ -2336,15 +2350,40 @@ function buildDeferredGroupedFieldSetResult( incrementalContext: IncrementalContext, deferredFragmentRecords: ReadonlyArray, path: Path | undefined, - result: GraphQLResult>, + data: ObjMap, ): DeferredGroupedFieldSetResult { - const { errors } = incrementalContext; + const { errors, incrementalDataRecords } = incrementalContext; + if (incrementalDataRecords === undefined) { + return { + deferredFragmentRecords, + path: pathToArray(path), + result: + errors === undefined + ? { + data, + } + : { data, errors: [...errors.values()] }, + }; + } + + if (errors === undefined) { + return { + deferredFragmentRecords, + path: pathToArray(path), + result: { data }, + incrementalDataRecords, + }; + } + return { deferredFragmentRecords, path: pathToArray(path), - result: - errors === undefined ? { data: result[0] } : { data: result[0], errors }, - incrementalDataRecords: result[1], + result: { data, errors: [...errors.values()] }, + incrementalDataRecords: filterIncrementalDataRecords( + path, + errors, + incrementalDataRecords, + ), }; } @@ -2557,10 +2596,10 @@ function completeStreamItems( ); } - let result: PromiseOrValue>; + let completedItem; try { try { - result = completeValue( + completedItem = completeValue( exeContext, itemType, fieldGroup, @@ -2579,7 +2618,7 @@ function completeStreamItems( itemPath, incrementalContext, ); - result = [null, undefined]; + completedItem = null; } } catch (error) { return { @@ -2589,8 +2628,8 @@ function completeStreamItems( }; } - if (isPromise(result)) { - return result + if (isPromise(completedItem)) { + return completedItem .then(undefined, (rawError) => { handleFieldError( rawError, @@ -2600,7 +2639,7 @@ function completeStreamItems( itemPath, incrementalContext, ); - return [null, undefined] as GraphQLResult; + return null; }) .then( (resolvedItem) => @@ -2617,24 +2656,51 @@ function completeStreamItems( ); } - return buildStreamItemsResult(incrementalContext, streamRecord, result); + return buildStreamItemsResult( + incrementalContext, + streamRecord, + completedItem, + ); } function buildStreamItemsResult( incrementalContext: IncrementalContext, streamRecord: StreamRecord, - result: GraphQLResult, + completedItem: unknown, ): StreamItemsResult { - const { errors } = incrementalContext; + const { errors, incrementalDataRecords } = incrementalContext; + if (incrementalDataRecords === undefined) { + return { + streamRecord, + result: + errors === undefined + ? { items: [completedItem] } + : { + items: [completedItem], + errors: [...errors.values()], + }, + }; + } + + if (errors === undefined) { + return { + streamRecord, + result: { items: [completedItem] }, + incrementalDataRecords, + }; + } + + const path = incrementalContext.path; return { streamRecord, - result: - errors === undefined - ? { items: [result[0]] } - : { - items: [result[0]], - errors: [...errors], - }, - incrementalDataRecords: result[1], + result: { + items: [completedItem], + errors: [...errors.values()], + }, + incrementalDataRecords: filterIncrementalDataRecords( + path, + errors, + incrementalDataRecords, + ), }; } diff --git a/src/jsutils/promiseForObject.ts b/src/jsutils/promiseForObject.ts index 25b34139230..ff48d9f2180 100644 --- a/src/jsutils/promiseForObject.ts +++ b/src/jsutils/promiseForObject.ts @@ -7,10 +7,9 @@ import type { ObjMap } from './ObjMap.js'; * This is akin to bluebird's `Promise.props`, but implemented only using * `Promise.all` so it will work with any implementation of ES6 promises. */ -export async function promiseForObject( +export async function promiseForObject( object: ObjMap>, - callback: (object: ObjMap) => U, -): Promise { +): Promise> { const keys = Object.keys(object); const values = Object.values(object); @@ -19,5 +18,5 @@ export async function promiseForObject( for (let i = 0; i < keys.length; ++i) { resolvedObject[keys[i]] = resolvedValues[i]; } - return callback(resolvedObject); + return resolvedObject; }