diff --git a/packages/executor/src/execution/execute.ts b/packages/executor/src/execution/execute.ts index a9470347517..8cb688d025c 100644 --- a/packages/executor/src/execution/execute.ts +++ b/packages/executor/src/execution/execute.ts @@ -137,13 +137,15 @@ export interface ExecutionContext { subscribeFieldResolver: GraphQLFieldResolver; enableEarlyExecution: boolean; signal: AbortSignal | undefined; - errors: Array | undefined; + errors: Map | undefined; cancellableStreams: Set | undefined; + incrementalDataRecords: Array | undefined; } interface IncrementalContext { - errors: Array | undefined; + errors: Map | undefined; deferUsageSet?: DeferUsageSet | undefined; + incrementalDataRecords: Array | undefined; } export interface ExecutionArgs { @@ -166,8 +168,6 @@ interface StreamUsage { fieldGroup: FieldGroup; } -type GraphQLWrappedResult = [T, Array | undefined]; - /** * Implements the "Executing requests" section of the GraphQL specification, * including `@defer` and `@stream` as proposed in @@ -231,16 +231,77 @@ export function executeSync(args: ExecutionArgs): SingularExecutionResult { function buildDataResponse( exeContext: ExecutionContext, data: TData, - incrementalDataRecords: ReadonlyArray | undefined, ): SingularExecutionResult | IncrementalExecutionResults { - const errors = exeContext.errors; + 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); } - return buildIncrementalResponse(exeContext, data, errors, incrementalDataRecords); + const filteredIncrementalDataRecords = filterIncrementalDataRecords( + undefined, + errors, + incrementalDataRecords, + ); + + if (filteredIncrementalDataRecords.length === 0) { + return buildSingleResult(data, errors); + } + + return buildIncrementalResponse( + exeContext, + data, + Array.from(errors.values()), + filteredIncrementalDataRecords, + ); +} + +function buildSingleResult( + data: TData, + errors: ReadonlyMap | undefined, +): SingularExecutionResult { + 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 = incrementalDataRecord.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. + + currentPath = currentPath!.prev; + if (errors.has(currentPath)) { + filtered = true; + break; + } + paths.push(currentPath); + } + + if (!filtered) { + filteredIncrementalDataRecords.push(incrementalDataRecord); + } + } + + return filteredIncrementalDataRecords; +} /** * Essential assertions before executing to provide developer feedback for * improper use of the GraphQL library. @@ -365,8 +426,9 @@ export function buildExecutionContext( const collectedFields = collectFields(schema, fragments, variableValues, rootType, operation); let groupedFieldSet = collectedFields.groupedFieldSet; const newDeferUsages = collectedFields.newDeferUsages; - let graphqlWrappedResult: MaybePromise>; + let data: MaybePromise; if (newDeferUsages.length === 0) { - graphqlWrappedResult = executeRootGroupedFieldSet( + data = executeRootGroupedFieldSet( exeContext, operation.operation, rootType, @@ -419,7 +481,7 @@ function executeOperation( const newGroupedFieldSets = fieldPLan.newGroupedFieldSets; const newDeferMap = addNewDeferredFragments(newDeferUsages, new Map()); - graphqlWrappedResult = executeRootGroupedFieldSet( + data = executeRootGroupedFieldSet( exeContext, operation.operation, rootType, @@ -439,15 +501,12 @@ function executeOperation( newDeferMap, ); - graphqlWrappedResult = withNewDeferredGroupedFieldSets( - graphqlWrappedResult, - newDeferredGroupedFieldSetRecords, - ); + addIncrementalDataRecords(exeContext, newDeferredGroupedFieldSetRecords); } } - if (isPromise(graphqlWrappedResult)) { - return graphqlWrappedResult.then( - resolved => buildDataResponse(exeContext, resolved[0], resolved[1]), + if (isPromise(data)) { + return data.then( + resolved => buildDataResponse(exeContext, resolved), error => { if (exeContext.signal?.aborted) { throw exeContext.signal.reason; @@ -459,7 +518,7 @@ function executeOperation( }, ); } - return buildDataResponse(exeContext, graphqlWrappedResult[0], graphqlWrappedResult[1]); + return buildDataResponse(exeContext, data); } catch (error) { if (exeContext.signal?.aborted) { throw exeContext.signal.reason; @@ -475,8 +534,8 @@ function executeRootGroupedFieldSet( rootValue: unknown, groupedFieldSet: GroupedFieldSet, deferMap: ReadonlyMap | undefined, -): MaybePromise> { - let result: MaybePromise>; +): MaybePromise { + let result: MaybePromise; if (operation === 'mutation') { result = executeFieldsSerially( exeContext, @@ -496,45 +555,28 @@ function executeRootGroupedFieldSet( groupedFieldSet, undefined, deferMap, - ) as MaybePromise>; + ) as MaybePromise; } return result; } -function withNewDeferredGroupedFieldSets( - result: MaybePromise>, - newDeferredGroupedFieldSetRecords: ReadonlyArray, -): MaybePromise> { - if (isPromise(result)) { - return result.then(resolved => { - addIncrementalDataRecords(resolved, newDeferredGroupedFieldSetRecords); - return resolved; - }); - } - - addIncrementalDataRecords(result, newDeferredGroupedFieldSetRecords); - return result; -} - function addIncrementalDataRecords( - graphqlWrappedResult: GraphQLWrappedResult, - incrementalDataRecords: ReadonlyArray | undefined, + context: ExecutionContext | IncrementalContext, + newIncrementalDataRecords: ReadonlyArray, ): void { + const incrementalDataRecords = context.incrementalDataRecords; if (incrementalDataRecords === undefined) { + context.incrementalDataRecords = [...newIncrementalDataRecords]; return; } - if (graphqlWrappedResult[1] === undefined) { - graphqlWrappedResult[1] = [...incrementalDataRecords]; - } else { - graphqlWrappedResult[1].push(...incrementalDataRecords); - } + 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]; } /** @@ -549,10 +591,10 @@ function executeFieldsSerially( groupedFieldSet: GroupedFieldSet, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): MaybePromise> { +): MaybePromise { return promiseReduce( groupedFieldSet, - (graphqlWrappedResult, [responseName, fieldGroup]) => { + (results, [responseName, fieldGroup]) => { const fieldPath = addPath(path, responseName, parentType.name); if (exeContext.signal?.aborted) { throw exeContext.signal.reason; @@ -568,20 +610,18 @@ function executeFieldsSerially( deferMap, ); if (result === undefined) { - return graphqlWrappedResult; + return results; } if (isPromise(result)) { return result.then(resolved => { - graphqlWrappedResult[0][responseName] = resolved[0]; - addIncrementalDataRecords(graphqlWrappedResult, resolved[1]); - return graphqlWrappedResult; + results[responseName] = resolved; + return results; }); } - graphqlWrappedResult[0][responseName] = result[0]; - addIncrementalDataRecords(graphqlWrappedResult, result[1]); - return graphqlWrappedResult; + results[responseName] = result; + return results; }, - [Object.create(null), undefined] as GraphQLWrappedResult, + Object.create(null), ); } @@ -597,9 +637,8 @@ function executeFields( groupedFieldSet: GroupedFieldSet, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): MaybePromise>> { +): MaybePromise> { const results = Object.create(null); - const graphqlWrappedResult: GraphQLWrappedResult> = [results, undefined]; let containsPromise = false; try { @@ -620,28 +659,16 @@ function executeFields( ); if (result !== undefined) { + results[responseName] = result; if (isPromise(result)) { - results[responseName] = result.then(resolved => { - addIncrementalDataRecords(graphqlWrappedResult, resolved[1]); - return resolved[0]; - }); containsPromise = true; - } else { - results[responseName] = result[0]; - addIncrementalDataRecords(graphqlWrappedResult, 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 */ - }, - exeContext.signal, - ).finally(() => { + return promiseForObject(results, exeContext.signal).finally(() => { throw error; }) as never; } @@ -650,17 +677,13 @@ function executeFields( // If there are no promises, we can just return the object and any incrementalDataRecords if (!containsPromise) { - return graphqlWrappedResult; + 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, graphqlWrappedResult[1]], - exeContext.signal, - ); + return promiseForObject(results, exeContext.signal); } function toNodes(fieldGroup: FieldGroup): Array { @@ -681,7 +704,7 @@ function executeField( path: Path, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): MaybePromise> | undefined { +): MaybePromise | undefined { const fieldDef = getFieldDef(exeContext.schema, parentType, fieldGroup[0].node); if (!fieldDef) { return; @@ -735,13 +758,13 @@ function executeField( // to take a second callback for the error case. return completed.then(undefined, rawError => { handleFieldError(rawError, exeContext, returnType, fieldGroup, path, incrementalContext); - return [null, undefined]; + return null; }); } return completed; } catch (rawError) { handleFieldError(rawError, exeContext, returnType, fieldGroup, path, incrementalContext); - return [null, undefined]; + return null; } } @@ -793,10 +816,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); } /** @@ -829,7 +852,7 @@ function completeValue( result: unknown, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): MaybePromise> { +): MaybePromise { // If result is an Error, throw a located error. if (result instanceof Error) { throw result; @@ -848,7 +871,7 @@ function completeValue( incrementalContext, deferMap, ); - if ((completed as GraphQLWrappedResult)[0] === null) { + if (completed == null) { throw new Error( `Cannot return null for non-nullable field ${info.parentType.name}.${info.fieldName}.`, ); @@ -858,7 +881,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 @@ -878,7 +901,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 @@ -923,7 +946,7 @@ async function completePromisedValue( result: PromiseLike, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): Promise> { +): Promise { try { const resolved = await result; let completed = completeValue( @@ -943,7 +966,7 @@ async function completePromisedValue( return completed; } catch (rawError) { handleFieldError(rawError, exeContext, returnType, fieldGroup, path, incrementalContext); - return [null, undefined]; + return null; } } @@ -1026,13 +1049,12 @@ async function completeAsyncIteratorValue( asyncIterator: AsyncIterator, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): Promise>> { +): Promise> { exeContext.signal?.addEventListener('abort', () => { asyncIterator.return?.(); }); let containsPromise = false; const completedResults: Array = []; - const graphqlWrappedResult: GraphQLWrappedResult> = [completedResults, undefined]; let index = 0; const streamUsage = getStreamUsage(exeContext, fieldGroup, path); while (true) { @@ -1068,7 +1090,8 @@ async function completeAsyncIteratorValue( exeContext.cancellableStreams.add(streamRecord); } - addIncrementalDataRecords(graphqlWrappedResult, [streamRecord]); + const context = incrementalContext ?? exeContext; + addIncrementalDataRecords(context, [streamRecord]); break; } @@ -1093,7 +1116,6 @@ async function completeAsyncIteratorValue( completedResults.push( completePromisedListItemValue( item, - graphqlWrappedResult, exeContext, itemType, fieldGroup, @@ -1109,7 +1131,6 @@ async function completeAsyncIteratorValue( completeListItemValue( item, completedResults, - graphqlWrappedResult, exeContext, itemType, fieldGroup, @@ -1128,11 +1149,8 @@ async function completeAsyncIteratorValue( } return containsPromise - ? /* c8 ignore start */ Promise.all(completedResults).then(resolved => [ - resolved, - graphqlWrappedResult[1], - ]) - : /* c8 ignore stop */ graphqlWrappedResult; + ? /* c8 ignore start */ Promise.all(completedResults) + : /* c8 ignore stop */ completedResults; } /** @@ -1148,7 +1166,7 @@ function completeListValue( result: unknown, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): MaybePromise>> { +): MaybePromise> { const itemType = returnType.ofType; if (isAsyncIterable(result)) { @@ -1193,12 +1211,11 @@ function completeIterableValue( items: Iterable, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): MaybePromise>> { +): MaybePromise> { // 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 graphqlWrappedResult: GraphQLWrappedResult> = [completedResults, undefined]; let index = 0; const streamUsage = getStreamUsage(exeContext, fieldGroup, path); const iterator = items[Symbol.iterator](); @@ -1207,7 +1224,7 @@ function completeIterableValue( const item = iteration.value; if (streamUsage && index >= streamUsage.initialCount) { - const syncStreamRecord: StreamRecord = { + const streamRecord: StreamRecord = { label: streamUsage.label, path, streamItemQueue: buildSyncStreamItemQueue( @@ -1222,7 +1239,8 @@ function completeIterableValue( ), }; - addIncrementalDataRecords(graphqlWrappedResult, [syncStreamRecord]); + const context = incrementalContext ?? exeContext; + addIncrementalDataRecords(context, [streamRecord]); break; } @@ -1234,7 +1252,6 @@ function completeIterableValue( completedResults.push( completePromisedListItemValue( item, - graphqlWrappedResult, exeContext, itemType, fieldGroup, @@ -1249,7 +1266,6 @@ function completeIterableValue( completeListItemValue( item, completedResults, - graphqlWrappedResult, exeContext, itemType, fieldGroup, @@ -1266,9 +1282,7 @@ function completeIterableValue( iteration = iterator.next(); } - return containsPromise - ? Promise.all(completedResults).then(resolved => [resolved, graphqlWrappedResult[1]]) - : graphqlWrappedResult; + return containsPromise ? Promise.all(completedResults) : completedResults; } /** @@ -1279,7 +1293,6 @@ function completeIterableValue( function completeListItemValue( item: unknown, completedResults: Array, - parent: GraphQLWrappedResult>, exeContext: ExecutionContext, itemType: GraphQLOutputType, fieldGroup: FieldGroup, @@ -1304,29 +1317,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 => { - addIncrementalDataRecords(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]); - addIncrementalDataRecords(parent, completedItem[1]); + completedResults.push(completedItem); } catch (rawError) { handleFieldError(rawError, exeContext, itemType, fieldGroup, itemPath, incrementalContext); completedResults.push(null); @@ -1336,7 +1342,6 @@ function completeListItemValue( async function completePromisedListItemValue( item: unknown, - parent: GraphQLWrappedResult>, exeContext: ExecutionContext, itemType: GraphQLOutputType, fieldGroup: FieldGroup, @@ -1360,8 +1365,7 @@ async function completePromisedListItemValue( if (isPromise(completed)) { completed = await completed; } - addIncrementalDataRecords(parent, completed[1]); - return completed[0]; + return completed; } catch (rawError) { handleFieldError(rawError, exeContext, itemType, fieldGroup, itemPath, incrementalContext); return null; @@ -1410,7 +1414,7 @@ function completeAbstractValue( result: unknown, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): MaybePromise>> { +): MaybePromise> { const resolveTypeFn = returnType.resolveType ?? exeContext.typeResolver; const contextValue = exeContext.contextValue; const runtimeType = resolveTypeFn(result, contextValue, info, returnType); @@ -1516,7 +1520,7 @@ function completeObjectValue( result: unknown, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): MaybePromise>> { +): MaybePromise> { // 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. @@ -1610,7 +1614,7 @@ function collectAndExecuteSubfields( result: unknown, incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, -): MaybePromise>> { +): MaybePromise> { // Collect sub-fields to execute to complete this value. const collectedSubfields = collectSubfields(exeContext, returnType, fieldGroup); let groupedFieldSet = collectedSubfields.groupedFieldSet; @@ -1653,7 +1657,8 @@ function collectAndExecuteSubfields( newDeferMap, ); - return withNewDeferredGroupedFieldSets(subFields, newDeferredGroupedFieldSetRecords); + const context = incrementalContext ?? exeContext; + addIncrementalDataRecords(context, newDeferredGroupedFieldSetRecords); } return subFields; } @@ -2026,6 +2031,7 @@ function executeDeferredGroupedFieldSets( const deferredFragmentRecords = getDeferredFragmentRecords(deferUsageSet, deferMap); const deferredGroupedFieldSetRecord: DeferredGroupedFieldSetRecord = { + path, deferredFragmentRecords, result: undefined as unknown as BoxedPromiseOrValue, }; @@ -2041,6 +2047,7 @@ function executeDeferredGroupedFieldSets( { errors: undefined, deferUsageSet, + incrementalDataRecords: undefined, }, deferMap, ); @@ -2106,7 +2113,7 @@ function executeDeferredGroupedFieldSet( return result.then( resolved => buildDeferredGroupedFieldSetResult( - incrementalContext.errors, + incrementalContext, deferredGroupedFieldSetRecord, path, resolved, @@ -2120,7 +2127,7 @@ function executeDeferredGroupedFieldSet( } return buildDeferredGroupedFieldSetResult( - incrementalContext.errors, + incrementalContext, deferredGroupedFieldSetRecord, path, result, @@ -2128,16 +2135,35 @@ function executeDeferredGroupedFieldSet( } function buildDeferredGroupedFieldSetResult( - errors: ReadonlyArray | undefined, + incrementalContext: IncrementalContext, deferredGroupedFieldSetRecord: DeferredGroupedFieldSetRecord, path: Path | undefined, - result: GraphQLWrappedResult>, + data: Record, ): DeferredGroupedFieldSetResult { + const { errors, incrementalDataRecords } = incrementalContext; + if (incrementalDataRecords === undefined) { + return { + deferredGroupedFieldSetRecord, + path: pathToArray(path), + result: errors === undefined ? { data } : { data, errors: [...errors.values()] }, + incrementalDataRecords, + }; + } + + if (errors === undefined) { + return { + deferredGroupedFieldSetRecord, + path: pathToArray(path), + result: { data }, + incrementalDataRecords, + }; + } + return { deferredGroupedFieldSetRecord, 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), }; } @@ -2168,10 +2194,11 @@ function buildSyncStreamItemQueue( const initialPath = addPath(streamPath, initialIndex, undefined); const firstStreamItem = new BoxedPromiseOrValue( completeStreamItem( + streamPath, initialPath, initialItem, exeContext, - { errors: undefined }, + { errors: undefined, incrementalDataRecords: undefined }, fieldGroup, info, itemType, @@ -2199,10 +2226,11 @@ function buildSyncStreamItemQueue( const currentExecutor = () => completeStreamItem( + streamPath, itemPath, value, exeContext, - { errors: undefined }, + { errors: undefined, incrementalDataRecords: undefined }, fieldGroup, info, itemType, @@ -2218,7 +2246,7 @@ function buildSyncStreamItemQueue( currentIndex = initialIndex + 1; } - streamItemQueue.push(new BoxedPromiseOrValue({})); + streamItemQueue.push(new BoxedPromiseOrValue({ path: streamPath })); return firstStreamItem.value; }; @@ -2278,21 +2306,23 @@ async function getNextAsyncStreamItemResult( iteration = await asyncIterator.next(); } catch (error) { return { + path: streamPath, errors: [locatedError(error, toNodes(fieldGroup), pathToArray(streamPath))], }; } if (iteration.done) { - return {}; + return { path: streamPath }; } const itemPath = addPath(streamPath, index, undefined); const result = completeStreamItem( + streamPath, itemPath, iteration.value, exeContext, - { errors: undefined }, + { errors: undefined, incrementalDataRecords: undefined }, fieldGroup, info, itemType, @@ -2320,6 +2350,7 @@ async function getNextAsyncStreamItemResult( } function completeStreamItem( + streamPath: Path, itemPath: Path, item: unknown, exeContext: ExecutionContext, @@ -2339,14 +2370,15 @@ function completeStreamItem( incrementalContext, new Map(), ).then( - resolvedItem => buildStreamItemResult(incrementalContext.errors, resolvedItem), + resolvedItem => buildStreamItemResult(incrementalContext, streamPath, resolvedItem), error => ({ + path: streamPath, errors: withError(incrementalContext.errors, error), }), ); } - let result: MaybePromise>; + let result: MaybePromise; try { try { result = completeValue( @@ -2361,10 +2393,11 @@ function completeStreamItem( ); } catch (rawError) { handleFieldError(rawError, exeContext, itemType, fieldGroup, itemPath, incrementalContext); - result = [null, undefined]; + result = null; } } catch (error) { return { + path: streamPath, errors: withError(incrementalContext.errors, error as GraphQLError), }; } @@ -2373,30 +2406,55 @@ function completeStreamItem( return result .then(undefined, rawError => { handleFieldError(rawError, exeContext, itemType, fieldGroup, itemPath, incrementalContext); - return [null, undefined] as GraphQLWrappedResult; + return null; }) .then( - resolvedItem => buildStreamItemResult(incrementalContext.errors, resolvedItem), + resolvedItem => buildStreamItemResult(incrementalContext, streamPath, resolvedItem), error => ({ + path: streamPath, errors: withError(incrementalContext.errors, error), }), ); } - return buildStreamItemResult(incrementalContext.errors, result); + return buildStreamItemResult(incrementalContext, streamPath, result); } function buildStreamItemResult( - errors: ReadonlyArray | undefined, - result: GraphQLWrappedResult, + incrementalContext: IncrementalContext, + streamPath: Path, + item: unknown, ): StreamItemResult { + const { errors, incrementalDataRecords } = incrementalContext; + if (incrementalDataRecords === undefined) { + return { + path: streamPath, + item, + errors: errors === undefined ? undefined : [...errors.values()], + incrementalDataRecords, + }; + } + + if (errors === undefined) { + return { + path: streamPath, + item, + errors, + incrementalDataRecords, + }; + } + return { - item: result[0], - errors, - incrementalDataRecords: result[1], + path: streamPath, + item, + errors: [...errors.values()], + incrementalDataRecords: filterIncrementalDataRecords( + streamPath, + errors, + incrementalDataRecords, + ), }; } - /** * This method looks up the field on the given type definition. * It has special casing for the three introspection fields, diff --git a/packages/executor/src/execution/promiseForObject.ts b/packages/executor/src/execution/promiseForObject.ts index e259eed6781..4b9b512593c 100644 --- a/packages/executor/src/execution/promiseForObject.ts +++ b/packages/executor/src/execution/promiseForObject.ts @@ -1,3 +1,7 @@ +type ResolvedObject = { + [TKey in keyof TData]: TData[TKey] extends Promise ? TValue : TData[TKey]; +}; + /** * This function transforms a JS object `Record>` into * a `Promise>` @@ -5,12 +9,11 @@ * 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: TData, - callback: (object: TData) => TReturn, signal?: AbortSignal, -): Promise { - let resolvedObject = Object.create(null); +): Promise> { + const resolvedObject = Object.create(null); await new Promise((resolve, reject) => { signal?.addEventListener('abort', () => { reject(signal.reason); @@ -19,10 +22,7 @@ export async function promiseForObject( Object.entries(object as any).map(async ([key, value]) => { resolvedObject[key] = await value; }), - ).then(() => { - resolvedObject = callback(resolvedObject); - resolve(); - }, reject); + ).then(() => resolve(), reject); }); return resolvedObject; } diff --git a/packages/executor/src/execution/types.ts b/packages/executor/src/execution/types.ts index 4a55e2f8f04..81f43cc5383 100644 --- a/packages/executor/src/execution/types.ts +++ b/packages/executor/src/execution/types.ts @@ -193,6 +193,7 @@ export function isNonReconcilableDeferredGroupedFieldSetResult( } export interface DeferredGroupedFieldSetRecord { + path: Path | undefined; deferredFragmentRecords: ReadonlyArray; result: | BoxedPromiseOrValue @@ -209,6 +210,7 @@ export interface DeferredFragmentRecord { } export interface StreamItemResult { + path: Path; item?: unknown; incrementalDataRecords?: ReadonlyArray | undefined; errors?: ReadonlyArray | undefined;