Skip to content

Commit

Permalink
introduce executeRequest function
Browse files Browse the repository at this point in the history
...capable of processing requests of all operation types.

ExecutionArgs now augmented with all relevant arguments for
subscriptions as well.
  • Loading branch information
yaacovCR committed Oct 7, 2021
1 parent 781e1c1 commit 758d31e
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 85 deletions.
4 changes: 3 additions & 1 deletion src/execution/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
19 changes: 16 additions & 3 deletions src/execution/__tests__/subscribe-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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.',
Expand Down Expand Up @@ -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();
});
Expand All @@ -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<unknown>;
(await expectPromise(result)).toRejectWith(
'Subscription field must return Async Iterable. Received: "test".',
);
});
Expand Down Expand Up @@ -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: [
Expand Down
103 changes: 86 additions & 17 deletions src/execution/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,33 +153,94 @@ export interface ExecutionArgs {
contextValue?: unknown;
variableValues?: Maybe<{ readonly [variable: string]: unknown }>;
operationName?: Maybe<string>;
disableSubscription?: Maybe<boolean>;
fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
typeResolver?: Maybe<GraphQLTypeResolver<any, any>>;
subscribeFieldResolver?: Maybe<GraphQLFieldResolver<unknown, unknown>>;
}

// 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<ExecutionResult> {
// 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<ExecutionResult | AsyncGenerator<ExecutionResult, void, void>> {
const exeContext = buildExecutionContext(args);

// Return early errors if execution context failed.
if (!('schema' in exeContext)) {
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<ExecutionResult> {
return executeRequest({
...args,
disableSubscription: true,
}) as PromiseOrValue<ExecutionResult>;
}

/**
* Also implements the "Executing requests" section of the GraphQL specification.
* However, it guarantees to complete synchronously (or throw an error) assuming
Expand All @@ -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<AsyncGenerator<ExecutionResult, void, void> | ExecutionResult> {
return executeRequest(args) as Promise<
AsyncGenerator<ExecutionResult, void, void> | ExecutionResult
>;
}

/**
* Implements the "Executing operations" section of the spec for queries and
* mutations.
Expand Down Expand Up @@ -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<unknown>;
contextValue?: Maybe<unknown>;
variableValues?: Maybe<{ readonly [variable: string]: unknown }>;
operationName?: Maybe<string>;
fieldResolver?: Maybe<GraphQLFieldResolver<unknown, unknown>>;
typeResolver?: Maybe<GraphQLTypeResolver<unknown, unknown>>;
subscribeFieldResolver?: Maybe<GraphQLFieldResolver<unknown, unknown>>;
}): ReadonlyArray<GraphQLError> | ExecutionContext {
export function buildExecutionContext(
args: ExecutionArgs,
): ReadonlyArray<GraphQLError> | ExecutionContext {
const {
schema,
document,
Expand Down
7 changes: 3 additions & 4 deletions src/execution/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
60 changes: 0 additions & 60 deletions src/execution/subscribe.ts

This file was deleted.

0 comments on commit 758d31e

Please sign in to comment.