Skip to content

Commit

Permalink
createSourceEventStream: introduce named arguments and deprecate posi…
Browse files Browse the repository at this point in the history
…tional arguments (#3645)

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
  • Loading branch information
yaacovCR committed Jun 15, 2022
1 parent 1f8ba95 commit 59a73d6
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 37 deletions.
61 changes: 60 additions & 1 deletion 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';
Expand Down Expand Up @@ -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 }');
Expand Down
81 changes: 45 additions & 36 deletions src/execution/subscribe.ts
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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)) {
Expand Down

0 comments on commit 59a73d6

Please sign in to comment.