diff --git a/src/execution/__tests__/subscribe-test.ts b/src/execution/__tests__/subscribe-test.ts index 004429630e..097535259d 100644 --- a/src/execution/__tests__/subscribe-test.ts +++ b/src/execution/__tests__/subscribe-test.ts @@ -256,6 +256,38 @@ describe('Subscription Initialization Phase', () => { await subscription.return(); }); + it('uses a custom default subscribeFieldResolver', async () => { + const schema = new GraphQLSchema({ + query: DummyQueryType, + subscription: new GraphQLObjectType({ + name: 'Subscription', + fields: { + foo: { type: GraphQLString }, + }, + }), + }); + + async function* fooGenerator() { + yield { foo: 'FooValue' }; + } + + const subscription = await subscribe({ + schema, + document: parse('subscription { foo }'), + rootValue: { customFoo: fooGenerator }, + subscribeFieldResolver: (root) => root.customFoo(), + }); + invariant(isAsyncIterable(subscription)); + + expect(await subscription.next()).to.deep.equal({ + done: false, + value: { data: { foo: 'FooValue' } }, + }); + + // Close subscription + await subscription.return(); + }); + it('should only resolve the first field of invalid multi-field', async () => { async function* fooGenerator() { yield { foo: 'FooValue' }; diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 26342ed8ef..b0265567b8 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -114,6 +114,7 @@ export interface ExecutionContext { variableValues: { [variable: string]: unknown }; fieldResolver: GraphQLFieldResolver; typeResolver: GraphQLTypeResolver; + subscribeFieldResolver: GraphQLFieldResolver; errors: Array; } @@ -151,6 +152,7 @@ export interface ExecutionArgs { operationName?: Maybe; fieldResolver?: Maybe>; typeResolver?: Maybe>; + subscribeFieldResolver?: Maybe>; } /** @@ -164,46 +166,30 @@ export interface ExecutionArgs { * a GraphQLError will be thrown immediately explaining the invalid input. */ export function execute(args: ExecutionArgs): PromiseOrValue { - const { - schema, - document, - rootValue, - contextValue, - variableValues, - operationName, - fieldResolver, - typeResolver, - } = args; + const { schema, document, variableValues } = args; // If arguments are missing or incorrect, throw an 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, - fieldResolver, - typeResolver, - ); + const exeContext = buildExecutionContext(args); // Return early errors if execution context failed. if (!('schema' in exeContext)) { return { errors: exeContext }; } - // Return a Promise that will eventually resolve to the data described by - // The "Response" section of the GraphQL specification. - // + // Return data or a Promise that will eventually resolve to the data described + // by the "Response" section of the GraphQL specification. + // If errors are encountered while executing a GraphQL field, only that // field and its descendants will be omitted, and sibling fields will still // be executed. An execution which encounters errors will still result in a // resolved Promise. - const data = executeOperation(exeContext, exeContext.operation, rootValue); + const data = executeQueryOrMutationRootFields(exeContext); + + // Return the response. return buildResponse(exeContext, data); } @@ -271,15 +257,20 @@ export function assertValidExecutionArguments( * @internal */ export function buildExecutionContext( - schema: GraphQLSchema, - document: DocumentNode, - rootValue: unknown, - contextValue: unknown, - rawVariableValues: Maybe<{ readonly [variable: string]: unknown }>, - operationName: Maybe, - fieldResolver: Maybe>, - typeResolver?: Maybe>, + args: ExecutionArgs, ): ReadonlyArray | ExecutionContext { + const { + schema, + document, + rootValue, + contextValue, + variableValues: rawVariableValues, + operationName, + fieldResolver, + typeResolver, + subscribeFieldResolver, + } = args; + let operation: OperationDefinitionNode | undefined; const fragments: ObjMap = Object.create(null); for (const definition of document.definitions) { @@ -334,19 +325,19 @@ export function buildExecutionContext( variableValues: coercedVariableValues.coerced, fieldResolver: fieldResolver ?? defaultFieldResolver, typeResolver: typeResolver ?? defaultTypeResolver, + subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, errors: [], }; } /** - * Implements the "Executing operations" section of the spec. + * Executes the root fields specified by query or mutation operation. */ -function executeOperation( +function executeQueryOrMutationRootFields( exeContext: ExecutionContext, - operation: OperationDefinitionNode, - rootValue: unknown, ): PromiseOrValue | null> { - const type = getOperationRootType(exeContext.schema, operation); + const { schema, operation, rootValue } = exeContext; + const type = getOperationRootType(schema, operation); const fields = collectFields( exeContext.schema, exeContext.fragments, diff --git a/src/execution/subscribe.ts b/src/execution/subscribe.ts index 957724eb7f..30d550c164 100644 --- a/src/execution/subscribe.ts +++ b/src/execution/subscribe.ts @@ -13,7 +13,11 @@ import type { GraphQLFieldResolver } from '../type/definition'; import { getOperationRootType } from '../utilities/getOperationRootType'; -import type { ExecutionResult, ExecutionContext } from './execute'; +import type { + ExecutionArgs, + ExecutionResult, + ExecutionContext, +} from './execute'; import { collectFields } from './collectFields'; import { getArgumentValues } from './values'; import { @@ -25,16 +29,16 @@ import { } from './execute'; import { mapAsyncIterator } from './mapAsyncIterator'; -export interface SubscriptionArgs { - schema: GraphQLSchema; - document: DocumentNode; - rootValue?: unknown; - contextValue?: unknown; - variableValues?: Maybe<{ readonly [variable: string]: unknown }>; - operationName?: Maybe; - fieldResolver?: Maybe>; - subscribeFieldResolver?: Maybe>; -} +/** + * @deprecated use ExecutionArgs instead. + * + * ExecutionArgs has been broadened to include all properties + * within SubscriptionArgs. The SubscriptionArgs type is retained + * for backwards compatibility and will be removed in the next major + * version. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SubscriptionArgs extends ExecutionArgs {} /** * Implements the "Subscribe" algorithm described in the GraphQL specification. @@ -141,7 +145,7 @@ export async function createSourceEventStream( contextValue?: unknown, variableValues?: Maybe<{ readonly [variable: string]: unknown }>, operationName?: Maybe, - fieldResolver?: Maybe>, + subscribeFieldResolver?: Maybe>, ): Promise | ExecutionResult> { // If arguments are missing or incorrectly typed, this is an internal // developer mistake which should throw an early error. @@ -149,15 +153,15 @@ export async function createSourceEventStream( // If a valid execution context cannot be created due to incorrect arguments, // a "Response" with only errors is returned. - const exeContext = buildExecutionContext( + const exeContext = buildExecutionContext({ schema, document, rootValue, contextValue, variableValues, operationName, - fieldResolver, - ); + subscribeFieldResolver, + }); // Return early errors if execution context failed. if (!('schema' in exeContext)) { @@ -165,7 +169,7 @@ export async function createSourceEventStream( } try { - const eventStream = await executeSubscription(exeContext); + const eventStream = await executeSubscriptionRootField(exeContext); // Assert field returned an event stream, otherwise yield an error. if (!isAsyncIterable(eventStream)) { @@ -186,7 +190,7 @@ export async function createSourceEventStream( } } -async function executeSubscription( +async function executeSubscriptionRootField( exeContext: ExecutionContext, ): Promise { const { schema, fragments, operation, variableValues, rootValue } = @@ -228,7 +232,7 @@ async function executeSubscription( // Call the `subscribe()` resolver or the default resolver to produce an // AsyncIterable yielding raw payloads. - const resolveFn = fieldDef.subscribe ?? exeContext.fieldResolver; + const resolveFn = fieldDef.subscribe ?? exeContext.subscribeFieldResolver; const eventStream = await resolveFn(rootValue, args, contextValue, info); if (eventStream instanceof Error) {