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 | 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, subscribeFieldResolver?: Maybe>, -): Promise | ExecutionResult> { +): Promise | 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)) {