From ca567d0bc5181fe9ae054951f6f362e03da863f3 Mon Sep 17 00:00:00 2001
From: Yaacov Rydzinski <yaacovCR@gmail.com>
Date: Sun, 12 Jun 2022 16:57:54 +0300
Subject: [PATCH] createSourceEventStream: introduce named arguments and
 deprecate positional arguments

BACKPORT OF #3634

Deprecates the positional arguments to createSourceEventStream, to be removed in the next major version, in favor of named arguments.

Motivation:

1. aligns createSourceEventStream with the other exported entrypoints graphql, execute, and subscribe
2. allows simplification of mapSourceToResponse

suggested by @IvanGoncharov
---
 src/execution/__tests__/subscribe-test.ts | 61 ++++++++++++++++-
 src/execution/subscribe.ts                | 81 +++++++++++++----------
 2 files changed, 105 insertions(+), 37 deletions(-)

diff --git a/src/execution/__tests__/subscribe-test.ts b/src/execution/__tests__/subscribe-test.ts
index 54c5019ab0..e9ea0d0ace 100644
--- a/src/execution/__tests__/subscribe-test.ts
+++ b/src/execution/__tests__/subscribe-test.ts
@@ -1,4 +1,4 @@
-import { expect } from 'chai';
+import { assert, expect } from 'chai';
 import { describe, it } from 'mocha';
 
 import { expectJSON } from '../../__testUtils__/expectJSON';
@@ -377,6 +377,65 @@ describe('Subscription Initialization Phase', () => {
     );
   });
 
+  it('Deprecated: allows positional arguments to createSourceEventStream', async () => {
+    async function* fooGenerator() {
+      /* c8 ignore next 2 */
+      yield { foo: 'FooValue' };
+    }
+
+    const schema = new GraphQLSchema({
+      query: DummyQueryType,
+      subscription: new GraphQLObjectType({
+        name: 'Subscription',
+        fields: {
+          foo: { type: GraphQLString, subscribe: fooGenerator },
+        },
+      }),
+    });
+    const document = parse('subscription { foo }');
+
+    const eventStream = await createSourceEventStream(schema, document);
+    assert(isAsyncIterable(eventStream));
+  });
+
+  it('Deprecated: throws an error if document is missing when using positional arguments', async () => {
+    const document = parse('subscription { foo }');
+    const schema = new GraphQLSchema({
+      query: DummyQueryType,
+      subscription: new GraphQLObjectType({
+        name: 'Subscription',
+        fields: {
+          foo: { type: GraphQLString },
+        },
+      }),
+    });
+
+    // @ts-expect-error (schema must not be null)
+    (await expectPromise(createSourceEventStream(null, document))).toRejectWith(
+      'Expected null to be a GraphQL schema.',
+    );
+
+    (
+      await expectPromise(
+        createSourceEventStream(
+          // @ts-expect-error
+          undefined,
+          document,
+        ),
+      )
+    ).toRejectWith('Expected undefined to be a GraphQL schema.');
+
+    // @ts-expect-error (document must not be null)
+    (await expectPromise(createSourceEventStream(schema, null))).toRejectWith(
+      'Must provide document.',
+    );
+
+    // @ts-expect-error
+    (await expectPromise(createSourceEventStream(schema))).toRejectWith(
+      'Must provide document.',
+    );
+  });
+
   it('resolves to an error if schema does not support subscriptions', async () => {
     const schema = new GraphQLSchema({ query: DummyQueryType });
     const document = parse('subscription { unknownField }');
diff --git a/src/execution/subscribe.ts b/src/execution/subscribe.ts
index 91a8231538..8b20ec3374 100644
--- a/src/execution/subscribe.ts
+++ b/src/execution/subscribe.ts
@@ -58,26 +58,7 @@ export async function subscribe(
     'graphql@16 dropped long-deprecated support for positional arguments, please pass an object instead.',
   );
 
-  const {
-    schema,
-    document,
-    rootValue,
-    contextValue,
-    variableValues,
-    operationName,
-    fieldResolver,
-    subscribeFieldResolver,
-  } = args;
-
-  const resultOrStream = await createSourceEventStream(
-    schema,
-    document,
-    rootValue,
-    contextValue,
-    variableValues,
-    operationName,
-    subscribeFieldResolver,
-  );
+  const resultOrStream = await createSourceEventStream(args);
 
   if (!isAsyncIterable(resultOrStream)) {
     return resultOrStream;
@@ -91,19 +72,44 @@ export async function subscribe(
   // "ExecuteQuery" algorithm, for which `execute` is also used.
   const mapSourceToResponse = (payload: unknown) =>
     execute({
-      schema,
-      document,
+      ...args,
       rootValue: payload,
-      contextValue,
-      variableValues,
-      operationName,
-      fieldResolver,
     });
 
   // Map every source value to a ExecutionResult value as described above.
   return mapAsyncIterator(resultOrStream, mapSourceToResponse);
 }
 
+type BackwardsCompatibleArgs =
+  | [options: ExecutionArgs]
+  | [
+      schema: ExecutionArgs['schema'],
+      document: ExecutionArgs['document'],
+      rootValue?: ExecutionArgs['rootValue'],
+      contextValue?: ExecutionArgs['contextValue'],
+      variableValues?: ExecutionArgs['variableValues'],
+      operationName?: ExecutionArgs['operationName'],
+      subscribeFieldResolver?: ExecutionArgs['subscribeFieldResolver'],
+    ];
+
+function toNormalizedArgs(args: BackwardsCompatibleArgs): ExecutionArgs {
+  const firstArg = args[0];
+  if (firstArg && 'document' in firstArg) {
+    return firstArg;
+  }
+
+  return {
+    schema: firstArg,
+    // FIXME: when underlying TS bug fixed, see https://github.com/microsoft/TypeScript/issues/31613
+    document: args[1] as DocumentNode,
+    rootValue: args[2],
+    contextValue: args[3],
+    variableValues: args[4],
+    operationName: args[5],
+    subscribeFieldResolver: args[6],
+  };
+}
+
 /**
  * Implements the "CreateSourceEventStream" algorithm described in the
  * GraphQL specification, resolving the subscription source event stream.
@@ -132,6 +138,10 @@ export async function subscribe(
  * or otherwise separating these two steps. For more on this, see the
  * "Supporting Subscriptions at Scale" information in the GraphQL specification.
  */
+export async function createSourceEventStream(
+  args: ExecutionArgs,
+): Promise<AsyncIterable<unknown> | ExecutionResult>;
+/** @deprecated will be removed in next major version in favor of named arguments */
 export async function createSourceEventStream(
   schema: GraphQLSchema,
   document: DocumentNode,
@@ -140,22 +150,21 @@ export async function createSourceEventStream(
   variableValues?: Maybe<{ readonly [variable: string]: unknown }>,
   operationName?: Maybe<string>,
   subscribeFieldResolver?: Maybe<GraphQLFieldResolver<any, any>>,
-): Promise<AsyncIterable<unknown> | ExecutionResult> {
+): Promise<AsyncIterable<unknown> | ExecutionResult>;
+export async function createSourceEventStream(
+  ...rawArgs: BackwardsCompatibleArgs
+) {
+  const args = toNormalizedArgs(rawArgs);
+
+  const { schema, document, variableValues } = args;
+
   // If arguments are missing or incorrectly typed, this is an internal
   // developer mistake which should throw an early error.
   assertValidExecutionArguments(schema, document, variableValues);
 
   // If a valid execution context cannot be created due to incorrect arguments,
   // a "Response" with only errors is returned.
-  const exeContext = buildExecutionContext({
-    schema,
-    document,
-    rootValue,
-    contextValue,
-    variableValues,
-    operationName,
-    subscribeFieldResolver,
-  });
+  const exeContext = buildExecutionContext(args);
 
   // Return early errors if execution context failed.
   if (!('schema' in exeContext)) {