From e580f621a4e15e2bfbbf35e12a8b8ac0e6883bc1 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 6 Jun 2021 16:56:09 +0300 Subject: [PATCH] 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 | 28 +++++++++++++++++++++++- src/execution/execute.ts | 12 +++++++++- src/execution/index.ts | 1 + src/graphql.ts | 17 +++++++++++++- src/index.ts | 1 + src/subscription/subscribe.ts | 7 +++++- 6 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 5283aa4de51..b3090202580 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -18,7 +18,7 @@ import { GraphQLUnionType, } from '../../type/definition'; -import { execute, executeSync } from '../execute'; +import { Executor, execute, executeSync } from '../execute'; describe('Execute: Handles basic execution tasks', () => { it('throws if no document is provided', () => { @@ -1151,6 +1151,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 2ad862e9d52..345913d8e4d 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -128,6 +128,7 @@ export interface ExecutionArgs { operationName?: Maybe; fieldResolver?: Maybe>; typeResolver?: Maybe>; + CustomExecutor?: Maybe; } /** @@ -150,6 +151,7 @@ export function execute(args: ExecutionArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + CustomExecutor, } = args; // If arguments are missing or incorrect, throw an error. @@ -173,7 +175,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. @@ -302,6 +304,14 @@ export function buildExecutionContext( } /** + * 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 { diff --git a/src/execution/index.ts b/src/execution/index.ts index 5ae0706ec95..52c9aed00f8 100644 --- a/src/execution/index.ts +++ b/src/execution/index.ts @@ -1,6 +1,7 @@ export { pathToArray as responsePathAsArray } from '../jsutils/Path'; export { + Executor, execute, executeSync, defaultFieldResolver, diff --git a/src/graphql.ts b/src/graphql.ts index 03e6b95882e..c8cff1681a0 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -14,7 +14,7 @@ import type { import type { GraphQLSchema } from './type/schema'; import { validateSchema } from './type/validate'; -import type { ExecutionResult } from './execution/execute'; +import type { ExecutionResult, Executor } from './execution/execute'; import { execute } from './execution/execute'; /** @@ -55,6 +55,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 +77,7 @@ export interface GraphQLArgs { operationName?: Maybe; fieldResolver?: Maybe>; typeResolver?: Maybe>; + CustomExecutor?: Maybe; } export function graphql(args: GraphQLArgs): Promise { @@ -99,6 +112,7 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + CustomExecutor, } = args; // Validate Schema @@ -131,5 +145,6 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + CustomExecutor, }); } diff --git a/src/index.ts b/src/index.ts index b9aec6a43ae..b2bccf4deba 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 3f813f4dd6c..368a14a1129 100644 --- a/src/subscription/subscribe.ts +++ b/src/subscription/subscribe.ts @@ -35,6 +35,7 @@ export interface SubscriptionArgs { operationName?: Maybe; fieldResolver?: Maybe>; subscribeFieldResolver?: Maybe>; + CustomExecutor?: Maybe; } /** @@ -70,6 +71,7 @@ export async function subscribe( operationName, fieldResolver, subscribeFieldResolver, + CustomExecutor, } = args; const resultOrStream = await createSourceEventStream( @@ -80,6 +82,7 @@ export async function subscribe( variableValues, operationName, subscribeFieldResolver, + CustomExecutor, ); if (!isAsyncIterable(resultOrStream)) { @@ -101,6 +104,7 @@ export async function subscribe( variableValues, operationName, fieldResolver, + CustomExecutor, }); // Map every source value to a ExecutionResult value as described above. @@ -143,6 +147,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. @@ -165,7 +170,7 @@ export async function createSourceEventStream( return { errors: exeContext }; } - const executor = new SubscriptionExecutor(exeContext); + const executor = new (CustomExecutor ?? SubscriptionExecutor)(exeContext); const eventStream = await executor.executeSubscription();