From fd40497b09961e6671b6d6ac46a7ed0b8d3f1a5b Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 21 Jun 2022 22:12:21 +0300 Subject: [PATCH 01/13] refactor: DRY with context building and *Impl functions The `execute`/`executeImpl` and `createSourceEventStream`/`createSourceEventStreamImpl` functions follow the same basic pattern of building the contet and using it to run a function. This PR extracts that pattern into a separate function. For good measure, the same pattern in applied to the soon-to-be-deprecated `subscribe` function. Hheavier refactoring is on the way from @IvanGoncharov (see https://github.com/graphql/graphql-js/pull/3639#pullrequestreview-1013806199), but in the meantime, this consolidates the common pattern without any breaking changes. --- src/execution/execute.ts | 45 ++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index cc3cbb9cbf..f2420c9969 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -165,6 +165,13 @@ export interface ExecutionArgs { * a GraphQLError will be thrown immediately explaining the invalid input. */ export function execute(args: ExecutionArgs): PromiseOrValue { + return prepareContextAndRunFn(args, executeImpl); +} + +function prepareContextAndRunFn( + args: ExecutionArgs, + fn: (exeContext: ExecutionContext) => T, +): ExecutionResult | T { // If a valid execution context cannot be created due to incorrect arguments, // a "Response" with only errors is returned. const exeContext = buildExecutionContext(args); @@ -174,7 +181,7 @@ export function execute(args: ExecutionArgs): PromiseOrValue { return { errors: exeContext }; } - return executeImpl(exeContext); + return fn(exeContext); } function executeImpl( @@ -1107,24 +1114,17 @@ export function subscribe( ): PromiseOrValue< AsyncGenerator | ExecutionResult > { - // If a valid execution context cannot be created due to incorrect arguments, - // a "Response" with only errors is returned. - const exeContext = buildExecutionContext(args); + return prepareContextAndRunFn(args, (exeContext: ExecutionContext) => { + const resultOrStream = createSourceEventStreamImpl(exeContext); - // Return early errors if execution context failed. - if (!('schema' in exeContext)) { - return { errors: exeContext }; - } - - const resultOrStream = createSourceEventStreamImpl(exeContext); - - if (isPromise(resultOrStream)) { - return resultOrStream.then((resolvedResultOrStream) => - mapSourceToResponse(exeContext, resolvedResultOrStream), - ); - } + if (isPromise(resultOrStream)) { + return resultOrStream.then((resolvedResultOrStream) => + mapSourceToResponse(exeContext, resolvedResultOrStream), + ); + } - return mapSourceToResponse(exeContext, resultOrStream); + return mapSourceToResponse(exeContext, resultOrStream); + }); } function mapSourceToResponse( @@ -1179,16 +1179,7 @@ function mapSourceToResponse( export function createSourceEventStream( args: ExecutionArgs, ): PromiseOrValue | ExecutionResult> { - // If a valid execution context cannot be created due to incorrect arguments, - // a "Response" with only errors is returned. - const exeContext = buildExecutionContext(args); - - // Return early errors if execution context failed. - if (!('schema' in exeContext)) { - return { errors: exeContext }; - } - - return createSourceEventStreamImpl(exeContext); + return prepareContextAndRunFn(args, createSourceEventStreamImpl); } function createSourceEventStreamImpl( From 0d297d91123c61663202f6f6c8b7023d3f791c52 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Wed, 22 Jun 2022 21:56:58 +0300 Subject: [PATCH 02/13] refactor: rename executeSubscription to executeSubscriptionRootField ...to executeSubscriptionRootField => because that's what it does! --- src/execution/execute.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index f2420c9969..0585ea24f1 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -1186,7 +1186,7 @@ function createSourceEventStreamImpl( exeContext: ExecutionContext, ): PromiseOrValue | ExecutionResult> { try { - const eventStream = executeSubscription(exeContext); + const eventStream = executeSubscriptionRootField(exeContext); if (isPromise(eventStream)) { return eventStream.then(undefined, (error) => ({ errors: [error] })); } @@ -1197,7 +1197,7 @@ function createSourceEventStreamImpl( } } -function executeSubscription( +function executeSubscriptionRootField( exeContext: ExecutionContext, ): PromiseOrValue> { const { schema, fragments, operation, variableValues, rootValue } = From 3e65a546d40a2b6f637560e949f9a5fe917ce08b Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Wed, 22 Jun 2022 22:00:52 +0300 Subject: [PATCH 03/13] refactor: rename executeOperation to executeQueryOrMutationRootFields ...because that's what it does! The execution of an operation returns a map of data/errors, not just the data. --- src/execution/execute.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 0585ea24f1..453a3d00bb 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -184,6 +184,9 @@ function prepareContextAndRunFn( return fn(exeContext); } +/** + * Implements the "Executing operations" section of the spec for queries and mutations. + */ function executeImpl( exeContext: ExecutionContext, ): PromiseOrValue { @@ -199,7 +202,7 @@ function executeImpl( // at which point we still log the error and null the parent field, which // in this case is the entire response. try { - const result = executeOperation(exeContext); + const result = executeQueryOrMutationRootFields(exeContext); if (isPromise(result)) { return result.then( (data) => buildResponse(data, exeContext.errors), @@ -343,10 +346,7 @@ function buildPerEventExecutionContext( }; } -/** - * Implements the "Executing operations" section of the spec. - */ -function executeOperation( +function executeQueryOrMutationRootFields( exeContext: ExecutionContext, ): PromiseOrValue> { const { operation, schema, fragments, variableValues, rootValue } = From a9a078cd35bada2e51851594fac8f2e2d6132aae Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 14 Jun 2022 13:06:52 +0300 Subject: [PATCH 04/13] execute: integrate subscriptions `execute` no longer runs the query algorithm for subscription operations. Rather, subscription operations are performed, as per the spec. `subscribe` is deprecated. --- integrationTests/ts/basic-test.ts | 11 +- src/__tests__/starWarsIntrospection-test.ts | 5 +- src/execution/__tests__/executor-test.ts | 19 +++- src/execution/__tests__/lists-test.ts | 4 +- src/execution/__tests__/nonnull-test.ts | 10 +- src/execution/__tests__/subscribe-test.ts | 75 ++++++++----- src/execution/execute.ts | 103 ++++++++++++------ src/graphql.ts | 14 ++- src/type/__tests__/enumType-test.ts | 17 ++- .../__tests__/buildASTSchema-test.ts | 2 + .../__tests__/buildClientSchema-test.ts | 3 + src/utilities/introspectionFromSchema.ts | 2 + 12 files changed, 187 insertions(+), 78 deletions(-) diff --git a/integrationTests/ts/basic-test.ts b/integrationTests/ts/basic-test.ts index a28bd840e7..bfbca75c3d 100644 --- a/integrationTests/ts/basic-test.ts +++ b/integrationTests/ts/basic-test.ts @@ -23,12 +23,13 @@ const queryType: GraphQLObjectType = new GraphQLObjectType({ const schema: GraphQLSchema = new GraphQLSchema({ query: queryType }); -const result: ExecutionResult = graphqlSync({ - schema, - source: ` +const result: ExecutionResult | AsyncGenerator = + graphqlSync({ + schema, + source: ` query helloWho($who: String){ test(who: $who) } `, - variableValues: { who: 'Dolly' }, -}); + variableValues: { who: 'Dolly' }, + }); diff --git a/src/__tests__/starWarsIntrospection-test.ts b/src/__tests__/starWarsIntrospection-test.ts index d637787c4a..840d246f64 100644 --- a/src/__tests__/starWarsIntrospection-test.ts +++ b/src/__tests__/starWarsIntrospection-test.ts @@ -1,6 +1,8 @@ -import { expect } from 'chai'; +import { assert, expect } from 'chai'; import { describe, it } from 'mocha'; +import { isAsyncIterable } from '../jsutils/isAsyncIterable'; + import { graphqlSync } from '../graphql'; import { StarWarsSchema } from './starWarsSchema'; @@ -8,6 +10,7 @@ import { StarWarsSchema } from './starWarsSchema'; function queryStarWars(source: string) { const result = graphqlSync({ schema: StarWarsSchema, source }); expect(Object.keys(result)).to.deep.equal(['data']); + assert(!isAsyncIterable(result)); return result.data; } diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 60b203dc05..173906bf64 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -4,6 +4,7 @@ import { describe, it } from 'mocha'; import { expectJSON } from '../../__testUtils__/expectJSON'; import { inspect } from '../../jsutils/inspect'; +import { isAsyncIterable } from '../../jsutils/isAsyncIterable'; import { Kind } from '../../language/kinds'; import { parse } from '../../language/parser'; @@ -833,7 +834,7 @@ describe('Execute: Handles basic execution tasks', () => { expect(result).to.deep.equal({ data: { c: 'd' } }); }); - it('uses the subscription schema for subscriptions', () => { + it('uses the subscription schema for subscriptions', async () => { const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Q', @@ -852,11 +853,22 @@ describe('Execute: Handles basic execution tasks', () => { query Q { a } subscription S { a } `); - const rootValue = { a: 'b', c: 'd' }; + const rootValue = { + // eslint-disable-next-line @typescript-eslint/require-await + async *a() { + yield { a: 'b' }; /* c8 ignore start */ + } /* c8 ignore stop */, + c: 'd', + }; const operationName = 'S'; const result = executeSync({ schema, document, rootValue, operationName }); - expect(result).to.deep.equal({ data: { a: 'b' } }); + + assert(isAsyncIterable(result)); + expect(await result.next()).to.deep.equal({ + value: { data: { a: 'b' } }, + done: false, + }); }); it('resolves to an error if schema does not support operation', () => { @@ -895,7 +907,6 @@ describe('Execute: Handles basic execution tasks', () => { expectJSON( executeSync({ schema, document, operationName: 'S' }), ).toDeepEqual({ - data: null, errors: [ { message: diff --git a/src/execution/__tests__/lists-test.ts b/src/execution/__tests__/lists-test.ts index 3fdd77ab56..d6c4ba8408 100644 --- a/src/execution/__tests__/lists-test.ts +++ b/src/execution/__tests__/lists-test.ts @@ -85,7 +85,9 @@ describe('Execute: Accepts async iterables as list value', () => { function completeObjectList( resolve: GraphQLFieldResolver<{ index: number }, unknown>, - ): PromiseOrValue { + ): PromiseOrValue< + ExecutionResult | AsyncGenerator + > { const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', diff --git a/src/execution/__tests__/nonnull-test.ts b/src/execution/__tests__/nonnull-test.ts index 427f2a64d6..ce3d0dcef5 100644 --- a/src/execution/__tests__/nonnull-test.ts +++ b/src/execution/__tests__/nonnull-test.ts @@ -1,8 +1,11 @@ -import { expect } from 'chai'; +import { assert, expect } from 'chai'; import { describe, it } from 'mocha'; import { expectJSON } from '../../__testUtils__/expectJSON'; +import { isAsyncIterable } from '../../jsutils/isAsyncIterable'; +import type { PromiseOrValue } from '../../jsutils/PromiseOrValue'; + import { parse } from '../../language/parser'; import { GraphQLNonNull, GraphQLObjectType } from '../../type/definition'; @@ -109,7 +112,9 @@ const schema = buildSchema(` function executeQuery( query: string, rootValue: unknown, -): ExecutionResult | Promise { +): PromiseOrValue< + ExecutionResult | AsyncGenerator +> { return execute({ schema, document: parse(query), rootValue }); } @@ -132,6 +137,7 @@ async function executeSyncAndAsync(query: string, rootValue: unknown) { rootValue, }); + assert(!isAsyncIterable(syncResult)); expectJSON(asyncResult).toDeepEqual(patchData(syncResult)); return syncResult; } diff --git a/src/execution/__tests__/subscribe-test.ts b/src/execution/__tests__/subscribe-test.ts index 5f256ca868..902a99b47b 100644 --- a/src/execution/__tests__/subscribe-test.ts +++ b/src/execution/__tests__/subscribe-test.ts @@ -15,7 +15,7 @@ import { GraphQLBoolean, GraphQLInt, GraphQLString } from '../../type/scalars'; import { GraphQLSchema } from '../../type/schema'; import type { ExecutionArgs, ExecutionResult } from '../execute'; -import { createSourceEventStream, subscribe } from '../execute'; +import { createSourceEventStream, execute, subscribe } from '../execute'; import { SimplePubSub } from './simplePubSub'; @@ -122,7 +122,7 @@ function createSubscription(pubsub: SimplePubSub) { }), }; - return subscribe({ schema: emailSchema, document, rootValue: data }); + return execute({ schema: emailSchema, document, rootValue: data }); } // TODO: consider adding this method to testUtils (with tests) @@ -150,22 +150,46 @@ function expectPromise(maybePromise: unknown) { }; } -// TODO: consider adding this method to testUtils (with tests) +// TODO: consider adding these method to testUtils (with tests) function expectEqualPromisesOrValues( - value1: PromiseOrValue, - value2: PromiseOrValue, + items: ReadonlyArray>, ): PromiseOrValue { - if (isPromise(value1)) { - assert(isPromise(value2)); - return Promise.all([value1, value2]).then((resolved) => { - expectJSON(resolved[1]).toDeepEqual(resolved[0]); - return resolved[0]; - }); + if (isPromise(items[0])) { + if (assertAllPromises(items)) { + return Promise.all(items).then(expectMatchingValues); + } + } else if (assertNoPromises(items)) { + return expectMatchingValues(items); } + /* c8 ignore next 3 */ + // Not reachable, all possible output types have been considered. + assert(false, 'Receives mixture of promises and values.'); +} - assert(!isPromise(value2)); - expectJSON(value2).toDeepEqual(value1); - return value1; +function expectMatchingValues(values: ReadonlyArray): T { + const remainingValues = values.slice(1); + for (const value of remainingValues) { + expectJSON(value).toDeepEqual(values[0]); + } + return values[0]; +} + +function assertAllPromises( + items: ReadonlyArray>, +): items is ReadonlyArray> { + for (const item of items) { + assert(isPromise(item)); + } + return true; +} + +function assertNoPromises( + items: ReadonlyArray>, +): items is ReadonlyArray { + for (const item of items) { + assert(!isPromise(item)); + } + return true; } const DummyQueryType = new GraphQLObjectType({ @@ -195,10 +219,11 @@ function subscribeWithBadFn( function subscribeWithBadArgs( args: ExecutionArgs, ): PromiseOrValue> { - return expectEqualPromisesOrValues( - subscribe(args), + return expectEqualPromisesOrValues([ + execute(args), createSourceEventStream(args), - ); + subscribe(args), + ]); } /* eslint-disable @typescript-eslint/require-await */ @@ -220,7 +245,7 @@ describe('Subscription Initialization Phase', () => { yield { foo: 'FooValue' }; } - const subscription = subscribe({ + const subscription = execute({ schema, document: parse('subscription { foo }'), rootValue: { foo: fooGenerator }, @@ -256,7 +281,7 @@ describe('Subscription Initialization Phase', () => { }), }); - const subscription = subscribe({ + const subscription = execute({ schema, document: parse('subscription { foo }'), }); @@ -294,7 +319,7 @@ describe('Subscription Initialization Phase', () => { }), }); - const promise = subscribe({ + const promise = execute({ schema, document: parse('subscription { foo }'), }); @@ -329,7 +354,7 @@ describe('Subscription Initialization Phase', () => { yield { foo: 'FooValue' }; } - const subscription = subscribe({ + const subscription = execute({ schema, document: parse('subscription { foo }'), rootValue: { customFoo: fooGenerator }, @@ -379,7 +404,7 @@ describe('Subscription Initialization Phase', () => { }), }); - const subscription = subscribe({ + const subscription = execute({ schema, document: parse('subscription { foo bar }'), }); @@ -530,7 +555,7 @@ describe('Subscription Initialization Phase', () => { } `); - // If we receive variables that cannot be coerced correctly, subscribe() will + // If we receive variables that cannot be coerced correctly, execute() will // resolve to an ExecutionResult that contains an informative error description. const result = subscribeWithBadArgs({ schema, document, variableValues }); expectJSON(result).toDeepEqual({ @@ -945,7 +970,7 @@ describe('Subscription Publish Phase', () => { }); const document = parse('subscription { newMessage }'); - const subscription = subscribe({ schema, document }); + const subscription = execute({ schema, document }); assert(isAsyncIterable(subscription)); expect(await subscription.next()).to.deep.equal({ @@ -1006,7 +1031,7 @@ describe('Subscription Publish Phase', () => { }); const document = parse('subscription { newMessage }'); - const subscription = subscribe({ schema, document }); + const subscription = execute({ schema, document }); assert(isAsyncIterable(subscription)); expect(await subscription.next()).to.deep.equal({ diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 453a3d00bb..4add12d6c3 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -154,6 +154,14 @@ export interface ExecutionArgs { subscribeFieldResolver?: Maybe>; } +type FieldsExecutor = ( + exeContext: ExecutionContext, + parentType: GraphQLObjectType, + sourceValue: unknown, + path: Path | undefined, + fields: Map>, +) => PromiseOrValue>; + /** * Implements the "Executing requests" section of the GraphQL specification. * @@ -164,8 +172,12 @@ export interface ExecutionArgs { * If the arguments to this function do not result in a legal execution context, * a GraphQLError will be thrown immediately explaining the invalid input. */ -export function execute(args: ExecutionArgs): PromiseOrValue { - return prepareContextAndRunFn(args, executeImpl); +export function execute( + args: ExecutionArgs, +): PromiseOrValue< + ExecutionResult | AsyncGenerator +> { + return prepareContextAndRunFn(args, executeOperation); } function prepareContextAndRunFn( @@ -185,10 +197,41 @@ function prepareContextAndRunFn( } /** - * Implements the "Executing operations" section of the spec for queries and mutations. + * Implements the "Executing operations" section of the spec. */ -function executeImpl( +function executeOperation( + exeContext: ExecutionContext, +): PromiseOrValue< + ExecutionResult | AsyncGenerator +> { + const operationType = exeContext.operation.operation; + + if (operationType === OperationTypeNode.QUERY) { + return executeQuery(exeContext); + } + + if (operationType === OperationTypeNode.MUTATION) { + return executeMutation(exeContext); + } + + return executeSubscription(exeContext); +} + +function executeQuery( exeContext: ExecutionContext, +): PromiseOrValue { + return executeQueryOrMutation(exeContext, executeFields); +} + +function executeMutation( + exeContext: ExecutionContext, +): PromiseOrValue { + return executeQueryOrMutation(exeContext, executeFieldsSerially); +} + +function executeQueryOrMutation( + exeContext: ExecutionContext, + fieldsExecutor: FieldsExecutor, ): PromiseOrValue { // Return a Promise that will eventually resolve to the data described by // The "Response" section of the GraphQL specification. @@ -202,7 +245,7 @@ function executeImpl( // at which point we still log the error and null the parent field, which // in this case is the entire response. try { - const result = executeQueryOrMutationRootFields(exeContext); + const result = executeQueryOrMutationRootFields(exeContext, fieldsExecutor); if (isPromise(result)) { return result.then( (data) => buildResponse(data, exeContext.errors), @@ -224,7 +267,9 @@ function executeImpl( * However, it guarantees to complete synchronously (or throw an error) assuming * that all field resolvers are also synchronous. */ -export function executeSync(args: ExecutionArgs): ExecutionResult { +export function executeSync( + args: ExecutionArgs, +): ExecutionResult | AsyncGenerator { const result = execute(args); // Assert that the execution was synchronous. @@ -348,6 +393,7 @@ function buildPerEventExecutionContext( function executeQueryOrMutationRootFields( exeContext: ExecutionContext, + fieldsExecutor: FieldsExecutor, ): PromiseOrValue> { const { operation, schema, fragments, variableValues, rootValue } = exeContext; @@ -368,22 +414,7 @@ function executeQueryOrMutationRootFields( ); const path = undefined; - switch (operation.operation) { - case OperationTypeNode.QUERY: - return executeFields(exeContext, rootType, rootValue, path, rootFields); - case OperationTypeNode.MUTATION: - return executeFieldsSerially( - exeContext, - rootType, - rootValue, - path, - rootFields, - ); - case OperationTypeNode.SUBSCRIPTION: - // TODO: deprecate `subscribe` and move all logic here - // Temporary solution until we finish merging execute and subscribe together - return executeFields(exeContext, rootType, rootValue, path, rootFields); - } + return fieldsExecutor(exeContext, rootType, rootValue, path, rootFields); } /** @@ -1108,23 +1139,31 @@ export const defaultFieldResolver: GraphQLFieldResolver = * yields a stream of ExecutionResults representing the response stream. * * Accepts either an object with named arguments, or individual arguments. + * + * @deprecated subscribe will be removed in v18; use execute instead */ export function subscribe( args: ExecutionArgs, ): PromiseOrValue< AsyncGenerator | ExecutionResult > { - return prepareContextAndRunFn(args, (exeContext: ExecutionContext) => { - const resultOrStream = createSourceEventStreamImpl(exeContext); + return execute(args); +} - if (isPromise(resultOrStream)) { - return resultOrStream.then((resolvedResultOrStream) => - mapSourceToResponse(exeContext, resolvedResultOrStream), - ); - } +function executeSubscription( + exeContext: ExecutionContext, +): PromiseOrValue< + ExecutionResult | AsyncGenerator +> { + const resultOrStream = createSourceEventStreamImpl(exeContext); - return mapSourceToResponse(exeContext, resultOrStream); - }); + if (isPromise(resultOrStream)) { + return resultOrStream.then((resolvedResultOrStream) => + mapSourceToResponse(exeContext, resolvedResultOrStream), + ); + } + + return mapSourceToResponse(exeContext, resultOrStream); } function mapSourceToResponse( @@ -1144,7 +1183,7 @@ function mapSourceToResponse( // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the // "ExecuteQuery" algorithm, for which `execute` is also used. return mapAsyncIterator(resultOrStream, (payload: unknown) => - executeImpl(buildPerEventExecutionContext(exeContext, payload)), + executeQuery(buildPerEventExecutionContext(exeContext, payload)), ); } diff --git a/src/graphql.ts b/src/graphql.ts index ffad9123c1..8cc5ff19fa 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -67,7 +67,9 @@ export interface GraphQLArgs { typeResolver?: Maybe>; } -export function graphql(args: GraphQLArgs): Promise { +export function graphql( + args: GraphQLArgs, +): Promise> { // Always return a Promise for a consistent API. return new Promise((resolve) => resolve(graphqlImpl(args))); } @@ -78,7 +80,9 @@ export function graphql(args: GraphQLArgs): Promise { * However, it guarantees to complete synchronously (or throw an error) assuming * that all field resolvers are also synchronous. */ -export function graphqlSync(args: GraphQLArgs): ExecutionResult { +export function graphqlSync( + args: GraphQLArgs, +): ExecutionResult | AsyncGenerator { const result = graphqlImpl(args); // Assert that the execution was synchronous. @@ -89,7 +93,11 @@ export function graphqlSync(args: GraphQLArgs): ExecutionResult { return result; } -function graphqlImpl(args: GraphQLArgs): PromiseOrValue { +function graphqlImpl( + args: GraphQLArgs, +): PromiseOrValue< + ExecutionResult | AsyncGenerator +> { const { schema, source, diff --git a/src/type/__tests__/enumType-test.ts b/src/type/__tests__/enumType-test.ts index 35e9f94b00..f6fbdbceec 100644 --- a/src/type/__tests__/enumType-test.ts +++ b/src/type/__tests__/enumType-test.ts @@ -1,8 +1,10 @@ -import { expect } from 'chai'; +import { assert, expect } from 'chai'; import { describe, it } from 'mocha'; import { expectJSON } from '../../__testUtils__/expectJSON'; +import { isAsyncIterable } from '../../jsutils/isAsyncIterable'; + import { introspectionFromSchema } from '../../utilities/introspectionFromSchema'; import { graphqlSync } from '../../graphql'; @@ -104,7 +106,10 @@ const SubscriptionType = new GraphQLObjectType({ subscribeToEnum: { type: ColorType, args: { color: { type: ColorType } }, - resolve: (_source, { color }) => color, + // eslint-disable-next-line @typescript-eslint/require-await + async *subscribe(_source, { color }) { + yield { subscribeToEnum: color }; /* c8 ignore start */ + } /* c8 ignore stop */, }, }, }); @@ -248,13 +253,15 @@ describe('Type System: Enum Values', () => { }); }); - it('accepts enum literals as input arguments to subscriptions', () => { + it('accepts enum literals as input arguments to subscriptions', async () => { const doc = 'subscription ($color: Color!) { subscribeToEnum(color: $color) }'; const result = executeQuery(doc, { color: 'GREEN' }); - expect(result).to.deep.equal({ - data: { subscribeToEnum: 'GREEN' }, + assert(isAsyncIterable(result)); + expect(await result.next()).to.deep.equal({ + value: { data: { subscribeToEnum: 'GREEN' } }, + done: false, }); }); diff --git a/src/utilities/__tests__/buildASTSchema-test.ts b/src/utilities/__tests__/buildASTSchema-test.ts index 435abc2d7a..e7945c80e5 100644 --- a/src/utilities/__tests__/buildASTSchema-test.ts +++ b/src/utilities/__tests__/buildASTSchema-test.ts @@ -3,6 +3,7 @@ import { describe, it } from 'mocha'; import { dedent } from '../../__testUtils__/dedent'; +import { isAsyncIterable } from '../../jsutils/isAsyncIterable'; import type { Maybe } from '../../jsutils/Maybe'; import type { ASTNode } from '../../language/ast'; @@ -76,6 +77,7 @@ describe('Schema Builder', () => { source: '{ str }', rootValue: { str: 123 }, }); + assert(!isAsyncIterable(result)); expect(result.data).to.deep.equal({ str: '123' }); }); diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts index 8c043f0e77..cb90567974 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -3,6 +3,8 @@ import { describe, it } from 'mocha'; import { dedent } from '../../__testUtils__/dedent'; +import { isAsyncIterable } from '../../jsutils/isAsyncIterable'; + import { assertEnumType, GraphQLEnumType, @@ -591,6 +593,7 @@ describe('Type System: build schema from introspection', () => { variableValues: { v: 'baz' }, }); + assert(!isAsyncIterable(result)); expect(result.data).to.deep.equal({ foo: 'bar' }); }); diff --git a/src/utilities/introspectionFromSchema.ts b/src/utilities/introspectionFromSchema.ts index 78c1b30244..5daed3fb28 100644 --- a/src/utilities/introspectionFromSchema.ts +++ b/src/utilities/introspectionFromSchema.ts @@ -1,4 +1,5 @@ import { invariant } from '../jsutils/invariant'; +import { isAsyncIterable } from '../jsutils/isAsyncIterable'; import { parse } from '../language/parser'; @@ -35,6 +36,7 @@ export function introspectionFromSchema( const document = parse(getIntrospectionQuery(optionsWithDefaults)); const result = executeSync({ schema, document }); + invariant(!isAsyncIterable(result)); invariant(result.errors == null && result.data != null); return result.data as any; } From 9fc1d93a6d22f26489362a126088f60cae15a16a Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Wed, 22 Jun 2022 05:32:14 +0300 Subject: [PATCH 05/13] introduce executeSubscriptionEvent to replace the old exported functionality from execute --- src/execution/__tests__/executor-test.ts | 15 ++++++++++++++- src/execution/execute.ts | 10 ++++++++++ src/execution/index.ts | 1 + src/index.ts | 1 + src/type/__tests__/enumType-test.ts | 1 + 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 173906bf64..8e8a16774a 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -20,7 +20,7 @@ import { import { GraphQLBoolean, GraphQLInt, GraphQLString } from '../../type/scalars'; import { GraphQLSchema } from '../../type/schema'; -import { execute, executeSync } from '../execute'; +import { execute, executeSubscriptionEvent, executeSync } from '../execute'; describe('Execute: Handles basic execution tasks', () => { it('executes arbitrary code', async () => { @@ -915,6 +915,19 @@ describe('Execute: Handles basic execution tasks', () => { }, ], }); + + expectJSON( + executeSubscriptionEvent({ schema, document, operationName: 'S' }), + ).toDeepEqual({ + data: null, + errors: [ + { + message: + 'Schema is not configured to execute subscription operation.', + locations: [{ line: 4, column: 7 }], + }, + ], + }); }); it('correct field ordering despite execution order', async () => { diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 4add12d6c3..d45a248271 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -1221,6 +1221,16 @@ export function createSourceEventStream( return prepareContextAndRunFn(args, createSourceEventStreamImpl); } +/** + * Implements the "ExecuteSubscriptionEvent" algorithm described in the + * GraphQL specification. + */ +export function executeSubscriptionEvent( + args: ExecutionArgs, +): PromiseOrValue { + return prepareContextAndRunFn(args, executeQuery); +} + function createSourceEventStreamImpl( exeContext: ExecutionContext, ): PromiseOrValue | ExecutionResult> { diff --git a/src/execution/index.ts b/src/execution/index.ts index b27a2c291c..289174d8b6 100644 --- a/src/execution/index.ts +++ b/src/execution/index.ts @@ -3,6 +3,7 @@ export { pathToArray as responsePathAsArray } from '../jsutils/Path'; export { createSourceEventStream, execute, + executeSubscriptionEvent, executeSync, defaultFieldResolver, defaultTypeResolver, diff --git a/src/index.ts b/src/index.ts index bce254f808..9e7c439e83 100644 --- a/src/index.ts +++ b/src/index.ts @@ -321,6 +321,7 @@ export { getDirectiveValues, subscribe, createSourceEventStream, + executeSubscriptionEvent, } from './execution/index'; export type { diff --git a/src/type/__tests__/enumType-test.ts b/src/type/__tests__/enumType-test.ts index f6fbdbceec..cf33869e3a 100644 --- a/src/type/__tests__/enumType-test.ts +++ b/src/type/__tests__/enumType-test.ts @@ -110,6 +110,7 @@ const SubscriptionType = new GraphQLObjectType({ async *subscribe(_source, { color }) { yield { subscribeToEnum: color }; /* c8 ignore start */ } /* c8 ignore stop */, + resolve: (_source, { color }) => color, }, }, }); From 60684ee0d28ea8b433c1ebb658e27a54a0f484c7 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 28 Jun 2022 19:17:07 +0300 Subject: [PATCH 06/13] export buildDocumentInfo and coerceVariableValues helpers --- src/execution/execute.ts | 91 ++++++++++++++++++++++++++++------------ src/execution/values.ts | 2 +- 2 files changed, 66 insertions(+), 27 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index d45a248271..4890201dc7 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -22,6 +22,7 @@ import type { FieldNode, FragmentDefinitionNode, OperationDefinitionNode, + VariableDefinitionNode, } from '../language/ast'; import { OperationTypeNode } from '../language/ast'; import { Kind } from '../language/kinds'; @@ -52,6 +53,7 @@ import { collectSubfields as _collectSubfields, } from './collectFields'; import { mapAsyncIterator } from './mapAsyncIterator'; +import type { CoercedVariableValues } from './values'; import { getArgumentValues, getVariableValues } from './values'; /* eslint-disable max-params */ @@ -101,15 +103,27 @@ const collectSubfields = memoize3( /** * Data that must be available at all points during query execution. * - * Namely, schema of the type system that is currently executing, - * and the fragments defined in the query document + * Namely, the fragments defined in the query document and the + * original operation. + * + * This interface is provided separately in the expectation that + * advanced clients may cache this information separately. */ -export interface ExecutionContext { - schema: GraphQLSchema; +export interface DocumentInfo { fragments: ObjMap; + operation: OperationDefinitionNode; +} + +/** + * Data that must be available at all points during query execution. + * + * Also included is data accumulated during the course of execution, + * i.e. errors. + */ +export interface ExecutionContext extends DocumentInfo { + schema: GraphQLSchema; rootValue: unknown; contextValue: unknown; - operation: OperationDefinitionNode; variableValues: { [variable: string]: unknown }; fieldResolver: GraphQLFieldResolver; typeResolver: GraphQLTypeResolver; @@ -184,7 +198,7 @@ function prepareContextAndRunFn( args: ExecutionArgs, fn: (exeContext: ExecutionContext) => T, ): ExecutionResult | T { - // If a valid execution context cannot be created due to incorrect arguments, + // If a valid execution info object cannot be created due to incorrect arguments, // a "Response" with only errors is returned. const exeContext = buildExecutionContext(args); @@ -318,6 +332,42 @@ export function buildExecutionContext( // If the schema used for execution is invalid, throw an error. assertValidSchema(schema); + const documentInfo = buildDocumentInfo(document, operationName); + + if (!('fragments' in documentInfo)) { + return documentInfo; + } + + const { fragments, operation } = documentInfo; + + const coercedVariableValues = coerceVariableValues( + schema, + operation.variableDefinitions, + rawVariableValues, + ); + + if (coercedVariableValues.errors) { + return coercedVariableValues.errors; + } + + return { + schema, + fragments, + rootValue, + contextValue, + operation, + variableValues: coercedVariableValues.coerced, + fieldResolver: fieldResolver ?? defaultFieldResolver, + typeResolver: typeResolver ?? defaultTypeResolver, + subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, + errors: [], + }; +} + +function buildDocumentInfo( + document: DocumentNode, + operationName: Maybe, +): ReadonlyArray | DocumentInfo { let operation: OperationDefinitionNode | undefined; const fragments: ObjMap = Object.create(null); for (const definition of document.definitions) { @@ -351,33 +401,22 @@ export function buildExecutionContext( return [new GraphQLError('Must provide an operation.')]; } + return { fragments, operation }; +} + +export function coerceVariableValues( + schema: GraphQLSchema, // FIXME: https://github.com/graphql/graphql-js/issues/2203 /* c8 ignore next */ - const variableDefinitions = operation.variableDefinitions ?? []; - - const coercedVariableValues = getVariableValues( + variableDefinitions: ReadonlyArray = [], + rawVariableValues?: Maybe<{ readonly [variable: string]: unknown }>, +): CoercedVariableValues { + return getVariableValues( schema, variableDefinitions, rawVariableValues ?? {}, { maxErrors: 50 }, ); - - if (coercedVariableValues.errors) { - return coercedVariableValues.errors; - } - - return { - schema, - fragments, - rootValue, - contextValue, - operation, - variableValues: coercedVariableValues.coerced, - fieldResolver: fieldResolver ?? defaultFieldResolver, - typeResolver: typeResolver ?? defaultTypeResolver, - subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, - errors: [], - }; } function buildPerEventExecutionContext( diff --git a/src/execution/values.ts b/src/execution/values.ts index 023e028109..7efbdc6b45 100644 --- a/src/execution/values.ts +++ b/src/execution/values.ts @@ -23,7 +23,7 @@ import { coerceInputValue } from '../utilities/coerceInputValue'; import { typeFromAST } from '../utilities/typeFromAST'; import { valueFromAST } from '../utilities/valueFromAST'; -type CoercedVariableValues = +export type CoercedVariableValues = | { errors: ReadonlyArray; coerced?: never } | { coerced: { [variable: string]: unknown }; errors?: never }; From a0d722502e7310243ba6da1d3b7d7b9f2e5998da Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 28 Jun 2022 21:37:44 +0300 Subject: [PATCH 07/13] introduce ExecutionInfo without internal execution context separates internal errors array from the remainder of the Execution data. obviates the need for the buildPerEventExecutionContext function => as described by the spec, all execution info is the same for execution of subscription events except for the root value, which is set as the payload the separate executionContext variable introduced is similar to the payloadContext that will be necessary to support defer/stream --- src/execution/execute.ts | 205 +++++++++++++++++++++++---------------- 1 file changed, 120 insertions(+), 85 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 4890201dc7..55d07a6a21 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -67,14 +67,14 @@ import { getArgumentValues, getVariableValues } from './values'; */ const collectSubfields = memoize3( ( - exeContext: ExecutionContext, + exeInfo: ExecutionInfo, returnType: GraphQLObjectType, fieldNodes: ReadonlyArray, ) => _collectSubfields( - exeContext.schema, - exeContext.fragments, - exeContext.variableValues, + exeInfo.schema, + exeInfo.fragments, + exeInfo.variableValues, returnType, fieldNodes, ), @@ -116,11 +116,8 @@ export interface DocumentInfo { /** * Data that must be available at all points during query execution. - * - * Also included is data accumulated during the course of execution, - * i.e. errors. */ -export interface ExecutionContext extends DocumentInfo { +export interface ExecutionInfo extends DocumentInfo { schema: GraphQLSchema; rootValue: unknown; contextValue: unknown; @@ -128,6 +125,12 @@ export interface ExecutionContext extends DocumentInfo { fieldResolver: GraphQLFieldResolver; typeResolver: GraphQLTypeResolver; subscribeFieldResolver: GraphQLFieldResolver; +} + +/** + * Internal data representing the current state of execution. + */ +export interface ExecutionContext { errors: Array; } @@ -169,6 +172,7 @@ export interface ExecutionArgs { } type FieldsExecutor = ( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, parentType: GraphQLObjectType, sourceValue: unknown, @@ -196,54 +200,53 @@ export function execute( function prepareContextAndRunFn( args: ExecutionArgs, - fn: (exeContext: ExecutionContext) => T, + fn: (exeInfo: ExecutionInfo) => T, ): ExecutionResult | T { // If a valid execution info object cannot be created due to incorrect arguments, // a "Response" with only errors is returned. - const exeContext = buildExecutionContext(args); + const exeInfo = buildExecutionInfo(args); // Return early errors if execution context failed. - if (!('schema' in exeContext)) { - return { errors: exeContext }; + if (!('schema' in exeInfo)) { + return { errors: exeInfo }; } - return fn(exeContext); + return fn(exeInfo); } /** * Implements the "Executing operations" section of the spec. */ function executeOperation( - exeContext: ExecutionContext, + exeInfo: ExecutionInfo, ): PromiseOrValue< ExecutionResult | AsyncGenerator > { - const operationType = exeContext.operation.operation; + const operationType = exeInfo.operation.operation; if (operationType === OperationTypeNode.QUERY) { - return executeQuery(exeContext); + return executeQuery(exeInfo); } if (operationType === OperationTypeNode.MUTATION) { - return executeMutation(exeContext); + return executeMutation(exeInfo); } - return executeSubscription(exeContext); + return executeSubscription(exeInfo); } -function executeQuery( - exeContext: ExecutionContext, -): PromiseOrValue { - return executeQueryOrMutation(exeContext, executeFields); +function executeQuery(exeInfo: ExecutionInfo): PromiseOrValue { + return executeQueryOrMutation(exeInfo, { errors: [] }, executeFields); } function executeMutation( - exeContext: ExecutionContext, + exeInfo: ExecutionInfo, ): PromiseOrValue { - return executeQueryOrMutation(exeContext, executeFieldsSerially); + return executeQueryOrMutation(exeInfo, { errors: [] }, executeFieldsSerially); } function executeQueryOrMutation( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, fieldsExecutor: FieldsExecutor, ): PromiseOrValue { @@ -259,7 +262,11 @@ function executeQueryOrMutation( // at which point we still log the error and null the parent field, which // in this case is the entire response. try { - const result = executeQueryOrMutationRootFields(exeContext, fieldsExecutor); + const result = executeQueryOrMutationRootFields( + exeInfo, + exeContext, + fieldsExecutor, + ); if (isPromise(result)) { return result.then( (data) => buildResponse(data, exeContext.errors), @@ -306,7 +313,7 @@ function buildResponse( } /** - * Constructs a ExecutionContext object from the arguments passed to + * Constructs a ExecutionInfo object from the arguments passed to * execute, which we will pass throughout the other execution methods. * * Throws a GraphQLError if a valid execution context cannot be created. @@ -314,9 +321,9 @@ function buildResponse( * TODO: consider no longer exporting this function * @internal */ -export function buildExecutionContext( +export function buildExecutionInfo( args: ExecutionArgs, -): ReadonlyArray | ExecutionContext { +): ReadonlyArray | ExecutionInfo { const { schema, document, @@ -360,7 +367,6 @@ export function buildExecutionContext( fieldResolver: fieldResolver ?? defaultFieldResolver, typeResolver: typeResolver ?? defaultTypeResolver, subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, - errors: [], }; } @@ -419,23 +425,12 @@ export function coerceVariableValues( ); } -function buildPerEventExecutionContext( - exeContext: ExecutionContext, - payload: unknown, -): ExecutionContext { - return { - ...exeContext, - rootValue: payload, - errors: [], - }; -} - function executeQueryOrMutationRootFields( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, fieldsExecutor: FieldsExecutor, ): PromiseOrValue> { - const { operation, schema, fragments, variableValues, rootValue } = - exeContext; + const { operation, schema, fragments, variableValues, rootValue } = exeInfo; const rootType = schema.getRootType(operation.operation); if (rootType == null) { throw new GraphQLError( @@ -453,7 +448,14 @@ function executeQueryOrMutationRootFields( ); const path = undefined; - return fieldsExecutor(exeContext, rootType, rootValue, path, rootFields); + return fieldsExecutor( + exeInfo, + exeContext, + rootType, + rootValue, + path, + rootFields, + ); } /** @@ -461,6 +463,7 @@ function executeQueryOrMutationRootFields( * for fields that must be executed serially. */ function executeFieldsSerially( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, parentType: GraphQLObjectType, sourceValue: unknown, @@ -472,6 +475,7 @@ function executeFieldsSerially( (results, [responseName, fieldNodes]) => { const fieldPath = addPath(path, responseName, parentType.name); const result = executeField( + exeInfo, exeContext, parentType, sourceValue, @@ -499,6 +503,7 @@ function executeFieldsSerially( * for fields that may be executed in parallel. */ function executeFields( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, parentType: GraphQLObjectType, sourceValue: unknown, @@ -511,6 +516,7 @@ function executeFields( for (const [responseName, fieldNodes] of fields.entries()) { const fieldPath = addPath(path, responseName, parentType.name); const result = executeField( + exeInfo, exeContext, parentType, sourceValue, @@ -544,6 +550,7 @@ function executeFields( * serialize scalars, or execute the sub-selection-set for objects. */ function executeField( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, parentType: GraphQLObjectType, source: unknown, @@ -551,16 +558,16 @@ function executeField( path: Path, ): PromiseOrValue { const fieldName = fieldNodes[0].name.value; - const fieldDef = exeContext.schema.getField(parentType, fieldName); + const fieldDef = exeInfo.schema.getField(parentType, fieldName); if (!fieldDef) { return; } const returnType = fieldDef.type; - const resolveFn = fieldDef.resolve ?? exeContext.fieldResolver; + const resolveFn = fieldDef.resolve ?? exeInfo.fieldResolver; const info = buildResolveInfo( - exeContext, + exeInfo, fieldDef, fieldNodes, parentType, @@ -575,23 +582,32 @@ function executeField( const args = getArgumentValues( fieldDef, fieldNodes[0], - exeContext.variableValues, + exeInfo.variableValues, ); // The resolve function's optional third argument is a context value that // is provided to every resolve function within an execution. It is commonly // used to represent an authenticated user, or request-specific caches. - const contextValue = exeContext.contextValue; + const contextValue = exeInfo.contextValue; const result = resolveFn(source, args, contextValue, info); let completed; if (isPromise(result)) { completed = result.then((resolved) => - completeValue(exeContext, returnType, fieldNodes, info, path, resolved), + completeValue( + exeInfo, + exeContext, + returnType, + fieldNodes, + info, + path, + resolved, + ), ); } else { completed = completeValue( + exeInfo, exeContext, returnType, fieldNodes, @@ -621,7 +637,7 @@ function executeField( * @internal */ export function buildResolveInfo( - exeContext: ExecutionContext, + exeInfo: ExecutionInfo, fieldDef: GraphQLField, fieldNodes: ReadonlyArray, parentType: GraphQLObjectType, @@ -635,11 +651,11 @@ export function buildResolveInfo( returnType: fieldDef.type, parentType, path, - schema: exeContext.schema, - fragments: exeContext.fragments, - rootValue: exeContext.rootValue, - operation: exeContext.operation, - variableValues: exeContext.variableValues, + schema: exeInfo.schema, + fragments: exeInfo.fragments, + rootValue: exeInfo.rootValue, + operation: exeInfo.operation, + variableValues: exeInfo.variableValues, }; } @@ -682,6 +698,7 @@ function handleFieldError( * value by executing all sub-selections. */ function completeValue( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, returnType: GraphQLOutputType, fieldNodes: ReadonlyArray, @@ -698,6 +715,7 @@ function completeValue( // if result is null. if (isNonNullType(returnType)) { const completed = completeValue( + exeInfo, exeContext, returnType.ofType, fieldNodes, @@ -721,6 +739,7 @@ function completeValue( // If field type is List, complete each item in the list with the inner type if (isListType(returnType)) { return completeListValue( + exeInfo, exeContext, returnType, fieldNodes, @@ -740,6 +759,7 @@ function completeValue( // runtime Object type and complete for that type. if (isAbstractType(returnType)) { return completeAbstractValue( + exeInfo, exeContext, returnType, fieldNodes, @@ -752,6 +772,7 @@ function completeValue( // If field type is Object, execute and complete all sub-selections. if (isObjectType(returnType)) { return completeObjectValue( + exeInfo, exeContext, returnType, fieldNodes, @@ -773,6 +794,7 @@ function completeValue( * recursively until all the results are completed. */ async function completeAsyncIteratorValue( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, itemType: GraphQLOutputType, fieldNodes: ReadonlyArray, @@ -796,6 +818,7 @@ async function completeAsyncIteratorValue( try { // TODO can the error checking logic be consolidated with completeListValue? const completedItem = completeValue( + exeInfo, exeContext, itemType, fieldNodes, @@ -832,6 +855,7 @@ async function completeAsyncIteratorValue( * inner type */ function completeListValue( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, returnType: GraphQLList, fieldNodes: ReadonlyArray, @@ -845,6 +869,7 @@ function completeListValue( const iterator = result[Symbol.asyncIterator](); return completeAsyncIteratorValue( + exeInfo, exeContext, itemType, fieldNodes, @@ -872,6 +897,7 @@ function completeListValue( if (isPromise(item)) { completedItem = item.then((resolved) => completeValue( + exeInfo, exeContext, itemType, fieldNodes, @@ -882,6 +908,7 @@ function completeListValue( ); } else { completedItem = completeValue( + exeInfo, exeContext, itemType, fieldNodes, @@ -937,6 +964,7 @@ function completeLeafValue( * of that value, then complete the value for that type. */ function completeAbstractValue( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, returnType: GraphQLAbstractType, fieldNodes: ReadonlyArray, @@ -944,17 +972,18 @@ function completeAbstractValue( path: Path, result: unknown, ): PromiseOrValue> { - const resolveTypeFn = returnType.resolveType ?? exeContext.typeResolver; - const contextValue = exeContext.contextValue; + const resolveTypeFn = returnType.resolveType ?? exeInfo.typeResolver; + const contextValue = exeInfo.contextValue; const runtimeType = resolveTypeFn(result, contextValue, info, returnType); if (isPromise(runtimeType)) { return runtimeType.then((resolvedRuntimeType) => completeObjectValue( + exeInfo, exeContext, ensureValidRuntimeType( resolvedRuntimeType, - exeContext, + exeInfo, returnType, fieldNodes, info, @@ -969,10 +998,11 @@ function completeAbstractValue( } return completeObjectValue( + exeInfo, exeContext, ensureValidRuntimeType( runtimeType, - exeContext, + exeInfo, returnType, fieldNodes, info, @@ -987,7 +1017,7 @@ function completeAbstractValue( function ensureValidRuntimeType( runtimeTypeName: unknown, - exeContext: ExecutionContext, + exeInfo: ExecutionInfo, returnType: GraphQLAbstractType, fieldNodes: ReadonlyArray, info: GraphQLResolveInfo, @@ -1015,7 +1045,7 @@ function ensureValidRuntimeType( ); } - const runtimeType = exeContext.schema.getType(runtimeTypeName); + const runtimeType = exeInfo.schema.getType(runtimeTypeName); if (runtimeType == null) { throw new GraphQLError( `Abstract type "${returnType.name}" was resolved to a type "${runtimeTypeName}" that does not exist inside the schema.`, @@ -1030,7 +1060,7 @@ function ensureValidRuntimeType( ); } - if (!exeContext.schema.isSubType(returnType, runtimeType)) { + if (!exeInfo.schema.isSubType(returnType, runtimeType)) { throw new GraphQLError( `Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`, { nodes: fieldNodes }, @@ -1044,6 +1074,7 @@ function ensureValidRuntimeType( * Complete an Object value by executing all sub-selections. */ function completeObjectValue( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, returnType: GraphQLObjectType, fieldNodes: ReadonlyArray, @@ -1052,13 +1083,13 @@ function completeObjectValue( result: unknown, ): PromiseOrValue> { // Collect sub-fields to execute to complete this value. - const subFieldNodes = collectSubfields(exeContext, returnType, fieldNodes); + const subFieldNodes = collectSubfields(exeInfo, returnType, fieldNodes); // If there is an isTypeOf predicate function, call it with the // current result. If isTypeOf returns false, then raise an error rather // than continuing execution. if (returnType.isTypeOf) { - const isTypeOf = returnType.isTypeOf(result, exeContext.contextValue, info); + const isTypeOf = returnType.isTypeOf(result, exeInfo.contextValue, info); if (isPromise(isTypeOf)) { return isTypeOf.then((resolvedIsTypeOf) => { @@ -1066,6 +1097,7 @@ function completeObjectValue( throw invalidReturnTypeError(returnType, result, fieldNodes); } return executeFields( + exeInfo, exeContext, returnType, result, @@ -1080,7 +1112,14 @@ function completeObjectValue( } } - return executeFields(exeContext, returnType, result, path, subFieldNodes); + return executeFields( + exeInfo, + exeContext, + returnType, + result, + path, + subFieldNodes, + ); } function invalidReturnTypeError( @@ -1190,23 +1229,23 @@ export function subscribe( } function executeSubscription( - exeContext: ExecutionContext, + exeInfo: ExecutionInfo, ): PromiseOrValue< ExecutionResult | AsyncGenerator > { - const resultOrStream = createSourceEventStreamImpl(exeContext); + const resultOrStream = createSourceEventStreamImpl(exeInfo); if (isPromise(resultOrStream)) { return resultOrStream.then((resolvedResultOrStream) => - mapSourceToResponse(exeContext, resolvedResultOrStream), + mapSourceToResponse(exeInfo, resolvedResultOrStream), ); } - return mapSourceToResponse(exeContext, resultOrStream); + return mapSourceToResponse(exeInfo, resultOrStream); } function mapSourceToResponse( - exeContext: ExecutionContext, + exeInfo: ExecutionInfo, resultOrStream: ExecutionResult | AsyncIterable, ): PromiseOrValue< AsyncGenerator | ExecutionResult @@ -1222,7 +1261,10 @@ function mapSourceToResponse( // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the // "ExecuteQuery" algorithm, for which `execute` is also used. return mapAsyncIterator(resultOrStream, (payload: unknown) => - executeQuery(buildPerEventExecutionContext(exeContext, payload)), + executeQuery({ + ...exeInfo, + rootValue: payload, + }), ); } @@ -1271,10 +1313,10 @@ export function executeSubscriptionEvent( } function createSourceEventStreamImpl( - exeContext: ExecutionContext, + exeInfo: ExecutionInfo, ): PromiseOrValue | ExecutionResult> { try { - const eventStream = executeSubscriptionRootField(exeContext); + const eventStream = executeSubscriptionRootField(exeInfo); if (isPromise(eventStream)) { return eventStream.then(undefined, (error) => ({ errors: [error] })); } @@ -1286,10 +1328,9 @@ function createSourceEventStreamImpl( } function executeSubscriptionRootField( - exeContext: ExecutionContext, + exeInfo: ExecutionInfo, ): PromiseOrValue> { - const { schema, fragments, operation, variableValues, rootValue } = - exeContext; + const { schema, fragments, operation, variableValues, rootValue } = exeInfo; const rootType = schema.getSubscriptionType(); if (rootType == null) { @@ -1318,13 +1359,7 @@ function executeSubscriptionRootField( } const path = addPath(undefined, responseName, rootType.name); - const info = buildResolveInfo( - exeContext, - fieldDef, - fieldNodes, - rootType, - path, - ); + const info = buildResolveInfo(exeInfo, fieldDef, fieldNodes, rootType, path); try { // Implements the "ResolveFieldEventStream" algorithm from GraphQL specification. @@ -1337,11 +1372,11 @@ function executeSubscriptionRootField( // The resolve function's optional third argument is a context value that // is provided to every resolve function within an execution. It is commonly // used to represent an authenticated user, or request-specific caches. - const contextValue = exeContext.contextValue; + const contextValue = exeInfo.contextValue; // Call the `subscribe()` resolver or the default resolver to produce an // AsyncIterable yielding raw payloads. - const resolveFn = fieldDef.subscribe ?? exeContext.subscribeFieldResolver; + const resolveFn = fieldDef.subscribe ?? exeInfo.subscribeFieldResolver; const result = resolveFn(rootValue, args, contextValue, info); if (isPromise(result)) { From b565e0ffe42846cc540dcad189abfe6324882654 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 28 Jun 2022 21:54:32 +0300 Subject: [PATCH 08/13] deprecate createSourceEventStream and executeSubscriptionEvent Advanced clients setting up services that only process subscriptions should be encouraged to create a custom ExecutionInfo object and use the corresponding internal functions that take ExecutionInfo objects rather than raw arguments. These functions are currently createSourceEventStreamImpl and executeQuery but when createSourceEventStream is removed, reateSourceEventStreamImpl will be renamed to createSourceEventStream --- src/execution/execute.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 55d07a6a21..524dd14b4a 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -235,7 +235,9 @@ function executeOperation( return executeSubscription(exeInfo); } -function executeQuery(exeInfo: ExecutionInfo): PromiseOrValue { +export function executeQuery( + exeInfo: ExecutionInfo, +): PromiseOrValue { return executeQueryOrMutation(exeInfo, { errors: [] }, executeFields); } @@ -1295,6 +1297,8 @@ function mapSourceToResponse( * different process or machine than the stateless GraphQL execution engine, * or otherwise separating these two steps. For more on this, see the * "Supporting Subscriptions at Scale" information in the GraphQL specification. + * + * @deprecated will be removed in the next major version */ export function createSourceEventStream( args: ExecutionArgs, @@ -1305,6 +1309,8 @@ export function createSourceEventStream( /** * Implements the "ExecuteSubscriptionEvent" algorithm described in the * GraphQL specification. + * + * @deprecated will be removed in the next major version */ export function executeSubscriptionEvent( args: ExecutionArgs, @@ -1312,7 +1318,7 @@ export function executeSubscriptionEvent( return prepareContextAndRunFn(args, executeQuery); } -function createSourceEventStreamImpl( +export function createSourceEventStreamImpl( exeInfo: ExecutionInfo, ): PromiseOrValue | ExecutionResult> { try { From f8e454bae3e7cb270d9162173c989a6cb7d137c2 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 28 Jun 2022 21:56:38 +0300 Subject: [PATCH 09/13] remove createSourceEventStream and executeSubscriptionEvent in favor of the corresponding functions that take ExecutionInfo arguments, i.e. createSourceEventStreamImpl and executeQuery. Note that since createSourceEventStream has been removed, createSourceEventStreamImpl can be renamed to createSourceEventStream, so that the recommended function name has not in fact changed, buf the function now takes an ExecutionInfo argument --- src/execution/__tests__/executor-test.ts | 15 +-------------- src/execution/__tests__/subscribe-test.ts | 3 +-- src/execution/execute.ts | 22 +--------------------- src/execution/index.ts | 2 -- src/index.ts | 2 -- 5 files changed, 3 insertions(+), 41 deletions(-) diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 8e8a16774a..173906bf64 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -20,7 +20,7 @@ import { import { GraphQLBoolean, GraphQLInt, GraphQLString } from '../../type/scalars'; import { GraphQLSchema } from '../../type/schema'; -import { execute, executeSubscriptionEvent, executeSync } from '../execute'; +import { execute, executeSync } from '../execute'; describe('Execute: Handles basic execution tasks', () => { it('executes arbitrary code', async () => { @@ -915,19 +915,6 @@ describe('Execute: Handles basic execution tasks', () => { }, ], }); - - expectJSON( - executeSubscriptionEvent({ schema, document, operationName: 'S' }), - ).toDeepEqual({ - data: null, - errors: [ - { - message: - 'Schema is not configured to execute subscription operation.', - locations: [{ line: 4, column: 7 }], - }, - ], - }); }); it('correct field ordering despite execution order', async () => { diff --git a/src/execution/__tests__/subscribe-test.ts b/src/execution/__tests__/subscribe-test.ts index 902a99b47b..4a9905ea32 100644 --- a/src/execution/__tests__/subscribe-test.ts +++ b/src/execution/__tests__/subscribe-test.ts @@ -15,7 +15,7 @@ import { GraphQLBoolean, GraphQLInt, GraphQLString } from '../../type/scalars'; import { GraphQLSchema } from '../../type/schema'; import type { ExecutionArgs, ExecutionResult } from '../execute'; -import { createSourceEventStream, execute, subscribe } from '../execute'; +import { execute, subscribe } from '../execute'; import { SimplePubSub } from './simplePubSub'; @@ -221,7 +221,6 @@ function subscribeWithBadArgs( ): PromiseOrValue> { return expectEqualPromisesOrValues([ execute(args), - createSourceEventStream(args), subscribe(args), ]); } diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 524dd14b4a..daf776afc7 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -1235,7 +1235,7 @@ function executeSubscription( ): PromiseOrValue< ExecutionResult | AsyncGenerator > { - const resultOrStream = createSourceEventStreamImpl(exeInfo); + const resultOrStream = createSourceEventStream(exeInfo); if (isPromise(resultOrStream)) { return resultOrStream.then((resolvedResultOrStream) => @@ -1297,28 +1297,8 @@ function mapSourceToResponse( * different process or machine than the stateless GraphQL execution engine, * or otherwise separating these two steps. For more on this, see the * "Supporting Subscriptions at Scale" information in the GraphQL specification. - * - * @deprecated will be removed in the next major version */ export function createSourceEventStream( - args: ExecutionArgs, -): PromiseOrValue | ExecutionResult> { - return prepareContextAndRunFn(args, createSourceEventStreamImpl); -} - -/** - * Implements the "ExecuteSubscriptionEvent" algorithm described in the - * GraphQL specification. - * - * @deprecated will be removed in the next major version - */ -export function executeSubscriptionEvent( - args: ExecutionArgs, -): PromiseOrValue { - return prepareContextAndRunFn(args, executeQuery); -} - -export function createSourceEventStreamImpl( exeInfo: ExecutionInfo, ): PromiseOrValue | ExecutionResult> { try { diff --git a/src/execution/index.ts b/src/execution/index.ts index 289174d8b6..662c2f3368 100644 --- a/src/execution/index.ts +++ b/src/execution/index.ts @@ -1,9 +1,7 @@ export { pathToArray as responsePathAsArray } from '../jsutils/Path'; export { - createSourceEventStream, execute, - executeSubscriptionEvent, executeSync, defaultFieldResolver, defaultTypeResolver, diff --git a/src/index.ts b/src/index.ts index 9e7c439e83..562033b2bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -320,8 +320,6 @@ export { getVariableValues, getDirectiveValues, subscribe, - createSourceEventStream, - executeSubscriptionEvent, } from './execution/index'; export type { From 075324654cd4373c4c4a037532a0420ed923e143 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 28 Jun 2022 22:03:10 +0300 Subject: [PATCH 10/13] remove deprecated subscribe function --- src/execution/__tests__/subscribe-test.ts | 65 +++-------------------- src/execution/execute.ts | 12 ----- src/execution/index.ts | 1 - src/index.ts | 1 - 4 files changed, 7 insertions(+), 72 deletions(-) diff --git a/src/execution/__tests__/subscribe-test.ts b/src/execution/__tests__/subscribe-test.ts index 4a9905ea32..5adad73503 100644 --- a/src/execution/__tests__/subscribe-test.ts +++ b/src/execution/__tests__/subscribe-test.ts @@ -14,8 +14,8 @@ import { GraphQLList, GraphQLObjectType } from '../../type/definition'; import { GraphQLBoolean, GraphQLInt, GraphQLString } from '../../type/scalars'; import { GraphQLSchema } from '../../type/schema'; -import type { ExecutionArgs, ExecutionResult } from '../execute'; -import { execute, subscribe } from '../execute'; +import type { ExecutionResult } from '../execute'; +import { execute } from '../execute'; import { SimplePubSub } from './simplePubSub'; @@ -150,48 +150,6 @@ function expectPromise(maybePromise: unknown) { }; } -// TODO: consider adding these method to testUtils (with tests) -function expectEqualPromisesOrValues( - items: ReadonlyArray>, -): PromiseOrValue { - if (isPromise(items[0])) { - if (assertAllPromises(items)) { - return Promise.all(items).then(expectMatchingValues); - } - } else if (assertNoPromises(items)) { - return expectMatchingValues(items); - } - /* c8 ignore next 3 */ - // Not reachable, all possible output types have been considered. - assert(false, 'Receives mixture of promises and values.'); -} - -function expectMatchingValues(values: ReadonlyArray): T { - const remainingValues = values.slice(1); - for (const value of remainingValues) { - expectJSON(value).toDeepEqual(values[0]); - } - return values[0]; -} - -function assertAllPromises( - items: ReadonlyArray>, -): items is ReadonlyArray> { - for (const item of items) { - assert(isPromise(item)); - } - return true; -} - -function assertNoPromises( - items: ReadonlyArray>, -): items is ReadonlyArray { - for (const item of items) { - assert(!isPromise(item)); - } - return true; -} - const DummyQueryType = new GraphQLObjectType({ name: 'Query', fields: { @@ -213,16 +171,7 @@ function subscribeWithBadFn( }); const document = parse('subscription { foo }'); - return subscribeWithBadArgs({ schema, document }); -} - -function subscribeWithBadArgs( - args: ExecutionArgs, -): PromiseOrValue> { - return expectEqualPromisesOrValues([ - execute(args), - subscribe(args), - ]); + return execute({ schema, document }); } /* eslint-disable @typescript-eslint/require-await */ @@ -424,7 +373,7 @@ describe('Subscription Initialization Phase', () => { const schema = new GraphQLSchema({ query: DummyQueryType }); const document = parse('subscription { unknownField }'); - const result = subscribeWithBadArgs({ schema, document }); + const result = execute({ schema, document }); expectJSON(result).toDeepEqual({ errors: [ { @@ -448,7 +397,7 @@ describe('Subscription Initialization Phase', () => { }); const document = parse('subscription { unknownField }'); - const result = subscribeWithBadArgs({ schema, document }); + const result = execute({ schema, document }); expectJSON(result).toDeepEqual({ errors: [ { @@ -471,7 +420,7 @@ describe('Subscription Initialization Phase', () => { }); // @ts-expect-error - expect(() => subscribeWithBadArgs({ schema, document: {} })).to.throw(); + expect(() => execute({ schema, document: {} })).to.throw(); }); it('throws an error if subscribe does not return an iterator', async () => { @@ -556,7 +505,7 @@ describe('Subscription Initialization Phase', () => { // If we receive variables that cannot be coerced correctly, execute() will // resolve to an ExecutionResult that contains an informative error description. - const result = subscribeWithBadArgs({ schema, document, variableValues }); + const result = execute({ schema, document, variableValues }); expectJSON(result).toDeepEqual({ errors: [ { diff --git a/src/execution/execute.ts b/src/execution/execute.ts index daf776afc7..ff07100c10 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -1217,19 +1217,7 @@ export const defaultFieldResolver: GraphQLFieldResolver = * * If the operation succeeded, the promise resolves to an AsyncIterator, which * yields a stream of ExecutionResults representing the response stream. - * - * Accepts either an object with named arguments, or individual arguments. - * - * @deprecated subscribe will be removed in v18; use execute instead */ -export function subscribe( - args: ExecutionArgs, -): PromiseOrValue< - AsyncGenerator | ExecutionResult -> { - return execute(args); -} - function executeSubscription( exeInfo: ExecutionInfo, ): PromiseOrValue< diff --git a/src/execution/index.ts b/src/execution/index.ts index 662c2f3368..2a7b5b4f46 100644 --- a/src/execution/index.ts +++ b/src/execution/index.ts @@ -5,7 +5,6 @@ export { executeSync, defaultFieldResolver, defaultTypeResolver, - subscribe, } from './execute'; export type { diff --git a/src/index.ts b/src/index.ts index 562033b2bc..ec12598556 100644 --- a/src/index.ts +++ b/src/index.ts @@ -319,7 +319,6 @@ export { getArgumentValues, getVariableValues, getDirectiveValues, - subscribe, } from './execution/index'; export type { From 6b5bddb5213ce97bc6951d3960836c5dd4f2971c Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 28 Jun 2022 22:05:56 +0300 Subject: [PATCH 11/13] remove unnecessary function `prepareContextAndRunFn` --- src/execution/execute.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index ff07100c10..8b7f367b79 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -195,13 +195,6 @@ export function execute( ): PromiseOrValue< ExecutionResult | AsyncGenerator > { - return prepareContextAndRunFn(args, executeOperation); -} - -function prepareContextAndRunFn( - args: ExecutionArgs, - fn: (exeInfo: ExecutionInfo) => T, -): ExecutionResult | T { // If a valid execution info object cannot be created due to incorrect arguments, // a "Response" with only errors is returned. const exeInfo = buildExecutionInfo(args); @@ -211,7 +204,7 @@ function prepareContextAndRunFn( return { errors: exeInfo }; } - return fn(exeInfo); + return executeOperation(exeInfo); } /** From 2bbae47bcba297101985123453bcdad15cc55977 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 28 Jun 2022 22:06:52 +0300 Subject: [PATCH 12/13] export additional helpers --- src/execution/execute.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 8b7f367b79..58fbefed72 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -210,7 +210,7 @@ export function execute( /** * Implements the "Executing operations" section of the spec. */ -function executeOperation( +export function executeOperation( exeInfo: ExecutionInfo, ): PromiseOrValue< ExecutionResult | AsyncGenerator @@ -234,7 +234,7 @@ export function executeQuery( return executeQueryOrMutation(exeInfo, { errors: [] }, executeFields); } -function executeMutation( +export function executeMutation( exeInfo: ExecutionInfo, ): PromiseOrValue { return executeQueryOrMutation(exeInfo, { errors: [] }, executeFieldsSerially); @@ -1211,7 +1211,7 @@ export const defaultFieldResolver: GraphQLFieldResolver = * If the operation succeeded, the promise resolves to an AsyncIterator, which * yields a stream of ExecutionResults representing the response stream. */ -function executeSubscription( +export function executeSubscription( exeInfo: ExecutionInfo, ): PromiseOrValue< ExecutionResult | AsyncGenerator From 3f69fb3581931a9e6ac459430f0ba5d01332121f Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 28 Jun 2022 22:16:57 +0300 Subject: [PATCH 13/13] allow customization of execution for subscription events and export the default --- src/execution/execute.ts | 27 +++++++++++++++++++++++---- src/execution/index.ts | 1 + src/index.ts | 1 + 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 58fbefed72..7ad93ed869 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -125,6 +125,10 @@ export interface ExecutionInfo extends DocumentInfo { fieldResolver: GraphQLFieldResolver; typeResolver: GraphQLTypeResolver; subscribeFieldResolver: GraphQLFieldResolver; + subscriptionEventExecutor: ( + exeInfo: ExecutionInfo, + payload: unknown, + ) => PromiseOrValue; } /** @@ -169,6 +173,12 @@ export interface ExecutionArgs { fieldResolver?: Maybe>; typeResolver?: Maybe>; subscribeFieldResolver?: Maybe>; + subscriptionEventExecutor?: Maybe< + ( + exeInfo: ExecutionInfo, + payload: unknown, + ) => PromiseOrValue + >; } type FieldsExecutor = ( @@ -329,6 +339,7 @@ export function buildExecutionInfo( fieldResolver, typeResolver, subscribeFieldResolver, + subscriptionEventExecutor, } = args; // If the schema used for execution is invalid, throw an error. @@ -362,6 +373,8 @@ export function buildExecutionInfo( fieldResolver: fieldResolver ?? defaultFieldResolver, typeResolver: typeResolver ?? defaultTypeResolver, subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, + subscriptionEventExecutor: + subscriptionEventExecutor ?? defaultSubscriptionEventExecutor, }; } @@ -1244,13 +1257,19 @@ function mapSourceToResponse( // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the // "ExecuteQuery" algorithm, for which `execute` is also used. return mapAsyncIterator(resultOrStream, (payload: unknown) => - executeQuery({ - ...exeInfo, - rootValue: payload, - }), + exeInfo.subscriptionEventExecutor(exeInfo, payload), ); } +export const defaultSubscriptionEventExecutor = ( + exeInfo: ExecutionInfo, + payload: unknown, +): PromiseOrValue => + executeQuery({ + ...exeInfo, + rootValue: payload, + }); + /** * Implements the "CreateSourceEventStream" algorithm described in the * GraphQL specification, resolving the subscription source event stream. diff --git a/src/execution/index.ts b/src/execution/index.ts index 2a7b5b4f46..f17e4bc5d7 100644 --- a/src/execution/index.ts +++ b/src/execution/index.ts @@ -5,6 +5,7 @@ export { executeSync, defaultFieldResolver, defaultTypeResolver, + defaultSubscriptionEventExecutor, } from './execute'; export type { diff --git a/src/index.ts b/src/index.ts index ec12598556..32a0b25974 100644 --- a/src/index.ts +++ b/src/index.ts @@ -315,6 +315,7 @@ export { executeSync, defaultFieldResolver, defaultTypeResolver, + defaultSubscriptionEventExecutor, responsePathAsArray, getArgumentValues, getVariableValues,