/** @category Execution */
import { inspect } from '../jsutils/inspect.ts';
import { isAsyncIterable } from '../jsutils/isAsyncIterable.ts';
import { isObjectLike } from '../jsutils/isObjectLike.ts';
import { isPromise, isPromiseLike } from '../jsutils/isPromise.ts';
import type { ObjMap } from '../jsutils/ObjMap.ts';
import { addPath, pathToArray } from '../jsutils/Path.ts';
import type { PromiseOrValue } from '../jsutils/PromiseOrValue.ts';
import { ensureGraphQLError } from '../error/ensureGraphQLError.ts';
import { GraphQLError } from '../error/GraphQLError.ts';
import { locatedError } from '../error/locatedError.ts';
import type {
FieldNode,
FragmentDefinitionNode,
OperationDefinitionNode,
} from '../language/ast.ts';
import { Kind } from '../language/kinds.ts';
import { isSubscriptionOperationDefinitionNode } from '../language/predicates.ts';
import { GraphQLDisableErrorPropagationDirective } from '../type/directives.ts';
import type {
GraphQLFieldResolver,
GraphQLTypeResolver,
} from '../type/index.ts';
import { assertValidSchema } from '../type/index.ts';
import { getOperationAST } from '../utilities/getOperationAST.ts';
import type { GraphQLExecuteContext } from '../diagnostics.ts';
import {
executeChannel,
executeVariableCoercionChannel,
shouldTrace,
subscribeChannel,
traceMixed,
} from '../diagnostics.ts';
import { buildResolveInfo } from './buildResolveInfo.ts';
import { cancellablePromise } from './cancellablePromise.ts';
import type { FieldDetailsList, FragmentDetails } from './collectFields.ts';
import { collectFields } from './collectFields.ts';
import { createSharedExecutionContext } from './createSharedExecutionContext.ts';
import type {
ExecutionArgs,
ValidatedExecutionArgs,
ValidatedSubscriptionArgs,
} from './ExecutionArgs.ts';
import type { ExecutionResult } from './Executor.ts';
import { Executor } from './Executor.ts';
import { ExecutorThrowingOnIncremental } from './ExecutorThrowingOnIncremental.ts';
import type { GraphQLVariableSignature } from './getVariableSignature.ts';
import { getVariableSignature } from './getVariableSignature.ts';
import type { ExperimentalIncrementalExecutionResults } from './incremental/IncrementalExecutor.ts';
import { IncrementalExecutor } from './incremental/IncrementalExecutor.ts';
import { mapAsyncIterable } from './mapAsyncIterable.ts';
import { getArgumentValues, getVariableValues } from './values.ts';
const UNEXPECTED_EXPERIMENTAL_DIRECTIVES =
'The provided schema unexpectedly contains experimental directives (@defer or @stream). These directives may only be utilized if experimental execution features are explicitly enabled.';
/** Function used to execute a validated root selection set for a subscription event. */
export type RootSelectionSetExecutor = (
validatedExecutionArgs: ValidatedSubscriptionArgs,
) => PromiseOrValue<ExecutionResult>;
/**
* Implements the "Executing requests" section of the GraphQL specification.
*
* 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 schema is invalid, an error will be thrown immediately. GraphQL
* request errors, including missing operations and variable coercion errors,
* are returned in an errors-only ExecutionResult.
*
* Field errors are collected into the response instead of rejecting the
* returned promise. Only the field that produced the error and its descendants
* are omitted; sibling fields continue to execute. Errors from fields of
* non-null type may propagate to the nearest nullable parent, which can be the
* entire response data.
*
* This function does not support incremental delivery (`@defer` and `@stream`).
* Use `experimentalExecuteIncrementally` to execute operations with
* incremental delivery enabled.
* @param args - The arguments used to perform the operation.
* @returns A completed execution result, or a promise resolving to one when execution is asynchronous.
* @example
* ```ts
* import { parse } from 'graphql/language';
* import { buildSchema } from 'graphql/utilities';
* import { execute } from 'graphql/execution';
*
* const schema = buildSchema(`
* type Query {
* greeting(name: String!): String
* }
* `);
*
* const result = await execute({
* schema,
* document: parse('query ($name: String!) { greeting(name: $name) }'),
* rootValue: {
* greeting: ({ name }) => `Hello, ${name}!`,
* },
* variableValues: { name: 'Ada' },
* });
*
* result; // => { data: { greeting: 'Hello, Ada!' } }
* ```
*/
export function execute(args: ExecutionArgs): PromiseOrValue<ExecutionResult> {
if (!shouldTrace(executeChannel)) {
return executeImpl(args);
}
return traceMixed(executeChannel, buildOperationContextFromArgs(args), () =>
executeImpl(args),
);
}
/**
* Build a graphql:execute channel context from raw ExecutionArgs. Defers
* resolution of the operation AST to a lazy getter so the cost of walking
* the document is only paid if a subscriber reads it.
* @internal
*/
function buildOperationContextFromArgs(
args: ExecutionArgs,
): Omit<GraphQLExecuteContext, 'error' | 'result'> {
let operation: OperationDefinitionNode | null | undefined;
const resolveOperation = (): OperationDefinitionNode | null | undefined => {
if (operation === undefined) {
operation = getOperationAST(args.document, args.operationName);
}
return operation;
};
return {
schema: args.schema,
document: args.document,
rawVariableValues: args.variableValues,
get operationName() {
return args.operationName ?? resolveOperation()?.name?.value;
},
get operationType() {
return resolveOperation()?.operation;
},
};
}
function executeImpl(args: ExecutionArgs): PromiseOrValue<ExecutionResult> {
if (args.schema.getDirective('defer') || args.schema.getDirective('stream')) {
throw new Error(UNEXPECTED_EXPERIMENTAL_DIRECTIVES);
}
const validatedExecutionArgs = validateExecutionArgs(args);
// Return early errors if execution context failed.
if (!('schema' in validatedExecutionArgs)) {
return { errors: validatedExecutionArgs };
}
return executeRootSelectionSet(validatedExecutionArgs);
}
/**
* Implements the "Executing requests" section of the GraphQL specification,
* including `@defer` and `@stream` as proposed in
* https://github.com/graphql/graphql-spec/pull/742
*
* This function returns either a single ExecutionResult, or an
* ExperimentalIncrementalExecutionResults object containing an `initialResult`
* and a stream of `subsequentResults`.
*
* If the schema is invalid, an error will be thrown immediately. GraphQL
* request errors, including missing operations and variable coercion errors,
* are returned in an errors-only ExecutionResult.
* @param args - Execution arguments for the GraphQL operation.
* @returns A single execution result or incremental execution results.
* @example
* ```ts
* import { parse } from 'graphql/language';
* import { buildSchema } from 'graphql/utilities';
* import { experimentalExecuteIncrementally } from 'graphql/execution';
*
* const schema = buildSchema(`
* type Query {
* greeting: String
* }
* `);
*
* const result = await experimentalExecuteIncrementally({
* schema,
* document: parse('{ greeting }'),
* rootValue: { greeting: 'Hello' },
* });
*
* result; // => { data: { greeting: 'Hello' } }
* ```
* @category Incremental Execution
*/
export function experimentalExecuteIncrementally(
args: ExecutionArgs,
): PromiseOrValue<ExecutionResult | ExperimentalIncrementalExecutionResults> {
if (!shouldTrace(executeChannel)) {
return experimentalExecuteIncrementallyImpl(args);
}
return traceMixed(executeChannel, buildOperationContextFromArgs(args), () =>
experimentalExecuteIncrementallyImpl(args),
);
}
function experimentalExecuteIncrementallyImpl(
args: ExecutionArgs,
): PromiseOrValue<ExecutionResult | ExperimentalIncrementalExecutionResults> {
// If the request cannot produce valid execution arguments, return a
// "Response" with only errors.
const validatedExecutionArgs = validateExecutionArgs(args);
// Return early errors if execution context failed.
if (!('schema' in validatedExecutionArgs)) {
return { errors: validatedExecutionArgs };
}
return experimentalExecuteRootSelectionSet(validatedExecutionArgs);
}
/** @internal */
export function executeIgnoringIncremental(
args: ExecutionArgs,
): PromiseOrValue<ExecutionResult | ExperimentalIncrementalExecutionResults> {
if (!shouldTrace(executeChannel)) {
return executeIgnoringIncrementalImpl(args);
}
return traceMixed(executeChannel, buildOperationContextFromArgs(args), () =>
executeIgnoringIncrementalImpl(args),
);
}
function executeIgnoringIncrementalImpl(
args: ExecutionArgs,
): PromiseOrValue<ExecutionResult | ExperimentalIncrementalExecutionResults> {
// If the request cannot produce valid execution arguments, return a
// "Response" with only errors.
const validatedExecutionArgs = validateExecutionArgs(args);
// Return early errors if execution context failed.
if (!('schema' in validatedExecutionArgs)) {
return { errors: validatedExecutionArgs };
}
return executeRootSelectionSetIgnoringIncremental(validatedExecutionArgs);
}
/**
* Implements the "Executing operations" section of the spec.
*
* Returns either a synchronous ExecutionResult, or a Promise for an
* ExecutionResult, described by the "Response" section of the GraphQL
* specification.
*
* If errors are encountered while executing a GraphQL field, only that field
* and its descendants will be omitted, and sibling fields will still be
* executed. These field errors are collected into the returned result instead
* of being thrown or rejecting the returned promise.
*
* Errors from sub-fields of a NonNull type may propagate to the top level, at
* which point we still collect the error and null the parent field, which in
* this case is the entire response.
* @param validatedExecutionArgs - Validated execution arguments.
* @returns Execution result for the operation root selection set.
* @example
* ```ts
* import assert from 'node:assert';
* import { parse } from 'graphql/language';
* import { buildSchema } from 'graphql/utilities';
* import {
* executeRootSelectionSet,
* validateExecutionArgs,
* } from 'graphql/execution';
*
* const schema = buildSchema('type Query { greeting: String }');
* const validatedArgs = validateExecutionArgs({
* schema,
* document: parse('{ greeting }'),
* rootValue: { greeting: 'Hello' },
* });
*
* assert('schema' in validatedArgs);
*
* const result = await executeRootSelectionSet(validatedArgs);
* result; // => { data: { greeting: 'Hello' } }
* ```
*/
export function executeRootSelectionSet(
validatedExecutionArgs: ValidatedExecutionArgs,
): PromiseOrValue<ExecutionResult> {
return new ExecutorThrowingOnIncremental(
validatedExecutionArgs,
).executeRootSelectionSet();
}
/**
* Executes the operation root selection set with incremental delivery enabled.
* @param validatedExecutionArgs - Validated execution arguments.
* @returns A single execution result or incremental execution results.
* @example
* ```ts
* import assert from 'node:assert';
* import { parse } from 'graphql/language';
* import { buildSchema } from 'graphql/utilities';
* import {
* experimentalExecuteRootSelectionSet,
* validateExecutionArgs,
* } from 'graphql/execution';
*
* const schema = buildSchema('type Query { greeting: String }');
* const validatedArgs = validateExecutionArgs({
* schema,
* document: parse('{ greeting }'),
* rootValue: { greeting: 'Hello' },
* });
*
* assert('schema' in validatedArgs);
*
* const result = await experimentalExecuteRootSelectionSet(validatedArgs);
* result; // => { data: { greeting: 'Hello' } }
* ```
* @category Incremental Execution
*/
export function experimentalExecuteRootSelectionSet(
validatedExecutionArgs: ValidatedExecutionArgs,
): PromiseOrValue<ExecutionResult | ExperimentalIncrementalExecutionResults> {
return new IncrementalExecutor(
validatedExecutionArgs,
).executeRootSelectionSet();
}
/** @internal */
export function executeRootSelectionSetIgnoringIncremental(
validatedExecutionArgs: ValidatedExecutionArgs,
): PromiseOrValue<ExecutionResult | ExperimentalIncrementalExecutionResults> {
return new Executor(validatedExecutionArgs).executeRootSelectionSet();
}
/**
* Also implements the "Executing requests" section of the GraphQL specification.
* However, it guarantees to complete synchronously (or throw an error) assuming
* that all field resolvers are also synchronous.
* @param args - The arguments used to perform the operation.
* @returns Completed execution output for a synchronous operation.
* @example
* ```ts
* import { parse } from 'graphql/language';
* import { buildSchema } from 'graphql/utilities';
* import { executeSync } from 'graphql/execution';
*
* const schema = buildSchema('type Query { greeting: String }');
*
* const result = executeSync({
* schema,
* document: parse('{ greeting }'),
* rootValue: { greeting: 'Hello' },
* });
*
* result; // => { data: { greeting: 'Hello' } }
* ```
*/
export function executeSync(args: ExecutionArgs): ExecutionResult {
const result = experimentalExecuteIncrementally(args);
// Assert that the execution was synchronous.
if (isPromise(result) || 'initialResult' in result) {
throw new Error('GraphQL execution failed to complete synchronously.');
}
return result;
}
/**
* Executes a subscription operation once for a single source event.
*
* Field errors are collected into the returned result instead of being thrown
* or rejecting the returned promise.
* @param validatedExecutionArgs - Validated subscription execution arguments.
* @returns Execution result for the subscription event.
* @example
* ```ts
* import assert from 'node:assert';
* import { parse } from 'graphql/language';
* import { buildSchema } from 'graphql/utilities';
* import {
* executeSubscriptionEvent,
* validateSubscriptionArgs,
* } from 'graphql/execution';
*
* const schema = buildSchema(`
* type Query {
* noop: String
* }
*
* type Subscription {
* greeting: String
* }
* `);
* const validatedArgs = validateSubscriptionArgs({
* schema,
* document: parse('subscription { greeting }'),
* rootValue: { greeting: 'Hello' },
* });
*
* assert('schema' in validatedArgs);
*
* const result = await executeSubscriptionEvent(validatedArgs);
* result; // => { data: { greeting: 'Hello' } }
* ```
*/
export function executeSubscriptionEvent(
validatedExecutionArgs: ValidatedSubscriptionArgs,
): PromiseOrValue<ExecutionResult> {
return new ExecutorThrowingOnIncremental(
validatedExecutionArgs,
).executeRootSelectionSet(false);
}
/**
* Implements the "Subscribe" algorithm described in the GraphQL specification.
*
* Returns either an AsyncGenerator (if successful), an ExecutionResult (error),
* or a Promise for one of those results. The call will throw immediately if
* the schema is invalid or the selected operation is not a subscription.
*
* GraphQL request errors, including missing operations and variable coercion
* errors, return or resolve to a GraphQL Response (ExecutionResult) with
* descriptive errors and no data.
*
* If the source stream could not be created due to faulty subscription resolver
* logic, a non-async-iterable resolver result, or a system error, the
* function will return or resolve to a single ExecutionResult containing
* `errors` and no `data`.
*
* If the operation succeeded, the function returns or resolves to an
* AsyncGenerator, which yields a stream of ExecutionResults representing the
* response stream.
*
* This function does not support incremental delivery (`@defer` and `@stream`).
* If an operation which would defer or stream data is executed with this
* function, a field error will be raised at the location of the `@defer` or
* `@stream` directive.
*
* Accepts an object with named arguments.
* @param args - Execution arguments for the subscription operation.
* @returns A response stream for a valid subscription, or an execution result containing errors.
* @example
* ```ts
* // Use a same-named rootValue function to provide the source event stream.
* import assert from 'node:assert';
* import { parse } from 'graphql/language';
* import { buildSchema } from 'graphql/utilities';
* import { subscribe } from 'graphql/execution';
*
* async function* greetings() {
* yield { greeting: 'Hello' };
* yield { greeting: 'Bonjour' };
* }
*
* const schema = buildSchema(`
* type Query {
* noop: String
* }
*
* type Subscription {
* greeting: String
* }
* `);
*
* const result = await subscribe({
* schema,
* document: parse('subscription { greeting }'),
* rootValue: { greeting: () => greetings() },
* });
*
* assert('next' in result);
*
* const firstPayload = await result.next();
* firstPayload.value; // => { data: { greeting: 'Hello' } }
* ```
* @example
* ```ts
* // This variant supplies events through a custom subscribeFieldResolver.
* import assert from 'node:assert';
* import { parse } from 'graphql/language';
* import { buildSchema } from 'graphql/utilities';
* import { subscribe } from 'graphql/execution';
*
* async function* defaultGreetings() {
* yield { greeting: 'Hello' };
* }
*
* async function* frenchGreetings() {
* yield { greeting: 'Bonjour' };
* }
*
* const schema = buildSchema(`
* type Query {
* noop: String
* }
*
* type Subscription {
* greeting(locale: String): String
* }
* `);
*
* const result = await subscribe({
* schema,
* document: parse(
* 'subscription Greeting($locale: String) { greeting(locale: $locale) }',
* ),
* rootValue: {
* greeting: (args, contextValue) => {
* const locale = args.locale ?? contextValue.defaultLocale;
* return locale === 'fr' ? frenchGreetings() : defaultGreetings();
* },
* },
* contextValue: { defaultLocale: 'fr' },
* variableValues: { locale: 'fr' },
* operationName: 'Greeting',
* subscribeFieldResolver: (rootValue, args, contextValue, info) => {
* args.locale; // => 'fr'
* return rootValue[info.fieldName](args, contextValue);
* },
* });
*
* assert('next' in result);
*
* const firstPayload = await result.next();
* firstPayload.value; // => { data: { greeting: 'Bonjour' } }
* ```
*/
export function subscribe(
args: ExecutionArgs,
): PromiseOrValue<
AsyncGenerator<ExecutionResult, void, void> | ExecutionResult
> {
if (!shouldTrace(subscribeChannel)) {
return subscribeImpl(args);
}
return traceMixed(subscribeChannel, buildOperationContextFromArgs(args), () =>
subscribeImpl(args),
);
}
function subscribeImpl(
args: ExecutionArgs,
): PromiseOrValue<
AsyncGenerator<ExecutionResult, void, void> | ExecutionResult
> {
// If the request cannot produce valid execution arguments, return a
// "Response" with only errors.
const validatedExecutionArgs = validateSubscriptionArgs(args);
// Return early errors if execution context failed.
if (!('schema' in validatedExecutionArgs)) {
return { errors: validatedExecutionArgs };
}
const resultOrStream = createSourceEventStream(validatedExecutionArgs);
if (isPromise(resultOrStream)) {
return resultOrStream.then((resolvedResultOrStream) =>
isAsyncIterable(resolvedResultOrStream)
? mapSourceToResponseEvent(
validatedExecutionArgs,
resolvedResultOrStream,
)
: resolvedResultOrStream,
);
}
return isAsyncIterable(resultOrStream)
? mapSourceToResponseEvent(validatedExecutionArgs, resultOrStream)
: resultOrStream;
}
/**
* Implements the "CreateSourceEventStream" algorithm described in the
* GraphQL specification, resolving the subscription source event stream for a
* previously validated subscription request.
*
* Returns either an AsyncIterable (if successful), an ExecutionResult (error),
* or a Promise for one of those results. The call will throw immediately if
* it is not passed validated execution arguments.
*
* If the validated arguments 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 a system error, the function will return or
* resolve to a single ExecutionResult containing `errors` and no `data`.
*
* If the operation succeeded, the function returns or resolves to the
* AsyncIterable for the event stream returned by the resolver.
*
* A Source Event Stream represents a sequence of events, each of which triggers
* a GraphQL execution for that event.
*
* This may be useful when hosting the stateful subscription service in a
* different process or machine than the stateless GraphQL execution engine,
* or otherwise separating these two steps. For more on this, see the
* "Supporting Subscriptions at Scale" information in the GraphQL specification.
* @param validatedExecutionArgs - Validated subscription execution arguments.
* @returns A source event stream, or an execution result containing errors.
* @example
* ```ts
* import assert from 'node:assert';
* import { parse } from 'graphql/language';
* import { buildSchema } from 'graphql/utilities';
* import {
* createSourceEventStream,
* validateSubscriptionArgs,
* } from 'graphql/execution';
*
* async function* greetings() {
* yield { greeting: 'Hello' };
* }
*
* const schema = buildSchema(`
* type Query {
* noop: String
* }
*
* type Subscription {
* greeting: String
* }
* `);
* const validatedArgs = validateSubscriptionArgs({
* schema,
* document: parse('subscription { greeting }'),
* rootValue: { greeting: () => greetings() },
* });
*
* assert('schema' in validatedArgs);
*
* const stream = await createSourceEventStream(validatedArgs);
* Symbol.asyncIterator in stream; // => true
* ```
*/
export function createSourceEventStream(
validatedExecutionArgs: ValidatedSubscriptionArgs,
): PromiseOrValue<AsyncIterable<unknown> | ExecutionResult> {
if (!('operation' in validatedExecutionArgs)) {
throw new GraphQLError(
'Passing ExecutionArgs to createSourceEventStream() was removed in graphql-js@17.0.0; call validateSubscriptionArgs() first and pass the result instead, or use subscribe() for the full subscription pipeline.',
);
}
try {
const eventStream = executeSubscription(validatedExecutionArgs);
if (isPromise(eventStream)) {
return eventStream.then(undefined, (error: unknown) => ({
errors: [ensureGraphQLError(error)],
}));
}
return eventStream;
} catch (error) {
return { errors: [ensureGraphQLError(error)] };
}
}
/**
* Validates the arguments passed to execute, subscribe, and their lower-level
* helpers.
*
* Throws if the schema is invalid. GraphQL request errors, including variable
* coercion errors, are returned as a GraphQLError array.
* @param args - Execution arguments to validate.
* @returns Validated execution arguments, or validation errors.
* @example
* ```ts
* import assert from 'node:assert';
* import { parse } from 'graphql/language';
* import { buildSchema } from 'graphql/utilities';
* import { validateExecutionArgs } from 'graphql/execution';
*
* const schema = buildSchema(`
* interface Named {
* name: String!
* }
*
* type User implements Named {
* name: String!
* }
*
* type Query {
* viewer: Named
* }
* `);
* const abortController = new AbortController();
* const validatedArgs = validateExecutionArgs({
* schema,
* document: parse('query Viewer { viewer { __typename name } }'),
* rootValue: { viewer: { kind: 'user', name: 'Ada' } },
* contextValue: { locale: 'en' },
* operationName: 'Viewer',
* fieldResolver: (source, _args, contextValue, info) => {
* contextValue.locale; // => 'en'
* return source[info.fieldName];
* },
* typeResolver: (value) => {
* return value.kind === 'user' ? 'User' : undefined;
* },
* hideSuggestions: true,
* abortSignal: abortController.signal,
* enableEarlyExecution: true,
* hooks: {
* asyncWorkFinished: () => {},
* },
* options: { maxCoercionErrors: 1 },
* });
*
* assert('operation' in validatedArgs);
*
* validatedArgs.operation.name?.value; // => 'Viewer'
* validatedArgs.hideSuggestions; // => true
* ```
*/
export function validateExecutionArgs(
args: ExecutionArgs,
): ReadonlyArray<GraphQLError> | ValidatedExecutionArgs {
const {
schema,
document,
rootValue,
contextValue,
variableValues: rawVariableValues,
operationName,
fieldResolver,
typeResolver,
subscribeFieldResolver,
abortSignal: externalAbortSignal,
enableEarlyExecution,
hooks,
options,
} = args;
// If the schema used for execution is invalid, throw an error.
assertValidSchema(schema);
let operation: OperationDefinitionNode | undefined;
const fragmentDefinitions: ObjMap<FragmentDefinitionNode> =
Object.create(null);
const fragments: ObjMap<FragmentDetails> = Object.create(null);
const fragmentVariableSignatureErrors: Array<GraphQLError> = [];
for (const definition of document.definitions) {
switch (definition.kind) {
case Kind.OPERATION_DEFINITION:
if (operationName == null) {
if (operation !== undefined) {
return [
new GraphQLError(
'Must provide operation name if query contains multiple operations.',
),
];
}
operation = definition;
} else if (definition.name?.value === operationName) {
operation = definition;
}
break;
case Kind.FRAGMENT_DEFINITION: {
fragmentDefinitions[definition.name.value] = definition;
let variableSignatures;
if (definition.variableDefinitions) {
const signatures: ObjMap<GraphQLVariableSignature> =
Object.create(null);
for (const varDef of definition.variableDefinitions) {
const signature = getVariableSignature(schema, varDef);
if (signature instanceof GraphQLError) {
fragmentVariableSignatureErrors.push(signature);
continue;
}
signatures[signature.name] = signature;
}
variableSignatures = signatures;
}
fragments[definition.name.value] = { definition, variableSignatures };
break;
}
default:
// ignore non-executable definitions
}
}
if (!operation) {
if (operationName != null) {
return [new GraphQLError(`Unknown operation named "${operationName}".`)];
}
return [new GraphQLError('Must provide an operation.')];
}
if (fragmentVariableSignatureErrors.length > 0) {
return fragmentVariableSignatureErrors;
}
const variableDefinitions = operation.variableDefinitions ?? [];
const hideSuggestions = args.hideSuggestions ?? false;
const coercionInput = rawVariableValues ?? {};
const coercionOptions = {
maxErrors: options?.maxCoercionErrors ?? 50,
hideSuggestions,
};
const coercionChannel = executeVariableCoercionChannel;
const variableValuesOrErrors = shouldTrace(coercionChannel)
? traceMixed(
coercionChannel,
{
schema,
document,
operation,
rawVariableValues,
operationName: operation.name?.value,
operationType: operation.operation,
},
() =>
getVariableValues(
schema,
variableDefinitions,
coercionInput,
coercionOptions,
),
)
: getVariableValues(
schema,
variableDefinitions,
coercionInput,
coercionOptions,
);
if (variableValuesOrErrors.errors) {
return variableValuesOrErrors.errors;
}
const errorPropagation = !operation.directives?.find(
(directive) =>
directive.name.value === GraphQLDisableErrorPropagationDirective.name,
);
return {
schema,
document,
fragmentDefinitions,
fragments,
rootValue,
contextValue,
operation,
variableValues: variableValuesOrErrors.variableValues,
fieldResolver: fieldResolver ?? defaultFieldResolver,
typeResolver: typeResolver ?? defaultTypeResolver,
subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver,
hideSuggestions,
errorPropagation,
externalAbortSignal: externalAbortSignal ?? undefined,
enableEarlyExecution: enableEarlyExecution === true,
hooks: hooks ?? undefined,
rawVariableValues,
};
}
/**
* Validates execution arguments for a subscription operation.
*
* Throws if the schema is invalid or the selected operation is not a
* subscription. GraphQL request errors, including variable coercion errors, are
* returned as a GraphQLError array.
* @param args - Execution arguments to validate.
* @returns Validated subscription execution arguments, or validation errors.
* @example
* ```ts
* import assert from 'node:assert';
* import { parse } from 'graphql/language';
* import { buildSchema } from 'graphql/utilities';
* import { validateSubscriptionArgs } from 'graphql/execution';
*
* const schema = buildSchema(`
* type Query {
* noop: String
* }
*
* type Subscription {
* greeting: String
* }
* `);
* const validatedArgs = validateSubscriptionArgs({
* schema,
* document: parse('subscription { greeting }'),
* });
*
* assert('operation' in validatedArgs);
*
* validatedArgs.operation.operation; // => 'subscription'
* ```
*/
export function validateSubscriptionArgs(
args: ExecutionArgs,
): ReadonlyArray<GraphQLError> | ValidatedSubscriptionArgs {
const validatedExecutionArgs = validateExecutionArgs(args);
if (!('schema' in validatedExecutionArgs)) {
return validatedExecutionArgs;
}
assertSubscriptionExecutionArgs(validatedExecutionArgs);
return validatedExecutionArgs;
}
function assertSubscriptionExecutionArgs(
validatedExecutionArgs: ValidatedExecutionArgs,
): asserts validatedExecutionArgs is ValidatedSubscriptionArgs {
if (
!isSubscriptionOperationDefinitionNode(validatedExecutionArgs.operation)
) {
throw new GraphQLError('Expected subscription operation.');
}
}
/**
* If a resolveType function is not given, then a default resolve behavior is
* used which attempts two strategies:
*
* First, See if the provided value has a `__typename` field defined, if so, use
* that value as name of the resolved type.
*
* Otherwise, test each possible type for the abstract type by calling
* isTypeOf for the object being coerced, returning the first type that matches.
*/
export const defaultTypeResolver: GraphQLTypeResolver<unknown, unknown> =
function (value, contextValue, info, abstractType) {
// First, look for `__typename`.
if (isObjectLike(value) && typeof value.__typename === 'string') {
return value.__typename;
}
// Otherwise, test each possible type.
const possibleTypes = info.schema.getPossibleTypes(abstractType);
const promisedIsTypeOfResults: Array<PromiseLike<boolean>> = [];
try {
for (let i = 0; i < possibleTypes.length; i++) {
const type = possibleTypes[i];
if (type.isTypeOf) {
const isTypeOfResult = type.isTypeOf(value, contextValue, info);
if (isPromiseLike(isTypeOfResult)) {
promisedIsTypeOfResults[i] = isTypeOfResult;
} else if (isTypeOfResult) {
if (promisedIsTypeOfResults.length) {
info.getAsyncHelpers().track(promisedIsTypeOfResults);
}
return type.name;
}
}
}
} catch (error) {
if (promisedIsTypeOfResults.length) {
info.getAsyncHelpers().track(promisedIsTypeOfResults);
}
throw error;
}
if (promisedIsTypeOfResults.length) {
return info
.getAsyncHelpers()
.promiseAll(promisedIsTypeOfResults)
.then((isTypeOfResults) => {
for (let i = 0; i < isTypeOfResults.length; i++) {
if (isTypeOfResults[i]) {
return possibleTypes[i].name;
}
}
});
}
};
/**
* If a resolve function is not given, then a default resolve behavior is used
* which takes the property of the source object of the same name as the field
* and returns it as the result, or if it's a function, returns the result
* of calling that function while passing along args and context value.
*/
export const defaultFieldResolver: GraphQLFieldResolver<unknown, unknown> =
function (source: any, args, contextValue, info) {
// ensure source is a value for which property access is acceptable.
if (isObjectLike(source) || typeof source === 'function') {
const property = source[info.fieldName];
if (typeof property === 'function') {
return source[info.fieldName](args, contextValue, info);
}
return property;
}
};
/**
* Implements the "MapSourceToResponseEvent" algorithm described in the
* GraphQL specification, mapping each event from a subscription source event
* stream to an ExecutionResult in the response stream.
* @param validatedExecutionArgs - Validated subscription execution arguments.
* @param sourceEventStream - Source event stream returned by the subscription resolver.
* @param rootSelectionSetExecutor - Function used to execute each source event.
* @returns A response stream of execution results.
* @example
* ```ts
* import assert from 'node:assert';
* import { parse } from 'graphql/language';
* import { buildSchema } from 'graphql/utilities';
* import {
* mapSourceToResponseEvent,
* validateSubscriptionArgs,
* } from 'graphql/execution';
*
* async function* events() {
* yield { greeting: 'Hello' };
* }
*
* const schema = buildSchema(`
* type Query {
* noop: String
* }
*
* type Subscription {
* greeting: String
* }
* `);
* const validatedArgs = validateSubscriptionArgs({
* schema,
* document: parse('subscription { greeting }'),
* });
*
* assert('schema' in validatedArgs);
*
* const responseStream = mapSourceToResponseEvent(validatedArgs, events());
* const firstPayload = await responseStream.next();
*
* firstPayload.value; // => { data: { greeting: 'Hello' } }
* ```
*/
export function mapSourceToResponseEvent(
validatedExecutionArgs: ValidatedSubscriptionArgs,
sourceEventStream: AsyncIterable<unknown>,
rootSelectionSetExecutor: RootSelectionSetExecutor = executeSubscriptionEvent,
): AsyncGenerator<ExecutionResult, void, void> {
// For each payload yielded from a subscription, map it over the normal
// GraphQL `execute` function, with `payload` as the rootValue.
function mapFn(payload: unknown): PromiseOrValue<ExecutionResult> {
const perEventExecutionArgs: ValidatedSubscriptionArgs = {
...validatedExecutionArgs,
rootValue: payload,
};
return rootSelectionSetExecutor(perEventExecutionArgs);
}
const externalAbortSignal = validatedExecutionArgs.externalAbortSignal;
if (externalAbortSignal) {
const generator = mapAsyncIterable(sourceEventStream, mapFn);
return {
...generator,
next: () => cancellablePromise(generator.next(), externalAbortSignal),
};
}
return mapAsyncIterable(sourceEventStream, mapFn);
}
function executeSubscription(
validatedExecutionArgs: ValidatedExecutionArgs,
): PromiseOrValue<AsyncIterable<unknown>> {
const {
schema,
fragments,
rootValue,
contextValue,
operation,
variableValues,
hideSuggestions,
externalAbortSignal,
} = validatedExecutionArgs;
const rootType = schema.getSubscriptionType();
if (rootType == null) {
throw new GraphQLError(
'Schema is not configured to execute subscription operation.',
{ nodes: operation },
);
}
const { groupedFieldSet } = collectFields(
schema,
fragments,
variableValues,
rootType,
operation.selectionSet,
hideSuggestions,
);
const firstRootField = groupedFieldSet.entries().next().value as [
string,
FieldDetailsList,
];
const [responseName, fieldDetailsList] = firstRootField;
const firstFieldDetails = fieldDetailsList[0];
const firstNode = firstFieldDetails.node;
const fieldName = firstNode.name.value;
const fieldDef = schema.getField(rootType, fieldName);
const fieldNodes = fieldDetailsList.map((fieldDetails) => fieldDetails.node);
if (!fieldDef) {
throw new GraphQLError(
`The subscription field "${fieldName}" is not defined.`,
{ nodes: fieldNodes },
);
}
const sharedExecutionContext =
createSharedExecutionContext(externalAbortSignal);
const path = addPath(undefined, responseName, rootType.name);
const info = buildResolveInfo(
validatedExecutionArgs,
fieldDef,
fieldNodes,
rootType,
path,
sharedExecutionContext.getAbortSignal,
sharedExecutionContext.getAsyncHelpers,
);
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,
firstNode,
variableValues,
firstFieldDetails.fragmentVariableValues,
hideSuggestions,
);
// Call the `subscribe()` resolver or the default resolver to produce an
// AsyncIterable yielding raw payloads.
const resolveFn =
fieldDef.subscribe ?? validatedExecutionArgs.subscribeFieldResolver;
// 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(rootValue, args, contextValue, info);
if (isPromiseLike(result)) {
const promisedResult = Promise.resolve(result);
const promise = externalAbortSignal
? cancellablePromise(promisedResult, externalAbortSignal)
: promisedResult;
return promise
.then(assertEventStream)
.then(undefined, (error: unknown) => {
throw locatedError(
error,
toNodes(fieldDetailsList),
pathToArray(path),
);
});
}
return assertEventStream(result);
} catch (error) {
throw locatedError(error, fieldNodes, pathToArray(path));
}
}
function assertEventStream(result: unknown): AsyncIterable<unknown> {
if (result instanceof Error) {
throw result;
}
// Assert field returned an event stream, otherwise yield an error.
if (!isAsyncIterable(result)) {
throw new GraphQLError(
'Subscription field must return Async Iterable. ' +
`Received: ${inspect(result)}.`,
);
}
return result;
}
function toNodes(fieldDetailsList: FieldDetailsList): ReadonlyArray<FieldNode> {
return fieldDetailsList.map((fieldDetails) => fieldDetails.node);
}