import { inspect } from '../jsutils/inspect.js';
import { invariant } from '../jsutils/invariant.js';
import { isAsyncIterable } from '../jsutils/isAsyncIterable.js';
import { isIterableObject } from '../jsutils/isIterableObject.js';
import { isObjectLike } from '../jsutils/isObjectLike.js';
import { isPromise } from '../jsutils/isPromise.js';
import type { Maybe } from '../jsutils/Maybe.js';
import { memoize3 } from '../jsutils/memoize3.js';
import type { ObjMap } from '../jsutils/ObjMap.js';
import type { Path } from '../jsutils/Path.js';
import { addPath, pathToArray } from '../jsutils/Path.js';
import { promiseForObject } from '../jsutils/promiseForObject.js';
import type { PromiseOrValue } from '../jsutils/PromiseOrValue.js';
import { promiseReduce } from '../jsutils/promiseReduce.js';
import type { GraphQLFormattedError } from '../error/GraphQLError.js';
import { GraphQLError } from '../error/GraphQLError.js';
import { locatedError } from '../error/locatedError.js';
import type {
DocumentNode,
FieldNode,
FragmentDefinitionNode,
OperationDefinitionNode,
} from '../language/ast.js';
import { OperationTypeNode } from '../language/ast.js';
import { Kind } from '../language/kinds.js';
import type {
GraphQLAbstractType,
GraphQLField,
GraphQLFieldResolver,
GraphQLLeafType,
GraphQLList,
GraphQLObjectType,
GraphQLOutputType,
GraphQLResolveInfo,
GraphQLTypeResolver,
} from '../type/definition.js';
import {
isAbstractType,
isLeafType,
isListType,
isNonNullType,
isObjectType,
} from '../type/definition.js';
import { GraphQLStreamDirective } from '../type/directives.js';
import type { GraphQLSchema } from '../type/schema.js';
import { assertValidSchema } from '../type/validate.js';
import {
collectFields,
collectSubfields as _collectSubfields,
} from './collectFields.js';
import { mapAsyncIterable } from './mapAsyncIterable.js';
import {
getArgumentValues,
getDirectiveValues,
getVariableValues,
} from './values.js';
const collectSubfields = memoize3(
(
exeContext: ExecutionContext,
returnType: GraphQLObjectType,
fieldNodes: ReadonlyArray<FieldNode>,
) =>
_collectSubfields(
exeContext.schema,
exeContext.fragments,
exeContext.variableValues,
exeContext.operation,
returnType,
fieldNodes,
),
);
export interface ExecutionContext {
schema: GraphQLSchema;
fragments: ObjMap<FragmentDefinitionNode>;
rootValue: unknown;
contextValue: unknown;
operation: OperationDefinitionNode;
variableValues: { [variable: string]: unknown };
fieldResolver: GraphQLFieldResolver<any, any>;
typeResolver: GraphQLTypeResolver<any, any>;
subscribeFieldResolver: GraphQLFieldResolver<any, any>;
errors: Array<GraphQLError>;
subsequentPayloads: Set<AsyncPayloadRecord>;
}
export interface ExecutionResult<
TData = ObjMap<unknown>,
TExtensions = ObjMap<unknown>,
> {
errors?: ReadonlyArray<GraphQLError>;
data?: TData | null;
extensions?: TExtensions;
}
export interface FormattedExecutionResult<
TData = ObjMap<unknown>,
TExtensions = ObjMap<unknown>,
> {
errors?: ReadonlyArray<GraphQLFormattedError>;
data?: TData | null;
extensions?: TExtensions;
}
export interface ExperimentalIncrementalExecutionResults<
TData = ObjMap<unknown>,
TExtensions = ObjMap<unknown>,
> {
initialResult: InitialIncrementalExecutionResult<TData, TExtensions>;
subsequentResults: AsyncGenerator<
SubsequentIncrementalExecutionResult<TData, TExtensions>,
void,
void
>;
}
export interface InitialIncrementalExecutionResult<
TData = ObjMap<unknown>,
TExtensions = ObjMap<unknown>,
> extends ExecutionResult<TData, TExtensions> {
hasNext: boolean;
incremental?: ReadonlyArray<IncrementalResult<TData, TExtensions>>;
extensions?: TExtensions;
}
export interface FormattedInitialIncrementalExecutionResult<
TData = ObjMap<unknown>,
TExtensions = ObjMap<unknown>,
> extends FormattedExecutionResult<TData, TExtensions> {
hasNext: boolean;
incremental?: ReadonlyArray<FormattedIncrementalResult<TData, TExtensions>>;
extensions?: TExtensions;
}
export interface SubsequentIncrementalExecutionResult<
TData = ObjMap<unknown>,
TExtensions = ObjMap<unknown>,
> {
hasNext: boolean;
incremental?: ReadonlyArray<IncrementalResult<TData, TExtensions>>;
extensions?: TExtensions;
}
export interface FormattedSubsequentIncrementalExecutionResult<
TData = ObjMap<unknown>,
TExtensions = ObjMap<unknown>,
> {
hasNext: boolean;
incremental?: ReadonlyArray<FormattedIncrementalResult<TData, TExtensions>>;
extensions?: TExtensions;
}
export interface IncrementalDeferResult<
TData = ObjMap<unknown>,
TExtensions = ObjMap<unknown>,
> extends ExecutionResult<TData, TExtensions> {
path?: ReadonlyArray<string | number>;
label?: string;
}
export interface FormattedIncrementalDeferResult<
TData = ObjMap<unknown>,
TExtensions = ObjMap<unknown>,
> extends FormattedExecutionResult<TData, TExtensions> {
path?: ReadonlyArray<string | number>;
label?: string;
}
export interface IncrementalStreamResult<
TData = Array<unknown>,
TExtensions = ObjMap<unknown>,
> {
errors?: ReadonlyArray<GraphQLError>;
items?: TData | null;
path?: ReadonlyArray<string | number>;
label?: string;
extensions?: TExtensions;
}
export interface FormattedIncrementalStreamResult<
TData = Array<unknown>,
TExtensions = ObjMap<unknown>,
> {
errors?: ReadonlyArray<GraphQLFormattedError>;
items?: TData | null;
path?: ReadonlyArray<string | number>;
label?: string;
extensions?: TExtensions;
}
export type IncrementalResult<
TData = ObjMap<unknown>,
TExtensions = ObjMap<unknown>,
> =
| IncrementalDeferResult<TData, TExtensions>
| IncrementalStreamResult<TData, TExtensions>;
export type FormattedIncrementalResult<
TData = ObjMap<unknown>,
TExtensions = ObjMap<unknown>,
> =
| FormattedIncrementalDeferResult<TData, TExtensions>
| FormattedIncrementalStreamResult<TData, TExtensions>;
export interface ExecutionArgs {
schema: GraphQLSchema;
document: DocumentNode;
rootValue?: unknown;
contextValue?: unknown;
variableValues?: Maybe<{ readonly [variable: string]: unknown }>;
operationName?: Maybe<string>;
fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
typeResolver?: Maybe<GraphQLTypeResolver<any, any>>;
subscribeFieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
}
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.';
const UNEXPECTED_MULTIPLE_PAYLOADS =
'Executing this GraphQL operation would unexpectedly produce multiple payloads (due to @defer or @stream directive)';
export function execute(args: ExecutionArgs): PromiseOrValue<ExecutionResult> {
if (args.schema.getDirective('defer') || args.schema.getDirective('stream')) {
throw new Error(UNEXPECTED_EXPERIMENTAL_DIRECTIVES);
}
const result = experimentalExecuteIncrementally(args);
if (!isPromise(result)) {
if ('initialResult' in result) {
throw new Error(UNEXPECTED_MULTIPLE_PAYLOADS);
}
return result;
}
return result.then((incrementalResult) => {
if ('initialResult' in incrementalResult) {
throw new Error(UNEXPECTED_MULTIPLE_PAYLOADS);
}
return incrementalResult;
});
}
export function experimentalExecuteIncrementally(
args: ExecutionArgs,
): PromiseOrValue<ExecutionResult | ExperimentalIncrementalExecutionResults> {
const exeContext = buildExecutionContext(args);
if (!('schema' in exeContext)) {
return { errors: exeContext };
}
return executeImpl(exeContext);
}
function executeImpl(
exeContext: ExecutionContext,
): PromiseOrValue<ExecutionResult | ExperimentalIncrementalExecutionResults> {
try {
const result = executeOperation(exeContext);
if (isPromise(result)) {
return result.then(
(data) => {
const initialResult = buildResponse(data, exeContext.errors);
if (exeContext.subsequentPayloads.size > 0) {
return {
initialResult: {
...initialResult,
hasNext: true,
},
subsequentResults: yieldSubsequentPayloads(exeContext),
};
}
return initialResult;
},
(error) => {
exeContext.errors.push(error);
return buildResponse(null, exeContext.errors);
},
);
}
const initialResult = buildResponse(result, exeContext.errors);
if (exeContext.subsequentPayloads.size > 0) {
return {
initialResult: {
...initialResult,
hasNext: true,
},
subsequentResults: yieldSubsequentPayloads(exeContext),
};
}
return initialResult;
} catch (error) {
exeContext.errors.push(error);
return buildResponse(null, exeContext.errors);
}
}
export function executeSync(args: ExecutionArgs): ExecutionResult {
const result = experimentalExecuteIncrementally(args);
if (isPromise(result) || 'initialResult' in result) {
throw new Error('GraphQL execution failed to complete synchronously.');
}
return result;
}
function buildResponse(
data: ObjMap<unknown> | null,
errors: ReadonlyArray<GraphQLError>,
): ExecutionResult {
return errors.length === 0 ? { data } : { errors, data };
}
export function buildExecutionContext(
args: ExecutionArgs,
): ReadonlyArray<GraphQLError> | ExecutionContext {
const {
schema,
document,
rootValue,
contextValue,
variableValues: rawVariableValues,
operationName,
fieldResolver,
typeResolver,
subscribeFieldResolver,
} = args;
assertValidSchema(schema);
let operation: OperationDefinitionNode | undefined;
const fragments: ObjMap<FragmentDefinitionNode> = Object.create(null);
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:
fragments[definition.name.value] = definition;
break;
default:
}
}
if (!operation) {
if (operationName != null) {
return [new GraphQLError(`Unknown operation named "${operationName}".`)];
}
return [new GraphQLError('Must provide an operation.')];
}
const variableDefinitions = operation.variableDefinitions ?? [];
const coercedVariableValues = getVariableValues(
schema,
variableDefinitions,
rawVariableValues ?? {},
{ maxErrors: 50 },
);
if (coercedVariableValues.errors) {
return coercedVariableValues.errors;
}
return {
schema,
fragments,
rootValue,
contextValue,
operation,
variableValues: coercedVariableValues.coerced,
fieldResolver: fieldResolver ?? defaultFieldResolver,
typeResolver: typeResolver ?? defaultTypeResolver,
subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver,
subsequentPayloads: new Set(),
errors: [],
};
}
function buildPerEventExecutionContext(
exeContext: ExecutionContext,
payload: unknown,
): ExecutionContext {
return {
...exeContext,
rootValue: payload,
subsequentPayloads: new Set(),
errors: [],
};
}
function executeOperation(
exeContext: ExecutionContext,
): PromiseOrValue<ObjMap<unknown>> {
const { operation, schema, fragments, variableValues, rootValue } =
exeContext;
const rootType = schema.getRootType(operation.operation);
if (rootType == null) {
throw new GraphQLError(
`Schema is not configured to execute ${operation.operation} operation.`,
{ nodes: operation },
);
}
const { fields: rootFields, patches } = collectFields(
schema,
fragments,
variableValues,
rootType,
operation,
);
const path = undefined;
let result;
switch (operation.operation) {
case OperationTypeNode.QUERY:
result = executeFields(exeContext, rootType, rootValue, path, rootFields);
break;
case OperationTypeNode.MUTATION:
result = executeFieldsSerially(
exeContext,
rootType,
rootValue,
path,
rootFields,
);
break;
case OperationTypeNode.SUBSCRIPTION:
result = executeFields(exeContext, rootType, rootValue, path, rootFields);
}
for (const patch of patches) {
const { label, fields: patchFields } = patch;
executeDeferredFragment(
exeContext,
rootType,
rootValue,
patchFields,
label,
path,
);
}
return result;
}
function executeFieldsSerially(
exeContext: ExecutionContext,
parentType: GraphQLObjectType,
sourceValue: unknown,
path: Path | undefined,
fields: Map<string, ReadonlyArray<FieldNode>>,
): PromiseOrValue<ObjMap<unknown>> {
return promiseReduce(
fields,
(results, [responseName, fieldNodes]) => {
const fieldPath = addPath(path, responseName, parentType.name);
const result = executeField(
exeContext,
parentType,
sourceValue,
fieldNodes,
fieldPath,
);
if (result === undefined) {
return results;
}
if (isPromise(result)) {
return result.then((resolvedResult) => {
results[responseName] = resolvedResult;
return results;
});
}
results[responseName] = result;
return results;
},
Object.create(null),
);
}
function executeFields(
exeContext: ExecutionContext,
parentType: GraphQLObjectType,
sourceValue: unknown,
path: Path | undefined,
fields: Map<string, ReadonlyArray<FieldNode>>,
asyncPayloadRecord?: AsyncPayloadRecord,
): PromiseOrValue<ObjMap<unknown>> {
const results = Object.create(null);
let containsPromise = false;
try {
for (const [responseName, fieldNodes] of fields) {
const fieldPath = addPath(path, responseName, parentType.name);
const result = executeField(
exeContext,
parentType,
sourceValue,
fieldNodes,
fieldPath,
asyncPayloadRecord,
);
if (result !== undefined) {
results[responseName] = result;
if (isPromise(result)) {
containsPromise = true;
}
}
}
} catch (error) {
if (containsPromise) {
return promiseForObject(results).finally(() => {
throw error;
});
}
throw error;
}
if (!containsPromise) {
return results;
}
return promiseForObject(results);
}
function executeField(
exeContext: ExecutionContext,
parentType: GraphQLObjectType,
source: unknown,
fieldNodes: ReadonlyArray<FieldNode>,
path: Path,
asyncPayloadRecord?: AsyncPayloadRecord,
): PromiseOrValue<unknown> {
const errors = asyncPayloadRecord?.errors ?? exeContext.errors;
const fieldName = fieldNodes[0].name.value;
const fieldDef = exeContext.schema.getField(parentType, fieldName);
if (!fieldDef) {
return;
}
const returnType = fieldDef.type;
const resolveFn = fieldDef.resolve ?? exeContext.fieldResolver;
const info = buildResolveInfo(
exeContext,
fieldDef,
fieldNodes,
parentType,
path,
);
try {
const args = getArgumentValues(
fieldDef,
fieldNodes[0],
exeContext.variableValues,
);
const contextValue = exeContext.contextValue;
const result = resolveFn(source, args, contextValue, info);
if (isPromise(result)) {
return completePromisedValue(
exeContext,
returnType,
fieldNodes,
info,
path,
result,
asyncPayloadRecord,
);
}
const completed = completeValue(
exeContext,
returnType,
fieldNodes,
info,
path,
result,
asyncPayloadRecord,
);
if (isPromise(completed)) {
return completed.then(undefined, (rawError) => {
const error = locatedError(rawError, fieldNodes, pathToArray(path));
const handledError = handleFieldError(error, returnType, errors);
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
return handledError;
});
}
return completed;
} catch (rawError) {
const error = locatedError(rawError, fieldNodes, pathToArray(path));
const handledError = handleFieldError(error, returnType, errors);
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
return handledError;
}
}
export function buildResolveInfo(
exeContext: ExecutionContext,
fieldDef: GraphQLField<unknown, unknown>,
fieldNodes: ReadonlyArray<FieldNode>,
parentType: GraphQLObjectType,
path: Path,
): GraphQLResolveInfo {
return {
fieldName: fieldDef.name,
fieldNodes,
returnType: fieldDef.type,
parentType,
path,
schema: exeContext.schema,
fragments: exeContext.fragments,
rootValue: exeContext.rootValue,
operation: exeContext.operation,
variableValues: exeContext.variableValues,
};
}
function handleFieldError(
error: GraphQLError,
returnType: GraphQLOutputType,
errors: Array<GraphQLError>,
): null {
if (isNonNullType(returnType)) {
throw error;
}
errors.push(error);
return null;
}
function completeValue(
exeContext: ExecutionContext,
returnType: GraphQLOutputType,
fieldNodes: ReadonlyArray<FieldNode>,
info: GraphQLResolveInfo,
path: Path,
result: unknown,
asyncPayloadRecord?: AsyncPayloadRecord,
): PromiseOrValue<unknown> {
if (result instanceof Error) {
throw result;
}
if (isNonNullType(returnType)) {
const completed = completeValue(
exeContext,
returnType.ofType,
fieldNodes,
info,
path,
result,
asyncPayloadRecord,
);
if (completed === null) {
throw new Error(
`Cannot return null for non-nullable field ${info.parentType.name}.${info.fieldName}.`,
);
}
return completed;
}
if (result == null) {
return null;
}
if (isListType(returnType)) {
return completeListValue(
exeContext,
returnType,
fieldNodes,
info,
path,
result,
asyncPayloadRecord,
);
}
if (isLeafType(returnType)) {
return completeLeafValue(returnType, result);
}
if (isAbstractType(returnType)) {
return completeAbstractValue(
exeContext,
returnType,
fieldNodes,
info,
path,
result,
asyncPayloadRecord,
);
}
if (isObjectType(returnType)) {
return completeObjectValue(
exeContext,
returnType,
fieldNodes,
info,
path,
result,
asyncPayloadRecord,
);
}
invariant(
false,
'Cannot complete value of unexpected output type: ' + inspect(returnType),
);
}
async function completePromisedValue(
exeContext: ExecutionContext,
returnType: GraphQLOutputType,
fieldNodes: ReadonlyArray<FieldNode>,
info: GraphQLResolveInfo,
path: Path,
result: Promise<unknown>,
asyncPayloadRecord?: AsyncPayloadRecord,
): Promise<unknown> {
try {
const resolved = await result;
let completed = completeValue(
exeContext,
returnType,
fieldNodes,
info,
path,
resolved,
asyncPayloadRecord,
);
if (isPromise(completed)) {
completed = await completed;
}
return completed;
} catch (rawError) {
const errors = asyncPayloadRecord?.errors ?? exeContext.errors;
const error = locatedError(rawError, fieldNodes, pathToArray(path));
const handledError = handleFieldError(error, returnType, errors);
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
return handledError;
}
}
function getStreamValues(
exeContext: ExecutionContext,
fieldNodes: ReadonlyArray<FieldNode>,
path: Path,
):
| undefined
| {
initialCount: number | undefined;
label: string | undefined;
} {
if (typeof path.key === 'number') {
return;
}
const stream = getDirectiveValues(
GraphQLStreamDirective,
fieldNodes[0],
exeContext.variableValues,
);
if (!stream) {
return;
}
if (stream.if === false) {
return;
}
invariant(
typeof stream.initialCount === 'number',
'initialCount must be a number',
);
invariant(
stream.initialCount >= 0,
'initialCount must be a positive integer',
);
invariant(
exeContext.operation.operation !== OperationTypeNode.SUBSCRIPTION,
'`@stream` directive not supported on subscription operations. Disable `@stream` by setting the `if` argument to `false`.',
);
return {
initialCount: stream.initialCount,
label: typeof stream.label === 'string' ? stream.label : undefined,
};
}
async function completeAsyncIteratorValue(
exeContext: ExecutionContext,
itemType: GraphQLOutputType,
fieldNodes: ReadonlyArray<FieldNode>,
info: GraphQLResolveInfo,
path: Path,
iterator: AsyncIterator<unknown>,
asyncPayloadRecord?: AsyncPayloadRecord,
): Promise<ReadonlyArray<unknown>> {
const errors = asyncPayloadRecord?.errors ?? exeContext.errors;
const stream = getStreamValues(exeContext, fieldNodes, path);
let containsPromise = false;
const completedResults: Array<unknown> = [];
let index = 0;
while (true) {
if (
stream &&
typeof stream.initialCount === 'number' &&
index >= stream.initialCount
) {
executeStreamIterator(
index,
iterator,
exeContext,
fieldNodes,
info,
itemType,
path,
stream.label,
asyncPayloadRecord,
);
break;
}
const itemPath = addPath(path, index, undefined);
let iteration;
try {
iteration = await iterator.next();
if (iteration.done) {
break;
}
} catch (rawError) {
const error = locatedError(rawError, fieldNodes, pathToArray(itemPath));
completedResults.push(handleFieldError(error, itemType, errors));
break;
}
if (
completeListItemValue(
iteration.value,
completedResults,
errors,
exeContext,
itemType,
fieldNodes,
info,
itemPath,
asyncPayloadRecord,
)
) {
containsPromise = true;
}
index += 1;
}
return containsPromise ? Promise.all(completedResults) : completedResults;
}
function completeListValue(
exeContext: ExecutionContext,
returnType: GraphQLList<GraphQLOutputType>,
fieldNodes: ReadonlyArray<FieldNode>,
info: GraphQLResolveInfo,
path: Path,
result: unknown,
asyncPayloadRecord?: AsyncPayloadRecord,
): PromiseOrValue<ReadonlyArray<unknown>> {
const itemType = returnType.ofType;
const errors = asyncPayloadRecord?.errors ?? exeContext.errors;
if (isAsyncIterable(result)) {
const iterator = result[Symbol.asyncIterator]();
return completeAsyncIteratorValue(
exeContext,
itemType,
fieldNodes,
info,
path,
iterator,
asyncPayloadRecord,
);
}
if (!isIterableObject(result)) {
throw new GraphQLError(
`Expected Iterable, but did not find one for field "${info.parentType.name}.${info.fieldName}".`,
);
}
const stream = getStreamValues(exeContext, fieldNodes, path);
let containsPromise = false;
let previousAsyncPayloadRecord = asyncPayloadRecord;
const completedResults: Array<unknown> = [];
let index = 0;
for (const item of result) {
const itemPath = addPath(path, index, undefined);
if (
stream &&
typeof stream.initialCount === 'number' &&
index >= stream.initialCount
) {
previousAsyncPayloadRecord = executeStreamField(
path,
itemPath,
item,
exeContext,
fieldNodes,
info,
itemType,
stream.label,
previousAsyncPayloadRecord,
);
index++;
continue;
}
if (
completeListItemValue(
item,
completedResults,
errors,
exeContext,
itemType,
fieldNodes,
info,
itemPath,
asyncPayloadRecord,
)
) {
containsPromise = true;
}
index++;
}
return containsPromise ? Promise.all(completedResults) : completedResults;
}
function completeListItemValue(
item: unknown,
completedResults: Array<unknown>,
errors: Array<GraphQLError>,
exeContext: ExecutionContext,
itemType: GraphQLOutputType,
fieldNodes: ReadonlyArray<FieldNode>,
info: GraphQLResolveInfo,
itemPath: Path,
asyncPayloadRecord?: AsyncPayloadRecord,
): boolean {
if (isPromise(item)) {
completedResults.push(
completePromisedValue(
exeContext,
itemType,
fieldNodes,
info,
itemPath,
item,
asyncPayloadRecord,
),
);
return true;
}
try {
const completedItem = completeValue(
exeContext,
itemType,
fieldNodes,
info,
itemPath,
item,
asyncPayloadRecord,
);
if (isPromise(completedItem)) {
completedResults.push(
completedItem.then(undefined, (rawError) => {
const error = locatedError(
rawError,
fieldNodes,
pathToArray(itemPath),
);
const handledError = handleFieldError(error, itemType, errors);
filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord);
return handledError;
}),
);
return true;
}
completedResults.push(completedItem);
} catch (rawError) {
const error = locatedError(rawError, fieldNodes, pathToArray(itemPath));
const handledError = handleFieldError(error, itemType, errors);
filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord);
completedResults.push(handledError);
}
return false;
}
function completeLeafValue(
returnType: GraphQLLeafType,
result: unknown,
): unknown {
const serializedResult = returnType.serialize(result);
if (serializedResult == null) {
throw new Error(
`Expected \`${inspect(returnType)}.serialize(${inspect(result)})\` to ` +
`return non-nullable value, returned: ${inspect(serializedResult)}`,
);
}
return serializedResult;
}
function completeAbstractValue(
exeContext: ExecutionContext,
returnType: GraphQLAbstractType,
fieldNodes: ReadonlyArray<FieldNode>,
info: GraphQLResolveInfo,
path: Path,
result: unknown,
asyncPayloadRecord?: AsyncPayloadRecord,
): PromiseOrValue<ObjMap<unknown>> {
const resolveTypeFn = returnType.resolveType ?? exeContext.typeResolver;
const contextValue = exeContext.contextValue;
const runtimeType = resolveTypeFn(result, contextValue, info, returnType);
if (isPromise(runtimeType)) {
return runtimeType.then((resolvedRuntimeType) =>
completeObjectValue(
exeContext,
ensureValidRuntimeType(
resolvedRuntimeType,
exeContext,
returnType,
fieldNodes,
info,
result,
),
fieldNodes,
info,
path,
result,
asyncPayloadRecord,
),
);
}
return completeObjectValue(
exeContext,
ensureValidRuntimeType(
runtimeType,
exeContext,
returnType,
fieldNodes,
info,
result,
),
fieldNodes,
info,
path,
result,
asyncPayloadRecord,
);
}
function ensureValidRuntimeType(
runtimeTypeName: unknown,
exeContext: ExecutionContext,
returnType: GraphQLAbstractType,
fieldNodes: ReadonlyArray<FieldNode>,
info: GraphQLResolveInfo,
result: unknown,
): GraphQLObjectType {
if (runtimeTypeName == null) {
throw new GraphQLError(
`Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}". Either the "${returnType.name}" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`,
{ nodes: fieldNodes },
);
}
if (isObjectType(runtimeTypeName)) {
throw new GraphQLError(
'Support for returning GraphQLObjectType from resolveType was removed in graphql-js@16.0.0 please return type name instead.',
);
}
if (typeof runtimeTypeName !== 'string') {
throw new GraphQLError(
`Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}" with ` +
`value ${inspect(result)}, received "${inspect(runtimeTypeName)}".`,
);
}
const runtimeType = exeContext.schema.getType(runtimeTypeName);
if (runtimeType == null) {
throw new GraphQLError(
`Abstract type "${returnType.name}" was resolved to a type "${runtimeTypeName}" that does not exist inside the schema.`,
{ nodes: fieldNodes },
);
}
if (!isObjectType(runtimeType)) {
throw new GraphQLError(
`Abstract type "${returnType.name}" was resolved to a non-object type "${runtimeTypeName}".`,
{ nodes: fieldNodes },
);
}
if (!exeContext.schema.isSubType(returnType, runtimeType)) {
throw new GraphQLError(
`Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`,
{ nodes: fieldNodes },
);
}
return runtimeType;
}
function completeObjectValue(
exeContext: ExecutionContext,
returnType: GraphQLObjectType,
fieldNodes: ReadonlyArray<FieldNode>,
info: GraphQLResolveInfo,
path: Path,
result: unknown,
asyncPayloadRecord?: AsyncPayloadRecord,
): PromiseOrValue<ObjMap<unknown>> {
if (returnType.isTypeOf) {
const isTypeOf = returnType.isTypeOf(result, exeContext.contextValue, info);
if (isPromise(isTypeOf)) {
return isTypeOf.then((resolvedIsTypeOf) => {
if (!resolvedIsTypeOf) {
throw invalidReturnTypeError(returnType, result, fieldNodes);
}
return collectAndExecuteSubfields(
exeContext,
returnType,
fieldNodes,
path,
result,
asyncPayloadRecord,
);
});
}
if (!isTypeOf) {
throw invalidReturnTypeError(returnType, result, fieldNodes);
}
}
return collectAndExecuteSubfields(
exeContext,
returnType,
fieldNodes,
path,
result,
asyncPayloadRecord,
);
}
function invalidReturnTypeError(
returnType: GraphQLObjectType,
result: unknown,
fieldNodes: ReadonlyArray<FieldNode>,
): GraphQLError {
return new GraphQLError(
`Expected value of type "${returnType.name}" but got: ${inspect(result)}.`,
{ nodes: fieldNodes },
);
}
function collectAndExecuteSubfields(
exeContext: ExecutionContext,
returnType: GraphQLObjectType,
fieldNodes: ReadonlyArray<FieldNode>,
path: Path,
result: unknown,
asyncPayloadRecord?: AsyncPayloadRecord,
): PromiseOrValue<ObjMap<unknown>> {
const { fields: subFieldNodes, patches: subPatches } = collectSubfields(
exeContext,
returnType,
fieldNodes,
);
const subFields = executeFields(
exeContext,
returnType,
result,
path,
subFieldNodes,
asyncPayloadRecord,
);
for (const subPatch of subPatches) {
const { label, fields: subPatchFieldNodes } = subPatch;
executeDeferredFragment(
exeContext,
returnType,
result,
subPatchFieldNodes,
label,
path,
asyncPayloadRecord,
);
}
return subFields;
}
export const defaultTypeResolver: GraphQLTypeResolver<unknown, unknown> =
function (value, contextValue, info, abstractType) {
if (isObjectLike(value) && typeof value.__typename === 'string') {
return value.__typename;
}
const possibleTypes = info.schema.getPossibleTypes(abstractType);
const promisedIsTypeOfResults = [];
for (let i = 0; i < possibleTypes.length; i++) {
const type = possibleTypes[i];
if (type.isTypeOf) {
const isTypeOfResult = type.isTypeOf(value, contextValue, info);
if (isPromise(isTypeOfResult)) {
promisedIsTypeOfResults[i] = isTypeOfResult;
} else if (isTypeOfResult) {
return type.name;
}
}
}
if (promisedIsTypeOfResults.length) {
return Promise.all(promisedIsTypeOfResults).then((isTypeOfResults) => {
for (let i = 0; i < isTypeOfResults.length; i++) {
if (isTypeOfResults[i]) {
return possibleTypes[i].name;
}
}
});
}
};
export const defaultFieldResolver: GraphQLFieldResolver<unknown, unknown> =
function (source: any, args, contextValue, info) {
if (isObjectLike(source) || typeof source === 'function') {
const property = source[info.fieldName];
if (typeof property === 'function') {
return source[info.fieldName](args, contextValue, info);
}
return property;
}
};
export function subscribe(
args: ExecutionArgs,
): PromiseOrValue<
AsyncGenerator<ExecutionResult, void, void> | ExecutionResult
> {
const exeContext = buildExecutionContext(args);
if (!('schema' in exeContext)) {
return { errors: exeContext };
}
const resultOrStream = createSourceEventStreamImpl(exeContext);
if (isPromise(resultOrStream)) {
return resultOrStream.then((resolvedResultOrStream) =>
mapSourceToResponse(exeContext, resolvedResultOrStream),
);
}
return mapSourceToResponse(exeContext, resultOrStream);
}
function mapSourceToResponse(
exeContext: ExecutionContext,
resultOrStream: ExecutionResult | AsyncIterable<unknown>,
): AsyncGenerator<ExecutionResult, void, void> | ExecutionResult {
if (!isAsyncIterable(resultOrStream)) {
return resultOrStream;
}
return mapAsyncIterable(
resultOrStream,
(payload: unknown) =>
executeImpl(
buildPerEventExecutionContext(exeContext, payload),
) as ExecutionResult,
);
}
export function createSourceEventStream(
args: ExecutionArgs,
): PromiseOrValue<AsyncIterable<unknown> | ExecutionResult> {
const exeContext = buildExecutionContext(args);
if (!('schema' in exeContext)) {
return { errors: exeContext };
}
return createSourceEventStreamImpl(exeContext);
}
function createSourceEventStreamImpl(
exeContext: ExecutionContext,
): PromiseOrValue<AsyncIterable<unknown> | ExecutionResult> {
try {
const eventStream = executeSubscription(exeContext);
if (isPromise(eventStream)) {
return eventStream.then(undefined, (error) => ({ errors: [error] }));
}
return eventStream;
} catch (error) {
return { errors: [error] };
}
}
function executeSubscription(
exeContext: ExecutionContext,
): PromiseOrValue<AsyncIterable<unknown>> {
const { schema, fragments, operation, variableValues, rootValue } =
exeContext;
const rootType = schema.getSubscriptionType();
if (rootType == null) {
throw new GraphQLError(
'Schema is not configured to execute subscription operation.',
{ nodes: operation },
);
}
const { fields: rootFields } = collectFields(
schema,
fragments,
variableValues,
rootType,
operation,
);
const firstRootField = rootFields.entries().next().value;
const [responseName, fieldNodes] = firstRootField;
const fieldName = fieldNodes[0].name.value;
const fieldDef = schema.getField(rootType, fieldName);
if (!fieldDef) {
throw new GraphQLError(
`The subscription field "${fieldName}" is not defined.`,
{ nodes: fieldNodes },
);
}
const path = addPath(undefined, responseName, rootType.name);
const info = buildResolveInfo(
exeContext,
fieldDef,
fieldNodes,
rootType,
path,
);
try {
const args = getArgumentValues(fieldDef, fieldNodes[0], variableValues);
const contextValue = exeContext.contextValue;
const resolveFn = fieldDef.subscribe ?? exeContext.subscribeFieldResolver;
const result = resolveFn(rootValue, args, contextValue, info);
if (isPromise(result)) {
return result.then(assertEventStream).then(undefined, (error) => {
throw locatedError(error, fieldNodes, 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;
}
if (!isAsyncIterable(result)) {
throw new GraphQLError(
'Subscription field must return Async Iterable. ' +
`Received: ${inspect(result)}.`,
);
}
return result;
}
function executeDeferredFragment(
exeContext: ExecutionContext,
parentType: GraphQLObjectType,
sourceValue: unknown,
fields: Map<string, ReadonlyArray<FieldNode>>,
label?: string,
path?: Path,
parentContext?: AsyncPayloadRecord,
): void {
const asyncPayloadRecord = new DeferredFragmentRecord({
label,
path,
parentContext,
exeContext,
});
let promiseOrData;
try {
promiseOrData = executeFields(
exeContext,
parentType,
sourceValue,
path,
fields,
asyncPayloadRecord,
);
if (isPromise(promiseOrData)) {
promiseOrData = promiseOrData.then(null, (e) => {
asyncPayloadRecord.errors.push(e);
return null;
});
}
} catch (e) {
asyncPayloadRecord.errors.push(e);
promiseOrData = null;
}
asyncPayloadRecord.addData(promiseOrData);
}
function executeStreamField(
path: Path,
itemPath: Path,
item: PromiseOrValue<unknown>,
exeContext: ExecutionContext,
fieldNodes: ReadonlyArray<FieldNode>,
info: GraphQLResolveInfo,
itemType: GraphQLOutputType,
label?: string,
parentContext?: AsyncPayloadRecord,
): AsyncPayloadRecord {
const asyncPayloadRecord = new StreamRecord({
label,
path: itemPath,
parentContext,
exeContext,
});
if (isPromise(item)) {
const completedItems = completePromisedValue(
exeContext,
itemType,
fieldNodes,
info,
itemPath,
item,
asyncPayloadRecord,
).then(
(value) => [value],
(error) => {
asyncPayloadRecord.errors.push(error);
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
return null;
},
);
asyncPayloadRecord.addItems(completedItems);
return asyncPayloadRecord;
}
let completedItem: PromiseOrValue<unknown>;
try {
try {
completedItem = completeValue(
exeContext,
itemType,
fieldNodes,
info,
itemPath,
item,
asyncPayloadRecord,
);
} catch (rawError) {
const error = locatedError(rawError, fieldNodes, pathToArray(itemPath));
completedItem = handleFieldError(
error,
itemType,
asyncPayloadRecord.errors,
);
filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord);
}
} catch (error) {
asyncPayloadRecord.errors.push(error);
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
asyncPayloadRecord.addItems(null);
return asyncPayloadRecord;
}
if (isPromise(completedItem)) {
const completedItems = completedItem
.then(undefined, (rawError) => {
const error = locatedError(rawError, fieldNodes, pathToArray(itemPath));
const handledError = handleFieldError(
error,
itemType,
asyncPayloadRecord.errors,
);
filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord);
return handledError;
})
.then(
(value) => [value],
(error) => {
asyncPayloadRecord.errors.push(error);
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
return null;
},
);
asyncPayloadRecord.addItems(completedItems);
return asyncPayloadRecord;
}
asyncPayloadRecord.addItems([completedItem]);
return asyncPayloadRecord;
}
async function executeStreamIteratorItem(
iterator: AsyncIterator<unknown>,
exeContext: ExecutionContext,
fieldNodes: ReadonlyArray<FieldNode>,
info: GraphQLResolveInfo,
itemType: GraphQLOutputType,
asyncPayloadRecord: StreamRecord,
itemPath: Path,
): Promise<IteratorResult<unknown>> {
let item;
try {
const { value, done } = await iterator.next();
if (done) {
asyncPayloadRecord.setIsCompletedIterator();
return { done, value: undefined };
}
item = value;
} catch (rawError) {
const error = locatedError(rawError, fieldNodes, pathToArray(itemPath));
const value = handleFieldError(error, itemType, asyncPayloadRecord.errors);
return { done: true, value };
}
let completedItem;
try {
completedItem = completeValue(
exeContext,
itemType,
fieldNodes,
info,
itemPath,
item,
asyncPayloadRecord,
);
if (isPromise(completedItem)) {
completedItem = completedItem.then(undefined, (rawError) => {
const error = locatedError(rawError, fieldNodes, pathToArray(itemPath));
const handledError = handleFieldError(
error,
itemType,
asyncPayloadRecord.errors,
);
filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord);
return handledError;
});
}
return { done: false, value: completedItem };
} catch (rawError) {
const error = locatedError(rawError, fieldNodes, pathToArray(itemPath));
const value = handleFieldError(error, itemType, asyncPayloadRecord.errors);
filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord);
return { done: false, value };
}
}
async function executeStreamIterator(
initialIndex: number,
iterator: AsyncIterator<unknown>,
exeContext: ExecutionContext,
fieldNodes: ReadonlyArray<FieldNode>,
info: GraphQLResolveInfo,
itemType: GraphQLOutputType,
path: Path,
label?: string,
parentContext?: AsyncPayloadRecord,
): Promise<void> {
let index = initialIndex;
let previousAsyncPayloadRecord = parentContext ?? undefined;
while (true) {
const itemPath = addPath(path, index, undefined);
const asyncPayloadRecord = new StreamRecord({
label,
path: itemPath,
parentContext: previousAsyncPayloadRecord,
iterator,
exeContext,
});
let iteration;
try {
iteration = await executeStreamIteratorItem(
iterator,
exeContext,
fieldNodes,
info,
itemType,
asyncPayloadRecord,
itemPath,
);
} catch (error) {
asyncPayloadRecord.errors.push(error);
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
asyncPayloadRecord.addItems(null);
if (iterator?.return) {
iterator.return().catch(() => {
});
}
return;
}
const { done, value: completedItem } = iteration;
let completedItems: PromiseOrValue<Array<unknown> | null>;
if (isPromise(completedItem)) {
completedItems = completedItem.then(
(value) => [value],
(error) => {
asyncPayloadRecord.errors.push(error);
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
return null;
},
);
} else {
completedItems = [completedItem];
}
asyncPayloadRecord.addItems(completedItems);
if (done) {
break;
}
previousAsyncPayloadRecord = asyncPayloadRecord;
index++;
}
}
function filterSubsequentPayloads(
exeContext: ExecutionContext,
nullPath: Path,
currentAsyncRecord: AsyncPayloadRecord | undefined,
): void {
const nullPathArray = pathToArray(nullPath);
exeContext.subsequentPayloads.forEach((asyncRecord) => {
if (asyncRecord === currentAsyncRecord) {
return;
}
for (let i = 0; i < nullPathArray.length; i++) {
if (asyncRecord.path[i] !== nullPathArray[i]) {
return;
}
}
if (isStreamPayload(asyncRecord) && asyncRecord.iterator?.return) {
asyncRecord.iterator.return().catch(() => {
});
}
exeContext.subsequentPayloads.delete(asyncRecord);
});
}
function getCompletedIncrementalResults(
exeContext: ExecutionContext,
): Array<IncrementalResult> {
const incrementalResults: Array<IncrementalResult> = [];
for (const asyncPayloadRecord of exeContext.subsequentPayloads) {
const incrementalResult: IncrementalResult = {};
if (!asyncPayloadRecord.isCompleted) {
continue;
}
exeContext.subsequentPayloads.delete(asyncPayloadRecord);
if (isStreamPayload(asyncPayloadRecord)) {
const items = asyncPayloadRecord.items;
if (asyncPayloadRecord.isCompletedIterator) {
continue;
}
(incrementalResult as IncrementalStreamResult).items = items;
} else {
const data = asyncPayloadRecord.data;
(incrementalResult as IncrementalDeferResult).data = data ?? null;
}
incrementalResult.path = asyncPayloadRecord.path;
if (asyncPayloadRecord.label) {
incrementalResult.label = asyncPayloadRecord.label;
}
if (asyncPayloadRecord.errors.length > 0) {
incrementalResult.errors = asyncPayloadRecord.errors;
}
incrementalResults.push(incrementalResult);
}
return incrementalResults;
}
function yieldSubsequentPayloads(
exeContext: ExecutionContext,
): AsyncGenerator<SubsequentIncrementalExecutionResult, void, void> {
let isDone = false;
async function next(): Promise<
IteratorResult<SubsequentIncrementalExecutionResult, void>
> {
if (isDone) {
return { value: undefined, done: true };
}
await Promise.race(
Array.from(exeContext.subsequentPayloads).map((p) => p.promise),
);
if (isDone) {
return { value: undefined, done: true };
}
const incremental = getCompletedIncrementalResults(exeContext);
const hasNext = exeContext.subsequentPayloads.size > 0;
if (!incremental.length && hasNext) {
return next();
}
if (!hasNext) {
isDone = true;
}
return {
value: incremental.length ? { incremental, hasNext } : { hasNext },
done: false,
};
}
function returnStreamIterators() {
const promises: Array<Promise<IteratorResult<unknown>>> = [];
exeContext.subsequentPayloads.forEach((asyncPayloadRecord) => {
if (
isStreamPayload(asyncPayloadRecord) &&
asyncPayloadRecord.iterator?.return
) {
promises.push(asyncPayloadRecord.iterator.return());
}
});
return Promise.all(promises);
}
return {
[Symbol.asyncIterator]() {
return this;
},
next,
async return(): Promise<
IteratorResult<SubsequentIncrementalExecutionResult, void>
> {
await returnStreamIterators();
isDone = true;
return { value: undefined, done: true };
},
async throw(
error?: unknown,
): Promise<IteratorResult<SubsequentIncrementalExecutionResult, void>> {
await returnStreamIterators();
isDone = true;
return Promise.reject(error);
},
};
}
class DeferredFragmentRecord {
type: 'defer';
errors: Array<GraphQLError>;
label: string | undefined;
path: Array<string | number>;
promise: Promise<void>;
data: ObjMap<unknown> | null;
parentContext: AsyncPayloadRecord | undefined;
isCompleted: boolean;
_exeContext: ExecutionContext;
_resolve?: (arg: PromiseOrValue<ObjMap<unknown> | null>) => void;
constructor(opts: {
label: string | undefined;
path: Path | undefined;
parentContext: AsyncPayloadRecord | undefined;
exeContext: ExecutionContext;
}) {
this.type = 'defer';
this.label = opts.label;
this.path = pathToArray(opts.path);
this.parentContext = opts.parentContext;
this.errors = [];
this._exeContext = opts.exeContext;
this._exeContext.subsequentPayloads.add(this);
this.isCompleted = false;
this.data = null;
this.promise = new Promise<ObjMap<unknown> | null>((resolve) => {
this._resolve = (promiseOrValue) => {
resolve(promiseOrValue);
};
}).then((data) => {
this.data = data;
this.isCompleted = true;
});
}
addData(data: PromiseOrValue<ObjMap<unknown> | null>) {
const parentData = this.parentContext?.promise;
if (parentData) {
this._resolve?.(parentData.then(() => data));
return;
}
this._resolve?.(data);
}
}
class StreamRecord {
type: 'stream';
errors: Array<GraphQLError>;
label: string | undefined;
path: Array<string | number>;
items: Array<unknown> | null;
promise: Promise<void>;
parentContext: AsyncPayloadRecord | undefined;
iterator: AsyncIterator<unknown> | undefined;
isCompletedIterator?: boolean;
isCompleted: boolean;
_exeContext: ExecutionContext;
_resolve?: (arg: PromiseOrValue<Array<unknown> | null>) => void;
constructor(opts: {
label: string | undefined;
path: Path | undefined;
iterator?: AsyncIterator<unknown>;
parentContext: AsyncPayloadRecord | undefined;
exeContext: ExecutionContext;
}) {
this.type = 'stream';
this.items = null;
this.label = opts.label;
this.path = pathToArray(opts.path);
this.parentContext = opts.parentContext;
this.iterator = opts.iterator;
this.errors = [];
this._exeContext = opts.exeContext;
this._exeContext.subsequentPayloads.add(this);
this.isCompleted = false;
this.items = null;
this.promise = new Promise<Array<unknown> | null>((resolve) => {
this._resolve = (promiseOrValue) => {
resolve(promiseOrValue);
};
}).then((items) => {
this.items = items;
this.isCompleted = true;
});
}
addItems(items: PromiseOrValue<Array<unknown> | null>) {
const parentData = this.parentContext?.promise;
if (parentData) {
this._resolve?.(parentData.then(() => items));
return;
}
this._resolve?.(items);
}
setIsCompletedIterator() {
this.isCompletedIterator = true;
}
}
type AsyncPayloadRecord = DeferredFragmentRecord | StreamRecord;
function isStreamPayload(
asyncPayload: AsyncPayloadRecord,
): asyncPayload is StreamRecord {
return asyncPayload.type === 'stream';
}