From 36ea4d99c61cf1589869c9405f5786bdeda2b34b Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Wed, 16 Jun 2021 20:29:21 +0300 Subject: [PATCH 1/6] refactor: collectFields to separate utility --- src/execution/collectFields.ts | 159 ++++++++++++++++++ src/execution/execute.ts | 158 +---------------- src/subscription/subscribe.ts | 9 +- .../rules/SingleFieldSubscriptionsRule.ts | 19 +-- 4 files changed, 175 insertions(+), 170 deletions(-) create mode 100644 src/execution/collectFields.ts diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts new file mode 100644 index 0000000000..2da9440969 --- /dev/null +++ b/src/execution/collectFields.ts @@ -0,0 +1,159 @@ +import type { ObjMap } from '../jsutils/ObjMap'; + +import type { + SelectionSetNode, + FieldNode, + FragmentSpreadNode, + InlineFragmentNode, + FragmentDefinitionNode, +} from '../language/ast'; +import { Kind } from '../language/kinds'; + +import type { GraphQLSchema } from '../type/schema'; +import type { GraphQLObjectType } from '../type/definition'; +import { + GraphQLIncludeDirective, + GraphQLSkipDirective, +} from '../type/directives'; +import { isAbstractType } from '../type/definition'; + +import { typeFromAST } from '../utilities/typeFromAST'; + +import { getDirectiveValues } from './values'; + +/** + * Given a selectionSet, adds all of the fields in that selection to + * the passed in map of fields, and returns it at the end. + * + * CollectFields requires the "runtime type" of an object. For a field which + * returns an Interface or Union type, the "runtime type" will be the actual + * Object type returned by that field. + * + * @internal + */ +export function collectFields( + schema: GraphQLSchema, + fragments: ObjMap, + variableValues: { [variable: string]: unknown }, + runtimeType: GraphQLObjectType, + selectionSet: SelectionSetNode, + fields: Map>, + visitedFragmentNames: Set, +): Map> { + for (const selection of selectionSet.selections) { + switch (selection.kind) { + case Kind.FIELD: { + if (!shouldIncludeNode(variableValues, selection)) { + continue; + } + const name = getFieldEntryKey(selection); + const fieldList = fields.get(name); + if (fieldList !== undefined) { + fieldList.push(selection); + } else { + fields.set(name, [selection]); + } + break; + } + case Kind.INLINE_FRAGMENT: { + if ( + !shouldIncludeNode(variableValues, selection) || + !doesFragmentConditionMatch(schema, selection, runtimeType) + ) { + continue; + } + collectFields( + schema, + fragments, + variableValues, + runtimeType, + selection.selectionSet, + fields, + visitedFragmentNames, + ); + break; + } + case Kind.FRAGMENT_SPREAD: { + const fragName = selection.name.value; + if ( + visitedFragmentNames.has(fragName) || + !shouldIncludeNode(variableValues, selection) + ) { + continue; + } + visitedFragmentNames.add(fragName); + const fragment = fragments[fragName]; + if ( + !fragment || + !doesFragmentConditionMatch(schema, fragment, runtimeType) + ) { + continue; + } + collectFields( + schema, + fragments, + variableValues, + runtimeType, + fragment.selectionSet, + fields, + visitedFragmentNames, + ); + break; + } + } + } + return fields; +} + +/** + * Determines if a field should be included based on the @include and @skip + * directives, where @skip has higher precedence than @include. + */ +function shouldIncludeNode( + variableValues: { [variable: string]: unknown }, + node: FragmentSpreadNode | FieldNode | InlineFragmentNode, +): boolean { + const skip = getDirectiveValues(GraphQLSkipDirective, node, variableValues); + if (skip?.if === true) { + return false; + } + + const include = getDirectiveValues( + GraphQLIncludeDirective, + node, + variableValues, + ); + if (include?.if === false) { + return false; + } + return true; +} + +/** + * Determines if a fragment is applicable to the given type. + */ +function doesFragmentConditionMatch( + schema: GraphQLSchema, + fragment: FragmentDefinitionNode | InlineFragmentNode, + type: GraphQLObjectType, +): boolean { + const typeConditionNode = fragment.typeCondition; + if (!typeConditionNode) { + return true; + } + const conditionalType = typeFromAST(schema, typeConditionNode); + if (conditionalType === type) { + return true; + } + if (isAbstractType(conditionalType)) { + return schema.isSubType(conditionalType, type); + } + return false; +} + +/** + * Implements the logic to compute the key of a given field's entry + */ +function getFieldEntryKey(node: FieldNode): string { + return node.alias ? node.alias.value : node.name.value; +} diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 985d3173ed..4d7bc386db 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -20,10 +20,7 @@ import { locatedError } from '../error/locatedError'; import type { DocumentNode, OperationDefinitionNode, - SelectionSetNode, FieldNode, - FragmentSpreadNode, - InlineFragmentNode, FragmentDefinitionNode, } from '../language/ast'; import { Kind } from '../language/kinds'; @@ -46,10 +43,6 @@ import { TypeMetaFieldDef, TypeNameMetaFieldDef, } from '../type/introspection'; -import { - GraphQLIncludeDirective, - GraphQLSkipDirective, -} from '../type/directives'; import { isObjectType, isAbstractType, @@ -58,14 +51,10 @@ import { isNonNullType, } from '../type/definition'; -import { typeFromAST } from '../utilities/typeFromAST'; import { getOperationRootType } from '../utilities/getOperationRootType'; -import { - getVariableValues, - getArgumentValues, - getDirectiveValues, -} from './values'; +import { getVariableValues, getArgumentValues } from './values'; +import { collectFields } from './collectFields'; /** * Terminology @@ -336,7 +325,9 @@ function executeOperation( ): PromiseOrValue | null> { const type = getOperationRootType(exeContext.schema, operation); const fields = collectFields( - exeContext, + exeContext.schema, + exeContext.fragments, + exeContext.variableValues, type, operation.selectionSet, new Map(), @@ -447,141 +438,6 @@ function executeFields( return promiseForObject(results); } -/** - * Given a selectionSet, adds all of the fields in that selection to - * the passed in map of fields, and returns it at the end. - * - * CollectFields requires the "runtime type" of an object. For a field which - * returns an Interface or Union type, the "runtime type" will be the actual - * Object type returned by that field. - * - * @internal - */ -export function collectFields( - exeContext: ExecutionContext, - runtimeType: GraphQLObjectType, - selectionSet: SelectionSetNode, - fields: Map>, - visitedFragmentNames: Set, -): Map> { - for (const selection of selectionSet.selections) { - switch (selection.kind) { - case Kind.FIELD: { - if (!shouldIncludeNode(exeContext, selection)) { - continue; - } - const name = getFieldEntryKey(selection); - const fieldList = fields.get(name); - if (fieldList !== undefined) { - fieldList.push(selection); - } else { - fields.set(name, [selection]); - } - break; - } - case Kind.INLINE_FRAGMENT: { - if ( - !shouldIncludeNode(exeContext, selection) || - !doesFragmentConditionMatch(exeContext, selection, runtimeType) - ) { - continue; - } - collectFields( - exeContext, - runtimeType, - selection.selectionSet, - fields, - visitedFragmentNames, - ); - break; - } - case Kind.FRAGMENT_SPREAD: { - const fragName = selection.name.value; - if ( - visitedFragmentNames.has(fragName) || - !shouldIncludeNode(exeContext, selection) - ) { - continue; - } - visitedFragmentNames.add(fragName); - const fragment = exeContext.fragments[fragName]; - if ( - !fragment || - !doesFragmentConditionMatch(exeContext, fragment, runtimeType) - ) { - continue; - } - collectFields( - exeContext, - runtimeType, - fragment.selectionSet, - fields, - visitedFragmentNames, - ); - break; - } - } - } - return fields; -} - -/** - * Determines if a field should be included based on the @include and @skip - * directives, where @skip has higher precedence than @include. - */ -function shouldIncludeNode( - exeContext: ExecutionContext, - node: FragmentSpreadNode | FieldNode | InlineFragmentNode, -): boolean { - const skip = getDirectiveValues( - GraphQLSkipDirective, - node, - exeContext.variableValues, - ); - if (skip?.if === true) { - return false; - } - - const include = getDirectiveValues( - GraphQLIncludeDirective, - node, - exeContext.variableValues, - ); - if (include?.if === false) { - return false; - } - return true; -} - -/** - * Determines if a fragment is applicable to the given type. - */ -function doesFragmentConditionMatch( - exeContext: ExecutionContext, - fragment: FragmentDefinitionNode | InlineFragmentNode, - type: GraphQLObjectType, -): boolean { - const typeConditionNode = fragment.typeCondition; - if (!typeConditionNode) { - return true; - } - const conditionalType = typeFromAST(exeContext.schema, typeConditionNode); - if (conditionalType === type) { - return true; - } - if (isAbstractType(conditionalType)) { - return exeContext.schema.isSubType(conditionalType, type); - } - return false; -} - -/** - * Implements the logic to compute the key of a given field's entry - */ -function getFieldEntryKey(node: FieldNode): string { - return node.alias ? node.alias.value : node.name.value; -} - /** * Implements the "Executing field" section of the spec * In particular, this function figures out the value that the field returns by @@ -1081,7 +937,9 @@ function _collectSubfields( for (const node of fieldNodes) { if (node.selectionSet) { subFieldNodes = collectFields( - exeContext, + exeContext.schema, + exeContext.fragments, + exeContext.variableValues, returnType, node.selectionSet, subFieldNodes, diff --git a/src/subscription/subscribe.ts b/src/subscription/subscribe.ts index 6b4c6c13bf..6fbca779df 100644 --- a/src/subscription/subscribe.ts +++ b/src/subscription/subscribe.ts @@ -9,12 +9,12 @@ import { locatedError } from '../error/locatedError'; import type { DocumentNode } from '../language/ast'; import type { ExecutionResult, ExecutionContext } from '../execution/execute'; +import { collectFields } from '../execution/collectFields'; import { getArgumentValues } from '../execution/values'; import { assertValidExecutionArguments, buildExecutionContext, buildResolveInfo, - collectFields, execute, getFieldDef, } from '../execution/execute'; @@ -189,10 +189,13 @@ export async function createSourceEventStream( async function executeSubscription( exeContext: ExecutionContext, ): Promise { - const { schema, operation, variableValues, rootValue } = exeContext; + const { schema, fragments, operation, variableValues, rootValue } = + exeContext; const type = getOperationRootType(schema, operation); const fields = collectFields( - exeContext, + schema, + fragments, + variableValues, type, operation.selectionSet, new Map(), diff --git a/src/validation/rules/SingleFieldSubscriptionsRule.ts b/src/validation/rules/SingleFieldSubscriptionsRule.ts index 0098ddc3d7..736f0f006f 100644 --- a/src/validation/rules/SingleFieldSubscriptionsRule.ts +++ b/src/validation/rules/SingleFieldSubscriptionsRule.ts @@ -8,12 +8,7 @@ import type { } from '../../language/ast'; import { Kind } from '../../language/kinds'; -import type { ExecutionContext } from '../../execution/execute'; -import { - collectFields, - defaultFieldResolver, - defaultTypeResolver, -} from '../../execution/execute'; +import { collectFields } from '../../execution/collectFields'; import type { ValidationContext } from '../ValidationContext'; @@ -43,20 +38,10 @@ export function SingleFieldSubscriptionsRule( fragments[definition.name.value] = definition; } } - // FIXME: refactor out `collectFields` into utility function that doesn't need fake context. - const fakeExecutionContext: ExecutionContext = { + const fields = collectFields( schema, fragments, - rootValue: undefined, - contextValue: undefined, - operation: node, variableValues, - fieldResolver: defaultFieldResolver, - typeResolver: defaultTypeResolver, - errors: [], - }; - const fields = collectFields( - fakeExecutionContext, subscriptionType, node.selectionSet, new Map(), From e8228c57e76660c60f99e2b52a5c6baa43ae27d1 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Wed, 16 Jun 2021 21:49:43 +0300 Subject: [PATCH 2/6] refactor: execution methods into Executor class see: https://github.com/graphql/graphql-js/pull/3163#issuecomment-859546546 depends on #3184 --- src/execution/execute.ts | 1092 ++++++++++++++++----------------- src/jsutils/memoize2.ts | 28 + src/jsutils/memoize3.ts | 37 -- src/subscription/subscribe.ts | 104 ++-- 4 files changed, 618 insertions(+), 643 deletions(-) create mode 100644 src/jsutils/memoize2.ts delete mode 100644 src/jsutils/memoize3.ts diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 4d7bc386db..2ad862e9d5 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -3,7 +3,7 @@ import type { ObjMap } from '../jsutils/ObjMap'; import type { PromiseOrValue } from '../jsutils/PromiseOrValue'; import type { Maybe } from '../jsutils/Maybe'; import { inspect } from '../jsutils/inspect'; -import { memoize3 } from '../jsutils/memoize3'; +import { memoize2 } from '../jsutils/memoize2'; import { invariant } from '../jsutils/invariant'; import { devAssert } from '../jsutils/devAssert'; import { isPromise } from '../jsutils/isPromise'; @@ -173,6 +173,8 @@ export function execute(args: ExecutionArgs): PromiseOrValue { return { errors: exeContext }; } + const executor = new Executor(exeContext); + // Return a Promise that will eventually resolve to the data described by // The "Response" section of the GraphQL specification. // @@ -180,8 +182,8 @@ export function execute(args: ExecutionArgs): PromiseOrValue { // field and its descendants will be omitted, and sibling fields will still // be executed. An execution which encounters errors will still result in a // resolved Promise. - const data = executeOperation(exeContext, exeContext.operation, rootValue); - return buildResponse(exeContext, data); + const data = executor.executeOperation(); + return executor.buildResponse(data); } /** @@ -200,22 +202,6 @@ 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( - exeContext: ExecutionContext, - data: PromiseOrValue | null>, -): PromiseOrValue { - if (isPromise(data)) { - return data.then((resolved) => buildResponse(exeContext, resolved)); - } - 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. @@ -316,598 +302,617 @@ export function buildExecutionContext( } /** - * Implements the "Executing operations" section of the spec. + * @internal */ -function executeOperation( - exeContext: ExecutionContext, - operation: OperationDefinitionNode, - rootValue: unknown, -): PromiseOrValue | null> { - const type = getOperationRootType(exeContext.schema, operation); - const fields = collectFields( - exeContext.schema, - exeContext.fragments, - exeContext.variableValues, - type, - operation.selectionSet, - new Map(), - new Set(), +export class Executor { + protected _exeContext: ExecutionContext; + /** + * 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 = memoize2( + (returnType: GraphQLObjectType, fieldNodes: ReadonlyArray) => + this._collectSubfields(returnType, fieldNodes), ); - 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); - }); + constructor(exeContext: ExecutionContext) { + this._exeContext = exeContext; + } + + /** + * Implements the "Executing operations" section of the spec. + */ + public executeOperation(): PromiseOrValue | null> { + const { schema, fragments, rootValue, operation, variableValues, errors } = + this._exeContext; + const type = getOperationRootType(schema, operation); + const fields = collectFields( + schema, + fragments, + variableValues, + type, + operation.selectionSet, + new Map(), + new Set(), + ); + + 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(type, rootValue, path, fields) + : this.executeFields(type, rootValue, path, fields); + if (isPromise(result)) { + return result.then(undefined, (error) => { + errors.push(error); + return Promise.resolve(null); + }); + } + return result; + } catch (error) { + errors.push(error); + return 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. - */ -function executeFieldsSerially( - exeContext: ExecutionContext, - parentType: GraphQLObjectType, - sourceValue: unknown, - path: Path | undefined, - fields: Map>, -): PromiseOrValue> { - return promiseReduce( - fields.entries(), - (results, [responseName, fieldNodes]) => { + /** + * Given a completed execution context and data, build the { errors, data } + * response defined by the "Response" section of the GraphQL specification. + */ + public buildResponse( + data: PromiseOrValue | null>, + ): PromiseOrValue { + if (isPromise(data)) { + return data.then((resolved) => this.buildResponse(resolved)); + } + const errors = this._exeContext.errors; + return errors.length === 0 ? { data } : { errors, data }; + } + + /** + * Implements the "Executing selection sets" section of the spec + * for fields that must be executed serially. + */ + protected executeFieldsSerially( + 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( + 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( + 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, + const result = this.executeField( 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, - parentType, - sourceValue, - fieldNodes, - fieldPath, - ); + // 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( + parentType: GraphQLObjectType, + source: unknown, + fieldNodes: ReadonlyArray, + path: Path, + ): PromiseOrValue { + const { schema, contextValue, variableValues, fieldResolver } = + this._exeContext; + + const fieldDef = getFieldDef(schema, parentType, fieldNodes[0]); + if (!fieldDef) { + return; + } + + const returnType = fieldDef.type; + const resolveFn = fieldDef.resolve ?? fieldResolver; - if (result !== undefined) { - results[responseName] = result; + const info = this.buildResolveInfo(fieldDef, fieldNodes, parentType, path); + + // 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], 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 result = resolveFn(source, args, contextValue, info); + + let completed; if (isPromise(result)) { - containsPromise = true; + completed = result.then((resolved) => + this.completeValue(returnType, fieldNodes, info, path, resolved), + ); + } else { + completed = this.completeValue( + 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); + }); + } + return completed; + } catch (rawError) { + const error = locatedError(rawError, fieldNodes, pathToArray(path)); + return this.handleFieldError(error, returnType); } } - // If there are no promises, we can just return the object - if (!containsPromise) { - return results; + /** + * @internal + */ + protected buildResolveInfo( + fieldDef: GraphQLField, + fieldNodes: ReadonlyArray, + parentType: GraphQLObjectType, + path: Path, + ): GraphQLResolveInfo { + const { schema, fragments, rootValue, operation, variableValues } = + this._exeContext; + + // 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, + fragments, + rootValue, + operation, + 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, + ): 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. + this._exeContext.errors.push(error); + return null; } - const returnType = fieldDef.type; - const resolveFn = fieldDef.resolve ?? exeContext.fieldResolver; - - const info = buildResolveInfo( - exeContext, - fieldDef, - fieldNodes, - parentType, - path, - ); - - // 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); + /** + * 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( + 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; + } - let completed; - if (isPromise(result)) { - completed = result.then((resolved) => - completeValue(exeContext, returnType, fieldNodes, info, path, resolved), - ); - } else { - completed = completeValue( - exeContext, - returnType, + // If field type is NonNull, complete for inner type, and throw field error + // if result is null. + if (isNonNullType(returnType)) { + const completed = this.completeValue( + 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; } - 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 result value is null or undefined then return null. + if (result == null) { + return null; } - return completed; - } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(path)); - return handleFieldError(error, returnType, exeContext); - } -} - -/** - * @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, - }; -} - -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; - } - // 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 field type is List, complete each item in the list with the inner type + if (isListType(returnType)) { + return this.completeListValue(returnType, fieldNodes, info, path, result); + } -/** - * 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; - } + // 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); + } - // 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) { - throw new Error( - `Cannot return null for non-nullable field ${info.parentType.name}.${info.fieldName}.`, + // 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( + returnType, + fieldNodes, + info, + path, + result, ); } - return completed; - } - - // If result value is null or undefined then return null. - if (result == null) { - return null; - } - - // If field type is List, complete each item in the list with the inner type - if (isListType(returnType)) { - return completeListValue( - exeContext, - returnType, - fieldNodes, - info, - path, - result, - ); - } - - // 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); - } - // 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, - ); - } + // 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( + returnType, + fieldNodes, + info, + path, + result, + ); + } - // 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( - 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), ); } - // 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}".`, - ); - } + /** + * Complete a list value by completing each item in the list with the + * inner type + */ + protected completeListValue( + 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}".`, + ); + } - // 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( - exeContext, + // 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(itemType, fieldNodes, info, itemPath, resolved), + ); + } else { + completedItem = this.completeValue( 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), + item, ); - return handleFieldError(error, itemType, exeContext); - }); + } + + 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); + }); + } + return completedItem; + } catch (rawError) { + const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); + return this.handleFieldError(error, itemType); } - return completedItem; - } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); - return handleFieldError(error, itemType, exeContext); - } - }); + }); - return containsPromise ? Promise.all(completedResults) : completedResults; -} + 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 === undefined) { - throw new Error( - `Expected a value of type "${inspect(returnType)}" but ` + - `received: ${inspect(result)}`, - ); + /** + * 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 === undefined) { + throw new Error( + `Expected a value of type "${inspect(returnType)}" but ` + + `received: ${inspect(result)}`, + ); + } + return 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, + /** + * 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( + returnType: GraphQLAbstractType, + fieldNodes: ReadonlyArray, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + ): PromiseOrValue> { + const { contextValue, typeResolver } = this._exeContext; + + const resolveTypeFn = returnType.resolveType ?? typeResolver; + const runtimeType = resolveTypeFn(result, contextValue, info, returnType); + + if (isPromise(runtimeType)) { + return runtimeType.then((resolvedRuntimeType) => + this.completeObjectValue( + this.ensureValidRuntimeType( + resolvedRuntimeType, + returnType, + fieldNodes, + info, + result, + ), fieldNodes, info, + path, result, ), + ); + } + + return this.completeObjectValue( + this.ensureValidRuntimeType( + runtimeType, + returnType, fieldNodes, info, - path, result, ), - ); - } - - return completeObjectValue( - exeContext, - ensureValidRuntimeType( - runtimeType, - exeContext, - returnType, fieldNodes, info, + path, result, - ), - fieldNodes, - info, - path, - result, - ); -} - -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.`, - 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.', - ); - } + protected ensureValidRuntimeType( + runtimeTypeName: unknown, + 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 (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)}".`, - ); - } + // 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.', + ); + } - 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 (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)}".`, + ); + } - if (!isObjectType(runtimeType)) { - throw new GraphQLError( - `Abstract type "${returnType.name}" was resolved to a non-object type "${runtimeTypeName}".`, - fieldNodes, - ); - } + const { schema } = this._exeContext; + const runtimeType = 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 (!exeContext.schema.isSubType(returnType, runtimeType)) { - throw new GraphQLError( - `Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`, - fieldNodes, - ); + if (!isObjectType(runtimeType)) { + throw new GraphQLError( + `Abstract type "${returnType.name}" was resolved to a non-object type "${runtimeTypeName}".`, + fieldNodes, + ); + } + + if (!schema.isSubType(returnType, runtimeType)) { + throw new GraphQLError( + `Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`, + fieldNodes, + ); + } + + return runtimeType; } - return runtimeType; -} + /** + * Complete an Object value by executing all sub-selections. + */ + protected completeObjectValue( + returnType: GraphQLObjectType, + fieldNodes: ReadonlyArray, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + ): PromiseOrValue> { + // Collect sub-fields to execute to complete this value. + const subFieldNodes = this.collectSubfields(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, + this._exeContext.contextValue, + info, + ); -/** - * 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, + if (isPromise(isTypeOf)) { + return isTypeOf.then((resolvedIsTypeOf) => { + if (!resolvedIsTypeOf) { + throw invalidReturnTypeError(returnType, result, fieldNodes); + } + return this.executeFields(returnType, result, path, subFieldNodes); + }); + } + + if (!isTypeOf) { + throw invalidReturnTypeError(returnType, result, fieldNodes); + } + } + + return this.executeFields(returnType, result, path, subFieldNodes); + } + + /** + * A collection of relevant subfields with regard to the return type. + * See 'collectSubfields' above for the memoized version. + */ + protected _collectSubfields( + returnType: GraphQLObjectType, + fieldNodes: ReadonlyArray, + ): Map> { + const { schema, fragments, variableValues } = this._exeContext; + + let subFieldNodes = new Map(); + const visitedFragmentNames = new Set(); + for (const node of fieldNodes) { + if (node.selectionSet) { + subFieldNodes = collectFields( + schema, + fragments, + variableValues, returnType, - result, - path, + node.selectionSet, subFieldNodes, + visitedFragmentNames, ); - }); - } - - if (!isTypeOf) { - throw invalidReturnTypeError(returnType, result, fieldNodes); + } } + return subFieldNodes; } - - return executeFields(exeContext, returnType, result, path, subFieldNodes); } function invalidReturnTypeError( @@ -921,35 +926,6 @@ function invalidReturnTypeError( ); } -/** - * 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(_collectSubfields); -function _collectSubfields( - exeContext: ExecutionContext, - returnType: GraphQLObjectType, - fieldNodes: ReadonlyArray, -): Map> { - let subFieldNodes = new Map(); - const visitedFragmentNames = new Set(); - for (const node of fieldNodes) { - if (node.selectionSet) { - subFieldNodes = collectFields( - exeContext.schema, - exeContext.fragments, - exeContext.variableValues, - returnType, - node.selectionSet, - subFieldNodes, - visitedFragmentNames, - ); - } - } - return subFieldNodes; -} - /** * If a resolveType function is not given, then a default resolve behavior is * used which attempts two strategies: diff --git a/src/jsutils/memoize2.ts b/src/jsutils/memoize2.ts new file mode 100644 index 0000000000..f63c3f5c11 --- /dev/null +++ b/src/jsutils/memoize2.ts @@ -0,0 +1,28 @@ +/** + * Memoizes the provided three-argument function. + */ +export function memoize2( + fn: (a1: A1, a2: A2) => R, +): (a1: A1, a2: A2) => R { + let cache0: WeakMap>; + + return function memoized(a1, a2) { + if (cache0 === undefined) { + cache0 = new WeakMap(); + } + + let cache1 = cache0.get(a1); + if (cache1 === undefined) { + cache1 = new WeakMap(); + cache0.set(a1, cache1); + } + + let fnResult = cache1.get(a2); + if (fnResult === undefined) { + fnResult = fn(a1, a2); + cache1.set(a2, fnResult); + } + + return fnResult; + }; +} diff --git a/src/jsutils/memoize3.ts b/src/jsutils/memoize3.ts deleted file mode 100644 index 213cb95d10..0000000000 --- a/src/jsutils/memoize3.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Memoizes the provided three-argument function. - */ -export function memoize3< - A1 extends object, - A2 extends object, - A3 extends object, - R, ->(fn: (a1: A1, a2: A2, a3: A3) => R): (a1: A1, a2: A2, a3: A3) => R { - let cache0: WeakMap>>; - - return function memoized(a1, a2, a3) { - if (cache0 === undefined) { - cache0 = new WeakMap(); - } - - let cache1 = cache0.get(a1); - if (cache1 === undefined) { - cache1 = new WeakMap(); - cache0.set(a1, cache1); - } - - let cache2 = cache1.get(a2); - if (cache2 === undefined) { - cache2 = new WeakMap(); - cache1.set(a2, cache2); - } - - let fnResult = cache2.get(a3); - if (fnResult === undefined) { - fnResult = fn(a1, a2, a3); - cache2.set(a3, fnResult); - } - - return fnResult; - }; -} diff --git a/src/subscription/subscribe.ts b/src/subscription/subscribe.ts index 6fbca779df..3f813f4dd6 100644 --- a/src/subscription/subscribe.ts +++ b/src/subscription/subscribe.ts @@ -8,13 +8,13 @@ import { locatedError } from '../error/locatedError'; import type { DocumentNode } from '../language/ast'; -import type { ExecutionResult, ExecutionContext } from '../execution/execute'; +import type { ExecutionResult } from '../execution/execute'; import { collectFields } from '../execution/collectFields'; import { getArgumentValues } from '../execution/values'; import { + Executor, assertValidExecutionArguments, buildExecutionContext, - buildResolveInfo, execute, getFieldDef, } from '../execution/execute'; @@ -165,7 +165,9 @@ export async function createSourceEventStream( return { errors: exeContext }; } - const eventStream = await executeSubscription(exeContext); + const executor = new SubscriptionExecutor(exeContext); + + const eventStream = await executor.executeSubscription(); // Assert field returned an event stream, otherwise yield an error. if (!isAsyncIterable(eventStream)) { @@ -186,58 +188,64 @@ export async function createSourceEventStream( } } -async function executeSubscription( - exeContext: ExecutionContext, -): Promise { - const { schema, fragments, operation, variableValues, rootValue } = - exeContext; - const type = getOperationRootType(schema, operation); - const fields = collectFields( - schema, - fragments, - variableValues, - type, - operation.selectionSet, - new Map(), - new Set(), - ); - 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, +class SubscriptionExecutor extends Executor { + public async executeSubscription(): Promise { + const { + schema, + fragments, + rootValue, + contextValue, + operation, + variableValues, + fieldResolver, + } = this._exeContext; + const type = getOperationRootType(schema, operation); + const fields = collectFields( + schema, + fragments, + variableValues, + type, + operation.selectionSet, + new Map(), + new Set(), ); - } + 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); + const path = addPath(undefined, responseName, type.name); + const info = this.buildResolveInfo(fieldDef, fieldNodes, type, path); - try { - // Implements the "ResolveFieldEventStream" algorithm from GraphQL specification. - // It differs from "ResolveFieldValue" due to providing a different `resolveFn`. + 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); + // 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 ?? fieldResolver; - // 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); + // 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 eventStream = await resolveFn(rootValue, args, contextValue, info); - if (eventStream instanceof Error) { - throw eventStream; + if (eventStream instanceof Error) { + throw eventStream; + } + return eventStream; + } catch (error) { + throw locatedError(error, fieldNodes, pathToArray(path)); } - return eventStream; - } catch (error) { - throw locatedError(error, fieldNodes, pathToArray(path)); } } From 5670a660f49641949cfe03c4a2e22f1972bbdd70 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Thu, 17 Jun 2021 22:29:50 +0300 Subject: [PATCH 3/6] don't make things protected unnecessarily --- src/execution/execute.ts | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 2ad862e9d5..66e1a76f67 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -305,17 +305,18 @@ export function buildExecutionContext( * @internal */ export class Executor { - protected _exeContext: ExecutionContext; /** * 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 = memoize2( + collectSubfields = memoize2( (returnType: GraphQLObjectType, fieldNodes: ReadonlyArray) => this._collectSubfields(returnType, fieldNodes), ); + protected _exeContext: ExecutionContext; + constructor(exeContext: ExecutionContext) { this._exeContext = exeContext; } @@ -323,7 +324,7 @@ export class Executor { /** * Implements the "Executing operations" section of the spec. */ - public executeOperation(): PromiseOrValue | null> { + executeOperation(): PromiseOrValue | null> { const { schema, fragments, rootValue, operation, variableValues, errors } = this._exeContext; const type = getOperationRootType(schema, operation); @@ -364,7 +365,7 @@ export class Executor { * Given a completed execution context and data, build the { errors, data } * response defined by the "Response" section of the GraphQL specification. */ - public buildResponse( + buildResponse( data: PromiseOrValue | null>, ): PromiseOrValue { if (isPromise(data)) { @@ -378,7 +379,7 @@ export class Executor { * Implements the "Executing selection sets" section of the spec * for fields that must be executed serially. */ - protected executeFieldsSerially( + executeFieldsSerially( parentType: GraphQLObjectType, sourceValue: unknown, path: Path | undefined, @@ -414,7 +415,7 @@ export class Executor { * Implements the "Executing selection sets" section of the spec * for fields that may be executed in parallel. */ - protected executeFields( + executeFields( parentType: GraphQLObjectType, sourceValue: unknown, path: Path | undefined, @@ -457,7 +458,7 @@ export class Executor { * calling its resolve function, then calls completeValue to complete promises, * serialize scalars, or execute the sub-selection-set for objects. */ - protected executeField( + executeField( parentType: GraphQLObjectType, source: unknown, fieldNodes: ReadonlyArray, @@ -521,7 +522,7 @@ export class Executor { /** * @internal */ - protected buildResolveInfo( + buildResolveInfo( fieldDef: GraphQLField, fieldNodes: ReadonlyArray, parentType: GraphQLObjectType, @@ -546,10 +547,7 @@ export class Executor { }; } - protected handleFieldError( - error: GraphQLError, - returnType: GraphQLOutputType, - ): null { + handleFieldError(error: GraphQLError, returnType: GraphQLOutputType): 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)) { @@ -583,7 +581,7 @@ export class Executor { * Otherwise, the field type expects a sub-selection set, and will complete the * value by executing all sub-selections. */ - protected completeValue( + completeValue( returnType: GraphQLOutputType, fieldNodes: ReadonlyArray, info: GraphQLResolveInfo, @@ -664,7 +662,7 @@ export class Executor { * Complete a list value by completing each item in the list with the * inner type */ - protected completeListValue( + completeListValue( returnType: GraphQLList, fieldNodes: ReadonlyArray, info: GraphQLResolveInfo, @@ -728,10 +726,7 @@ export class Executor { * 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 { + completeLeafValue(returnType: GraphQLLeafType, result: unknown): unknown { const serializedResult = returnType.serialize(result); if (serializedResult === undefined) { throw new Error( @@ -746,7 +741,7 @@ export class Executor { * 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( + completeAbstractValue( returnType: GraphQLAbstractType, fieldNodes: ReadonlyArray, info: GraphQLResolveInfo, @@ -791,7 +786,7 @@ export class Executor { ); } - protected ensureValidRuntimeType( + ensureValidRuntimeType( runtimeTypeName: unknown, returnType: GraphQLAbstractType, fieldNodes: ReadonlyArray, @@ -849,7 +844,7 @@ export class Executor { /** * Complete an Object value by executing all sub-selections. */ - protected completeObjectValue( + completeObjectValue( returnType: GraphQLObjectType, fieldNodes: ReadonlyArray, info: GraphQLResolveInfo, From 515b9f6337e61f93b1c2cc8a470242160f8029e1 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Thu, 17 Jun 2021 22:41:28 +0300 Subject: [PATCH 4/6] spread exeContext properties into the class --- src/execution/execute.ts | 115 ++++++++++++++++++++-------------- src/subscription/subscribe.ts | 39 +++++++----- 2 files changed, 90 insertions(+), 64 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 66e1a76f67..f1daaeb300 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -315,25 +315,51 @@ export class Executor { this._collectSubfields(returnType, fieldNodes), ); - protected _exeContext: ExecutionContext; - - constructor(exeContext: ExecutionContext) { - this._exeContext = exeContext; + protected _schema: GraphQLSchema; + protected _fragments: ObjMap; + protected _rootValue: unknown; + protected _contextValue: unknown; + protected _operation: OperationDefinitionNode; + protected _variableValues: { [variable: string]: unknown }; + protected _fieldResolver: GraphQLFieldResolver; + protected _typeResolver: GraphQLTypeResolver; + protected _errors: Array; + + constructor({ + schema, + fragments, + rootValue, + contextValue, + operation, + variableValues, + fieldResolver, + typeResolver, + errors, + }: ExecutionContext) { + this._schema = schema; + this._fragments = fragments; + this._rootValue = rootValue; + this._contextValue = contextValue; + this._operation = operation; + this._variableValues = variableValues; + this._fieldResolver = fieldResolver; + this._typeResolver = typeResolver; + this._errors = errors; } /** * Implements the "Executing operations" section of the spec. */ executeOperation(): PromiseOrValue | null> { - const { schema, fragments, rootValue, operation, variableValues, errors } = - this._exeContext; - const type = getOperationRootType(schema, operation); + const { _schema, _fragments, _rootValue, _operation, _variableValues } = + this; + const type = getOperationRootType(_schema, _operation); const fields = collectFields( - schema, - fragments, - variableValues, + _schema, + _fragments, + _variableValues, type, - operation.selectionSet, + _operation.selectionSet, new Map(), new Set(), ); @@ -345,18 +371,18 @@ export class Executor { // in this case is the entire response. try { const result = - operation.operation === 'mutation' - ? this.executeFieldsSerially(type, rootValue, path, fields) - : this.executeFields(type, rootValue, path, fields); + _operation.operation === 'mutation' + ? this.executeFieldsSerially(type, _rootValue, path, fields) + : this.executeFields(type, _rootValue, path, fields); if (isPromise(result)) { return result.then(undefined, (error) => { - errors.push(error); + this._errors.push(error); return Promise.resolve(null); }); } return result; } catch (error) { - errors.push(error); + this._errors.push(error); return null; } } @@ -371,8 +397,9 @@ export class Executor { if (isPromise(data)) { return data.then((resolved) => this.buildResponse(resolved)); } - const errors = this._exeContext.errors; - return errors.length === 0 ? { data } : { errors, data }; + return this._errors.length === 0 + ? { data } + : { errors: this._errors, data }; } /** @@ -464,16 +491,15 @@ export class Executor { fieldNodes: ReadonlyArray, path: Path, ): PromiseOrValue { - const { schema, contextValue, variableValues, fieldResolver } = - this._exeContext; + const { _schema, _contextValue, _variableValues, _fieldResolver } = this; - const fieldDef = getFieldDef(schema, parentType, fieldNodes[0]); + const fieldDef = getFieldDef(_schema, parentType, fieldNodes[0]); if (!fieldDef) { return; } const returnType = fieldDef.type; - const resolveFn = fieldDef.resolve ?? fieldResolver; + const resolveFn = fieldDef.resolve ?? _fieldResolver; const info = this.buildResolveInfo(fieldDef, fieldNodes, parentType, path); @@ -482,12 +508,12 @@ export class Executor { // 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], variableValues); + 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 result = resolveFn(source, args, contextValue, info); + const result = resolveFn(source, args, _contextValue, info); let completed; if (isPromise(result)) { @@ -528,8 +554,8 @@ export class Executor { parentType: GraphQLObjectType, path: Path, ): GraphQLResolveInfo { - const { schema, fragments, rootValue, operation, variableValues } = - this._exeContext; + const { _schema, _fragments, _rootValue, _operation, _variableValues } = + this; // The resolve function's optional fourth argument is a collection of // information about the current execution state. @@ -539,11 +565,11 @@ export class Executor { returnType: fieldDef.type, parentType, path, - schema, - fragments, - rootValue, - operation, - variableValues, + schema: _schema, + fragments: _fragments, + rootValue: _rootValue, + operation: _operation, + variableValues: _variableValues, }; } @@ -556,7 +582,7 @@ export class Executor { // Otherwise, error protection is applied, logging the error and resolving // a null value for this field if one is encountered. - this._exeContext.errors.push(error); + this._errors.push(error); return null; } @@ -748,10 +774,10 @@ export class Executor { path: Path, result: unknown, ): PromiseOrValue> { - const { contextValue, typeResolver } = this._exeContext; + const { _contextValue, _typeResolver } = this; - const resolveTypeFn = returnType.resolveType ?? typeResolver; - const runtimeType = resolveTypeFn(result, contextValue, info, returnType); + const resolveTypeFn = returnType.resolveType ?? _typeResolver; + const runtimeType = resolveTypeFn(result, _contextValue, info, returnType); if (isPromise(runtimeType)) { return runtimeType.then((resolvedRuntimeType) => @@ -815,8 +841,7 @@ export class Executor { ); } - const { schema } = this._exeContext; - const runtimeType = schema.getType(runtimeTypeName); + const runtimeType = this._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.`, @@ -831,7 +856,7 @@ export class Executor { ); } - if (!schema.isSubType(returnType, runtimeType)) { + if (!this._schema.isSubType(returnType, runtimeType)) { throw new GraphQLError( `Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`, fieldNodes, @@ -858,11 +883,7 @@ export class Executor { // current result. If isTypeOf returns false, then raise an error rather // than continuing execution. if (returnType.isTypeOf) { - const isTypeOf = returnType.isTypeOf( - result, - this._exeContext.contextValue, - info, - ); + const isTypeOf = returnType.isTypeOf(result, this._contextValue, info); if (isPromise(isTypeOf)) { return isTypeOf.then((resolvedIsTypeOf) => { @@ -889,16 +910,16 @@ export class Executor { returnType: GraphQLObjectType, fieldNodes: ReadonlyArray, ): Map> { - const { schema, fragments, variableValues } = this._exeContext; + const { _schema, _fragments, _variableValues } = this; let subFieldNodes = new Map(); const visitedFragmentNames = new Set(); for (const node of fieldNodes) { if (node.selectionSet) { subFieldNodes = collectFields( - schema, - fragments, - variableValues, + _schema, + _fragments, + _variableValues, returnType, node.selectionSet, subFieldNodes, diff --git a/src/subscription/subscribe.ts b/src/subscription/subscribe.ts index 3f813f4dd6..c596b765a2 100644 --- a/src/subscription/subscribe.ts +++ b/src/subscription/subscribe.ts @@ -191,26 +191,26 @@ export async function createSourceEventStream( class SubscriptionExecutor extends Executor { public async executeSubscription(): Promise { const { - schema, - fragments, - rootValue, - contextValue, - operation, - variableValues, - fieldResolver, - } = this._exeContext; - const type = getOperationRootType(schema, operation); + _schema, + _fragments, + _rootValue, + _contextValue, + _operation, + _variableValues, + _fieldResolver, + } = this; + const type = getOperationRootType(_schema, _operation); const fields = collectFields( - schema, - fragments, - variableValues, + _schema, + _fragments, + _variableValues, type, - operation.selectionSet, + _operation.selectionSet, new Map(), new Set(), ); const [responseName, fieldNodes] = [...fields.entries()][0]; - const fieldDef = getFieldDef(schema, type, fieldNodes[0]); + const fieldDef = getFieldDef(_schema, type, fieldNodes[0]); if (!fieldDef) { const fieldName = fieldNodes[0].name.value; @@ -229,16 +229,21 @@ class SubscriptionExecutor extends Executor { // 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); + const args = getArgumentValues(fieldDef, fieldNodes[0], _variableValues); // Call the `subscribe()` resolver or the default resolver to produce an // AsyncIterable yielding raw payloads. - const resolveFn = fieldDef.subscribe ?? fieldResolver; + const resolveFn = fieldDef.subscribe ?? _fieldResolver; // 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 eventStream = await resolveFn(rootValue, args, contextValue, info); + const eventStream = await resolveFn( + _rootValue, + args, + _contextValue, + info, + ); if (eventStream instanceof Error) { throw eventStream; From 913b16f32321eda642d9d3c5b2705c1f3826c721 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Thu, 17 Jun 2021 22:56:24 +0300 Subject: [PATCH 5/6] split Executor class into separate file note that the Parser class is contained in lowercase parser.ts --- src/execution/execute.ts | 710 +------------------------------- src/execution/executor.ts | 735 ++++++++++++++++++++++++++++++++++ src/execution/index.ts | 2 + src/subscription/subscribe.ts | 3 +- 4 files changed, 740 insertions(+), 710 deletions(-) create mode 100644 src/execution/executor.ts diff --git a/src/execution/execute.ts b/src/execution/execute.ts index f1daaeb300..e55fe6f1b0 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -1,60 +1,29 @@ -import type { Path } from '../jsutils/Path'; import type { ObjMap } from '../jsutils/ObjMap'; import type { PromiseOrValue } from '../jsutils/PromiseOrValue'; import type { Maybe } from '../jsutils/Maybe'; -import { inspect } from '../jsutils/inspect'; -import { memoize2 } from '../jsutils/memoize2'; -import { invariant } from '../jsutils/invariant'; import { devAssert } from '../jsutils/devAssert'; import { isPromise } from '../jsutils/isPromise'; import { isObjectLike } from '../jsutils/isObjectLike'; -import { promiseReduce } from '../jsutils/promiseReduce'; -import { promiseForObject } from '../jsutils/promiseForObject'; -import { addPath, pathToArray } from '../jsutils/Path'; -import { isIterableObject } from '../jsutils/isIterableObject'; import type { GraphQLFormattedError } from '../error/formatError'; import { GraphQLError } from '../error/GraphQLError'; -import { locatedError } from '../error/locatedError'; import type { DocumentNode, OperationDefinitionNode, - FieldNode, FragmentDefinitionNode, } from '../language/ast'; import { Kind } from '../language/kinds'; import type { GraphQLSchema } from '../type/schema'; import type { - GraphQLObjectType, - GraphQLOutputType, - GraphQLLeafType, - GraphQLAbstractType, - GraphQLField, GraphQLFieldResolver, - GraphQLResolveInfo, GraphQLTypeResolver, - GraphQLList, } from '../type/definition'; import { assertValidSchema } from '../type/validate'; -import { - SchemaMetaFieldDef, - TypeMetaFieldDef, - TypeNameMetaFieldDef, -} from '../type/introspection'; -import { - isObjectType, - isAbstractType, - isLeafType, - isListType, - isNonNullType, -} from '../type/definition'; - -import { getOperationRootType } from '../utilities/getOperationRootType'; -import { getVariableValues, getArgumentValues } from './values'; -import { collectFields } from './collectFields'; +import { getVariableValues } from './values'; +import { Executor } from './executor'; /** * Terminology @@ -301,647 +270,6 @@ export function buildExecutionContext( }; } -/** - * @internal - */ -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. - */ - collectSubfields = memoize2( - (returnType: GraphQLObjectType, fieldNodes: ReadonlyArray) => - this._collectSubfields(returnType, fieldNodes), - ); - - protected _schema: GraphQLSchema; - protected _fragments: ObjMap; - protected _rootValue: unknown; - protected _contextValue: unknown; - protected _operation: OperationDefinitionNode; - protected _variableValues: { [variable: string]: unknown }; - protected _fieldResolver: GraphQLFieldResolver; - protected _typeResolver: GraphQLTypeResolver; - protected _errors: Array; - - constructor({ - schema, - fragments, - rootValue, - contextValue, - operation, - variableValues, - fieldResolver, - typeResolver, - errors, - }: ExecutionContext) { - this._schema = schema; - this._fragments = fragments; - this._rootValue = rootValue; - this._contextValue = contextValue; - this._operation = operation; - this._variableValues = variableValues; - this._fieldResolver = fieldResolver; - this._typeResolver = typeResolver; - this._errors = errors; - } - - /** - * Implements the "Executing operations" section of the spec. - */ - executeOperation(): PromiseOrValue | null> { - const { _schema, _fragments, _rootValue, _operation, _variableValues } = - this; - const type = getOperationRootType(_schema, _operation); - const fields = collectFields( - _schema, - _fragments, - _variableValues, - type, - _operation.selectionSet, - new Map(), - new Set(), - ); - - 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(type, _rootValue, path, fields) - : this.executeFields(type, _rootValue, path, fields); - if (isPromise(result)) { - return result.then(undefined, (error) => { - this._errors.push(error); - return Promise.resolve(null); - }); - } - return result; - } catch (error) { - this._errors.push(error); - return null; - } - } - - /** - * Given a completed execution context and data, build the { errors, data } - * response defined by the "Response" section of the GraphQL specification. - */ - buildResponse( - data: PromiseOrValue | null>, - ): PromiseOrValue { - if (isPromise(data)) { - return data.then((resolved) => this.buildResponse(resolved)); - } - return this._errors.length === 0 - ? { data } - : { errors: this._errors, data }; - } - - /** - * Implements the "Executing selection sets" section of the spec - * for fields that must be executed serially. - */ - executeFieldsSerially( - 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( - 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. - */ - executeFields( - 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 = this.executeField( - parentType, - sourceValue, - fieldNodes, - fieldPath, - ); - - if (result !== undefined) { - results[responseName] = result; - if (isPromise(result)) { - containsPromise = true; - } - } - } - - // If there are no promises, we can just return the object - if (!containsPromise) { - return results; - } - - // 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. - */ - executeField( - parentType: GraphQLObjectType, - source: unknown, - fieldNodes: ReadonlyArray, - path: Path, - ): PromiseOrValue { - const { _schema, _contextValue, _variableValues, _fieldResolver } = this; - - const fieldDef = getFieldDef(_schema, parentType, fieldNodes[0]); - if (!fieldDef) { - return; - } - - const returnType = fieldDef.type; - const resolveFn = fieldDef.resolve ?? _fieldResolver; - - const info = this.buildResolveInfo(fieldDef, fieldNodes, parentType, path); - - // 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], _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 result = resolveFn(source, args, _contextValue, info); - - let completed; - if (isPromise(result)) { - completed = result.then((resolved) => - this.completeValue(returnType, fieldNodes, info, path, resolved), - ); - } else { - completed = this.completeValue( - 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); - }); - } - return completed; - } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(path)); - return this.handleFieldError(error, returnType); - } - } - - /** - * @internal - */ - buildResolveInfo( - fieldDef: GraphQLField, - fieldNodes: ReadonlyArray, - parentType: GraphQLObjectType, - path: Path, - ): GraphQLResolveInfo { - const { _schema, _fragments, _rootValue, _operation, _variableValues } = - this; - - // 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: _schema, - fragments: _fragments, - rootValue: _rootValue, - operation: _operation, - variableValues: _variableValues, - }; - } - - handleFieldError(error: GraphQLError, returnType: GraphQLOutputType): 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; - } - - // Otherwise, error protection is applied, logging the error and resolving - // a null value for this field if one is encountered. - this._errors.push(error); - return null; - } - - /** - * 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. - */ - completeValue( - 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; - } - - // If field type is NonNull, complete for inner type, and throw field error - // if result is null. - if (isNonNullType(returnType)) { - const completed = this.completeValue( - 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; - } - - // If result value is null or undefined then return null. - if (result == null) { - return null; - } - - // If field type is List, complete each item in the list with the inner type - if (isListType(returnType)) { - return this.completeListValue(returnType, fieldNodes, info, path, result); - } - - // 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); - } - - // 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( - returnType, - fieldNodes, - info, - path, - result, - ); - } - - // 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( - 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 - */ - completeListValue( - 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}".`, - ); - } - - // 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(itemType, fieldNodes, info, itemPath, resolved), - ); - } else { - completedItem = this.completeValue( - 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), - ); - return this.handleFieldError(error, itemType); - }); - } - return completedItem; - } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); - return this.handleFieldError(error, itemType); - } - }); - - return containsPromise ? Promise.all(completedResults) : completedResults; - } - - /** - * Complete a Scalar or Enum by serializing to a valid value, returning - * null if serialization is not possible. - */ - completeLeafValue(returnType: GraphQLLeafType, result: unknown): unknown { - const serializedResult = returnType.serialize(result); - if (serializedResult === undefined) { - throw new Error( - `Expected a value of type "${inspect(returnType)}" but ` + - `received: ${inspect(result)}`, - ); - } - 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. - */ - completeAbstractValue( - returnType: GraphQLAbstractType, - fieldNodes: ReadonlyArray, - info: GraphQLResolveInfo, - path: Path, - result: unknown, - ): PromiseOrValue> { - const { _contextValue, _typeResolver } = this; - - const resolveTypeFn = returnType.resolveType ?? _typeResolver; - const runtimeType = resolveTypeFn(result, _contextValue, info, returnType); - - if (isPromise(runtimeType)) { - return runtimeType.then((resolvedRuntimeType) => - this.completeObjectValue( - this.ensureValidRuntimeType( - resolvedRuntimeType, - returnType, - fieldNodes, - info, - result, - ), - fieldNodes, - info, - path, - result, - ), - ); - } - - return this.completeObjectValue( - this.ensureValidRuntimeType( - runtimeType, - returnType, - fieldNodes, - info, - result, - ), - fieldNodes, - info, - path, - result, - ); - } - - ensureValidRuntimeType( - runtimeTypeName: unknown, - 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, - ); - } - - // 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 = this._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 (!this._schema.isSubType(returnType, runtimeType)) { - throw new GraphQLError( - `Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`, - fieldNodes, - ); - } - - return runtimeType; - } - - /** - * Complete an Object value by executing all sub-selections. - */ - completeObjectValue( - returnType: GraphQLObjectType, - fieldNodes: ReadonlyArray, - info: GraphQLResolveInfo, - path: Path, - result: unknown, - ): PromiseOrValue> { - // Collect sub-fields to execute to complete this value. - const subFieldNodes = this.collectSubfields(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, this._contextValue, info); - - if (isPromise(isTypeOf)) { - return isTypeOf.then((resolvedIsTypeOf) => { - if (!resolvedIsTypeOf) { - throw invalidReturnTypeError(returnType, result, fieldNodes); - } - return this.executeFields(returnType, result, path, subFieldNodes); - }); - } - - if (!isTypeOf) { - throw invalidReturnTypeError(returnType, result, fieldNodes); - } - } - - return this.executeFields(returnType, result, path, subFieldNodes); - } - - /** - * A collection of relevant subfields with regard to the return type. - * See 'collectSubfields' above for the memoized version. - */ - protected _collectSubfields( - returnType: GraphQLObjectType, - fieldNodes: ReadonlyArray, - ): Map> { - const { _schema, _fragments, _variableValues } = this; - - let subFieldNodes = new Map(); - const visitedFragmentNames = new Set(); - for (const node of fieldNodes) { - if (node.selectionSet) { - subFieldNodes = collectFields( - _schema, - _fragments, - _variableValues, - returnType, - node.selectionSet, - subFieldNodes, - visitedFragmentNames, - ); - } - } - return subFieldNodes; - } -} - -function invalidReturnTypeError( - returnType: GraphQLObjectType, - result: unknown, - fieldNodes: ReadonlyArray, -): GraphQLError { - return new GraphQLError( - `Expected value of type "${returnType.name}" but got: ${inspect(result)}.`, - fieldNodes, - ); -} - /** * If a resolveType function is not given, then a default resolve behavior is * used which attempts two strategies: @@ -1005,37 +333,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]; -} diff --git a/src/execution/executor.ts b/src/execution/executor.ts new file mode 100644 index 0000000000..ed877acb5f --- /dev/null +++ b/src/execution/executor.ts @@ -0,0 +1,735 @@ +import type { Path } from '../jsutils/Path'; +import type { ObjMap } from '../jsutils/ObjMap'; +import type { PromiseOrValue } from '../jsutils/PromiseOrValue'; +import type { Maybe } from '../jsutils/Maybe'; +import { inspect } from '../jsutils/inspect'; +import { memoize2 } from '../jsutils/memoize2'; +import { invariant } from '../jsutils/invariant'; +import { isPromise } from '../jsutils/isPromise'; +import { promiseReduce } from '../jsutils/promiseReduce'; +import { promiseForObject } from '../jsutils/promiseForObject'; +import { addPath, pathToArray } from '../jsutils/Path'; +import { isIterableObject } from '../jsutils/isIterableObject'; + +import { GraphQLError } from '../error/GraphQLError'; +import { locatedError } from '../error/locatedError'; + +import type { + OperationDefinitionNode, + FieldNode, + FragmentDefinitionNode, +} from '../language/ast'; + +import type { GraphQLSchema } from '../type/schema'; +import type { + GraphQLObjectType, + GraphQLOutputType, + GraphQLLeafType, + GraphQLAbstractType, + GraphQLField, + GraphQLFieldResolver, + GraphQLResolveInfo, + GraphQLTypeResolver, + GraphQLList, +} from '../type/definition'; +import { + SchemaMetaFieldDef, + TypeMetaFieldDef, + TypeNameMetaFieldDef, +} from '../type/introspection'; +import { + isObjectType, + isAbstractType, + isLeafType, + isListType, + isNonNullType, +} from '../type/definition'; + +import { getOperationRootType } from '../utilities/getOperationRootType'; + +import type { ExecutionContext, ExecutionResult } from './execute'; +import { getArgumentValues } from './values'; +import { collectFields } from './collectFields'; + +/** + * This class is exported only to assist people in implementing their own executors + * without duplicating too much code and should be used only as last resort for cases + * requiring custom execution or if certain features could not be contributed upstream. + * + * It is still part of the internal API and is versioned, so any changes to it are never + * considered breaking changes. If you still need to support multiple versions of the + * library, please use the `versionInfo` variable for version detection. + * + * @internal + */ +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. + */ + collectSubfields = memoize2( + (returnType: GraphQLObjectType, fieldNodes: ReadonlyArray) => + this._collectSubfields(returnType, fieldNodes), + ); + + protected _schema: GraphQLSchema; + protected _fragments: ObjMap; + protected _rootValue: unknown; + protected _contextValue: unknown; + protected _operation: OperationDefinitionNode; + protected _variableValues: { [variable: string]: unknown }; + protected _fieldResolver: GraphQLFieldResolver; + protected _typeResolver: GraphQLTypeResolver; + protected _errors: Array; + + constructor({ + schema, + fragments, + rootValue, + contextValue, + operation, + variableValues, + fieldResolver, + typeResolver, + errors, + }: ExecutionContext) { + this._schema = schema; + this._fragments = fragments; + this._rootValue = rootValue; + this._contextValue = contextValue; + this._operation = operation; + this._variableValues = variableValues; + this._fieldResolver = fieldResolver; + this._typeResolver = typeResolver; + this._errors = errors; + } + + /** + * Implements the "Executing operations" section of the spec. + */ + executeOperation(): PromiseOrValue | null> { + const { _schema, _fragments, _rootValue, _operation, _variableValues } = + this; + const type = getOperationRootType(_schema, _operation); + const fields = collectFields( + _schema, + _fragments, + _variableValues, + type, + _operation.selectionSet, + new Map(), + new Set(), + ); + + 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(type, _rootValue, path, fields) + : this.executeFields(type, _rootValue, path, fields); + if (isPromise(result)) { + return result.then(undefined, (error) => { + this._errors.push(error); + return Promise.resolve(null); + }); + } + return result; + } catch (error) { + this._errors.push(error); + return null; + } + } + + /** + * Given a completed execution context and data, build the { errors, data } + * response defined by the "Response" section of the GraphQL specification. + */ + buildResponse( + data: PromiseOrValue | null>, + ): PromiseOrValue { + if (isPromise(data)) { + return data.then((resolved) => this.buildResponse(resolved)); + } + return this._errors.length === 0 + ? { data } + : { errors: this._errors, data }; + } + + /** + * Implements the "Executing selection sets" section of the spec + * for fields that must be executed serially. + */ + executeFieldsSerially( + 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( + 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. + */ + executeFields( + 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 = this.executeField( + parentType, + sourceValue, + fieldNodes, + fieldPath, + ); + + if (result !== undefined) { + results[responseName] = result; + if (isPromise(result)) { + containsPromise = true; + } + } + } + + // If there are no promises, we can just return the object + if (!containsPromise) { + return results; + } + + // 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. + */ + executeField( + parentType: GraphQLObjectType, + source: unknown, + fieldNodes: ReadonlyArray, + path: Path, + ): PromiseOrValue { + const { _schema, _contextValue, _variableValues, _fieldResolver } = this; + + const fieldDef = getFieldDef(_schema, parentType, fieldNodes[0]); + if (!fieldDef) { + return; + } + + const returnType = fieldDef.type; + const resolveFn = fieldDef.resolve ?? _fieldResolver; + + const info = this.buildResolveInfo(fieldDef, fieldNodes, parentType, path); + + // 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], _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 result = resolveFn(source, args, _contextValue, info); + + let completed; + if (isPromise(result)) { + completed = result.then((resolved) => + this.completeValue(returnType, fieldNodes, info, path, resolved), + ); + } else { + completed = this.completeValue( + 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); + }); + } + return completed; + } catch (rawError) { + const error = locatedError(rawError, fieldNodes, pathToArray(path)); + return this.handleFieldError(error, returnType); + } + } + + /** + * @internal + */ + buildResolveInfo( + fieldDef: GraphQLField, + fieldNodes: ReadonlyArray, + parentType: GraphQLObjectType, + path: Path, + ): GraphQLResolveInfo { + const { _schema, _fragments, _rootValue, _operation, _variableValues } = + this; + + // 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: _schema, + fragments: _fragments, + rootValue: _rootValue, + operation: _operation, + variableValues: _variableValues, + }; + } + + handleFieldError(error: GraphQLError, returnType: GraphQLOutputType): 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; + } + + // Otherwise, error protection is applied, logging the error and resolving + // a null value for this field if one is encountered. + this._errors.push(error); + return null; + } + + /** + * 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. + */ + completeValue( + 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; + } + + // If field type is NonNull, complete for inner type, and throw field error + // if result is null. + if (isNonNullType(returnType)) { + const completed = this.completeValue( + 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; + } + + // If result value is null or undefined then return null. + if (result == null) { + return null; + } + + // If field type is List, complete each item in the list with the inner type + if (isListType(returnType)) { + return this.completeListValue(returnType, fieldNodes, info, path, result); + } + + // 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); + } + + // 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( + returnType, + fieldNodes, + info, + path, + result, + ); + } + + // 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( + 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 + */ + completeListValue( + 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}".`, + ); + } + + // 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(itemType, fieldNodes, info, itemPath, resolved), + ); + } else { + completedItem = this.completeValue( + 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), + ); + return this.handleFieldError(error, itemType); + }); + } + return completedItem; + } catch (rawError) { + const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); + return this.handleFieldError(error, itemType); + } + }); + + return containsPromise ? Promise.all(completedResults) : completedResults; + } + + /** + * Complete a Scalar or Enum by serializing to a valid value, returning + * null if serialization is not possible. + */ + completeLeafValue(returnType: GraphQLLeafType, result: unknown): unknown { + const serializedResult = returnType.serialize(result); + if (serializedResult === undefined) { + throw new Error( + `Expected a value of type "${inspect(returnType)}" but ` + + `received: ${inspect(result)}`, + ); + } + 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. + */ + completeAbstractValue( + returnType: GraphQLAbstractType, + fieldNodes: ReadonlyArray, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + ): PromiseOrValue> { + const { _contextValue, _typeResolver } = this; + + const resolveTypeFn = returnType.resolveType ?? _typeResolver; + const runtimeType = resolveTypeFn(result, _contextValue, info, returnType); + + if (isPromise(runtimeType)) { + return runtimeType.then((resolvedRuntimeType) => + this.completeObjectValue( + this.ensureValidRuntimeType( + resolvedRuntimeType, + returnType, + fieldNodes, + info, + result, + ), + fieldNodes, + info, + path, + result, + ), + ); + } + + return this.completeObjectValue( + this.ensureValidRuntimeType( + runtimeType, + returnType, + fieldNodes, + info, + result, + ), + fieldNodes, + info, + path, + result, + ); + } + + ensureValidRuntimeType( + runtimeTypeName: unknown, + 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, + ); + } + + // 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 = this._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 (!this._schema.isSubType(returnType, runtimeType)) { + throw new GraphQLError( + `Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`, + fieldNodes, + ); + } + + return runtimeType; + } + + /** + * Complete an Object value by executing all sub-selections. + */ + completeObjectValue( + returnType: GraphQLObjectType, + fieldNodes: ReadonlyArray, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + ): PromiseOrValue> { + // Collect sub-fields to execute to complete this value. + const subFieldNodes = this.collectSubfields(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, this._contextValue, info); + + if (isPromise(isTypeOf)) { + return isTypeOf.then((resolvedIsTypeOf) => { + if (!resolvedIsTypeOf) { + throw invalidReturnTypeError(returnType, result, fieldNodes); + } + return this.executeFields(returnType, result, path, subFieldNodes); + }); + } + + if (!isTypeOf) { + throw invalidReturnTypeError(returnType, result, fieldNodes); + } + } + + return this.executeFields(returnType, result, path, subFieldNodes); + } + + /** + * A collection of relevant subfields with regard to the return type. + * See 'collectSubfields' above for the memoized version. + */ + protected _collectSubfields( + returnType: GraphQLObjectType, + fieldNodes: ReadonlyArray, + ): Map> { + const { _schema, _fragments, _variableValues } = this; + + let subFieldNodes = new Map(); + const visitedFragmentNames = new Set(); + for (const node of fieldNodes) { + if (node.selectionSet) { + subFieldNodes = collectFields( + _schema, + _fragments, + _variableValues, + returnType, + node.selectionSet, + subFieldNodes, + visitedFragmentNames, + ); + } + } + return subFieldNodes; + } +} + +function invalidReturnTypeError( + returnType: GraphQLObjectType, + result: unknown, + fieldNodes: ReadonlyArray, +): GraphQLError { + return new GraphQLError( + `Expected value of type "${returnType.name}" but got: ${inspect(result)}.`, + fieldNodes, + ); +} + +/** + * 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]; +} diff --git a/src/execution/index.ts b/src/execution/index.ts index 5ae0706ec9..0acd3d1032 100644 --- a/src/execution/index.ts +++ b/src/execution/index.ts @@ -1,5 +1,7 @@ export { pathToArray as responsePathAsArray } from '../jsutils/Path'; +export { Executor } from './executor'; + export { execute, executeSync, diff --git a/src/subscription/subscribe.ts b/src/subscription/subscribe.ts index c596b765a2..03e7b00516 100644 --- a/src/subscription/subscribe.ts +++ b/src/subscription/subscribe.ts @@ -11,12 +11,11 @@ import type { DocumentNode } from '../language/ast'; import type { ExecutionResult } from '../execution/execute'; import { collectFields } from '../execution/collectFields'; import { getArgumentValues } from '../execution/values'; +import { Executor, getFieldDef } from '../execution/executor'; import { - Executor, assertValidExecutionArguments, buildExecutionContext, execute, - getFieldDef, } from '../execution/execute'; import type { GraphQLSchema } from '../type/schema'; From e3564947003ee42f1ce3f67ecd013cbb3e2d2b26 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Thu, 17 Jun 2021 22:56:24 +0300 Subject: [PATCH 6/6] feat: add CustomExecutor option to graphql, execute, and subscribe allows customization of the execution algorithm by overriding any of the protected members of the now exported internal Executor class. Reference: https://github.com/graphql/graphql-js/pull/3163#issuecomment-859546546 Note: This class is exported only to assist people in implementing their own executors without duplicating too much code and should be used only as last resort for cases requiring custom execution or if certain features could not be contributed upstream. It is still part of the internal API and is versioned, so any changes to it are never considered breaking changes. If you still need to support multiple versions of the library, please use the `versionInfo` variable for version detection. --- src/execution/__tests__/executor-test.ts | 27 ++++++++++++++++++++++++ src/execution/execute.ts | 4 +++- src/graphql.ts | 16 ++++++++++++++ src/index.ts | 1 + src/subscription/subscribe.ts | 7 +++++- 5 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 5283aa4de5..7fbdebfdb9 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -18,6 +18,7 @@ import { GraphQLUnionType, } from '../../type/definition'; +import { Executor } from '../executor'; import { execute, executeSync } from '../execute'; describe('Execute: Handles basic execution tasks', () => { @@ -1151,6 +1152,32 @@ describe('Execute: Handles basic execution tasks', () => { expect(result).to.deep.equal({ data: { foo: null } }); }); + it('uses a custom Executor', () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + foo: { type: GraphQLString }, + }, + }), + }); + const document = parse('{ foo }'); + + class CustomExecutor extends Executor { + executeField() { + return 'foo'; + } + } + + const result = executeSync({ + schema, + document, + CustomExecutor, + }); + + expect(result).to.deep.equal({ data: { foo: 'foo' } }); + }); + it('uses a custom field resolver', () => { const schema = new GraphQLSchema({ query: new GraphQLObjectType({ diff --git a/src/execution/execute.ts b/src/execution/execute.ts index e55fe6f1b0..4c588b5e9b 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -97,6 +97,7 @@ export interface ExecutionArgs { operationName?: Maybe; fieldResolver?: Maybe>; typeResolver?: Maybe>; + CustomExecutor?: Maybe; } /** @@ -119,6 +120,7 @@ export function execute(args: ExecutionArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + CustomExecutor, } = args; // If arguments are missing or incorrect, throw an error. @@ -142,7 +144,7 @@ export function execute(args: ExecutionArgs): PromiseOrValue { return { errors: exeContext }; } - const executor = new Executor(exeContext); + const executor = new (CustomExecutor ?? Executor)(exeContext); // Return a Promise that will eventually resolve to the data described by // The "Response" section of the GraphQL specification. diff --git a/src/graphql.ts b/src/graphql.ts index 03e6b95882..e42f1872ce 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -14,6 +14,7 @@ import type { import type { GraphQLSchema } from './type/schema'; import { validateSchema } from './type/validate'; +import type { Executor } from './execution/executor'; import type { ExecutionResult } from './execution/execute'; import { execute } from './execution/execute'; @@ -55,6 +56,18 @@ import { execute } from './execution/execute'; * A type resolver function to use when none is provided by the schema. * If not provided, the default type resolver is used (which looks for a * `__typename` field or alternatively calls the `isTypeOf` method). + * CustomExecutor: + * A custom Executor class to allow overriding execution behavior. + * + * Note: The Executor class is exported only to assist people in + * implementing their own executors without duplicating too much code and + * should be used only as last resort for cases requiring custom execution + * or if certain features could not be contributed upstream. + * + * It is still part of the internal API and is versioned, so any changes to + * it are never considered breaking changes. If you still need to support + * multiple versions of the library, please use the `versionInfo` variable + * for version detection. */ export interface GraphQLArgs { schema: GraphQLSchema; @@ -65,6 +78,7 @@ export interface GraphQLArgs { operationName?: Maybe; fieldResolver?: Maybe>; typeResolver?: Maybe>; + CustomExecutor?: Maybe; } export function graphql(args: GraphQLArgs): Promise { @@ -99,6 +113,7 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + CustomExecutor, } = args; // Validate Schema @@ -131,5 +146,6 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + CustomExecutor, }); } diff --git a/src/index.ts b/src/index.ts index b9aec6a43a..b2bccf4deb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -299,6 +299,7 @@ export type { /** Execute GraphQL queries. */ export { + Executor, execute, executeSync, defaultFieldResolver, diff --git a/src/subscription/subscribe.ts b/src/subscription/subscribe.ts index 03e7b00516..6ee9e36c75 100644 --- a/src/subscription/subscribe.ts +++ b/src/subscription/subscribe.ts @@ -34,6 +34,7 @@ export interface SubscriptionArgs { operationName?: Maybe; fieldResolver?: Maybe>; subscribeFieldResolver?: Maybe>; + CustomExecutor?: Maybe; } /** @@ -69,6 +70,7 @@ export async function subscribe( operationName, fieldResolver, subscribeFieldResolver, + CustomExecutor, } = args; const resultOrStream = await createSourceEventStream( @@ -79,6 +81,7 @@ export async function subscribe( variableValues, operationName, subscribeFieldResolver, + CustomExecutor, ); if (!isAsyncIterable(resultOrStream)) { @@ -100,6 +103,7 @@ export async function subscribe( variableValues, operationName, fieldResolver, + CustomExecutor, }); // Map every source value to a ExecutionResult value as described above. @@ -142,6 +146,7 @@ export async function createSourceEventStream( variableValues?: Maybe<{ readonly [variable: string]: unknown }>, operationName?: Maybe, fieldResolver?: Maybe>, + CustomExecutor?: Maybe, ): Promise | ExecutionResult> { // If arguments are missing or incorrectly typed, this is an internal // developer mistake which should throw an early error. @@ -164,7 +169,7 @@ export async function createSourceEventStream( return { errors: exeContext }; } - const executor = new SubscriptionExecutor(exeContext); + const executor = new (CustomExecutor ?? SubscriptionExecutor)(exeContext); const eventStream = await executor.executeSubscription();