From dbd11186fed5a3346f3548a5733e67bd65ee9e7e Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Fri, 17 Jun 2022 22:53:49 +0300 Subject: [PATCH 1/5] subscribe: fix missing path on unknown field error --- src/execution/__tests__/subscribe-test.ts | 1 + src/execution/execute.ts | 36 ++++++++++++----------- 2 files changed, 20 insertions(+), 17 deletions(-) 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..60d8fa0a77 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -1127,27 +1127,29 @@ function executeSubscription( rootType, operation.selectionSet, ); - const [responseName, fieldNodes] = [...rootFields.entries()][0]; - const fieldName = fieldNodes[0].name.value; - const fieldDef = schema.getField(rootType, fieldName); - - if (!fieldDef) { - throw new GraphQLError( - `The subscription field "${fieldName}" is not defined.`, - { nodes: fieldNodes }, - ); - } + const [responseName, fieldNodes] = [...rootFields.entries()][0]; const path = addPath(undefined, responseName, rootType.name); - const info = buildResolveInfo( - exeContext, - fieldDef, - fieldNodes, - rootType, - path, - ); try { + const fieldName = fieldNodes[0].name.value; + const fieldDef = schema.getField(rootType, fieldName); + + if (!fieldDef) { + throw new GraphQLError( + `The subscription field "${fieldName}" is not defined.`, + { nodes: fieldNodes }, + ); + } + + const info = buildResolveInfo( + exeContext, + fieldDef, + fieldNodes, + rootType, + path, + ); + // Implements the "ResolveFieldEventStream" algorithm from GraphQL specification. // It differs from "ResolveFieldValue" due to providing a different `resolveFn`. From a1b86acb7461703012e846f8ba2cc35db5e812e0 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Sat, 18 Jun 2022 00:33:28 +0300 Subject: [PATCH 2/5] step 1 --- src/execution/execute.ts | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 60d8fa0a77..3f917164af 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -231,6 +231,10 @@ function buildResponse( return errors.length === 0 ? { data } : { errors, data }; } +function buildErrorResponse(error: GraphQLError) { + return { errors: [error] }; +} + /** * Constructs a ExecutionContext object from the arguments passed to * execute, which we will pass throughout the other execution methods. @@ -1094,29 +1098,22 @@ export function createSourceEventStream( return { errors: exeContext }; } - try { - const eventStream = executeSubscription(exeContext); - if (isPromise(eventStream)) { - return eventStream.then(undefined, (error) => ({ errors: [error] })); - } - - return eventStream; - } catch (error) { - return { errors: [error] }; - } + return executeSubscription(exeContext); } function executeSubscription( exeContext: ExecutionContext, -): PromiseOrValue> { +): PromiseOrValue | ExecutionResult> { const { schema, fragments, operation, variableValues, rootValue } = exeContext; const rootType = schema.getSubscriptionType(); if (rootType == null) { - throw new GraphQLError( - 'Schema is not configured to execute subscription operation.', - { nodes: operation }, + return buildErrorResponse( + new GraphQLError( + 'Schema is not configured to execute subscription operation.', + { nodes: operation }, + ), ); } @@ -1168,14 +1165,20 @@ function executeSubscription( const result = resolveFn(rootValue, args, contextValue, info); if (isPromise(result)) { - return result.then(assertEventStream).then(undefined, (error) => { - throw locatedError(error, fieldNodes, pathToArray(path)); - }); + return result + .then(assertEventStream) + .then(undefined, (error) => + buildErrorResponse( + locatedError(error, fieldNodes, pathToArray(path)), + ), + ); } return assertEventStream(result); } catch (error) { - throw locatedError(error, fieldNodes, pathToArray(path)); + return buildErrorResponse( + locatedError(error, fieldNodes, pathToArray(path)), + ); } } From 974d92dbd982268e16ee8023a44e38a69d30d1cc Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Sat, 18 Jun 2022 00:33:53 +0300 Subject: [PATCH 3/5] step 2 --- src/execution/execute.ts | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 3f917164af..660eb5afa8 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -346,31 +346,36 @@ function executeOperation( rootType, operation.selectionSet, ); - const path = undefined; - - const { rootValue } = exeContext; switch (operation.operation) { case OperationTypeNode.QUERY: - return executeFields(exeContext, rootType, rootValue, path, rootFields); + return executeRootFields(exeContext, rootType, rootFields, false); case OperationTypeNode.MUTATION: - return executeFieldsSerially( - exeContext, - rootType, - rootValue, - path, - rootFields, - ); + return executeRootFields(exeContext, rootType, rootFields, true); 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 executeRootFields(exeContext, rootType, rootFields, false); } } +function executeRootFields( + exeContext: ExecutionContext, + rootType: GraphQLObjectType, + rootFields: Map>, + executeSerially: boolean, +): PromiseOrValue> { + const { rootValue } = exeContext; + const path = undefined; + + return executeSerially + ? executeFieldsSerially(exeContext, rootType, rootValue, path, rootFields) + : executeFields(exeContext, rootType, rootValue, path, rootFields); +} + /** * 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, From f5c9d66a06bd99261f7e14306262e3c21b1b74af Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Sat, 18 Jun 2022 16:18:12 +0300 Subject: [PATCH 4/5] step3 --- src/execution/__tests__/executor-test.ts | 3 - src/execution/execute.ts | 74 ++++++++++++------------ 2 files changed, 38 insertions(+), 39 deletions(-) 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/execute.ts b/src/execution/execute.ts index 660eb5afa8..baea6ad10b 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -174,34 +174,7 @@ export function execute(args: ExecutionArgs): PromiseOrValue { return { errors: exeContext }; } - // 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); - } + return executeOperation(exeContext, exeContext.operation); } /** @@ -330,12 +303,14 @@ export function buildExecutionContext( function executeOperation( exeContext: ExecutionContext, operation: OperationDefinitionNode, -): PromiseOrValue> { +): 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 }, + return buildErrorResponse( + new GraphQLError( + `Schema is not configured to execute ${operation.operation} operation.`, + { nodes: operation }, + ), ); } @@ -364,13 +339,40 @@ function executeRootFields( rootType: GraphQLObjectType, rootFields: Map>, executeSerially: boolean, -): PromiseOrValue> { +): PromiseOrValue { const { rootValue } = exeContext; const path = undefined; - return executeSerially - ? executeFieldsSerially(exeContext, rootType, rootValue, path, rootFields) - : executeFields(exeContext, rootType, rootValue, path, rootFields); + // 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, rootType, rootValue, path, rootFields) + : executeFields(exeContext, 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 }; + } } /** From 086a4f72ab03d6ce5d1b03e6952dc962c73c5344 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Tue, 21 Jun 2022 17:11:35 +0300 Subject: [PATCH 5/5] temp --- src/execution/execute.ts | 837 +++++++++++++++++++++++++++------------ 1 file changed, 588 insertions(+), 249 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index baea6ad10b..54aa300b25 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -165,16 +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 executeOperation(exeContext, exeContext.operation); + + const coerceVariableValuesReturn = executableRequest.coerceVariableValues( + args.variableValues, + ); + if (coerceVariableValuesReturn.errors !== undefined) { + return coerceVariableValuesReturn; + } + const coerceVariableValues = coerceVariableValuesReturn.result; + + return executableRequest.executeOperation( + coerceVariableValues, + args.contextValue, + args.rootValue, + ); } /** @@ -193,45 +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>; } -function buildErrorResponse(error: GraphQLError) { - return { errors: [error] }; +interface ExecutionPlan { + schema: GraphQLSchema; + operation: OperationDefinitionNode; + fragments: ObjMap; + rootType: GraphQLObjectType; + fieldResolver: GraphQLFieldResolver; + typeResolver: GraphQLTypeResolver; + subscribeFieldResolver: GraphQLFieldResolver; } -/** - * 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; +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); @@ -242,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) { @@ -263,48 +272,14 @@ 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.')]; - } - - // 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; + return buildErrorResponse(new GraphQLError('Must provide an operation.')); } - return { - schema, - fragments, - rootValue, - contextValue, - operation, - variableValues: coercedVariableValues.coerced, - fieldResolver: fieldResolver ?? defaultFieldResolver, - typeResolver: typeResolver ?? defaultTypeResolver, - subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, - errors: [], - }; -} - -/** - * Implements the "Executing operations" section of the spec. - */ -function executeOperation( - exeContext: ExecutionContext, - operation: OperationDefinitionNode, -): PromiseOrValue { - const rootType = exeContext.schema.getRootType(operation.operation); + const rootType = schema.getRootType(operation.operation); if (rootType == null) { return buildErrorResponse( new GraphQLError( @@ -314,67 +289,301 @@ function executeOperation( ); } - const rootFields = collectFields( - exeContext.schema, - exeContext.fragments, - exeContext.variableValues, + const result = { + schema, + operation, + fragments, rootType, - operation.selectionSet, + fieldResolver: options.fieldResolver ?? defaultFieldResolver, + typeResolver: options.typeResolver ?? defaultTypeResolver, + subscribeFieldResolver: + options.subscribeFieldResolver ?? defaultFieldResolver, + }; + return { result }; +} + +/** + * Constructs a ExecutableRequest object + */ +export function makeExecutableRequest( + schema: GraphQLSchema, + document: DocumentNode, + operationName: Maybe, + options: GraphQLExecutionPlanOptions = {}, +): ResultOrGraphQLErrors { + const makeExecutionPlanReturn = makeExecutionPlan( + schema, + document, + operationName, + options, ); - switch (operation.operation) { + if (makeExecutionPlanReturn.errors !== undefined) { + return makeExecutionPlanReturn; + } + const executionPlan = makeExecutionPlanReturn.result; + + switch (executionPlan.operation.operation) { case OperationTypeNode.QUERY: - return executeRootFields(exeContext, rootType, rootFields, false); + return { result: new ExecutableQueryRequestImpl(executionPlan) }; case OperationTypeNode.MUTATION: - return executeRootFields(exeContext, rootType, rootFields, true); + 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 executeRootFields(exeContext, rootType, rootFields, false); + return { result: new ExecutableSubscriptionRequestImpl(executionPlan) }; } } -function executeRootFields( - exeContext: ExecutionContext, - rootType: GraphQLObjectType, - rootFields: Map>, - executeSerially: boolean, -): PromiseOrValue { - const { rootValue } = exeContext; - 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, rootType, rootValue, path, rootFields) - : executeFields(exeContext, 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); - }, - ); +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 }; } - 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 root fields that must be executed serially. @@ -1030,163 +1239,293 @@ 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; - return executeSubscription(exeContext); -} + if (executableRequest.operationType !== OperationTypeNode.SUBSCRIPTION) { + throw new TypeError( + 'Can not execute `createSourceEventStream` on queries or mutations.', + ); + } -function executeSubscription( - exeContext: ExecutionContext, -): PromiseOrValue | ExecutionResult> { - const { schema, fragments, operation, variableValues, rootValue } = - exeContext; + const coerceVariableValuesReturn = executableRequest.coerceVariableValues( + args.variableValues, + ); + if (coerceVariableValuesReturn.errors !== undefined) { + return coerceVariableValuesReturn; + } + const coerceVariableValues = coerceVariableValuesReturn.result; - const rootType = schema.getSubscriptionType(); - if (rootType == null) { - return buildErrorResponse( - new GraphQLError( - 'Schema is not configured to execute subscription operation.', - { nodes: operation }, - ), + const createSourceEventStreamReturn = + executableRequest.createSourceEventStream( + coerceVariableValues, + args.contextValue, + args.rootValue, + ); + + if (isPromise(createSourceEventStreamReturn)) { + return createSourceEventStreamReturn.then( + (resolvedCreateSourceEventStreamReturn) => { + if (resolvedCreateSourceEventStreamReturn.errors !== undefined) { + return resolvedCreateSourceEventStreamReturn; + } + return resolvedCreateSourceEventStreamReturn.result; + }, ); } - const rootFields = collectFields( - schema, - fragments, - variableValues, - rootType, - operation.selectionSet, - ); + if (createSourceEventStreamReturn.errors !== undefined) { + return createSourceEventStreamReturn; + } + return createSourceEventStreamReturn.result; +} - const [responseName, fieldNodes] = [...rootFields.entries()][0]; - const path = addPath(undefined, responseName, rootType.name); +class ExecutableQueryRequestImpl + extends ExecutableRequestImpl + implements ExecutableQueryRequest +{ + operationType: OperationTypeNode.QUERY; - try { - const fieldName = fieldNodes[0].name.value; - const fieldDef = schema.getField(rootType, fieldName); + constructor(executionPlan: ExecutionPlan) { + super(executionPlan); + this.operationType = OperationTypeNode.QUERY; + } - if (!fieldDef) { - throw new GraphQLError( - `The subscription field "${fieldName}" is not defined.`, - { nodes: fieldNodes }, - ); - } + executeOperation( + variableValues: CoercedVariableValues, + contextValue?: unknown, + rootValue?: unknown, + ): PromiseOrValue { + return this._executeRootFields( + variableValues, + contextValue, + rootValue, + false, + ); + } +} - const info = buildResolveInfo( - exeContext, - fieldDef, - fieldNodes, - rootType, - path, +class ExecutableMutationRequestImpl + extends ExecutableRequestImpl + implements ExecutableMutationRequest +{ + operationType: OperationTypeNode.MUTATION; + + constructor(executionPlan: ExecutionPlan) { + super(executionPlan); + this.operationType = OperationTypeNode.MUTATION; + } + + executeOperation( + variableValues: CoercedVariableValues, + contextValue?: unknown, + rootValue?: unknown, + ): PromiseOrValue { + return this._executeRootFields( + variableValues, + contextValue, + rootValue, + true, ); + } +} - // Implements the "ResolveFieldEventStream" algorithm from GraphQL specification. - // It differs from "ResolveFieldValue" due to providing a different `resolveFn`. +class ExecutableSubscriptionRequestImpl + extends ExecutableRequestImpl + implements ExecutableSubscriptionRequest +{ + 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); + constructor(executionPlan: ExecutionPlan) { + super(executionPlan); + this.operationType = OperationTypeNode.SUBSCRIPTION; + } - // 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; + 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, + ); + } - // 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); + 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); - if (isPromise(result)) { - return result - .then(assertEventStream) - .then(undefined, (error) => - buildErrorResponse( - locatedError(error, fieldNodes, pathToArray(path)), - ), + try { + const fieldName = fieldNodes[0].name.value; + const fieldDef = this.schema.getField(this.rootType, fieldName); + + 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))], + }; } + } - return assertEventStream(result); - } catch (error) { - return buildErrorResponse( - 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), ); } + + executeSubscriptionEvent( + event: unknown, + variableValues: CoercedVariableValues, + contextValue?: unknown, + ): PromiseOrValue { + return this._executeRootFields(variableValues, contextValue, event, false); + } } function assertEventStream(result: unknown): AsyncIterable {