Repository: graphql/graphql-js (ts)

Issue:
Printing the @oneOf directive
It looks like currently the `printSchema` function doesn't have any handling for `@oneOf`.

I'm not sure if this is intentional or not, because I know printing directives has historically been a controversial topic, but my expectation would be that for directives that are part of the spec and have a corresponding option in the options when creating the type/field in code (eg `deprecationReason`/`@deprecated` ) would be printed when using  `printSchema`.

I'm happy to make a PR to add this, and it looks like it should be a pretty simple fix handled here: https://github.com/graphql/graphql-js/blob/main/src/utilities/printSchema.ts#L207C5-L207C5

Pull request description:
Add support for @oneOf directives in printSchema
fixes #3968 

Patch:
diff --git a/src/execution/IncrementalPublisher.ts b/src/execution/IncrementalPublisher.ts
index f8ac9365..760043c7 100644
--- a/src/execution/IncrementalPublisher.ts
+++ b/src/execution/IncrementalPublisher.ts
@@ -609,29 +609,29 @@ export class IncrementalPublisher {
     deferredGroupedFieldSetRecord: DeferredGroupedFieldSetRecord,
   ): IncrementalDeferResult {
     const { data, deferredFragmentRecords } = deferredGroupedFieldSetRecord;
-    let maxLength = deferredFragmentRecords[0].path.length;
-    let maxIndex = 0;
-    for (let i = 1; i < deferredFragmentRecords.length; i++) {
-      const deferredFragmentRecord = deferredFragmentRecords[i];
+    let maxLength: number | undefined;
+    let idWithLongestPath: string | undefined;
+    for (const deferredFragmentRecord of deferredFragmentRecords) {
+      const id = deferredFragmentRecord.id;
+      if (id === undefined) {
+        continue;
+      }
       const length = deferredFragmentRecord.path.length;
-      if (length > maxLength) {
+      if (maxLength === undefined || length > maxLength) {
         maxLength = length;
-        maxIndex = i;
+        idWithLongestPath = id;
       }
     }
-    const recordWithLongestPath = deferredFragmentRecords[maxIndex];
-    const longestPath = recordWithLongestPath.path;
-    const subPath = deferredGroupedFieldSetRecord.path.slice(
-      longestPath.length,
-    );
-    const id = recordWithLongestPath.id;
+    const subPath = deferredGroupedFieldSetRecord.path.slice(maxLength);
     const incrementalDeferResult: IncrementalDeferResult = {
       // safe because `data``is always defined when the record is completed
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       data: data!,
-      // safe because `id` is defined once the fragment has been released as pending
+      // safe because `id` is always defined once the fragment has been released
+      // as pending and at least one fragment has been completed, so must have been
+      // released as pending
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      id: id!,
+      id: idWithLongestPath!,
     };
 
     if (subPath.length > 0) {
@@ -666,10 +666,13 @@ export class IncrementalPublisher {
       return;
     }
 
-    if (subsequentResultRecord._pending.size === 0) {
-      this._push(subsequentResultRecord);
-    } else {
+    if (subsequentResultRecord._pending.size > 0) {
       this._introduce(subsequentResultRecord);
+    } else if (
+      subsequentResultRecord.deferredGroupedFieldSetRecords.size > 0 ||
+      subsequentResultRecord.children.size > 0
+    ) {
+      this._push(subsequentResultRecord);
     }
   }
 
diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts
index c987f64a..c4caffc6 100644
--- a/src/utilities/printSchema.ts
+++ b/src/utilities/printSchema.ts
@@ -204,7 +204,12 @@ function printInputObject(type: GraphQLInputObjectType): string {
   const fields = Object.values(type.getFields()).map(
     (f, i) => printDescription(f, '  ', !i) + '  ' + printInputValue(f),
   );
-  return printDescription(type) + `input ${type.name}` + printBlock(fields);
+  return (
+    printDescription(type) +
+    `input ${type.name}` +
+    (type.isOneOf ? ' @oneOf' : '') +
+    printBlock(fields)
+  );
 }
 
 function printFields(type: GraphQLObjectType | GraphQLInterfaceType): string {


Test patch:
diff --git a/src/execution/__tests__/defer-test.ts b/src/execution/__tests__/defer-test.ts
index 813f4651..261db67d 100644
--- a/src/execution/__tests__/defer-test.ts
+++ b/src/execution/__tests__/defer-test.ts
@@ -394,21 +394,13 @@ describe('Execute: defer directive', () => {
       }
     `);
     const result = await complete(document);
-    expectJSON(result).toDeepEqual([
-      {
-        data: {
-          hero: {
-            name: 'Luke',
-          },
+    expectJSON(result).toDeepEqual({
+      data: {
+        hero: {
+          name: 'Luke',
         },
-        pending: [{ id: '0', path: ['hero'], label: 'DeferTop' }],
-        hasNext: true,
-      },
-      {
-        completed: [{ id: '0' }],
-        hasNext: false,
       },
-    ]);
+    });
   });
   it('Can defer a fragment that is also not deferred, non-deferred fragment is first', async () => {
     const document = parse(`
@@ -423,21 +415,13 @@ describe('Execute: defer directive', () => {
       }
     `);
     const result = await complete(document);
-    expectJSON(result).toDeepEqual([
-      {
-        data: {
-          hero: {
-            name: 'Luke',
-          },
+    expectJSON(result).toDeepEqual({
+      data: {
+        hero: {
+          name: 'Luke',
         },
-        pending: [{ id: '0', path: ['hero'], label: 'DeferTop' }],
-        hasNext: true,
-      },
-      {
-        completed: [{ id: '0' }],
-        hasNext: false,
       },
-    ]);
+    });
   });
 
   it('Can defer an inline fragment', async () => {
@@ -481,19 +465,11 @@ describe('Execute: defer directive', () => {
       }
     `);
     const result = await complete(document);
-    expectJSON(result).toDeepEqual([
-      {
-        data: {
-          hero: {},
-        },
-        pending: [{ id: '0', path: ['hero'] }],
-        hasNext: true,
-      },
-      {
-        completed: [{ id: '0' }],
-        hasNext: false,
+    expectJSON(result).toDeepEqual({
+      data: {
+        hero: {},
       },
-    ]);
+    });
   });
 
   it('Can separately emit defer fragments with different labels with varying fields', async () => {
@@ -775,40 +751,18 @@ describe('Execute: defer directive', () => {
         data: { hero: { friends: [{}, {}, {}] } },
         pending: [
           { id: '0', path: ['hero', 'friends', 0] },
-          { id: '1', path: ['hero', 'friends', 0] },
-          { id: '2', path: ['hero', 'friends', 0] },
-          { id: '3', path: ['hero', 'friends', 0] },
-          { id: '4', path: ['hero', 'friends', 1] },
-          { id: '5', path: ['hero', 'friends', 1] },
-          { id: '6', path: ['hero', 'friends', 1] },
-          { id: '7', path: ['hero', 'friends', 1] },
-          { id: '8', path: ['hero', 'friends', 2] },
-          { id: '9', path: ['hero', 'friends', 2] },
-          { id: '10', path: ['hero', 'friends', 2] },
-          { id: '11', path: ['hero', 'friends', 2] },
+          { id: '1', path: ['hero', 'friends', 1] },
+          { id: '2', path: ['hero', 'friends', 2] },
         ],
         hasNext: true,
       },
       {
         incremental: [
           { data: { id: '2', name: 'Han' }, id: '0' },
-          { data: { id: '3', name: 'Leia' }, id: '4' },
-          { data: { id: '4', name: 'C-3PO' }, id: '8' },
-        ],
-        completed: [
-          { id: '1' },
-          { id: '2' },
-          { id: '3' },
-          { id: '5' },
-          { id: '6' },
-          { id: '7' },
-          { id: '9' },
-          { id: '10' },
-          { id: '11' },
-          { id: '0' },
-          { id: '4' },
-          { id: '8' },
+          { data: { id: '3', name: 'Leia' }, id: '1' },
+          { data: { id: '4', name: 'C-3PO' }, id: '2' },
         ],
+        completed: [{ id: '0' }, { id: '1' }, { id: '2' }],
         hasNext: false,
       },
     ]);
@@ -1494,21 +1448,13 @@ describe('Execute: defer directive', () => {
       }
     `);
     const result = await complete(document);
-    expectJSON(result).toDeepEqual([
-      {
-        data: {
-          hero: {
-            friends: [{ name: 'Han' }, { name: 'Leia' }, { name: 'C-3PO' }],
-          },
+    expectJSON(result).toDeepEqual({
+      data: {
+        hero: {
+          friends: [{ name: 'Han' }, { name: 'Leia' }, { name: 'C-3PO' }],
         },
-        pending: [{ id: '0', path: ['hero'] }],
-        hasNext: true,
       },
-      {
-        completed: [{ id: '0' }],
-        hasNext: false,
-      },
-    ]);
+    });
   });
 
   it('Deduplicates async iterable list fields', async () => {
@@ -1534,17 +1480,9 @@ describe('Execute: defer directive', () => {
         },
       },
     });
-    expectJSON(result).toDeepEqual([
-      {
-        data: { hero: { friends: [{ name: 'Han' }] } },
-        pending: [{ id: '0', path: ['hero'] }],
-        hasNext: true,
-      },
-      {
-        completed: [{ id: '0' }],
-        hasNext: false,
-      },
-    ]);
+    expectJSON(result).toDeepEqual({
+      data: { hero: { friends: [{ name: 'Han' }] } },
+    });
   });
 
   it('Deduplicates empty async iterable list fields', async () => {
@@ -1571,17 +1509,9 @@ describe('Execute: defer directive', () => {
         },
       },
     });
-    expectJSON(result).toDeepEqual([
-      {
-        data: { hero: { friends: [] } },
-        pending: [{ id: '0', path: ['hero'] }],
-        hasNext: true,
-      },
-      {
-        completed: [{ id: '0' }],
-        hasNext: false,
-      },
-    ]);
+    expectJSON(result).toDeepEqual({
+      data: { hero: { friends: [] } },
+    });
   });
 
   it('Does not deduplicate list fields with non-overlapping fields', async () => {
@@ -1655,17 +1585,9 @@ describe('Execute: defer directive', () => {
         friends: () => [],
       },
     });
-    expectJSON(result).toDeepEqual([
-      {
-        data: { hero: { friends: [] } },
-        pending: [{ id: '0', path: ['hero'] }],
-        hasNext: true,
-      },
-      {
-        completed: [{ id: '0' }],
-        hasNext: false,
-      },
-    ]);
+    expectJSON(result).toDeepEqual({
+      data: { hero: { friends: [] } },
+    });
   });
 
   it('Deduplicates null object fields', async () => {
@@ -1689,17 +1611,9 @@ describe('Execute: defer directive', () => {
         nestedObject: () => null,
       },
     });
-    expectJSON(result).toDeepEqual([
-      {
-        data: { hero: { nestedObject: null } },
-        pending: [{ id: '0', path: ['hero'] }],
-        hasNext: true,
-      },
-      {
-        completed: [{ id: '0' }],
-        hasNext: false,
-      },
-    ]);
+    expectJSON(result).toDeepEqual({
+      data: { hero: { nestedObject: null } },
+    });
   });
 
   it('Deduplicates promise object fields', async () => {
@@ -1722,17 +1636,9 @@ describe('Execute: defer directive', () => {
         nestedObject: () => Promise.resolve({ name: 'foo' }),
       },
     });
-    expectJSON(result).toDeepEqual([
-      {
-        data: { hero: { nestedObject: { name: 'foo' } } },
-        pending: [{ id: '0', path: ['hero'] }],
-        hasNext: true,
-      },
-      {
-        completed: [{ id: '0' }],
-        hasNext: false,
-      },
-    ]);
+    expectJSON(result).toDeepEqual({
+      data: { hero: { nestedObject: { name: 'foo' } } },
+    });
   });
 
   it('Handles errors thrown in deferred fragments', async () => {
diff --git a/src/execution/__tests__/stream-test.ts b/src/execution/__tests__/stream-test.ts
index 8970933c..6e1928f9 100644
--- a/src/execution/__tests__/stream-test.ts
+++ b/src/execution/__tests__/stream-test.ts
@@ -1780,33 +1780,29 @@ describe('Execute: stream directive', () => {
             nestedFriendList: [],
           },
         },
-        pending: [
-          { id: '0', path: ['nestedObject'] },
-          { id: '1', path: ['nestedObject', 'nestedFriendList'] },
-        ],
+        pending: [{ id: '0', path: ['nestedObject', 'nestedFriendList'] }],
         hasNext: true,
       },
       {
         incremental: [
           {
             items: [{ id: '1', name: 'Luke' }],
-            id: '1',
+            id: '0',
           },
         ],
-        completed: [{ id: '0' }],
         hasNext: true,
       },
       {
         incremental: [
           {
             items: [{ id: '2', name: 'Han' }],
-            id: '1',
+            id: '0',
           },
         ],
         hasNext: true,
       },
       {
-        completed: [{ id: '1' }],
+        completed: [{ id: '0' }],
         hasNext: false,
       },
     ]);
diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts
index 5b711664..78f793b1 100644
--- a/src/utilities/__tests__/printSchema-test.ts
+++ b/src/utilities/__tests__/printSchema-test.ts
@@ -509,6 +509,23 @@ describe('Type System Printer', () => {
     `);
   });
 
+  it('Print Input Type with @oneOf directive', () => {
+    const InputType = new GraphQLInputObjectType({
+      name: 'InputType',
+      isOneOf: true,
+      fields: {
+        int: { type: GraphQLInt },
+      },
+    });
+
+    const schema = new GraphQLSchema({ types: [InputType] });
+    expectPrintedSchema(schema).to.equal(dedent`
+      input InputType @oneOf {
+        int: Int
+      }
+    `);
+  });
+
   it('Custom Scalar', () => {
     const OddType = new GraphQLScalarType({ name: 'Odd' });