Repository: graphql/graphql-js
Pull request #3749 (2022-09-22 23:08:59)

polish: add additional test
note that filtering is still required for synchronous error bubbling with stream.

With defer, patches are not executed if the initial field set execution fails, but with stream, the initial items (perhaps of zero length) will execute and will create the stream record.

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *compassionate-pike-271cb3* processing.


|  Name | Link |
|---------------------------------|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 7e95f423d0bc9cb05ee3b0e3a7b3143ff8dbcad9 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/compassionate-pike-271cb3/deploys/632ceb0c956bdb000829c657 |
github-actions[bot]: Hi @yaacovCR, I'm @github-actions bot happy to help you with this PR 👋

<details>
<summary> Supported commands </summary>

Please post this commands in separate comments and only one per comment:
* `@github-actions run-benchmark` - Run benchmark comparing base and merge commits for this PR
* `@github-actions publish-pr-on-npm` - Build package from this PR and publish it on NPM
</details>


Patch:
diff --git a/src/execution/execute.ts b/src/execution/execute.ts
index 486a80fe..5e19e662 100644
--- a/src/execution/execute.ts
+++ b/src/execution/execute.ts
@@ -1027,6 +1027,7 @@ async function completeAsyncIteratorValue(
                 pathToArray(fieldPath),
               );
               const handledError = handleFieldError(error, itemType, errors);
+              filterSubsequentPayloads(exeContext, fieldPath);
               return handledError;
             }),
           );
@@ -1040,6 +1041,7 @@ async function completeAsyncIteratorValue(
           fieldNodes,
           pathToArray(fieldPath),
         );
+        filterSubsequentPayloads(exeContext, fieldPath);
         handleFieldError(error, itemType, errors);
       }
     } catch (rawError) {


Test patch:
diff --git a/src/execution/__tests__/defer-test.ts b/src/execution/__tests__/defer-test.ts
index 95c54479..417be985 100644
--- a/src/execution/__tests__/defer-test.ts
+++ b/src/execution/__tests__/defer-test.ts
@@ -26,6 +26,10 @@ const friendType = new GraphQLObjectType({
   fields: {
     id: { type: GraphQLID },
     name: { type: GraphQLString },
+    promiseNonNullErrorField: {
+      type: new GraphQLNonNull(GraphQLString),
+      resolve: () => Promise.resolve(null),
+    },
   },
   name: 'Friend',
 });
@@ -65,6 +69,12 @@ const heroType = new GraphQLObjectType({
       type: new GraphQLList(friendType),
       resolve: () => friends,
     },
+    asyncFriends: {
+      type: new GraphQLList(friendType),
+      async *resolve() {
+        yield await Promise.resolve(friends[0]);
+      },
+    },
   },
   name: 'Hero',
 });
@@ -657,6 +667,38 @@ describe('Execute: defer directive', () => {
     ]);
   });
 
+  it('Filters deferred payloads when a list item returned by an async iterable is nulled', async () => {
+    const document = parse(`
+    query {
+      hero {
+        asyncFriends {
+          promiseNonNullErrorField
+          ...NameFragment @defer 
+        }
+      }
+    }
+    fragment NameFragment on Friend {
+      name
+    }
+  `);
+    const result = await complete(document);
+    expectJSON(result).toDeepEqual({
+      data: {
+        hero: {
+          asyncFriends: [null],
+        },
+      },
+      errors: [
+        {
+          message:
+            'Cannot return null for non-nullable field Friend.promiseNonNullErrorField.',
+          locations: [{ line: 5, column: 11 }],
+          path: ['hero', 'asyncFriends', 0, 'promiseNonNullErrorField'],
+        },
+      ],
+    });
+  });
+
   it('original execute function throws error if anything is deferred and everything else is sync', () => {
     const doc = `
     query Deferred {
diff --git a/src/execution/__tests__/stream-test.ts b/src/execution/__tests__/stream-test.ts
index 8378d320..c4836eea 100644
--- a/src/execution/__tests__/stream-test.ts
+++ b/src/execution/__tests__/stream-test.ts
@@ -979,6 +979,39 @@ describe('Execute: stream directive', () => {
       },
     });
   });
+  it('Filters payloads that are nulled by a later synchronous error', async () => {
+    const document = parse(`
+      query { 
+        nestedObject {
+          nestedFriendList @stream(initialCount: 0) {
+            name
+          }
+          nonNullScalarField
+        }
+      }
+    `);
+    const result = await complete(document, {
+      nestedObject: {
+        async *nestedFriendList() {
+          yield await Promise.resolve(friends[0]); /* c8 ignore start */
+        } /* c8 ignore stop */,
+        nonNullScalarField: () => null,
+      },
+    });
+    expectJSON(result).toDeepEqual({
+      errors: [
+        {
+          message:
+            'Cannot return null for non-nullable field NestedObject.nonNullScalarField.',
+          locations: [{ line: 7, column: 11 }],
+          path: ['nestedObject', 'nonNullScalarField'],
+        },
+      ],
+      data: {
+        nestedObject: null,
+      },
+    });
+  });
   it('Does not filter payloads when null error is in a different path', async () => {
     const document = parse(`
       query {