Repository: graphql/graphql-js
Pull request #3634 (2022-06-10 04:10:15)

createSourceEventStream: introduce named arguments and deprecate positional arguments
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 

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


|  Name | Link |
|---------------------------------|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 4a8c0b55686dd80ce0c373fc93b28474167a60af |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/compassionate-pike-271cb3/deploys/62a2c428651a650008458844 |
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>

yaacovCR: To be backported to v16 with later PR to remove positional arguments in v17

Patch:
diff --git a/src/execution/subscribe.ts b/src/execution/subscribe.ts
index 9ff7cd21..74608d7d 100644
--- a/src/execution/subscribe.ts
+++ b/src/execution/subscribe.ts
@@ -54,60 +54,20 @@ export function subscribe(
 ): PromiseOrValue<
   AsyncGenerator<ExecutionResult, void, void> | ExecutionResult
 > {
-  const {
-    schema,
-    document,
-    rootValue,
-    contextValue,
-    variableValues,
-    operationName,
-    fieldResolver,
-    subscribeFieldResolver,
-  } = args;
-
-  const resultOrStream = createSourceEventStream(
-    schema,
-    document,
-    rootValue,
-    contextValue,
-    variableValues,
-    operationName,
-    subscribeFieldResolver,
-  );
+  const resultOrStream = createSourceEventStream(args);
 
   if (isPromise(resultOrStream)) {
     return resultOrStream.then((resolvedResultOrStream) =>
-      mapSourceToResponse(
-        schema,
-        document,
-        resolvedResultOrStream,
-        contextValue,
-        variableValues,
-        operationName,
-        fieldResolver,
-      ),
+      mapSourceToResponse(resolvedResultOrStream, args),
     );
   }
 
-  return mapSourceToResponse(
-    schema,
-    document,
-    resultOrStream,
-    contextValue,
-    variableValues,
-    operationName,
-    fieldResolver,
-  );
+  return mapSourceToResponse(resultOrStream, args);
 }
 
 function mapSourceToResponse(
-  schema: GraphQLSchema,
-  document: DocumentNode,
   resultOrStream: ExecutionResult | AsyncIterable<unknown>,
-  contextValue?: unknown,
-  variableValues?: Maybe<{ readonly [variable: string]: unknown }>,
-  operationName?: Maybe<string>,
-  fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>,
+  args: ExecutionArgs,
 ): PromiseOrValue<
   AsyncGenerator<ExecutionResult, void, void> | ExecutionResult
 > {
@@ -123,17 +83,42 @@ function mapSourceToResponse(
   // "ExecuteQuery" algorithm, for which `execute` is also used.
   return mapAsyncIterator(resultOrStream, (payload: unknown) =>
     execute({
-      schema,
-      document,
+      ...args,
       rootValue: payload,
-      contextValue,
-      variableValues,
-      operationName,
-      fieldResolver,
     }),
   );
 }
 
+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 ('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.
@@ -162,6 +147,10 @@ function mapSourceToResponse(
  * or otherwise separating these two steps. For more on this, see the
  * "Supporting Subscriptions at Scale" information in the GraphQL specification.
  */
+export function createSourceEventStream(
+  args: ExecutionArgs,
+): PromiseOrValue<AsyncIterable<unknown> | ExecutionResult>;
+/** @deprecated will be removed in next major version in favor of named arguments */
 export function createSourceEventStream(
   schema: GraphQLSchema,
   document: DocumentNode,
@@ -170,7 +159,18 @@ export function createSourceEventStream(
   variableValues?: Maybe<{ readonly [variable: string]: unknown }>,
   operationName?: Maybe<string>,
   subscribeFieldResolver?: Maybe<GraphQLFieldResolver<any, any>>,
-): PromiseOrValue<AsyncIterable<unknown> | ExecutionResult> {
+): PromiseOrValue<AsyncIterable<unknown> | ExecutionResult>;
+export function createSourceEventStream(...rawArgs: BackwardsCompatibleArgs) {
+  const {
+    schema,
+    document,
+    rootValue,
+    contextValue,
+    variableValues,
+    operationName,
+    subscribeFieldResolver,
+  } = toNormalizedArgs(rawArgs);
+
   // If arguments are missing or incorrectly typed, this is an internal
   // developer mistake which should throw an early error.
   assertValidExecutionArguments(schema, document, variableValues);


Test patch:
diff --git a/src/execution/__tests__/subscribe-test.ts b/src/execution/__tests__/subscribe-test.ts
index 03e3da28..b1ca7011 100644
--- a/src/execution/__tests__/subscribe-test.ts
+++ b/src/execution/__tests__/subscribe-test.ts
@@ -191,7 +191,7 @@ function subscribeWithBadFn(
 
   return expectEqualPromisesOrValues(
     subscribe({ schema, document }),
-    createSourceEventStream(schema, document),
+    createSourceEventStream({ schema, document }),
   );
 }
 
@@ -421,6 +421,49 @@ describe('Subscription Initialization Phase', () => {
     expect(() => subscribe({ schema })).to.throw('Must provide document.');
   });
 
+  it('Deprecated: allows positional arguments to createSourceEventStream', () => {
+    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 = createSourceEventStream(schema, document);
+    assert(isAsyncIterable(eventStream));
+  });
+
+  it('Deprecated: throws an error if document is missing when using positional arguments', async () => {
+    const schema = new GraphQLSchema({
+      query: DummyQueryType,
+      subscription: new GraphQLObjectType({
+        name: 'Subscription',
+        fields: {
+          foo: { type: GraphQLString },
+        },
+      }),
+    });
+
+    // @ts-expect-error
+    expect(() => createSourceEventStream(schema, null)).to.throw(
+      'Must provide document.',
+    );
+
+    // @ts-expect-error
+    expect(() => createSourceEventStream(schema)).to.throw(
+      '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 }');