diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 60b203dc05..7d22cdac72 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -871,7 +871,6 @@ describe('Execute: Handles basic execution tasks', () => { expectJSON( executeSync({ schema, document, operationName: 'Q' }), ).toDeepEqual({ - data: null, errors: [ { message: 'Schema is not configured to execute query operation.', @@ -883,7 +882,6 @@ describe('Execute: Handles basic execution tasks', () => { expectJSON( executeSync({ schema, document, operationName: 'M' }), ).toDeepEqual({ - data: null, errors: [ { message: 'Schema is not configured to execute mutation operation.', @@ -895,7 +893,6 @@ describe('Execute: Handles basic execution tasks', () => { expectJSON( executeSync({ schema, document, operationName: 'S' }), ).toDeepEqual({ - data: null, errors: [ { message: diff --git a/src/execution/__tests__/subscribe-test.ts b/src/execution/__tests__/subscribe-test.ts index 4852d86ad3..3046afc883 100644 --- a/src/execution/__tests__/subscribe-test.ts +++ b/src/execution/__tests__/subscribe-test.ts @@ -424,6 +424,7 @@ describe('Subscription Initialization Phase', () => { { message: 'The subscription field "unknownField" is not defined.', locations: [{ line: 1, column: 16 }], + path: ['unknownField'], }, ], }); diff --git a/src/execution/execute.ts b/src/execution/execute.ts index be0323ca76..54aa300b25 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -165,43 +165,38 @@ export interface ExecutionArgs { * a GraphQLError will be thrown immediately explaining the invalid input. */ export function execute(args: ExecutionArgs): PromiseOrValue { - // If a valid execution context cannot be created due to incorrect arguments, + // If a valid ExecutableRequest cannot be created due to incorrect arguments, // a "Response" with only errors is returned. - const exeContext = buildExecutionContext(args); + const makeExecutableRequestReturn = makeExecutableRequest( + args.schema, + args.document, + args.operationName, + { + fieldResolver: args.fieldResolver, + typeResolver: args.typeResolver, + subscribeFieldResolver: args.subscribeFieldResolver, + } + ); - // Return early errors if execution context failed. - if (!('schema' in exeContext)) { - return { errors: exeContext }; + if (makeExecutableRequestReturn.errors !== undefined) { + return makeExecutableRequestReturn; } + const executableRequest = makeExecutableRequestReturn.result; - // Return 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. - // - // Errors from sub-fields of a NonNull type may propagate to the top level, - // at which point we still log the error and null the parent field, which - // in this case is the entire response. - try { - const { operation } = exeContext; - const result = executeOperation(exeContext, operation); - if (isPromise(result)) { - return result.then( - (data) => buildResponse(data, exeContext.errors), - (error) => { - exeContext.errors.push(error); - return buildResponse(null, exeContext.errors); - }, - ); - } - return buildResponse(result, exeContext.errors); - } catch (error) { - exeContext.errors.push(error); - return buildResponse(null, exeContext.errors); + + const coerceVariableValuesReturn = executableRequest.coerceVariableValues( + args.variableValues, + ); + if (coerceVariableValuesReturn.errors !== undefined) { + return coerceVariableValuesReturn; } + const coerceVariableValues = coerceVariableValuesReturn.result; + + return executableRequest.executeOperation( + coerceVariableValues, + args.contextValue, + args.rootValue, + ); } /** @@ -220,41 +215,32 @@ export function executeSync(args: ExecutionArgs): ExecutionResult { return result; } -/** - * Given a completed execution context and data, build the `{ errors, data }` - * response defined by the "Response" section of the GraphQL specification. - */ -function buildResponse( - data: ObjMap | null, - errors: ReadonlyArray, -): ExecutionResult { - return errors.length === 0 ? { data } : { errors, data }; +interface GraphQLExecutionPlanOptions { + fieldResolver?: Maybe>; + typeResolver?: Maybe>; + subscribeFieldResolver?: Maybe>; } -/** - * Constructs a ExecutionContext 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. - * - * TODO: consider no longer exporting this function - * @internal - */ -export function buildExecutionContext( - args: ExecutionArgs, -): ReadonlyArray | ExecutionContext { - const { - schema, - document, - rootValue, - contextValue, - variableValues: rawVariableValues, - operationName, - fieldResolver, - typeResolver, - subscribeFieldResolver, - } = args; +interface ExecutionPlan { + schema: GraphQLSchema; + operation: OperationDefinitionNode; + fragments: ObjMap; + rootType: GraphQLObjectType; + fieldResolver: GraphQLFieldResolver; + typeResolver: GraphQLTypeResolver; + subscribeFieldResolver: GraphQLFieldResolver; +} +type ResultOrGraphQLErrors = + | { errors: ReadonlyArray; result?: never } + | { result: T; errors?: never }; + +export function makeExecutionPlan( + schema: GraphQLSchema, + document: DocumentNode, + operationName: Maybe, + options: GraphQLExecutionPlanOptions = {}, +): ResultOrGraphQLErrors { // If the schema used for execution is invalid, throw an error. assertValidSchema(schema); @@ -265,11 +251,11 @@ export function buildExecutionContext( case Kind.OPERATION_DEFINITION: if (operationName == null) { if (operation !== undefined) { - return [ + return buildErrorResponse( new GraphQLError( 'Must provide operation name if query contains multiple operations.', ), - ]; + ); } operation = definition; } else if (definition.name?.value === operationName) { @@ -286,87 +272,321 @@ export function buildExecutionContext( if (!operation) { if (operationName != null) { - return [new GraphQLError(`Unknown operation named "${operationName}".`)]; + return buildErrorResponse( + new GraphQLError(`Unknown operation named "${operationName}".`), + ); } - return [new GraphQLError('Must provide an operation.')]; + return buildErrorResponse(new GraphQLError('Must provide an operation.')); } - // FIXME: https://github.com/graphql/graphql-js/issues/2203 - /* c8 ignore next */ - const variableDefinitions = operation.variableDefinitions ?? []; - - const coercedVariableValues = getVariableValues( - schema, - variableDefinitions, - rawVariableValues ?? {}, - { maxErrors: 50 }, - ); - - if (coercedVariableValues.errors) { - return coercedVariableValues.errors; + const rootType = schema.getRootType(operation.operation); + if (rootType == null) { + return buildErrorResponse( + new GraphQLError( + `Schema is not configured to execute ${operation.operation} operation.`, + { nodes: operation }, + ), + ); } - return { + const result = { schema, - fragments, - rootValue, - contextValue, operation, - variableValues: coercedVariableValues.coerced, - fieldResolver: fieldResolver ?? defaultFieldResolver, - typeResolver: typeResolver ?? defaultTypeResolver, - subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, - errors: [], + fragments, + rootType, + fieldResolver: options.fieldResolver ?? defaultFieldResolver, + typeResolver: options.typeResolver ?? defaultTypeResolver, + subscribeFieldResolver: + options.subscribeFieldResolver ?? defaultFieldResolver, }; + return { result }; } /** - * Implements the "Executing operations" section of the spec. + * Constructs a ExecutableRequest object */ -function executeOperation( - exeContext: ExecutionContext, - operation: OperationDefinitionNode, -): PromiseOrValue> { - const rootType = exeContext.schema.getRootType(operation.operation); - if (rootType == null) { - throw new GraphQLError( - `Schema is not configured to execute ${operation.operation} operation.`, - { nodes: operation }, - ); - } - - const rootFields = collectFields( - exeContext.schema, - exeContext.fragments, - exeContext.variableValues, - rootType, - operation.selectionSet, +export function makeExecutableRequest( + schema: GraphQLSchema, + document: DocumentNode, + operationName: Maybe, + options: GraphQLExecutionPlanOptions = {}, +): ResultOrGraphQLErrors { + const makeExecutionPlanReturn = makeExecutionPlan( + schema, + document, + operationName, + options, ); - const path = undefined; - const { rootValue } = exeContext; + if (makeExecutionPlanReturn.errors !== undefined) { + return makeExecutionPlanReturn; + } + const executionPlan = makeExecutionPlanReturn.result; - switch (operation.operation) { + switch (executionPlan.operation.operation) { case OperationTypeNode.QUERY: - return executeFields(exeContext, rootType, rootValue, path, rootFields); + return { result: new ExecutableQueryRequestImpl(executionPlan) }; case OperationTypeNode.MUTATION: - return executeFieldsSerially( - exeContext, - rootType, - rootValue, - path, - rootFields, - ); + return { result: new ExecutableMutationRequestImpl(executionPlan) }; 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 { result: new ExecutableSubscriptionRequestImpl(executionPlan) }; + } +} + +class CoercedVariableValues { + coercedValues: { [variable: string]: unknown }; + + constructor(objMap: { [variable: string]: unknown }) { + this.coercedValues = objMap; + } +} + +export type ExecutableRequest = + | ExecutableQueryRequest + | ExecutableMutationRequest + | ExecutableSubscriptionRequest; + +export interface ExecutableQueryRequest { + operationType: OperationTypeNode.QUERY; + + coerceVariableValues: ( + rawVariableValues: Maybe<{ readonly [variable: string]: unknown }>, + ) => ResultOrGraphQLErrors; + + /** + * Implements the "ExecuteQuery" algorithm described in the GraphQL specification. + */ + executeOperation: ( + variableValues: CoercedVariableValues, + contextValue?: unknown, + rootValue?: unknown, + ) => PromiseOrValue; +} + +export interface ExecutableMutationRequest { + operationType: OperationTypeNode.MUTATION; + + coerceVariableValues: ( + rawVariableValues: Maybe<{ readonly [variable: string]: unknown }>, + ) => ResultOrGraphQLErrors; + + /** + * Implements the "ExecuteMutation" algorithm described in the GraphQL specification. + */ + executeOperation: ( + variableValues: CoercedVariableValues, + contextValue?: unknown, + rootValue?: unknown, + ) => PromiseOrValue; +} + +export interface ExecutableSubscriptionRequest { + operationType: OperationTypeNode.SUBSCRIPTION; + + coerceVariableValues: ( + rawVariableValues: Maybe<{ readonly [variable: string]: unknown }>, + ) => ResultOrGraphQLErrors; + + /** + * Implements the "ExecuteSubscription" algorithm described in the GraphQL specification. + */ + executeOperation: ( + variableValues: CoercedVariableValues, + contextValue?: unknown, + rootValue?: unknown, + ) => PromiseOrValue; + + /** + * Implements the "CreateSourceEventStream" algorithm described in the + * GraphQL specification, resolving the subscription source event stream. + * + * Returns a Promise which resolves to either an AsyncIterable (if successful) + * or an ExecutionResult (error). The promise will be rejected if the schema or + * other arguments to this function are invalid, or if the resolved event stream + * is not an async iterable. + * + * If the client-provided arguments to this function do not result in a + * compliant subscription, a GraphQL Response (ExecutionResult) with + * descriptive errors and no data will be returned. + * + * If the the source stream could not be created due to faulty subscription + * resolver logic or underlying systems, the promise will resolve to a single + * ExecutionResult containing `errors` and no `data`. + * + * If the operation succeeded, the promise resolves to the AsyncIterable for the + * event stream returned by the resolver. + * + * A Source Event Stream represents a sequence of events, each of which triggers + * a GraphQL execution for that event. + * + * This may be useful when hosting the stateful subscription service in a + * 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. + */ + createSourceEventStream: ( + variableValues: CoercedVariableValues, + contextValue?: unknown, + rootValue?: unknown, + ) => PromiseOrValue>>; + + mapSourceToResponse: ( + stream: AsyncIterable, + variableValues: CoercedVariableValues, + contextValue?: unknown, + ) => PromiseOrValue>; + + /** + * Implements the "ExecuteSubscriptionEvent" algorithm described in the GraphQL specification. + */ + executeSubscriptionEvent: ( + event: unknown, + variableValues: CoercedVariableValues, + contextValue?: unknown, + ) => PromiseOrValue; +} + +class ExecutableRequestImpl { + schema: GraphQLSchema; + operation: OperationDefinitionNode; + fragments: ObjMap; + rootType: GraphQLObjectType; + fieldResolver: GraphQLFieldResolver; + typeResolver: GraphQLTypeResolver; + subscribeFieldResolver: GraphQLFieldResolver; + + constructor(executionPlan: ExecutionPlan) { + this.schema = executionPlan.schema; + this.operation = executionPlan.operation; + this.fragments = executionPlan.fragments; + this.rootType = executionPlan.rootType; + this.fieldResolver = executionPlan.fieldResolver; + this.typeResolver = executionPlan.typeResolver; + this.subscribeFieldResolver = executionPlan.subscribeFieldResolver; + } + + coerceVariableValues( + rawVariableValues: Maybe<{ readonly [variable: string]: unknown }>, + ): ResultOrGraphQLErrors { + // FIXME: https://github.com/graphql/graphql-js/issues/2203 + /* c8 ignore next */ + const variableDefinitions = this.operation.variableDefinitions ?? []; + + const getVariableValuesReturn = getVariableValues( + this.schema, + variableDefinitions, + rawVariableValues ?? {}, + { maxErrors: 50 }, + ); + + if (getVariableValuesReturn.errors !== undefined) { + return getVariableValuesReturn; + } + return { + result: new CoercedVariableValues(getVariableValuesReturn.coerced), + }; + } + + _buildExecutionContext( + variableValues: CoercedVariableValues, + contextValue: unknown, + rootValue: unknown, + ): ExecutionContext { + return { + schema: this.schema, + fragments: this.fragments, + rootValue, + contextValue, + operation: this.operation, + variableValues: variableValues.coercedValues, + fieldResolver: this.fieldResolver, + typeResolver: this.typeResolver, + subscribeFieldResolver: this.subscribeFieldResolver, + errors: [], + }; + } + + _getRootFields(variableValues: CoercedVariableValues) { + return collectFields( + this.schema, + this.fragments, + variableValues.coercedValues, + this.rootType, + this.operation.selectionSet, + ); + } + + _executeRootFields( + variableValues: CoercedVariableValues, + contextValue: unknown, + rootValue: unknown, + executeSerially: boolean, + ): PromiseOrValue { + const exeContext = this._buildExecutionContext( + variableValues, + contextValue, + rootValue, + ); + const rootFields = this._getRootFields(variableValues); + const path = undefined; + + // Return 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. + // + // Errors from sub-fields of a NonNull type may propagate to the top level, + // at which point we still log the error and null the parent field, which + // in this case is the entire response. + try { + const data = executeSerially + ? executeFieldsSerially( + exeContext, + this.rootType, + rootValue, + path, + rootFields, + ) + : executeFields(exeContext, this.rootType, rootValue, path, rootFields); + + if (isPromise(data)) { + return data.then( + (resolvedData) => buildResponse(resolvedData, exeContext.errors), + (error) => { + exeContext.errors.push(error); + return buildResponse(null, exeContext.errors); + }, + ); + } + return buildResponse(data, exeContext.errors); + } catch (error) { + exeContext.errors.push(error); + return { errors: exeContext.errors, data: null }; + } } } +/** + * Given a completed execution context and data, build the `{ errors, data }` + * response defined by the "Response" section of the GraphQL specification. + */ +function buildResponse( + data: ObjMap | null, + errors: ReadonlyArray, +): ExecutionResult { + return errors.length === 0 ? { data } : { errors, data }; +} + +function buildErrorResponse(error: GraphQLError) { + return { errors: [error] }; +} + /** * Implements the "Executing selection sets" section of the spec - * for fields that must be executed serially. + * for root fields that must be executed serially. */ function executeFieldsSerially( exeContext: ExecutionContext, @@ -1019,161 +1239,292 @@ export function subscribe( ): PromiseOrValue< AsyncGenerator | ExecutionResult > { - const resultOrStream = createSourceEventStream(args); + const makeExecutableRequestReturn = makeExecutableRequest( + args.schema, + args.document, + args.operationName, + { + fieldResolver: args.fieldResolver, + typeResolver: args.typeResolver, + subscribeFieldResolver: args.subscribeFieldResolver, + } + ); + + if (makeExecutableRequestReturn.errors !== undefined) { + return makeExecutableRequestReturn; + } + const executableRequest = makeExecutableRequestReturn.result; - if (isPromise(resultOrStream)) { - return resultOrStream.then((resolvedResultOrStream) => - mapSourceToResponse(resolvedResultOrStream, args), + if (executableRequest.operationType !== OperationTypeNode.SUBSCRIPTION) { + throw new TypeError( + 'Can not execute `createSourceEventStream` on queries or mutations.', ); } - return mapSourceToResponse(resultOrStream, args); -} + const coerceVariableValuesReturn = executableRequest.coerceVariableValues( + args.variableValues, + ); + if (coerceVariableValuesReturn.errors !== undefined) { + return coerceVariableValuesReturn; + } + const coerceVariableValues = coerceVariableValuesReturn.result; -function mapSourceToResponse( - resultOrStream: ExecutionResult | AsyncIterable, - args: ExecutionArgs, -): PromiseOrValue< - AsyncGenerator | ExecutionResult -> { - if (!isAsyncIterable(resultOrStream)) { - return resultOrStream; + const createSourceEventStreamReturn = + executableRequest.createSourceEventStream( + coerceVariableValues, + args.contextValue, + args.rootValue, + ); + + if (isPromise(createSourceEventStreamReturn)) { + return createSourceEventStreamReturn.then( + (resolvedCreateSourceEventStreamReturn) => { + if (resolvedCreateSourceEventStreamReturn.errors !== undefined) { + return resolvedCreateSourceEventStreamReturn; + } + return executableRequest.mapSourceToResponse( + resolvedCreateSourceEventStreamReturn.result, + coerceVariableValues, + args.contextValue, + ); + }, + ); } - // For each payload yielded from a subscription, map it over the normal - // GraphQL `execute` function, with `payload` as the rootValue. - // This implements the "MapSourceToResponseEvent" algorithm described in - // the GraphQL specification. The `execute` function provides the - // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the - // "ExecuteQuery" algorithm, for which `execute` is also used. - return mapAsyncIterator(resultOrStream, (payload: unknown) => - execute({ - ...args, - rootValue: payload, - }), + if (createSourceEventStreamReturn.errors !== undefined) { + return createSourceEventStreamReturn; + } + return executableRequest.mapSourceToResponse( + createSourceEventStreamReturn.result, + coerceVariableValues, + args.contextValue, ); } -/** - * Implements the "CreateSourceEventStream" algorithm described in the - * GraphQL specification, resolving the subscription source event stream. - * - * Returns a Promise which resolves to either an AsyncIterable (if successful) - * or an ExecutionResult (error). The promise will be rejected if the schema or - * other arguments to this function are invalid, or if the resolved event stream - * is not an async iterable. - * - * If the client-provided arguments to this function do not result in a - * compliant subscription, a GraphQL Response (ExecutionResult) with - * descriptive errors and no data will be returned. - * - * If the the source stream could not be created due to faulty subscription - * resolver logic or underlying systems, the promise will resolve to a single - * ExecutionResult containing `errors` and no `data`. - * - * If the operation succeeded, the promise resolves to the AsyncIterable for the - * event stream returned by the resolver. - * - * A Source Event Stream represents a sequence of events, each of which triggers - * a GraphQL execution for that event. - * - * This may be useful when hosting the stateful subscription service in a - * 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 Please use `ExecutableSubscriptionRequest.createSourceEventStream` instead. */ export function createSourceEventStream( args: ExecutionArgs, ): PromiseOrValue | ExecutionResult> { - // If a valid execution context cannot be created due to incorrect arguments, + // If a valid ExecutableSubscriptionRequest cannot be created due to incorrect arguments, // a "Response" with only errors is returned. - const exeContext = buildExecutionContext(args); + const makeExecutableRequestReturn = makeExecutableRequest( + args.schema, + args.document, + args.operationName, + { + fieldResolver: args.fieldResolver, + typeResolver: args.typeResolver, + subscribeFieldResolver: args.subscribeFieldResolver, + } + ); - // Return early errors if execution context failed. - if (!('schema' in exeContext)) { - return { errors: exeContext }; + // Return early errors if execution request failed. + if (makeExecutableRequestReturn.errors !== undefined) { + return makeExecutableRequestReturn; } + const executableRequest = makeExecutableRequestReturn.result; - try { - const eventStream = executeSubscription(exeContext); - if (isPromise(eventStream)) { - return eventStream.then(undefined, (error) => ({ errors: [error] })); - } + if (executableRequest.operationType !== OperationTypeNode.SUBSCRIPTION) { + throw new TypeError( + 'Can not execute `createSourceEventStream` on queries or mutations.', + ); + } + + const coerceVariableValuesReturn = executableRequest.coerceVariableValues( + args.variableValues, + ); + if (coerceVariableValuesReturn.errors !== undefined) { + return coerceVariableValuesReturn; + } + const coerceVariableValues = coerceVariableValuesReturn.result; + + const createSourceEventStreamReturn = + executableRequest.createSourceEventStream( + coerceVariableValues, + args.contextValue, + args.rootValue, + ); - return eventStream; - } catch (error) { - return { errors: [error] }; + if (isPromise(createSourceEventStreamReturn)) { + return createSourceEventStreamReturn.then( + (resolvedCreateSourceEventStreamReturn) => { + if (resolvedCreateSourceEventStreamReturn.errors !== undefined) { + return resolvedCreateSourceEventStreamReturn; + } + return resolvedCreateSourceEventStreamReturn.result; + }, + ); } + + if (createSourceEventStreamReturn.errors !== undefined) { + return createSourceEventStreamReturn; + } + return createSourceEventStreamReturn.result; } -function executeSubscription( - exeContext: ExecutionContext, -): PromiseOrValue> { - const { schema, fragments, operation, variableValues, rootValue } = - exeContext; +class ExecutableQueryRequestImpl + extends ExecutableRequestImpl + implements ExecutableQueryRequest +{ + operationType: OperationTypeNode.QUERY; - const rootType = schema.getSubscriptionType(); - if (rootType == null) { - throw new GraphQLError( - 'Schema is not configured to execute subscription operation.', - { nodes: operation }, + constructor(executionPlan: ExecutionPlan) { + super(executionPlan); + this.operationType = OperationTypeNode.QUERY; + } + + executeOperation( + variableValues: CoercedVariableValues, + contextValue?: unknown, + rootValue?: unknown, + ): PromiseOrValue { + return this._executeRootFields( + variableValues, + contextValue, + rootValue, + false, ); } +} - const rootFields = collectFields( - schema, - fragments, - variableValues, - rootType, - operation.selectionSet, - ); - const [responseName, fieldNodes] = [...rootFields.entries()][0]; - const fieldName = fieldNodes[0].name.value; - const fieldDef = schema.getField(rootType, fieldName); +class ExecutableMutationRequestImpl + extends ExecutableRequestImpl + implements ExecutableMutationRequest +{ + operationType: OperationTypeNode.MUTATION; - if (!fieldDef) { - throw new GraphQLError( - `The subscription field "${fieldName}" is not defined.`, - { nodes: fieldNodes }, + constructor(executionPlan: ExecutionPlan) { + super(executionPlan); + this.operationType = OperationTypeNode.MUTATION; + } + + executeOperation( + variableValues: CoercedVariableValues, + contextValue?: unknown, + rootValue?: unknown, + ): PromiseOrValue { + return this._executeRootFields( + variableValues, + contextValue, + rootValue, + true, ); } +} - const path = addPath(undefined, responseName, rootType.name); - const info = buildResolveInfo( - exeContext, - fieldDef, - fieldNodes, - rootType, - path, - ); +class ExecutableSubscriptionRequestImpl + extends ExecutableRequestImpl + implements ExecutableSubscriptionRequest +{ + operationType: OperationTypeNode.SUBSCRIPTION; - try { - // Implements the "ResolveFieldEventStream" algorithm from GraphQL specification. - // It differs from "ResolveFieldValue" due to providing a different `resolveFn`. + constructor(executionPlan: ExecutionPlan) { + super(executionPlan); + this.operationType = OperationTypeNode.SUBSCRIPTION; + } - // Build a JS object of arguments from the field.arguments AST, using the - // variables scope to fulfill any variable references. - const args = getArgumentValues(fieldDef, fieldNodes[0], variableValues); + executeOperation( + variableValues: CoercedVariableValues, + contextValue?: unknown, + rootValue?: unknown, + ): PromiseOrValue { + // TODO: deprecate `subscribe` and move all logic here + // Temporary solution until we finish merging execute and subscribe together + return this._executeRootFields( + variableValues, + contextValue, + rootValue, + false, + ); + } - // 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; + createSourceEventStream( + variableValues: CoercedVariableValues, + contextValue?: unknown, + rootValue?: unknown, + ): PromiseOrValue>> { + const exeContext = this._buildExecutionContext( + variableValues, + contextValue, + rootValue, + ); + const rootFields = this._getRootFields(variableValues); + const [responseName, fieldNodes] = [...rootFields.entries()][0]; + const path = addPath(undefined, responseName, this.rootType.name); - // Call the `subscribe()` resolver or the default resolver to produce an - // AsyncIterable yielding raw payloads. - const resolveFn = fieldDef.subscribe ?? exeContext.subscribeFieldResolver; - const result = resolveFn(rootValue, args, contextValue, info); + try { + const fieldName = fieldNodes[0].name.value; + const fieldDef = this.schema.getField(this.rootType, fieldName); - if (isPromise(result)) { - return result.then(assertEventStream).then(undefined, (error) => { - throw locatedError(error, fieldNodes, pathToArray(path)); - }); + if (!fieldDef) { + throw new GraphQLError( + `The subscription field "${fieldName}" is not defined.`, + { nodes: fieldNodes }, + ); + } + + const info = buildResolveInfo( + exeContext, + fieldDef, + fieldNodes, + this.rootType, + path, + ); + + // Implements the "ResolveFieldEventStream" algorithm from GraphQL specification. + // It differs from "ResolveFieldValue" due to providing a different `resolveFn`. + + // Build a JS object of arguments from the field.arguments AST, using the + // variables scope to fulfill any variable references. + const args = getArgumentValues( + fieldDef, + fieldNodes[0], + exeContext.variableValues, + ); + + // Call the `subscribe()` resolver or the default resolver to produce an + // AsyncIterable yielding raw payloads. + const resolveFn = fieldDef.subscribe ?? exeContext.subscribeFieldResolver; + const result = resolveFn(rootValue, args, contextValue, info); + + if (isPromise(result)) { + return result + .then((resolved) => ({ result: assertEventStream(resolved) })) + .then(undefined, (error) => ({ + errors: [locatedError(error, fieldNodes, pathToArray(path))], + })); + } + + return { result: assertEventStream(result) }; + } catch (error) { + return { + errors: [locatedError(error, fieldNodes, pathToArray(path))], + }; } + } + + mapSourceToResponse( + stream: AsyncIterable, + variableValues: CoercedVariableValues, + contextValue?: unknown, + ): PromiseOrValue> { + // For each payload yielded from a subscription, map it over the normal + // GraphQL `execute` function, with `payload` as the rootValue. + // This implements the "MapSourceToResponseEvent" algorithm described in + // the GraphQL specification. The `execute` function provides the + // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the + // "ExecuteQuery" algorithm, for which `execute` is also used. + return mapAsyncIterator(stream, (event: unknown) => + this.executeSubscriptionEvent(event, variableValues, contextValue), + ); + } - return assertEventStream(result); - } catch (error) { - throw locatedError(error, fieldNodes, pathToArray(path)); + executeSubscriptionEvent( + event: unknown, + variableValues: CoercedVariableValues, + contextValue?: unknown, + ): PromiseOrValue { + return this._executeRootFields(variableValues, contextValue, event, false); } }