diff --git a/src/execution/execute.ts b/src/execution/execute.ts index bfc529de840..751bd9d62cf 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -2,7 +2,7 @@ import type { PromiseOrValue } from '../jsutils/PromiseOrValue'; import { isPromise } from '../jsutils/isPromise'; import type { ExecutionArgs, ExecutionResult } from './executor'; -import { buildExecutionContext, executeQueryOrMutation } from './executor'; +import { Executor } from './executor'; /** * Implements the "Executing requests" section of the GraphQL specification. @@ -15,17 +15,10 @@ import { buildExecutionContext, executeQueryOrMutation } from './executor'; * 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, - // 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 }; - } + const executor = new Executor(args); // Execute the operation. - return executeQueryOrMutation(exeContext); + return executor.executeQueryOrMutation(); } /** diff --git a/src/execution/executor.ts b/src/execution/executor.ts index 477c4465408..c04d0b0c8a1 100644 --- a/src/execution/executor.ts +++ b/src/execution/executor.ts @@ -61,26 +61,6 @@ import { } from './collectFields'; import { mapAsyncIterator } from './mapAsyncIterator'; -/** - * A memoized collection of relevant subfields with regard to the return - * type. Memoizing ensures the subfields are not repeatedly calculated, which - * saves overhead when resolving lists of values. - */ -const collectSubfields = memoize3( - ( - exeContext: ExecutionContext, - returnType: GraphQLObjectType, - fieldNodes: ReadonlyArray, - ) => - _collectSubfields( - exeContext.schema, - exeContext.fragments, - exeContext.variableValues, - returnType, - fieldNodes, - ), -); - /** * Terminology * @@ -158,319 +138,568 @@ export interface ExecutionArgs { } /** - * Implements the "Executing operations" section of the spec for queries and mutations. - * - * 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 - * resolved data. - * * @internal */ -export function executeQueryOrMutation( - exeContext: ExecutionContext, -): PromiseOrValue { - const data = executeQueryOrMutationRootFields(exeContext); - return buildResponse(exeContext, data); -} +export class Executor { + /** + * A memoized collection of relevant subfields with regard to the return + * type. Memoizing ensures the subfields are not repeatedly calculated, which + * saves overhead when resolving lists of values. + */ + protected collectSubfields = memoize3( + ( + exeContext: ExecutionContext, + returnType: GraphQLObjectType, + fieldNodes: ReadonlyArray, + ) => + _collectSubfields( + exeContext.schema, + exeContext.fragments, + exeContext.variableValues, + returnType, + fieldNodes, + ), + ); -/** - * Given a completed execution context and data, build the `{ errors, data }` - * response defined by the "Response" section of the GraphQL specification. - */ -function buildResponse( - exeContext: ExecutionContext, - data: PromiseOrValue | null>, -): PromiseOrValue { - if (isPromise(data)) { - return data.then((resolved) => buildResponse(exeContext, resolved)); + protected _exeContext: ExecutionContext | ReadonlyArray; + + constructor(argsOrExecutionContext: ExecutionArgs | ExecutionContext) { + if ('operation' in argsOrExecutionContext) { + this._exeContext = argsOrExecutionContext; + return; + } + + this._exeContext = this.buildExecutionContext(argsOrExecutionContext); } - return exeContext.errors.length === 0 - ? { data } - : { errors: exeContext.errors, data }; -} -/** - * Essential assertions before executing to provide developer feedback for - * improper use of the GraphQL library. - */ -function assertValidExecutionArguments( - schema: GraphQLSchema, - document: DocumentNode, - rawVariableValues: Maybe<{ readonly [variable: string]: unknown }>, -): void { - devAssert(document, 'Must provide document.'); - - // If the schema used for execution is invalid, throw an error. - assertValidSchema(schema); - - // Variables, if provided, must be an object. - devAssert( - rawVariableValues == null || isObjectLike(rawVariableValues), - 'Variables must be provided as an Object where each property is a variable value. Perhaps look to see if an unparsed JSON string was provided.', - ); -} + /** + * Implements the "Executing requests" section of the spec for queries and mutations. + * + * 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 + * resolved data. + */ + public executeQueryOrMutation(): PromiseOrValue { + const { _exeContext } = this; + + // If a valid execution context cannot be created due to incorrect arguments, + // a "Response" with only errors is returned. + if (!('schema' in _exeContext)) { + return { errors: _exeContext }; + } -/** - * Constructs a ExecutionContext object from the arguments passed to - * execute, which we will pass throughout the other execution methods. - * - * Returns an array of GraphQLErrors if a valid execution context - * cannot be created. - * - * @internal - */ -export function buildExecutionContext({ - schema, - document, - rootValue, - contextValue, - variableValues, - operationName, - fieldResolver, - typeResolver, - subscribeFieldResolver, -}: ExecutionArgs): ReadonlyArray | ExecutionContext { - // If arguments are missing or incorrectly typed, this is an internal - // developer mistake which should throw an error. - assertValidExecutionArguments(schema, document, variableValues); - - let operation: OperationDefinitionNode | undefined; - const fragments: ObjMap = Object.create(null); - for (const definition of document.definitions) { - switch (definition.kind) { - case Kind.OPERATION_DEFINITION: - if (operationName == null) { - if (operation !== undefined) { - return [ - new GraphQLError( - 'Must provide operation name if query contains multiple operations.', - ), - ]; - } - operation = definition; - } else if (definition.name?.value === operationName) { - operation = definition; - } - break; - case Kind.FRAGMENT_DEFINITION: - fragments[definition.name.value] = definition; - break; + return this.executeQueryOrMutationImpl(_exeContext); + } + + public async executeSubscription(): Promise< + AsyncGenerator | ExecutionResult + > { + const { _exeContext } = this; + + // If a valid execution context cannot be created due to incorrect arguments, + // a "Response" with only errors is returned. + if (!('schema' in _exeContext)) { + return { errors: _exeContext }; } + + return this.executeSubscriptionImpl(_exeContext); } - if (!operation) { - if (operationName != null) { - return [new GraphQLError(`Unknown operation named "${operationName}".`)]; + public async createSourceEventStream(): Promise< + AsyncIterable | ExecutionResult + > { + const { _exeContext } = this; + + // If a valid execution context cannot be created due to incorrect arguments, + // a "Response" with only errors is returned. + if (!('schema' in _exeContext)) { + return { errors: _exeContext }; } - return [new GraphQLError('Must provide an operation.')]; + + return this.createSourceEventStreamImpl(_exeContext); } - // istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203') - const variableDefinitions = operation.variableDefinitions ?? []; + protected executeQueryOrMutationImpl( + exeContext: ExecutionContext, + ): PromiseOrValue { + const data = this.executeQueryOrMutationRootFields(exeContext); + return this.buildResponse(exeContext, data); + } - const coercedVariableValues = getVariableValues( - schema, - variableDefinitions, - variableValues ?? {}, - { maxErrors: 50 }, - ); + /** + * Given a completed execution context and data, build the `{ errors, data }` + * response defined by the "Response" section of the GraphQL specification. + */ + protected buildResponse( + exeContext: ExecutionContext, + data: PromiseOrValue | null>, + ): PromiseOrValue { + if (isPromise(data)) { + return data.then((resolved) => this.buildResponse(exeContext, resolved)); + } + return exeContext.errors.length === 0 + ? { data } + : { errors: exeContext.errors, data }; + } - if (coercedVariableValues.errors) { - return coercedVariableValues.errors; + /** + * Essential assertions before executing to provide developer feedback for + * improper use of the GraphQL library. + */ + protected assertValidExecutionArguments( + schema: GraphQLSchema, + document: DocumentNode, + rawVariableValues: Maybe<{ readonly [variable: string]: unknown }>, + ): void { + devAssert(document, 'Must provide document.'); + + // If the schema used for execution is invalid, throw an error. + assertValidSchema(schema); + + // Variables, if provided, must be an object. + devAssert( + rawVariableValues == null || isObjectLike(rawVariableValues), + 'Variables must be provided as an Object where each property is a variable value. Perhaps look to see if an unparsed JSON string was provided.', + ); } - return { + /** + * Constructs a ExecutionContext object from the arguments passed to + * execute, which we will pass throughout the other execution methods. + * + * Returns an array of GraphQLErrors if a valid execution context + * cannot be created. + */ + protected buildExecutionContext({ schema, - fragments, + document, rootValue, contextValue, - operation, - variableValues: coercedVariableValues.coerced, - fieldResolver: fieldResolver ?? defaultFieldResolver, - typeResolver: typeResolver ?? defaultTypeResolver, - subscribeFieldResolver: subscribeFieldResolver ?? undefined, - errors: [], - }; -} + variableValues, + operationName, + fieldResolver, + typeResolver, + subscribeFieldResolver, + }: ExecutionArgs): ReadonlyArray | ExecutionContext { + // If arguments are missing or incorrectly typed, this is an internal + // developer mistake which should throw an error. + this.assertValidExecutionArguments(schema, document, variableValues); + + let operation: OperationDefinitionNode | undefined; + const fragments: ObjMap = Object.create(null); + for (const definition of document.definitions) { + switch (definition.kind) { + case Kind.OPERATION_DEFINITION: + if (operationName == null) { + if (operation !== undefined) { + return [ + new GraphQLError( + 'Must provide operation name if query contains multiple operations.', + ), + ]; + } + operation = definition; + } else if (definition.name?.value === operationName) { + operation = definition; + } + break; + case Kind.FRAGMENT_DEFINITION: + fragments[definition.name.value] = definition; + break; + } + } -/** - * Executes the root fields specified by query or mutation operation. - */ -function executeQueryOrMutationRootFields( - exeContext: ExecutionContext, -): PromiseOrValue | null> { - const { schema, operation, rootValue } = exeContext; - const type = getOperationRootType(schema, operation); - const fields = collectFields( - exeContext.schema, - exeContext.fragments, - exeContext.variableValues, - type, - operation.selectionSet, - ); + if (!operation) { + if (operationName != null) { + return [ + new GraphQLError(`Unknown operation named "${operationName}".`), + ]; + } + return [new GraphQLError('Must provide an operation.')]; + } - const path = undefined; - - // 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 result = - operation.operation === 'mutation' - ? executeFieldsSerially(exeContext, type, rootValue, path, fields) - : executeFields(exeContext, type, rootValue, path, fields); - if (isPromise(result)) { - return result.then(undefined, (error) => { - exeContext.errors.push(error); - return Promise.resolve(null); - }); + // istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203') + const variableDefinitions = operation.variableDefinitions ?? []; + + const coercedVariableValues = getVariableValues( + schema, + variableDefinitions, + variableValues ?? {}, + { maxErrors: 50 }, + ); + + if (coercedVariableValues.errors) { + return coercedVariableValues.errors; } - return result; - } catch (error) { - exeContext.errors.push(error); - return null; + + return { + schema, + fragments, + rootValue, + contextValue, + operation, + variableValues: coercedVariableValues.coerced, + fieldResolver: fieldResolver ?? defaultFieldResolver, + typeResolver: typeResolver ?? defaultTypeResolver, + subscribeFieldResolver: subscribeFieldResolver ?? undefined, + errors: [], + }; } -} -/** - * Implements the "Executing selection sets" section of the spec - * for fields that must be executed serially. - */ -function executeFieldsSerially( - exeContext: ExecutionContext, - parentType: GraphQLObjectType, - sourceValue: unknown, - path: Path | undefined, - fields: Map>, -): PromiseOrValue> { - return promiseReduce( - fields.entries(), - (results, [responseName, fieldNodes]) => { + /** + * Constructs a perPayload ExecutionContext object from an initial + * ExecutionObject and the payload value. + */ + protected buildPerPayloadExecutionContext( + exeContext: ExecutionContext, + payload: unknown, + ): ExecutionContext { + return { + ...exeContext, + rootValue: payload, + errors: [], + }; + } + + /** + * Executes the root fields specified by query or mutation operation. + */ + protected executeQueryOrMutationRootFields( + exeContext: ExecutionContext, + ): PromiseOrValue | null> { + const { schema, operation, rootValue } = exeContext; + const type = getOperationRootType(schema, operation); + const fields = collectFields( + exeContext.schema, + exeContext.fragments, + exeContext.variableValues, + type, + operation.selectionSet, + ); + + const path = undefined; + + // 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 result = + operation.operation === 'mutation' + ? this.executeFieldsSerially( + exeContext, + type, + rootValue, + path, + fields, + ) + : this.executeFields(exeContext, type, rootValue, path, fields); + if (isPromise(result)) { + return result.then(undefined, (error) => { + exeContext.errors.push(error); + return Promise.resolve(null); + }); + } + return result; + } catch (error) { + exeContext.errors.push(error); + return null; + } + } + + /** + * Implements the "Executing selection sets" section of the spec + * for fields that must be executed serially. + */ + protected executeFieldsSerially( + exeContext: ExecutionContext, + parentType: GraphQLObjectType, + sourceValue: unknown, + path: Path | undefined, + fields: Map>, + ): PromiseOrValue> { + return promiseReduce( + fields.entries(), + (results, [responseName, fieldNodes]) => { + const fieldPath = addPath(path, responseName, parentType.name); + const result = this.executeField( + exeContext, + parentType, + sourceValue, + fieldNodes, + fieldPath, + ); + if (result === undefined) { + return results; + } + if (isPromise(result)) { + return result.then((resolvedResult) => { + results[responseName] = resolvedResult; + return results; + }); + } + results[responseName] = result; + return results; + }, + Object.create(null), + ); + } + + /** + * Implements the "Executing selection sets" section of the spec + * for fields that may be executed in parallel. + */ + protected executeFields( + exeContext: ExecutionContext, + parentType: GraphQLObjectType, + sourceValue: unknown, + path: Path | undefined, + fields: Map>, + ): PromiseOrValue> { + const results = Object.create(null); + let containsPromise = false; + + for (const [responseName, fieldNodes] of fields.entries()) { const fieldPath = addPath(path, responseName, parentType.name); - const result = executeField( + const result = this.executeField( exeContext, parentType, sourceValue, fieldNodes, fieldPath, ); - if (result === undefined) { - return results; - } - if (isPromise(result)) { - return result.then((resolvedResult) => { - results[responseName] = resolvedResult; - return results; - }); + + if (result !== undefined) { + results[responseName] = result; + if (isPromise(result)) { + containsPromise = true; + } } - results[responseName] = result; + } + + // If there are no promises, we can just return the object + if (!containsPromise) { return results; - }, - Object.create(null), - ); -} + } -/** - * Implements the "Executing selection sets" section of the spec - * for fields that may be executed in parallel. - */ -function executeFields( - exeContext: ExecutionContext, - parentType: GraphQLObjectType, - sourceValue: unknown, - path: Path | undefined, - fields: Map>, -): PromiseOrValue> { - const results = Object.create(null); - let containsPromise = false; - - for (const [responseName, fieldNodes] of fields.entries()) { - const fieldPath = addPath(path, responseName, parentType.name); - const result = executeField( - exeContext, + // Otherwise, results is a map from field name to the result of resolving that + // field, which is possibly a promise. Return a promise that will return this + // same map, but with any promises replaced with the values they resolved to. + return promiseForObject(results); + } + + /** + * Implements the "Executing field" section of the spec + * In particular, this function figures out the value that the field returns by + * calling its resolve function, then calls completeValue to complete promises, + * serialize scalars, or execute the sub-selection-set for objects. + */ + protected executeField( + exeContext: ExecutionContext, + parentType: GraphQLObjectType, + source: unknown, + fieldNodes: ReadonlyArray, + path: Path, + ): PromiseOrValue { + const fieldDef = this.getFieldDef( + exeContext.schema, parentType, - sourceValue, + fieldNodes[0], + ); + if (!fieldDef) { + return; + } + + const returnType = fieldDef.type; + const resolveFn = fieldDef.resolve ?? exeContext.fieldResolver; + + const info = this.buildResolveInfo( + exeContext, + fieldDef, fieldNodes, - fieldPath, + parentType, + path, ); - if (result !== undefined) { - results[responseName] = result; + // Get the resolve function, regardless of if its result is normal or abrupt (error). + try { + // Build a JS object of arguments from the field.arguments AST, using the + // variables scope to fulfill any variable references. + // TODO: find a way to memoize, in case this field is within a List type. + const args = getArgumentValues( + fieldDef, + fieldNodes[0], + exeContext.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 result = resolveFn(source, args, contextValue, info); + + let completed; if (isPromise(result)) { - containsPromise = true; + completed = result.then((resolved) => + this.completeValue( + exeContext, + returnType, + fieldNodes, + info, + path, + resolved, + ), + ); + } else { + completed = this.completeValue( + exeContext, + returnType, + fieldNodes, + info, + path, + result, + ); } + + if (isPromise(completed)) { + // Note: we don't rely on a `catch` method, but we do expect "thenable" + // to take a second callback for the error case. + return completed.then(undefined, (rawError) => { + const error = locatedError(rawError, fieldNodes, pathToArray(path)); + return this.handleFieldError(error, returnType, exeContext); + }); + } + return completed; + } catch (rawError) { + const error = locatedError(rawError, fieldNodes, pathToArray(path)); + return this.handleFieldError(error, returnType, exeContext); } } - // If there are no promises, we can just return the object - if (!containsPromise) { - return results; + protected buildResolveInfo( + exeContext: ExecutionContext, + fieldDef: GraphQLField, + fieldNodes: ReadonlyArray, + parentType: GraphQLObjectType, + path: Path, + ): GraphQLResolveInfo { + // The resolve function's optional fourth argument is a collection of + // information about the current execution state. + return { + fieldName: fieldDef.name, + fieldNodes, + returnType: fieldDef.type, + parentType, + path, + schema: exeContext.schema, + fragments: exeContext.fragments, + rootValue: exeContext.rootValue, + operation: exeContext.operation, + variableValues: exeContext.variableValues, + }; } - // Otherwise, results is a map from field name to the result of resolving that - // field, which is possibly a promise. Return a promise that will return this - // same map, but with any promises replaced with the values they resolved to. - return promiseForObject(results); -} + protected handleFieldError( + error: GraphQLError, + returnType: GraphQLOutputType, + exeContext: ExecutionContext, + ): null { + // If the field type is non-nullable, then it is resolved without any + // protection from errors, however it still properly locates the error. + if (isNonNullType(returnType)) { + throw error; + } -/** - * Implements the "Executing field" section of the spec - * In particular, this function figures out the value that the field returns by - * calling its resolve function, then calls completeValue to complete promises, - * serialize scalars, or execute the sub-selection-set for objects. - */ -function executeField( - exeContext: ExecutionContext, - parentType: GraphQLObjectType, - source: unknown, - fieldNodes: ReadonlyArray, - path: Path, -): PromiseOrValue { - const fieldDef = getFieldDef(exeContext.schema, parentType, fieldNodes[0]); - if (!fieldDef) { - return; + // Otherwise, error protection is applied, logging the error and resolving + // a null value for this field if one is encountered. + exeContext.errors.push(error); + return null; } - const returnType = fieldDef.type; - const resolveFn = fieldDef.resolve ?? exeContext.fieldResolver; + /** + * Implements the instructions for completeValue as defined in the + * "Field entries" section of the spec. + * + * If the field type is Non-Null, then this recursively completes the value + * for the inner type. It throws a field error if that completion returns null, + * as per the "Nullability" section of the spec. + * + * If the field type is a List, then this recursively completes the value + * for the inner type on each item in the list. + * + * If the field type is a Scalar or Enum, ensures the completed value is a legal + * value of the type by calling the `serialize` method of GraphQL type + * definition. + * + * If the field is an abstract type, determine the runtime type of the value + * and then complete based on that type + * + * Otherwise, the field type expects a sub-selection set, and will complete the + * value by executing all sub-selections. + */ + protected completeValue( + exeContext: ExecutionContext, + returnType: GraphQLOutputType, + fieldNodes: ReadonlyArray, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + ): PromiseOrValue { + // If result is an Error, throw a located error. + if (result instanceof Error) { + throw result; + } - const info = buildResolveInfo( - exeContext, - fieldDef, - fieldNodes, - parentType, - path, - ); + // If field type is NonNull, complete for inner type, and throw field error + // if result is null. + if (isNonNullType(returnType)) { + const completed = this.completeValue( + exeContext, + returnType.ofType, + fieldNodes, + info, + path, + result, + ); + if (completed === null) { + throw new Error( + `Cannot return null for non-nullable field ${info.parentType.name}.${info.fieldName}.`, + ); + } + return completed; + } - // Get the resolve function, regardless of if its result is normal or abrupt (error). - try { - // Build a JS object of arguments from the field.arguments AST, using the - // variables scope to fulfill any variable references. - // TODO: find a way to memoize, in case this field is within a List type. - const args = getArgumentValues( - fieldDef, - fieldNodes[0], - exeContext.variableValues, - ); + // If result value is null or undefined then return null. + if (result == null) { + return null; + } - // 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; + // If field type is List, complete each item in the list with the inner type + if (isListType(returnType)) { + return this.completeListValue( + exeContext, + returnType, + fieldNodes, + info, + path, + result, + ); + } - const result = resolveFn(source, args, contextValue, info); + // If field type is a leaf type, Scalar or Enum, serialize to a valid value, + // returning null if serialization is not possible. + if (isLeafType(returnType)) { + return this.completeLeafValue(returnType, result); + } - let completed; - if (isPromise(result)) { - completed = result.then((resolved) => - completeValue(exeContext, returnType, fieldNodes, info, path, resolved), - ); - } else { - completed = completeValue( + // If field type is an abstract type, Interface or Union, determine the + // runtime Object type and complete for that type. + if (isAbstractType(returnType)) { + return this.completeAbstractValue( exeContext, returnType, fieldNodes, @@ -480,127 +709,165 @@ function executeField( ); } - if (isPromise(completed)) { - // Note: we don't rely on a `catch` method, but we do expect "thenable" - // to take a second callback for the error case. - return completed.then(undefined, (rawError) => { - const error = locatedError(rawError, fieldNodes, pathToArray(path)); - return handleFieldError(error, returnType, exeContext); - }); + // If field type is Object, execute and complete all sub-selections. + // istanbul ignore else (See: 'https://github.com/graphql/graphql-js/issues/2618') + if (isObjectType(returnType)) { + return this.completeObjectValue( + exeContext, + returnType, + fieldNodes, + info, + path, + result, + ); } - return completed; - } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(path)); - return handleFieldError(error, returnType, exeContext); + + // istanbul ignore next (Not reachable. All possible output types have been considered) + invariant( + false, + 'Cannot complete value of unexpected output type: ' + inspect(returnType), + ); } -} -/** - * @internal - */ -export function buildResolveInfo( - exeContext: ExecutionContext, - fieldDef: GraphQLField, - fieldNodes: ReadonlyArray, - parentType: GraphQLObjectType, - path: Path, -): GraphQLResolveInfo { - // The resolve function's optional fourth argument is a collection of - // information about the current execution state. - return { - fieldName: fieldDef.name, - fieldNodes, - returnType: fieldDef.type, - parentType, - path, - schema: exeContext.schema, - fragments: exeContext.fragments, - rootValue: exeContext.rootValue, - operation: exeContext.operation, - variableValues: exeContext.variableValues, - }; -} + /** + * Complete a list value by completing each item in the list with the + * inner type + */ + protected completeListValue( + exeContext: ExecutionContext, + returnType: GraphQLList, + fieldNodes: ReadonlyArray, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + ): PromiseOrValue> { + if (!isIterableObject(result)) { + throw new GraphQLError( + `Expected Iterable, but did not find one for field "${info.parentType.name}.${info.fieldName}".`, + ); + } -function handleFieldError( - error: GraphQLError, - returnType: GraphQLOutputType, - exeContext: ExecutionContext, -): null { - // If the field type is non-nullable, then it is resolved without any - // protection from errors, however it still properly locates the error. - if (isNonNullType(returnType)) { - throw error; - } + // This is specified as a simple map, however we're optimizing the path + // where the list contains no Promises by avoiding creating another Promise. + const itemType = returnType.ofType; + let containsPromise = false; + const completedResults = Array.from(result, (item, index) => { + // No need to modify the info object containing the path, + // since from here on it is not ever accessed by resolver functions. + const itemPath = addPath(path, index, undefined); + try { + let completedItem; + if (isPromise(item)) { + completedItem = item.then((resolved) => + this.completeValue( + exeContext, + itemType, + fieldNodes, + info, + itemPath, + resolved, + ), + ); + } else { + completedItem = this.completeValue( + exeContext, + itemType, + fieldNodes, + info, + itemPath, + item, + ); + } - // Otherwise, error protection is applied, logging the error and resolving - // a null value for this field if one is encountered. - exeContext.errors.push(error); - return null; -} + if (isPromise(completedItem)) { + containsPromise = true; + // Note: we don't rely on a `catch` method, but we do expect "thenable" + // to take a second callback for the error case. + return completedItem.then(undefined, (rawError) => { + const error = locatedError( + rawError, + fieldNodes, + pathToArray(itemPath), + ); + return this.handleFieldError(error, itemType, exeContext); + }); + } + return completedItem; + } catch (rawError) { + const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); + return this.handleFieldError(error, itemType, exeContext); + } + }); -/** - * Implements the instructions for completeValue as defined in the - * "Field entries" section of the spec. - * - * If the field type is Non-Null, then this recursively completes the value - * for the inner type. It throws a field error if that completion returns null, - * as per the "Nullability" section of the spec. - * - * If the field type is a List, then this recursively completes the value - * for the inner type on each item in the list. - * - * If the field type is a Scalar or Enum, ensures the completed value is a legal - * value of the type by calling the `serialize` method of GraphQL type - * definition. - * - * If the field is an abstract type, determine the runtime type of the value - * and then complete based on that type - * - * Otherwise, the field type expects a sub-selection set, and will complete the - * value by executing all sub-selections. - */ -function completeValue( - exeContext: ExecutionContext, - returnType: GraphQLOutputType, - fieldNodes: ReadonlyArray, - info: GraphQLResolveInfo, - path: Path, - result: unknown, -): PromiseOrValue { - // If result is an Error, throw a located error. - if (result instanceof Error) { - throw result; + return containsPromise ? Promise.all(completedResults) : completedResults; } - // If field type is NonNull, complete for inner type, and throw field error - // if result is null. - if (isNonNullType(returnType)) { - const completed = completeValue( - exeContext, - returnType.ofType, - fieldNodes, - info, - path, - result, - ); - if (completed === null) { + /** + * Complete a Scalar or Enum by serializing to a valid value, returning + * null if serialization is not possible. + */ + protected completeLeafValue( + returnType: GraphQLLeafType, + result: unknown, + ): unknown { + const serializedResult = returnType.serialize(result); + if (serializedResult == null) { throw new Error( - `Cannot return null for non-nullable field ${info.parentType.name}.${info.fieldName}.`, + `Expected \`${inspect(returnType)}.serialize(${inspect( + result, + )})\` to ` + + `return non-nullable value, returned: ${inspect(serializedResult)}`, ); } - return completed; + return serializedResult; } - // If result value is null or undefined then return null. - if (result == null) { - return null; - } + /** + * Complete a value of an abstract type by determining the runtime object type + * of that value, then complete the value for that type. + */ + protected completeAbstractValue( + exeContext: ExecutionContext, + returnType: GraphQLAbstractType, + fieldNodes: ReadonlyArray, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + ): PromiseOrValue> { + const resolveTypeFn = returnType.resolveType ?? exeContext.typeResolver; + const contextValue = exeContext.contextValue; + const runtimeType = resolveTypeFn(result, contextValue, info, returnType); - // If field type is List, complete each item in the list with the inner type - if (isListType(returnType)) { - return completeListValue( + if (isPromise(runtimeType)) { + return runtimeType.then((resolvedRuntimeType) => + this.completeObjectValue( + exeContext, + this.ensureValidRuntimeType( + resolvedRuntimeType, + exeContext, + returnType, + fieldNodes, + info, + result, + ), + fieldNodes, + info, + path, + result, + ), + ); + } + + return this.completeObjectValue( exeContext, - returnType, + this.ensureValidRuntimeType( + runtimeType, + exeContext, + returnType, + fieldNodes, + info, + result, + ), fieldNodes, info, path, @@ -608,296 +875,276 @@ function completeValue( ); } - // If field type is a leaf type, Scalar or Enum, serialize to a valid value, - // returning null if serialization is not possible. - if (isLeafType(returnType)) { - return completeLeafValue(returnType, result); - } + protected ensureValidRuntimeType( + runtimeTypeName: unknown, + exeContext: ExecutionContext, + returnType: GraphQLAbstractType, + fieldNodes: ReadonlyArray, + info: GraphQLResolveInfo, + result: unknown, + ): GraphQLObjectType { + if (runtimeTypeName == null) { + throw new GraphQLError( + `Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}". Either the "${returnType.name}" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`, + fieldNodes, + ); + } - // If field type is an abstract type, Interface or Union, determine the - // runtime Object type and complete for that type. - if (isAbstractType(returnType)) { - return completeAbstractValue( - exeContext, - returnType, - fieldNodes, - info, - path, - result, - ); + // releases before 16.0.0 supported returning `GraphQLObjectType` from `resolveType` + // TODO: remove in 17.0.0 release + if (isObjectType(runtimeTypeName)) { + throw new GraphQLError( + 'Support for returning GraphQLObjectType from resolveType was removed in graphql-js@16.0.0 please return type name instead.', + ); + } + + if (typeof runtimeTypeName !== 'string') { + throw new GraphQLError( + `Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}" with ` + + `value ${inspect(result)}, received "${inspect(runtimeTypeName)}".`, + ); + } + + const runtimeType = exeContext.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.`, + fieldNodes, + ); + } + + if (!isObjectType(runtimeType)) { + throw new GraphQLError( + `Abstract type "${returnType.name}" was resolved to a non-object type "${runtimeTypeName}".`, + fieldNodes, + ); + } + + if (!exeContext.schema.isSubType(returnType, runtimeType)) { + throw new GraphQLError( + `Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`, + fieldNodes, + ); + } + + return runtimeType; } - // If field type is Object, execute and complete all sub-selections. - // istanbul ignore else (See: 'https://github.com/graphql/graphql-js/issues/2618') - if (isObjectType(returnType)) { - return completeObjectValue( + /** + * Complete an Object value by executing all sub-selections. + */ + protected completeObjectValue( + exeContext: ExecutionContext, + returnType: GraphQLObjectType, + fieldNodes: ReadonlyArray, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + ): PromiseOrValue> { + // Collect sub-fields to execute to complete this value. + const subFieldNodes = this.collectSubfields( exeContext, returnType, fieldNodes, - info, - path, - result, ); - } - // istanbul ignore next (Not reachable. All possible output types have been considered) - invariant( - false, - 'Cannot complete value of unexpected output type: ' + inspect(returnType), - ); -} - -/** - * Complete a list value by completing each item in the list with the - * inner type - */ -function completeListValue( - exeContext: ExecutionContext, - returnType: GraphQLList, - fieldNodes: ReadonlyArray, - info: GraphQLResolveInfo, - path: Path, - result: unknown, -): PromiseOrValue> { - if (!isIterableObject(result)) { - throw new GraphQLError( - `Expected Iterable, but did not find one for field "${info.parentType.name}.${info.fieldName}".`, - ); - } + // 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, + ); - // This is specified as a simple map, however we're optimizing the path - // where the list contains no Promises by avoiding creating another Promise. - const itemType = returnType.ofType; - let containsPromise = false; - const completedResults = Array.from(result, (item, index) => { - // No need to modify the info object containing the path, - // since from here on it is not ever accessed by resolver functions. - const itemPath = addPath(path, index, undefined); - try { - let completedItem; - if (isPromise(item)) { - completedItem = item.then((resolved) => - completeValue( + if (isPromise(isTypeOf)) { + return isTypeOf.then((resolvedIsTypeOf) => { + if (!resolvedIsTypeOf) { + throw this.invalidReturnTypeError(returnType, result, fieldNodes); + } + return this.executeFields( exeContext, - itemType, - fieldNodes, - info, - itemPath, - resolved, - ), - ); - } else { - completedItem = completeValue( - exeContext, - itemType, - fieldNodes, - info, - itemPath, - item, - ); - } - - if (isPromise(completedItem)) { - containsPromise = true; - // Note: we don't rely on a `catch` method, but we do expect "thenable" - // to take a second callback for the error case. - return completedItem.then(undefined, (rawError) => { - const error = locatedError( - rawError, - fieldNodes, - pathToArray(itemPath), + returnType, + result, + path, + subFieldNodes, ); - return handleFieldError(error, itemType, exeContext); }); } - return completedItem; - } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); - return handleFieldError(error, itemType, exeContext); - } - }); - - return containsPromise ? Promise.all(completedResults) : completedResults; -} - -/** - * Complete a Scalar or Enum by serializing to a valid value, returning - * null if serialization is not possible. - */ -function completeLeafValue( - returnType: GraphQLLeafType, - result: unknown, -): unknown { - const serializedResult = returnType.serialize(result); - if (serializedResult == null) { - throw new Error( - `Expected \`${inspect(returnType)}.serialize(${inspect(result)})\` to ` + - `return non-nullable value, returned: ${inspect(serializedResult)}`, - ); - } - return serializedResult; -} -/** - * Complete a value of an abstract type by determining the runtime object type - * of that value, then complete the value for that type. - */ -function completeAbstractValue( - exeContext: ExecutionContext, - returnType: GraphQLAbstractType, - fieldNodes: ReadonlyArray, - info: GraphQLResolveInfo, - path: Path, - result: unknown, -): PromiseOrValue> { - const resolveTypeFn = returnType.resolveType ?? exeContext.typeResolver; - const contextValue = exeContext.contextValue; - const runtimeType = resolveTypeFn(result, contextValue, info, returnType); - - if (isPromise(runtimeType)) { - return runtimeType.then((resolvedRuntimeType) => - completeObjectValue( - exeContext, - ensureValidRuntimeType( - resolvedRuntimeType, - exeContext, - returnType, - fieldNodes, - info, - result, - ), - fieldNodes, - info, - path, - result, - ), - ); - } + if (!isTypeOf) { + throw this.invalidReturnTypeError(returnType, result, fieldNodes); + } + } - return completeObjectValue( - exeContext, - ensureValidRuntimeType( - runtimeType, + return this.executeFields( exeContext, returnType, - fieldNodes, - info, result, - ), - fieldNodes, - info, - path, - result, - ); -} + path, + subFieldNodes, + ); + } -function ensureValidRuntimeType( - runtimeTypeName: unknown, - exeContext: ExecutionContext, - returnType: GraphQLAbstractType, - fieldNodes: ReadonlyArray, - info: GraphQLResolveInfo, - result: unknown, -): GraphQLObjectType { - if (runtimeTypeName == null) { - throw new GraphQLError( - `Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}". Either the "${returnType.name}" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`, + protected invalidReturnTypeError( + returnType: GraphQLObjectType, + result: unknown, + fieldNodes: ReadonlyArray, + ): GraphQLError { + return new GraphQLError( + `Expected value of type "${returnType.name}" but got: ${inspect( + result, + )}.`, fieldNodes, ); } - // releases before 16.0.0 supported returning `GraphQLObjectType` from `resolveType` - // TODO: remove in 17.0.0 release - if (isObjectType(runtimeTypeName)) { - throw new GraphQLError( - 'Support for returning GraphQLObjectType from resolveType was removed in graphql-js@16.0.0 please return type name instead.', - ); + /** + * This method looks up the field on the given type definition. + * It has special casing for the three introspection fields, + * __schema, __type and __typename. __typename is special because + * it can always be queried as a field, even in situations where no + * other fields are allowed, like on a Union. __schema and __type + * could get automatically added to the query type, but that would + * require mutating type definitions, which would cause issues. + */ + protected getFieldDef( + schema: GraphQLSchema, + parentType: GraphQLObjectType, + fieldNode: FieldNode, + ): Maybe> { + const fieldName = fieldNode.name.value; + + if ( + fieldName === SchemaMetaFieldDef.name && + schema.getQueryType() === parentType + ) { + return SchemaMetaFieldDef; + } else if ( + fieldName === TypeMetaFieldDef.name && + schema.getQueryType() === parentType + ) { + return TypeMetaFieldDef; + } else if (fieldName === TypeNameMetaFieldDef.name) { + return TypeNameMetaFieldDef; + } + return parentType.getFields()[fieldName]; } - if (typeof runtimeTypeName !== 'string') { - throw new GraphQLError( - `Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}" with ` + - `value ${inspect(result)}, received "${inspect(runtimeTypeName)}".`, - ); + protected async executeSubscriptionImpl( + exeContext: ExecutionContext, + ): Promise | ExecutionResult> { + const resultOrStream = await this.createSourceEventStreamImpl(exeContext); + + if (!isAsyncIterable(resultOrStream)) { + return resultOrStream; + } + + // For each payload yielded from a subscription, map it over the normal + // GraphQL `executeQueryOrMutationImpl` method, with `payload` as the + // rootValue. + // This implements the "MapSourceToResponseEvent" algorithm described in + // the GraphQL specification. The `executeQueryOrMutationImpl` method + // provides the "ExecuteSubscriptionEvent" algorithm, as it is nearly + // identical to the "ExecuteQuery" algorithm, for which `execute` is also + // used. + const mapSourceToResponse = (payload: unknown) => { + const perPayloadExecutionContext = this.buildPerPayloadExecutionContext( + exeContext, + payload, + ); + return this.executeQueryOrMutationImpl(perPayloadExecutionContext); + }; + + // Map every source value to a ExecutionResult value as described above. + return mapAsyncIterator(resultOrStream, mapSourceToResponse); } - const runtimeType = exeContext.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.`, - fieldNodes, - ); + protected async createSourceEventStreamImpl( + exeContext: ExecutionContext, + ): Promise | ExecutionResult> { + try { + const eventStream = await this.executeSubscriptionRootField(exeContext); + + // Assert field returned an event stream, otherwise yield an error. + if (!isAsyncIterable(eventStream)) { + throw new Error( + 'Subscription field must return Async Iterable. ' + + `Received: ${inspect(eventStream)}.`, + ); + } + + return eventStream; + } catch (error) { + // If it GraphQLError, report it as an ExecutionResult, containing only errors and no data. + // Otherwise treat the error as a system-class error and re-throw it. + if (error instanceof GraphQLError) { + return { errors: [error] }; + } + throw error; + } } - if (!isObjectType(runtimeType)) { - throw new GraphQLError( - `Abstract type "${returnType.name}" was resolved to a non-object type "${runtimeTypeName}".`, - fieldNodes, + protected async executeSubscriptionRootField( + exeContext: ExecutionContext, + ): Promise { + const { schema, fragments, operation, variableValues, rootValue } = + exeContext; + const type = getOperationRootType(schema, operation); + const fields = collectFields( + schema, + fragments, + variableValues, + type, + operation.selectionSet, ); - } + const [responseName, fieldNodes] = [...fields.entries()][0]; + const fieldDef = this.getFieldDef(schema, type, fieldNodes[0]); - if (!exeContext.schema.isSubType(returnType, runtimeType)) { - throw new GraphQLError( - `Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`, + if (!fieldDef) { + const fieldName = fieldNodes[0].name.value; + throw new GraphQLError( + `The subscription field "${fieldName}" is not defined.`, + fieldNodes, + ); + } + + const path = addPath(undefined, responseName, type.name); + const info = this.buildResolveInfo( + exeContext, + fieldDef, fieldNodes, + type, + path, ); - } - return runtimeType; -} + try { + // Implements the "ResolveFieldEventStream" algorithm from GraphQL specification. + // It differs from "ResolveFieldValue" due to providing a different `resolveFn`. -/** - * Complete an Object value by executing all sub-selections. - */ -function completeObjectValue( - exeContext: ExecutionContext, - returnType: GraphQLObjectType, - fieldNodes: ReadonlyArray, - info: GraphQLResolveInfo, - path: Path, - result: unknown, -): PromiseOrValue> { - // Collect sub-fields to execute to complete this value. - const subFieldNodes = collectSubfields(exeContext, 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); - - if (isPromise(isTypeOf)) { - return isTypeOf.then((resolvedIsTypeOf) => { - if (!resolvedIsTypeOf) { - throw invalidReturnTypeError(returnType, result, fieldNodes); - } - return executeFields( - exeContext, - returnType, - result, - path, - subFieldNodes, - ); - }); - } + // 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); - if (!isTypeOf) { - throw invalidReturnTypeError(returnType, result, fieldNodes); - } - } + // 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; - return executeFields(exeContext, returnType, result, path, subFieldNodes); -} + // Call the `subscribe()` resolver or the default resolver to produce an + // AsyncIterable yielding raw payloads. + const resolveFn = fieldDef.subscribe ?? exeContext.fieldResolver; + const eventStream = await resolveFn(rootValue, args, contextValue, info); -function invalidReturnTypeError( - returnType: GraphQLObjectType, - result: unknown, - fieldNodes: ReadonlyArray, -): GraphQLError { - return new GraphQLError( - `Expected value of type "${returnType.name}" but got: ${inspect(result)}.`, - fieldNodes, - ); + if (eventStream instanceof Error) { + throw eventStream; + } + return eventStream; + } catch (error) { + throw locatedError(error, fieldNodes, pathToArray(path)); + } + } } /** @@ -963,142 +1210,3 @@ export const defaultFieldResolver: GraphQLFieldResolver = return property; } }; - -/** - * This method looks up the field on the given type definition. - * It has special casing for the three introspection fields, - * __schema, __type and __typename. __typename is special because - * it can always be queried as a field, even in situations where no - * other fields are allowed, like on a Union. __schema and __type - * could get automatically added to the query type, but that would - * require mutating type definitions, which would cause issues. - * - * @internal - */ -export function getFieldDef( - schema: GraphQLSchema, - parentType: GraphQLObjectType, - fieldNode: FieldNode, -): Maybe> { - const fieldName = fieldNode.name.value; - - if ( - fieldName === SchemaMetaFieldDef.name && - schema.getQueryType() === parentType - ) { - return SchemaMetaFieldDef; - } else if ( - fieldName === TypeMetaFieldDef.name && - schema.getQueryType() === parentType - ) { - return TypeMetaFieldDef; - } else if (fieldName === TypeNameMetaFieldDef.name) { - return TypeNameMetaFieldDef; - } - return parentType.getFields()[fieldName]; -} - -export async function executeSubscription( - exeContext: ExecutionContext, -): Promise | ExecutionResult> { - const resultOrStream = await createSourceEventStreamImpl(exeContext); - - if (!isAsyncIterable(resultOrStream)) { - return resultOrStream; - } - - // 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. - const mapSourceToResponse = (payload: unknown) => - executeQueryOrMutation({ - ...exeContext, - rootValue: payload, - errors: [], - }); - - // Map every source value to a ExecutionResult value as described above. - return mapAsyncIterator(resultOrStream, mapSourceToResponse); -} - -export async function createSourceEventStreamImpl( - exeContext: ExecutionContext, -): Promise | ExecutionResult> { - try { - const eventStream = await executeSubscriptionRootField(exeContext); - - // Assert field returned an event stream, otherwise yield an error. - if (!isAsyncIterable(eventStream)) { - throw new Error( - 'Subscription field must return Async Iterable. ' + - `Received: ${inspect(eventStream)}.`, - ); - } - - return eventStream; - } catch (error) { - // If it GraphQLError, report it as an ExecutionResult, containing only errors and no data. - // Otherwise treat the error as a system-class error and re-throw it. - if (error instanceof GraphQLError) { - return { errors: [error] }; - } - throw error; - } -} - -async function executeSubscriptionRootField( - exeContext: ExecutionContext, -): Promise { - const { schema, fragments, operation, variableValues, rootValue } = - exeContext; - const type = getOperationRootType(schema, operation); - const fields = collectFields( - schema, - fragments, - variableValues, - type, - operation.selectionSet, - ); - const [responseName, fieldNodes] = [...fields.entries()][0]; - const fieldDef = getFieldDef(schema, type, fieldNodes[0]); - - if (!fieldDef) { - const fieldName = fieldNodes[0].name.value; - throw new GraphQLError( - `The subscription field "${fieldName}" is not defined.`, - fieldNodes, - ); - } - - const path = addPath(undefined, responseName, type.name); - const info = buildResolveInfo(exeContext, fieldDef, fieldNodes, type, path); - - try { - // 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], 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; - - // Call the `subscribe()` resolver or the default resolver to produce an - // AsyncIterable yielding raw payloads. - const resolveFn = fieldDef.subscribe ?? exeContext.fieldResolver; - const eventStream = await resolveFn(rootValue, args, contextValue, info); - - if (eventStream instanceof Error) { - throw eventStream; - } - return eventStream; - } catch (error) { - throw locatedError(error, fieldNodes, pathToArray(path)); - } -} diff --git a/src/execution/subscribe.ts b/src/execution/subscribe.ts index 04796a092ea..d5a39820ad9 100644 --- a/src/execution/subscribe.ts +++ b/src/execution/subscribe.ts @@ -6,11 +6,7 @@ import type { GraphQLSchema } from '../type/schema'; import type { GraphQLFieldResolver } from '../type/definition'; import type { ExecutionArgs, ExecutionResult } from './executor'; -import { - buildExecutionContext, - createSourceEventStreamImpl, - executeSubscription, -} from './executor'; +import { Executor } from './executor'; /** * Note: ExecutionArgs has been broadened to include all properties @@ -45,16 +41,9 @@ export interface SubscriptionArgs extends ExecutionArgs {} export async function subscribe( args: SubscriptionArgs, ): Promise | ExecutionResult> { - // If a valid execution context cannot be created due to incorrect arguments, - // a "Response" with only errors is returned. - const exeContext = buildExecutionContext(args); + const executor = new Executor(args); - // Return early errors if execution context failed. - if (!('schema' in exeContext)) { - return { errors: exeContext }; - } - - return executeSubscription(exeContext); + return executor.executeSubscription(); } /** @@ -94,9 +83,7 @@ export async function createSourceEventStream( operationName?: Maybe, subscribeFieldResolver?: Maybe>, ): Promise | ExecutionResult> { - // If a valid execution context cannot be created due to incorrect arguments, - // a "Response" with only errors is returned. - const exeContext = buildExecutionContext({ + const executor = new Executor({ schema, document, rootValue, @@ -106,10 +93,5 @@ export async function createSourceEventStream( subscribeFieldResolver, }); - // Return early errors if execution context failed. - if (!('schema' in exeContext)) { - return { errors: exeContext }; - } - - return createSourceEventStreamImpl(exeContext); + return executor.createSourceEventStream(); }