From afdf56d36397fd9c109574802eb7c85418af11a6 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 6 Jun 2021 16:56:09 +0300 Subject: [PATCH] add fieldExecutor option to graphql, execute, and subscribe to specify a custom field executor, export the default field executor for easy wrapping --- src/execution/__tests__/executor-test.ts | 23 +++++++++++++ src/execution/execute.ts | 34 +++++++++++++------ src/execution/index.ts | 3 ++ src/graphql.ts | 13 ++++++- src/index.ts | 3 ++ src/subscription/subscribe.ts | 9 ++++- .../rules/SingleFieldSubscriptionsRule.ts | 3 ++ 7 files changed, 76 insertions(+), 12 deletions(-) diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 5283aa4de51..b47e3c26a3f 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -1151,6 +1151,29 @@ describe('Execute: Handles basic execution tasks', () => { expect(result).to.deep.equal({ data: { foo: null } }); }); + it('uses a custom field executor', () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + foo: { type: GraphQLString }, + }, + }), + }); + const document = parse('{ foo }'); + + const result = executeSync({ + schema, + document, + fieldExecutor() { + // For the purposes of test, just return the name of the field! + return 'foo'; + }, + }); + + 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 4b30409f987..40702a75bc0 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -102,9 +102,18 @@ export interface ExecutionContext { variableValues: { [variable: string]: unknown }; fieldResolver: GraphQLFieldResolver; typeResolver: GraphQLTypeResolver; + fieldExecutor: GraphQLFieldExecutor; errors: Array; } +export type GraphQLFieldExecutor = ( + exeContext: ExecutionContext, + parentType: GraphQLObjectType, + source: unknown, + fieldNodes: ReadonlyArray, + path: Path, +) => PromiseOrValue; + /** * The result of GraphQL execution. * @@ -139,6 +148,7 @@ export interface ExecutionArgs { operationName?: Maybe; fieldResolver?: Maybe>; typeResolver?: Maybe>; + fieldExecutor?: Maybe; } /** @@ -161,6 +171,7 @@ export function execute(args: ExecutionArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + fieldExecutor, } = args; // If arguments are missing or incorrect, throw an error. @@ -177,6 +188,7 @@ export function execute(args: ExecutionArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + fieldExecutor, ); // Return early errors if execution context failed. @@ -267,6 +279,7 @@ export function buildExecutionContext( operationName: Maybe, fieldResolver: Maybe>, typeResolver?: Maybe>, + fieldExecutor?: Maybe, ): ReadonlyArray | ExecutionContext { let operation: OperationDefinitionNode | undefined; const fragments: ObjMap = Object.create(null); @@ -322,6 +335,7 @@ export function buildExecutionContext( variableValues: coercedVariableValues.coerced, fieldResolver: fieldResolver ?? defaultFieldResolver, typeResolver: typeResolver ?? defaultTypeResolver, + fieldExecutor: fieldExecutor ?? defaultFieldExecutor, errors: [], }; } @@ -381,7 +395,7 @@ function executeFieldsSerially( fields.entries(), (results, [responseName, fieldNodes]) => { const fieldPath = addPath(path, responseName, parentType.name); - const result = executeField( + const result = exeContext.fieldExecutor( exeContext, parentType, sourceValue, @@ -420,7 +434,7 @@ function executeFields( for (const [responseName, fieldNodes] of fields.entries()) { const fieldPath = addPath(path, responseName, parentType.name); - const result = executeField( + const result = exeContext.fieldExecutor( exeContext, parentType, sourceValue, @@ -588,13 +602,13 @@ function getFieldEntryKey(node: FieldNode): string { * 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 { +export const defaultFieldExecutor: GraphQLFieldExecutor = ( + exeContext, + parentType, + source, + fieldNodes, + path, +) => { const fieldDef = getFieldDef(exeContext.schema, parentType, fieldNodes[0]); if (!fieldDef) { return; @@ -658,7 +672,7 @@ function executeField( const error = locatedError(rawError, fieldNodes, pathToArray(path)); return handleFieldError(error, returnType, exeContext); } -} +}; /** * @internal diff --git a/src/execution/index.ts b/src/execution/index.ts index 5ae0706ec95..ad9a809cf7e 100644 --- a/src/execution/index.ts +++ b/src/execution/index.ts @@ -3,14 +3,17 @@ export { pathToArray as responsePathAsArray } from '../jsutils/Path'; export { execute, executeSync, + defaultFieldExecutor, defaultFieldResolver, defaultTypeResolver, } from './execute'; export type { ExecutionArgs, + ExecutionContext, ExecutionResult, FormattedExecutionResult, + GraphQLFieldExecutor, } from './execute'; export { getDirectiveValues } from './values'; diff --git a/src/graphql.ts b/src/graphql.ts index 03e6b95882e..9f2c88165bc 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -14,7 +14,10 @@ import type { import type { GraphQLSchema } from './type/schema'; import { validateSchema } from './type/validate'; -import type { ExecutionResult } from './execution/execute'; +import type { + ExecutionResult, + GraphQLFieldExecutor, +} from './execution/execute'; import { execute } from './execution/execute'; /** @@ -55,6 +58,11 @@ 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). + * fieldExecutor: + * An executor function to use when one is not provided by the schema. + * If not provided, the default field executor is used (which properly + * calls the field resolver and completes values according to the + * GraphQL spec). */ export interface GraphQLArgs { schema: GraphQLSchema; @@ -65,6 +73,7 @@ export interface GraphQLArgs { operationName?: Maybe; fieldResolver?: Maybe>; typeResolver?: Maybe>; + fieldExecutor?: Maybe; } export function graphql(args: GraphQLArgs): Promise { @@ -99,6 +108,7 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + fieldExecutor, } = args; // Validate Schema @@ -131,5 +141,6 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + fieldExecutor, }); } diff --git a/src/index.ts b/src/index.ts index d9d02c9245b..6cc6a1e6e53 100644 --- a/src/index.ts +++ b/src/index.ts @@ -301,6 +301,7 @@ export type { export { execute, executeSync, + defaultFieldExecutor, defaultFieldResolver, defaultTypeResolver, responsePathAsArray, @@ -309,8 +310,10 @@ export { export type { ExecutionArgs, + ExecutionContext, ExecutionResult, FormattedExecutionResult, + GraphQLFieldExecutor, } from './execution/index'; export { subscribe, createSourceEventStream } from './subscription/index'; diff --git a/src/subscription/subscribe.ts b/src/subscription/subscribe.ts index 6b4c6c13bf9..61bbc43e467 100644 --- a/src/subscription/subscribe.ts +++ b/src/subscription/subscribe.ts @@ -8,7 +8,11 @@ import { locatedError } from '../error/locatedError'; import type { DocumentNode } from '../language/ast'; -import type { ExecutionResult, ExecutionContext } from '../execution/execute'; +import type { + ExecutionResult, + ExecutionContext, + GraphQLFieldExecutor, +} from '../execution/execute'; import { getArgumentValues } from '../execution/values'; import { assertValidExecutionArguments, @@ -34,6 +38,7 @@ export interface SubscriptionArgs { variableValues?: Maybe<{ readonly [variable: string]: unknown }>; operationName?: Maybe; fieldResolver?: Maybe>; + fieldExecutor?: Maybe; subscribeFieldResolver?: Maybe>; } @@ -69,6 +74,7 @@ export async function subscribe( variableValues, operationName, fieldResolver, + fieldExecutor, subscribeFieldResolver, } = args; @@ -101,6 +107,7 @@ export async function subscribe( variableValues, operationName, fieldResolver, + fieldExecutor, }); // Map every source value to a ExecutionResult value as described above. diff --git a/src/validation/rules/SingleFieldSubscriptionsRule.ts b/src/validation/rules/SingleFieldSubscriptionsRule.ts index 0098ddc3d74..a03396d4ca4 100644 --- a/src/validation/rules/SingleFieldSubscriptionsRule.ts +++ b/src/validation/rules/SingleFieldSubscriptionsRule.ts @@ -9,10 +9,12 @@ import type { import { Kind } from '../../language/kinds'; import type { ExecutionContext } from '../../execution/execute'; + import { collectFields, defaultFieldResolver, defaultTypeResolver, + defaultFieldExecutor, } from '../../execution/execute'; import type { ValidationContext } from '../ValidationContext'; @@ -53,6 +55,7 @@ export function SingleFieldSubscriptionsRule( variableValues, fieldResolver: defaultFieldResolver, typeResolver: defaultTypeResolver, + fieldExecutor: defaultFieldExecutor, errors: [], }; const fields = collectFields(