diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 1bc6c4267b..0c8fb1f838 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -363,7 +363,9 @@ function executeImpl( ...initialResult, hasNext: true, }, - subsequentResults: yieldSubsequentPayloads(exeContext), + subsequentResults: yieldSubsequentPayloads( + exeContext.subsequentPayloads, + ), }; } return initialResult; @@ -381,7 +383,9 @@ function executeImpl( ...initialResult, hasNext: true, }, - subsequentResults: yieldSubsequentPayloads(exeContext), + subsequentResults: yieldSubsequentPayloads( + exeContext.subsequentPayloads, + ), }; } return initialResult; @@ -687,7 +691,6 @@ function executeField( path: Path, asyncPayloadRecord?: AsyncPayloadRecord, ): PromiseOrValue { - const errors = asyncPayloadRecord?.errors ?? exeContext.errors; const fieldName = fieldNodes[0].name.value; const fieldDef = exeContext.schema.getField(parentType, fieldName); if (!fieldDef) { @@ -749,18 +752,26 @@ 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 handledError = handleFieldError(error, returnType, errors); - filterSubsequentPayloads(exeContext, path, asyncPayloadRecord); - return handledError; + const errors = asyncPayloadRecord?.errors ?? exeContext.errors; + addError(rawError, fieldNodes, returnType, path, errors); + filterSubsequentPayloads( + exeContext.subsequentPayloads, + path, + asyncPayloadRecord, + ); + return null; }); } return completed; } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(path)); - const handledError = handleFieldError(error, returnType, errors); - filterSubsequentPayloads(exeContext, path, asyncPayloadRecord); - return handledError; + const errors = asyncPayloadRecord?.errors ?? exeContext.errors; + addError(rawError, fieldNodes, returnType, path, errors); + filterSubsequentPayloads( + exeContext.subsequentPayloads, + path, + asyncPayloadRecord, + ); + return null; } } @@ -791,11 +802,15 @@ export function buildResolveInfo( }; } -function handleFieldError( - error: GraphQLError, +function addError( + rawError: unknown, + fieldNodes: ReadonlyArray, returnType: GraphQLOutputType, + path: Path, errors: Array, -): null { +): void { + const error = locatedError(rawError, fieldNodes, pathToArray(path)); + // If the field type is non-nullable, then it is resolved without any // protection from errors, however it still properly locates the error. if (isNonNullType(returnType)) { @@ -805,7 +820,6 @@ function handleFieldError( // Otherwise, error protection is applied, logging the error and resolving // a null value for this field if one is encountered. errors.push(error); - return null; } /** @@ -947,10 +961,13 @@ async function completePromisedValue( return completed; } catch (rawError) { const errors = asyncPayloadRecord?.errors ?? exeContext.errors; - const error = locatedError(rawError, fieldNodes, pathToArray(path)); - const handledError = handleFieldError(error, returnType, errors); - filterSubsequentPayloads(exeContext, path, asyncPayloadRecord); - return handledError; + addError(rawError, fieldNodes, returnType, path, errors); + filterSubsequentPayloads( + exeContext.subsequentPayloads, + path, + asyncPayloadRecord, + ); + return null; } } @@ -1024,7 +1041,6 @@ async function completeAsyncIteratorValue( iterator: AsyncIterator, asyncPayloadRecord?: AsyncPayloadRecord, ): Promise> { - const errors = asyncPayloadRecord?.errors ?? exeContext.errors; const stream = getStreamValues(exeContext, fieldNodes, path); let containsPromise = false; const completedResults: Array = []; @@ -1052,16 +1068,21 @@ async function completeAsyncIteratorValue( } const itemPath = addPath(path, index, undefined); - let iteration; + let iteration: IteratorResult; try { // eslint-disable-next-line no-await-in-loop iteration = await iterator.next(); - if (iteration.done) { - break; - } } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); - completedResults.push(handleFieldError(error, itemType, errors)); + // FIXME: add coverage for non-streamed async iterator error within a deferred payload + const errors = + /* c8 ignore start */ asyncPayloadRecord?.errors ?? + /* c8 ignore stop */ exeContext.errors; + addError(rawError, fieldNodes, itemType, itemPath, errors); + completedResults.push(null); + break; + } + + if (iteration.done) { break; } @@ -1069,7 +1090,6 @@ async function completeAsyncIteratorValue( completeListItemValue( iteration.value, completedResults, - errors, exeContext, itemType, fieldNodes, @@ -1099,7 +1119,6 @@ function completeListValue( asyncPayloadRecord?: AsyncPayloadRecord, ): PromiseOrValue> { const itemType = returnType.ofType; - const errors = asyncPayloadRecord?.errors ?? exeContext.errors; if (isAsyncIterable(result)) { const iterator = result[Symbol.asyncIterator](); @@ -1158,7 +1177,6 @@ function completeListValue( completeListItemValue( item, completedResults, - errors, exeContext, itemType, fieldNodes, @@ -1184,7 +1202,6 @@ function completeListValue( function completeListItemValue( item: unknown, completedResults: Array, - errors: Array, exeContext: ExecutionContext, itemType: GraphQLOutputType, fieldNodes: ReadonlyArray, @@ -1224,14 +1241,17 @@ function completeListItemValue( // to take a second callback for the error case. completedResults.push( completedItem.then(undefined, (rawError) => { - const error = locatedError( - rawError, - fieldNodes, - pathToArray(itemPath), + // FIXME: add coverage for async rejection of a promise item within a deferred payload + const errors = + /* c8 ignore start */ asyncPayloadRecord?.errors ?? + /* c8 ignore stop */ exeContext.errors; + addError(rawError, fieldNodes, itemType, itemPath, errors); + filterSubsequentPayloads( + exeContext.subsequentPayloads, + itemPath, + asyncPayloadRecord, ); - const handledError = handleFieldError(error, itemType, errors); - filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord); - return handledError; + return null; }), ); @@ -1240,10 +1260,17 @@ function completeListItemValue( completedResults.push(completedItem); } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); - const handledError = handleFieldError(error, itemType, errors); - filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord); - completedResults.push(handledError); + // FIXME: add coverage for sync rejection of a promise item within a deferred payload + const errors = + /* c8 ignore start */ asyncPayloadRecord?.errors ?? + /* c8 ignore stop */ exeContext.errors; + addError(rawError, fieldNodes, itemType, itemPath, errors); + filterSubsequentPayloads( + exeContext.subsequentPayloads, + itemPath, + asyncPayloadRecord, + ); + completedResults.push(null); } return false; @@ -1835,7 +1862,11 @@ function executeStreamField( (value) => [value], (error) => { asyncPayloadRecord.errors.push(error); - filterSubsequentPayloads(exeContext, path, asyncPayloadRecord); + filterSubsequentPayloads( + exeContext.subsequentPayloads, + path, + asyncPayloadRecord, + ); return null; }, ); @@ -1857,17 +1888,27 @@ function executeStreamField( asyncPayloadRecord, ); } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); - completedItem = handleFieldError( - error, + addError( + rawError, + fieldNodes, itemType, + itemPath, asyncPayloadRecord.errors, ); - filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord); + completedItem = null; + filterSubsequentPayloads( + exeContext.subsequentPayloads, + itemPath, + asyncPayloadRecord, + ); } } catch (error) { asyncPayloadRecord.errors.push(error); - filterSubsequentPayloads(exeContext, path, asyncPayloadRecord); + filterSubsequentPayloads( + exeContext.subsequentPayloads, + path, + asyncPayloadRecord, + ); asyncPayloadRecord.addItems(null); return asyncPayloadRecord; } @@ -1875,20 +1916,29 @@ function executeStreamField( if (isPromise(completedItem)) { const completedItems = completedItem .then(undefined, (rawError) => { - const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); - const handledError = handleFieldError( - error, + addError( + rawError, + fieldNodes, itemType, + itemPath, asyncPayloadRecord.errors, ); - filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord); - return handledError; + filterSubsequentPayloads( + exeContext.subsequentPayloads, + itemPath, + asyncPayloadRecord, + ); + return null; }) .then( (value) => [value], (error) => { asyncPayloadRecord.errors.push(error); - filterSubsequentPayloads(exeContext, path, asyncPayloadRecord); + filterSubsequentPayloads( + exeContext.subsequentPayloads, + path, + asyncPayloadRecord, + ); return null; }, ); @@ -1910,20 +1960,28 @@ async function executeStreamIteratorItem( asyncPayloadRecord: StreamRecord, itemPath: Path, ): Promise> { - let item; + let iteration: IteratorResult; try { - const { value, done } = await iterator.next(); - if (done) { - asyncPayloadRecord.setIsCompletedIterator(); - return { done, value: undefined }; - } - item = value; + iteration = await iterator.next(); } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); - const value = handleFieldError(error, itemType, asyncPayloadRecord.errors); + addError( + rawError, + fieldNodes, + itemType, + itemPath, + asyncPayloadRecord.errors, + ); // don't continue if iterator throws - return { done: true, value }; + return { done: true, value: null }; } + + const { done, value: item } = iteration; + + if (done) { + asyncPayloadRecord.setIsCompletedIterator(); + return { done, value: undefined }; + } + let completedItem; try { completedItem = completeValue( @@ -1938,22 +1996,36 @@ async function executeStreamIteratorItem( if (isPromise(completedItem)) { completedItem = completedItem.then(undefined, (rawError) => { - const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); - const handledError = handleFieldError( - error, + addError( + rawError, + fieldNodes, itemType, + itemPath, asyncPayloadRecord.errors, ); - filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord); - return handledError; + filterSubsequentPayloads( + exeContext.subsequentPayloads, + itemPath, + asyncPayloadRecord, + ); + return null; }); } return { done: false, value: completedItem }; } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); - const value = handleFieldError(error, itemType, asyncPayloadRecord.errors); - filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord); - return { done: false, value }; + addError( + rawError, + fieldNodes, + itemType, + itemPath, + asyncPayloadRecord.errors, + ); + filterSubsequentPayloads( + exeContext.subsequentPayloads, + itemPath, + asyncPayloadRecord, + ); + return { done: false, value: null }; } } @@ -1981,7 +2053,7 @@ async function executeStreamIterator( exeContext, }); - let iteration; + let iteration: IteratorResult; try { // eslint-disable-next-line no-await-in-loop iteration = await executeStreamIteratorItem( @@ -1995,7 +2067,11 @@ async function executeStreamIterator( ); } catch (error) { asyncPayloadRecord.errors.push(error); - filterSubsequentPayloads(exeContext, path, asyncPayloadRecord); + filterSubsequentPayloads( + exeContext.subsequentPayloads, + path, + asyncPayloadRecord, + ); asyncPayloadRecord.addItems(null); // entire stream has errored and bubbled upwards if (iterator?.return) { @@ -2014,7 +2090,11 @@ async function executeStreamIterator( (value) => [value], (error) => { asyncPayloadRecord.errors.push(error); - filterSubsequentPayloads(exeContext, path, asyncPayloadRecord); + filterSubsequentPayloads( + exeContext.subsequentPayloads, + path, + asyncPayloadRecord, + ); return null; }, ); @@ -2033,12 +2113,12 @@ async function executeStreamIterator( } function filterSubsequentPayloads( - exeContext: ExecutionContext, + subsequentPayloads: Set, nullPath: Path, currentAsyncRecord: AsyncPayloadRecord | undefined, ): void { const nullPathArray = pathToArray(nullPath); - exeContext.subsequentPayloads.forEach((asyncRecord) => { + subsequentPayloads.forEach((asyncRecord) => { if (asyncRecord === currentAsyncRecord) { // don't remove payload from where error originates return; @@ -2055,20 +2135,20 @@ function filterSubsequentPayloads( // ignore error }); } - exeContext.subsequentPayloads.delete(asyncRecord); + subsequentPayloads.delete(asyncRecord); }); } function getCompletedIncrementalResults( - exeContext: ExecutionContext, + subsequentPayloads: Set, ): Array { const incrementalResults: Array = []; - for (const asyncPayloadRecord of exeContext.subsequentPayloads) { + for (const asyncPayloadRecord of subsequentPayloads) { const incrementalResult: IncrementalResult = {}; if (!asyncPayloadRecord.isCompleted) { continue; } - exeContext.subsequentPayloads.delete(asyncPayloadRecord); + subsequentPayloads.delete(asyncPayloadRecord); if (isStreamPayload(asyncPayloadRecord)) { const items = asyncPayloadRecord.items; if (asyncPayloadRecord.isCompletedIterator) { @@ -2094,7 +2174,7 @@ function getCompletedIncrementalResults( } function yieldSubsequentPayloads( - exeContext: ExecutionContext, + subsequentPayloads: Set, ): AsyncGenerator { let isDone = false; @@ -2105,17 +2185,15 @@ function yieldSubsequentPayloads( return { value: undefined, done: true }; } - await Promise.race( - Array.from(exeContext.subsequentPayloads).map((p) => p.promise), - ); + await Promise.race(Array.from(subsequentPayloads).map((p) => p.promise)); if (isDone) { // a different call to next has exhausted all payloads return { value: undefined, done: true }; } - const incremental = getCompletedIncrementalResults(exeContext); - const hasNext = exeContext.subsequentPayloads.size > 0; + const incremental = getCompletedIncrementalResults(subsequentPayloads); + const hasNext = subsequentPayloads.size > 0; if (!incremental.length && hasNext) { return next(); @@ -2133,7 +2211,7 @@ function yieldSubsequentPayloads( function returnStreamIterators() { const promises: Array>> = []; - exeContext.subsequentPayloads.forEach((asyncPayloadRecord) => { + subsequentPayloads.forEach((asyncPayloadRecord) => { if ( isStreamPayload(asyncPayloadRecord) && asyncPayloadRecord.iterator?.return