diff --git a/src/execution/README.md b/src/execution/README.md index 6540f323fe..94d8ec10eb 100644 --- a/src/execution/README.md +++ b/src/execution/README.md @@ -3,7 +3,9 @@ The `graphql/execution` module is responsible for the execution phase of fulfilling a GraphQL request. +For queries and mutations: + ```js -import { execute } from 'graphql/execution'; // ES6 +import { executeRequest } from 'graphql/execution'; // ES6 var GraphQLExecution = require('graphql/execution'); // CommonJS ``` diff --git a/src/execution/__tests__/subscribe-test.ts b/src/execution/__tests__/subscribe-test.ts index d8bd7c122d..5f5d287b2b 100644 --- a/src/execution/__tests__/subscribe-test.ts +++ b/src/execution/__tests__/subscribe-test.ts @@ -14,8 +14,11 @@ import { GraphQLList, GraphQLObjectType } from '../../type/definition'; import { GraphQLInt, GraphQLString, GraphQLBoolean } from '../../type/scalars'; import type { ExecutionContext } from '../execute'; -import { buildExecutionContext, createSourceEventStream } from '../execute'; -import { subscribe } from '../subscribe'; +import { + buildExecutionContext, + createSourceEventStream, + subscribe, +} from '../execute'; import { SimplePubSub } from './simplePubSub'; @@ -316,6 +319,9 @@ describe('Subscription Initialization Phase', () => { }), }); + // NOTE: in contrast to the below tests, executeRequest() will directly throw the + // error rather than return a promise that rejects. + // @ts-expect-error (schema must not be null) (await expectPromise(subscribe({ schema: null, document }))).toRejectWith( 'Expected null to be a GraphQL schema.', @@ -371,6 +377,9 @@ describe('Subscription Initialization Phase', () => { }), }); + // NOTE: in contrast to the below test, executeRequest() will directly throw the + // error rather than return a promise that rejects. + // @ts-expect-error (await expectPromise(subscribe({ schema, document: {} }))).toReject(); }); @@ -391,7 +400,8 @@ describe('Subscription Initialization Phase', () => { const document = parse('subscription { foo }'); - (await expectPromise(subscribe({ schema, document }))).toRejectWith( + const result = subscribe({ schema, document }) as Promise; + (await expectPromise(result)).toRejectWith( 'Subscription field must return Async Iterable. Received: "test".', ); }); @@ -474,6 +484,9 @@ describe('Subscription Initialization Phase', () => { // If we receive variables that cannot be coerced correctly, subscribe() will // resolve to an ExecutionResult that contains an informative error description. + + // NOTE: in contrast, executeRequest() will directly return the error as an + // ExecutionResult rather than a promise. const result = await subscribe({ schema, document, variableValues }); expectJSON(result).to.deep.equal({ errors: [ diff --git a/src/execution/execute.ts b/src/execution/execute.ts index bb64c9fbf2..8bcadde5e9 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -153,23 +153,55 @@ export interface ExecutionArgs { contextValue?: unknown; variableValues?: Maybe<{ readonly [variable: string]: unknown }>; operationName?: Maybe; + disableSubscription?: Maybe; fieldResolver?: Maybe>; typeResolver?: Maybe>; + subscribeFieldResolver?: Maybe>; } +// Exported for backwards compatibility, see below. +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SubscriptionArgs extends ExecutionArgs {} + /** * Implements the "Executing requests" section of the GraphQL specification. * + * For queries and mutations: * Returns either a synchronous ExecutionResult (if all encountered resolvers * are synchronous), or a Promise of an ExecutionResult that will eventually be * resolved and never rejected. * - * If the arguments to this function do not result in a legal execution context, - * a GraphQLError will be thrown immediately explaining the invalid input. + * For subscriptions: + * Returns a Promise which resolves to either an AsyncIterator (if successful) + * or an ExecutionResult (error). The promise will be rejected if the resolved + * event stream is not an async iterable. + * + * If the source stream could not be created due to faulty subscription + * resolver logic or underlying systems, the promise will resolve to a single + * ExecutionResult containing `errors` and no `data`. + * + * If the operation succeeded, the promise resolves to an AsyncIterator, which + * yields a stream of ExecutionResults representing the response stream. + * + * For queries, mutations and subscriptions: + * If a valid execution context cannot be created due to incorrect arguments + * an ExecutionResult containing descriptive errors will be returned. + * + * NOTE: the below always async `subscribe` function will return a Promise + * that resolves to the ExecutionResult containing the errors, rather than the + * ExecutionResult itself. + * + * The `executeRequest` function, in contrast, aligns the return type in the + * case of incorrect arguments between all operation types. `executeRequest` + * always returns an ExecutionResult with the errors, even for subscriptions, + * rather than a promise that resolves to the ExecutionResult with the errors. + * */ -export function execute(args: ExecutionArgs): PromiseOrValue { - // If a valid execution context cannot be created due to incorrect arguments, - // a "Response" with only errors is returned. +export function executeRequest( + args: ExecutionArgs, +): + | ExecutionResult + | Promise> { const exeContext = buildExecutionContext(args); // Return early errors if execution context failed. @@ -177,9 +209,38 @@ export function execute(args: ExecutionArgs): PromiseOrValue { return { errors: exeContext }; } + if ( + !args.disableSubscription && + exeContext.operation.operation === 'subscription' + ) { + return executeSubscription(exeContext); + } + return executeQueryOrMutation(exeContext); } +/** + * Also implements the "Executing requests" section of the GraphQL specification. + * The `execute` function presumes the request contains a query or mutation + * and has a narrower return type than `executeRequest`. + * + * The below use of the new `disableSubscription` argument preserves the + * previous default behavior of executing documents containing subscription + * operations as queries. + * + * Note: In a future version the `execute` function may be entirely replaced + * with the `executeRequest` function, and the `executeRequest` function may + * be renamed to `execute`. The `disableSubscription` option may be replaced + * by an `operationType` option that changes that overrides the operation + * type stored within the document. + */ +export function execute(args: ExecutionArgs): PromiseOrValue { + return executeRequest({ + ...args, + disableSubscription: true, + }) as PromiseOrValue; +} + /** * Also implements the "Executing requests" section of the GraphQL specification. * However, it guarantees to complete synchronously (or throw an error) assuming @@ -196,6 +257,22 @@ export function executeSync(args: ExecutionArgs): ExecutionResult { return result; } +/** + * Also implements the "Executing requests" section of the GraphQL specification. + * The `subscribe` function presumes the request contains a subscription + * and has a narrower return type than `executeRequest`. + * + * Note: In a future version the `subscribe` function may be entirely replaced + * with the `executeRequest` function as above. + */ +export async function subscribe( + args: SubscriptionArgs, +): Promise | ExecutionResult> { + return executeRequest(args) as Promise< + AsyncGenerator | ExecutionResult + >; +} + /** * Implements the "Executing operations" section of the spec for queries and * mutations. @@ -250,23 +327,15 @@ export function assertValidExecutionArguments( /** * Constructs a ExecutionContext object from the arguments passed to - * execute, which we will pass throughout the other execution methods. + * executeRequest, which we will pass throughout the other execution methods. * * Throws a GraphQLError if a valid execution context cannot be created. * * @internal */ -export function buildExecutionContext(args: { - schema: GraphQLSchema; - document: DocumentNode; - rootValue?: Maybe; - contextValue?: Maybe; - variableValues?: Maybe<{ readonly [variable: string]: unknown }>; - operationName?: Maybe; - fieldResolver?: Maybe>; - typeResolver?: Maybe>; - subscribeFieldResolver?: Maybe>; -}): ReadonlyArray | ExecutionContext { +export function buildExecutionContext( + args: ExecutionArgs, +): ReadonlyArray | ExecutionContext { const { schema, document, diff --git a/src/execution/index.ts b/src/execution/index.ts index f2ba430127..c4180b92aa 100644 --- a/src/execution/index.ts +++ b/src/execution/index.ts @@ -3,19 +3,18 @@ export { pathToArray as responsePathAsArray } from '../jsutils/Path'; export { createSourceEventStream, execute, + executeRequest, executeSync, defaultFieldResolver, defaultTypeResolver, + subscribe, } from './execute'; export type { ExecutionArgs, ExecutionResult, FormattedExecutionResult, + SubscriptionArgs, } from './execute'; -export { subscribe } from './subscribe'; - -export type { SubscriptionArgs } from './subscribe'; - export { getDirectiveValues } from './values'; diff --git a/src/execution/subscribe.ts b/src/execution/subscribe.ts deleted file mode 100644 index e3ab237190..0000000000 --- a/src/execution/subscribe.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { Maybe } from '../jsutils/Maybe'; - -import type { DocumentNode } from '../language/ast'; - -import type { GraphQLSchema } from '../type/schema'; -import type { - GraphQLFieldResolver, - GraphQLTypeResolver, -} from '../type/definition'; - -import type { ExecutionResult } from './execute'; -import { buildExecutionContext, executeSubscription } from './execute'; - -export interface SubscriptionArgs { - schema: GraphQLSchema; - document: DocumentNode; - rootValue?: unknown; - contextValue?: unknown; - variableValues?: Maybe<{ readonly [variable: string]: unknown }>; - operationName?: Maybe; - fieldResolver?: Maybe>; - typeResolver?: Maybe>; - subscribeFieldResolver?: Maybe>; -} - -/** - * Implements the "Subscribe" algorithm described in the GraphQL specification. - * - * Returns a Promise which resolves to either an AsyncIterator (if successful) - * or an ExecutionResult (error). The promise will be rejected if the schema or - * other arguments to this function are invalid, or if the resolved event stream - * is not an async iterable. - * - * If the client-provided arguments to this function do not result in a - * compliant subscription, a GraphQL Response (ExecutionResult) with - * descriptive errors and no data will be returned. - * - * If the source stream could not be created due to faulty subscription - * resolver logic or underlying systems, the promise will resolve to a single - * ExecutionResult containing `errors` and no `data`. - * - * If the operation succeeded, the promise resolves to an AsyncIterator, which - * yields a stream of ExecutionResults representing the response stream. - * - * Accepts either an object with named arguments, or individual arguments. - */ -export async function subscribe( - args: SubscriptionArgs, -): Promise | ExecutionResult> { - // If a valid execution context cannot be created due to incorrect arguments, - // a "Response" with only errors is returned. - const exeContext = buildExecutionContext(args); - - // Return early errors if execution context failed. - if (!('schema' in exeContext)) { - return { errors: exeContext }; - } - - return executeSubscription(exeContext); -}