From 152e966b2605ac119be06d7d1c6e83a50356f559 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 10 Dec 2024 05:32:07 +0200 Subject: [PATCH 1/6] rename execute.ts --- src/execution/{execute.ts => Executor.ts} | 0 src/execution/__tests__/abstract-test.ts | 2 +- src/execution/__tests__/cancellation-test.ts | 2 +- src/execution/__tests__/defer-test.ts | 2 +- src/execution/__tests__/directives-test.ts | 2 +- src/execution/__tests__/executor-test.ts | 2 +- src/execution/__tests__/lists-test.ts | 2 +- src/execution/__tests__/mutations-test.ts | 2 +- src/execution/__tests__/nonnull-test.ts | 2 +- src/execution/__tests__/oneof-test.ts | 2 +- src/execution/__tests__/resolve-test.ts | 2 +- src/execution/__tests__/schema-test.ts | 2 +- src/execution/__tests__/stream-test.ts | 2 +- src/execution/__tests__/subscribe-test.ts | 4 ++-- src/execution/__tests__/sync-test.ts | 2 +- src/execution/__tests__/union-interface-test.ts | 2 +- src/execution/__tests__/variables-test.ts | 2 +- src/execution/index.ts | 4 ++-- src/graphql.ts | 2 +- src/utilities/introspectionFromSchema.ts | 2 +- 20 files changed, 21 insertions(+), 21 deletions(-) rename src/execution/{execute.ts => Executor.ts} (100%) diff --git a/src/execution/execute.ts b/src/execution/Executor.ts similarity index 100% rename from src/execution/execute.ts rename to src/execution/Executor.ts diff --git a/src/execution/__tests__/abstract-test.ts b/src/execution/__tests__/abstract-test.ts index 422f99c0c2..52b85f0952 100644 --- a/src/execution/__tests__/abstract-test.ts +++ b/src/execution/__tests__/abstract-test.ts @@ -17,7 +17,7 @@ import { GraphQLSchema } from '../../type/schema.js'; import { buildSchema } from '../../utilities/buildASTSchema.js'; -import { execute, executeSync } from '../execute.js'; +import { execute, executeSync } from '../Executor.js'; interface Context { async: boolean; diff --git a/src/execution/__tests__/cancellation-test.ts b/src/execution/__tests__/cancellation-test.ts index 3c2f41553f..050e6022f4 100644 --- a/src/execution/__tests__/cancellation-test.ts +++ b/src/execution/__tests__/cancellation-test.ts @@ -16,7 +16,7 @@ import { execute, experimentalExecuteIncrementally, subscribe, -} from '../execute.js'; +} from '../Executor.js'; import type { InitialIncrementalExecutionResult, SubsequentIncrementalExecutionResult, diff --git a/src/execution/__tests__/defer-test.ts b/src/execution/__tests__/defer-test.ts index 97dcfeceb6..40d6777cd8 100644 --- a/src/execution/__tests__/defer-test.ts +++ b/src/execution/__tests__/defer-test.ts @@ -18,7 +18,7 @@ import { import { GraphQLID, GraphQLString } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import { execute, experimentalExecuteIncrementally } from '../execute.js'; +import { execute, experimentalExecuteIncrementally } from '../Executor.js'; import type { InitialIncrementalExecutionResult, SubsequentIncrementalExecutionResult, diff --git a/src/execution/__tests__/directives-test.ts b/src/execution/__tests__/directives-test.ts index 2a89f07b6f..fee9a1cc4d 100644 --- a/src/execution/__tests__/directives-test.ts +++ b/src/execution/__tests__/directives-test.ts @@ -7,7 +7,7 @@ import { GraphQLObjectType } from '../../type/definition.js'; import { GraphQLString } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import { executeSync } from '../execute.js'; +import { executeSync } from '../Executor.js'; const schema = new GraphQLSchema({ query: new GraphQLObjectType({ diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 6e72609fec..6cb6c5f449 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -28,7 +28,7 @@ import { } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import { execute, executeSync } from '../execute.js'; +import { execute, executeSync } from '../Executor.js'; describe('Execute: Handles basic execution tasks', () => { it('executes arbitrary code', async () => { diff --git a/src/execution/__tests__/lists-test.ts b/src/execution/__tests__/lists-test.ts index 147520baca..d3c7427d0b 100644 --- a/src/execution/__tests__/lists-test.ts +++ b/src/execution/__tests__/lists-test.ts @@ -18,7 +18,7 @@ import { GraphQLSchema } from '../../type/schema.js'; import { buildSchema } from '../../utilities/buildASTSchema.js'; -import { execute, executeSync } from '../execute.js'; +import { execute, executeSync } from '../Executor.js'; import type { ExecutionResult } from '../types.js'; describe('Execute: Accepts any iterable as list value', () => { diff --git a/src/execution/__tests__/mutations-test.ts b/src/execution/__tests__/mutations-test.ts index 5697bf5250..8bc1619847 100644 --- a/src/execution/__tests__/mutations-test.ts +++ b/src/execution/__tests__/mutations-test.ts @@ -14,7 +14,7 @@ import { execute, executeSync, experimentalExecuteIncrementally, -} from '../execute.js'; +} from '../Executor.js'; class NumberHolder { theNumber: number; diff --git a/src/execution/__tests__/nonnull-test.ts b/src/execution/__tests__/nonnull-test.ts index ac92de1a30..21cb300568 100644 --- a/src/execution/__tests__/nonnull-test.ts +++ b/src/execution/__tests__/nonnull-test.ts @@ -14,7 +14,7 @@ import { GraphQLSchema } from '../../type/schema.js'; import { buildSchema } from '../../utilities/buildASTSchema.js'; -import { execute, executeSync } from '../execute.js'; +import { execute, executeSync } from '../Executor.js'; import type { ExecutionResult } from '../types.js'; const syncError = new Error('sync'); diff --git a/src/execution/__tests__/oneof-test.ts b/src/execution/__tests__/oneof-test.ts index c010a21b72..50a35d087f 100644 --- a/src/execution/__tests__/oneof-test.ts +++ b/src/execution/__tests__/oneof-test.ts @@ -6,7 +6,7 @@ import { parse } from '../../language/parser.js'; import { buildSchema } from '../../utilities/buildASTSchema.js'; -import { execute } from '../execute.js'; +import { execute } from '../Executor.js'; import type { ExecutionResult } from '../types.js'; const schema = buildSchema(` diff --git a/src/execution/__tests__/resolve-test.ts b/src/execution/__tests__/resolve-test.ts index b13a4266f0..e34384ff0d 100644 --- a/src/execution/__tests__/resolve-test.ts +++ b/src/execution/__tests__/resolve-test.ts @@ -8,7 +8,7 @@ import { GraphQLObjectType } from '../../type/definition.js'; import { GraphQLInt, GraphQLString } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import { executeSync } from '../execute.js'; +import { executeSync } from '../Executor.js'; describe('Execute: resolve function', () => { function testSchema(testField: GraphQLFieldConfig) { diff --git a/src/execution/__tests__/schema-test.ts b/src/execution/__tests__/schema-test.ts index 3e94ecf59a..f5c949fcc5 100644 --- a/src/execution/__tests__/schema-test.ts +++ b/src/execution/__tests__/schema-test.ts @@ -16,7 +16,7 @@ import { } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import { executeSync } from '../execute.js'; +import { executeSync } from '../Executor.js'; describe('Execute: Handles execution with a complex schema', () => { it('executes using a schema', () => { diff --git a/src/execution/__tests__/stream-test.ts b/src/execution/__tests__/stream-test.ts index d7bd7a6b48..dcf90ed3b8 100644 --- a/src/execution/__tests__/stream-test.ts +++ b/src/execution/__tests__/stream-test.ts @@ -19,7 +19,7 @@ import { import { GraphQLID, GraphQLString } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import { experimentalExecuteIncrementally } from '../execute.js'; +import { experimentalExecuteIncrementally } from '../Executor.js'; import type { InitialIncrementalExecutionResult, SubsequentIncrementalExecutionResult, diff --git a/src/execution/__tests__/subscribe-test.ts b/src/execution/__tests__/subscribe-test.ts index ffa1c85276..2c78d7d1b9 100644 --- a/src/execution/__tests__/subscribe-test.ts +++ b/src/execution/__tests__/subscribe-test.ts @@ -20,12 +20,12 @@ import { } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import type { ExecutionArgs } from '../execute.js'; +import type { ExecutionArgs } from '../Executor.js'; import { createSourceEventStream, executeSubscriptionEvent, subscribe, -} from '../execute.js'; +} from '../Executor.js'; import type { ExecutionResult } from '../types.js'; import { SimplePubSub } from './simplePubSub.js'; diff --git a/src/execution/__tests__/sync-test.ts b/src/execution/__tests__/sync-test.ts index f5efa4097c..ab242d2057 100644 --- a/src/execution/__tests__/sync-test.ts +++ b/src/execution/__tests__/sync-test.ts @@ -13,7 +13,7 @@ import { validate } from '../../validation/validate.js'; import { graphqlSync } from '../../graphql.js'; -import { execute, executeSync } from '../execute.js'; +import { execute, executeSync } from '../Executor.js'; describe('Execute: synchronously when possible', () => { const schema = new GraphQLSchema({ diff --git a/src/execution/__tests__/union-interface-test.ts b/src/execution/__tests__/union-interface-test.ts index 6f8408c487..2d0f54b647 100644 --- a/src/execution/__tests__/union-interface-test.ts +++ b/src/execution/__tests__/union-interface-test.ts @@ -14,7 +14,7 @@ import { import { GraphQLBoolean, GraphQLString } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import { execute, executeSync } from '../execute.js'; +import { execute, executeSync } from '../Executor.js'; class Dog { name: string; diff --git a/src/execution/__tests__/variables-test.ts b/src/execution/__tests__/variables-test.ts index eec35c99bc..c3ce5ab3f6 100644 --- a/src/execution/__tests__/variables-test.ts +++ b/src/execution/__tests__/variables-test.ts @@ -30,7 +30,7 @@ import { import { GraphQLBoolean, GraphQLString } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import { executeSync, experimentalExecuteIncrementally } from '../execute.js'; +import { executeSync, experimentalExecuteIncrementally } from '../Executor.js'; import { getVariableValues } from '../values.js'; const TestFaultyScalarGraphQLError = new GraphQLError( diff --git a/src/execution/index.ts b/src/execution/index.ts index fe9764c64f..cdbda26d7b 100644 --- a/src/execution/index.ts +++ b/src/execution/index.ts @@ -11,9 +11,9 @@ export { defaultFieldResolver, defaultTypeResolver, subscribe, -} from './execute.js'; +} from './Executor.js'; -export type { ExecutionArgs, ValidatedExecutionArgs } from './execute.js'; +export type { ExecutionArgs, ValidatedExecutionArgs } from './Executor.js'; export type { ExecutionResult, diff --git a/src/graphql.ts b/src/graphql.ts index ff11025968..13c840c0f3 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -14,7 +14,7 @@ import { validateSchema } from './type/validate.js'; import { validate } from './validation/validate.js'; -import { execute } from './execution/execute.js'; +import { execute } from './execution/Executor.js'; import type { ExecutionResult } from './execution/types.js'; /** diff --git a/src/utilities/introspectionFromSchema.ts b/src/utilities/introspectionFromSchema.ts index 375d53f119..f30d4273af 100644 --- a/src/utilities/introspectionFromSchema.ts +++ b/src/utilities/introspectionFromSchema.ts @@ -4,7 +4,7 @@ import { parse } from '../language/parser.js'; import type { GraphQLSchema } from '../type/schema.js'; -import { executeSync } from '../execution/execute.js'; +import { executeSync } from '../execution/Executor.js'; import type { IntrospectionOptions, From 29021883e267fe83a01c1929395971298a0fc8d6 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Thu, 19 Dec 2024 16:32:41 +0200 Subject: [PATCH 2/6] split Executor impl into separate file from API --- src/execution/Executor.ts | 613 +---------------- src/execution/__tests__/abstract-test.ts | 2 +- src/execution/__tests__/cancellation-test.ts | 2 +- src/execution/__tests__/defer-test.ts | 2 +- src/execution/__tests__/directives-test.ts | 2 +- src/execution/__tests__/executor-test.ts | 2 +- src/execution/__tests__/lists-test.ts | 2 +- src/execution/__tests__/mutations-test.ts | 2 +- src/execution/__tests__/nonnull-test.ts | 2 +- src/execution/__tests__/oneof-test.ts | 2 +- src/execution/__tests__/resolve-test.ts | 2 +- src/execution/__tests__/schema-test.ts | 2 +- src/execution/__tests__/stream-test.ts | 2 +- src/execution/__tests__/subscribe-test.ts | 4 +- src/execution/__tests__/sync-test.ts | 2 +- .../__tests__/union-interface-test.ts | 2 +- src/execution/__tests__/variables-test.ts | 2 +- src/execution/execute.ts | 646 ++++++++++++++++++ src/execution/index.ts | 6 +- src/graphql.ts | 2 +- src/utilities/introspectionFromSchema.ts | 2 +- 21 files changed, 672 insertions(+), 631 deletions(-) create mode 100644 src/execution/execute.ts diff --git a/src/execution/Executor.ts b/src/execution/Executor.ts index 8cd23ee307..d7195437e4 100644 --- a/src/execution/Executor.ts +++ b/src/execution/Executor.ts @@ -3,9 +3,7 @@ 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'; @@ -18,13 +16,11 @@ 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, @@ -46,7 +42,6 @@ import { } from '../type/definition.js'; import { GraphQLStreamDirective } from '../type/directives.js'; import type { GraphQLSchema } from '../type/schema.js'; -import { assertValidSchema } from '../type/validate.js'; import { AbortSignalListener, @@ -65,9 +60,7 @@ import { collectFields, collectSubfields as _collectSubfields, } from './collectFields.js'; -import { getVariableSignature } from './getVariableSignature.js'; import { buildIncrementalResponse } from './IncrementalPublisher.js'; -import { mapAsyncIterable } from './mapAsyncIterable.js'; import type { CancellableStreamRecord, CompletedExecutionGroup, @@ -81,12 +74,7 @@ import type { } from './types.js'; import { DeferredFragmentRecord } from './types.js'; import type { VariableValues } from './values.js'; -import { - experimentalGetArgumentValues, - getArgumentValues, - getDirectiveValues, - getVariableValues, -} from './values.js'; +import { experimentalGetArgumentValues, getDirectiveValues } from './values.js'; /* eslint-disable @typescript-eslint/max-params */ // This file contains a lot of such errors but we plan to refactor it anyway @@ -178,27 +166,7 @@ interface IncrementalContext { deferUsageSet?: DeferUsageSet | undefined; } -export interface ExecutionArgs { - schema: GraphQLSchema; - document: DocumentNode; - rootValue?: unknown; - contextValue?: unknown; - variableValues?: Maybe<{ readonly [variable: string]: unknown }>; - operationName?: Maybe; - fieldResolver?: Maybe>; - typeResolver?: Maybe>; - subscribeFieldResolver?: Maybe>; - perEventExecutor?: Maybe< - ( - validatedExecutionArgs: ValidatedExecutionArgs, - ) => PromiseOrValue - >; - enableEarlyExecution?: Maybe; - hideSuggestions?: Maybe; - abortSignal?: Maybe; -} - -export interface StreamUsage { +interface StreamUsage { label: string | undefined; initialCount: number; fieldDetailsList: FieldDetailsList; @@ -209,112 +177,7 @@ interface GraphQLWrappedResult { incrementalDataRecords: Array | undefined; } -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)'; - -/** - * 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 arguments to this function do not result in a legal execution context, - * a GraphQLError will be thrown immediately explaining the invalid input. - * - * This function does not support incremental delivery (`@defer` and `@stream`). - * If an operation which would defer or stream data is executed with this - * function, it will throw or return a rejected promise. - * Use `experimentalExecuteIncrementally` if you want to support incremental - * delivery. - */ -export function execute(args: ExecutionArgs): PromiseOrValue { - if (args.schema.getDirective('defer') || args.schema.getDirective('stream')) { - throw new Error(UNEXPECTED_EXPERIMENTAL_DIRECTIVES); - } - - const result = experimentalExecuteIncrementally(args); - // Multiple payloads could be encountered if the operation contains @defer or - // @stream directives and is not validated prior to execution - return ensureSinglePayload(result); -} - -function ensureSinglePayload( - result: PromiseOrValue< - ExecutionResult | ExperimentalIncrementalExecutionResults - >, -): PromiseOrValue { - if (isPromise(result)) { - return result.then((resolved) => { - if ('initialResult' in resolved) { - throw new Error(UNEXPECTED_MULTIPLE_PAYLOADS); - } - return resolved; - }); - } - if ('initialResult' in result) { - throw new Error(UNEXPECTED_MULTIPLE_PAYLOADS); - } - return result; -} - -/** - * 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 a Promise of an ExperimentalIncrementalExecutionResults - * object. This object either consists of a single ExecutionResult, or an - * object containing an `initialResult` and a stream of `subsequentResults`. - * - * If the arguments to this function do not result in a legal execution context, - * a GraphQLError will be thrown immediately explaining the invalid input. - */ -export function experimentalExecuteIncrementally( - args: ExecutionArgs, -): PromiseOrValue { - // If a valid execution context cannot be created due to incorrect arguments, - // a "Response" with only errors is returned. - const validatedExecutionArgs = validateExecutionArgs(args); - - // Return early errors if execution context failed. - if (!('schema' in validatedExecutionArgs)) { - return { errors: validatedExecutionArgs }; - } - - return experimentalExecuteQueryOrMutationOrSubscriptionEvent( - validatedExecutionArgs, - ); -} - -/** - * Implements the "Executing operations" section of the spec. - * - * Returns a Promise that will eventually resolve to the data 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. An execution which encounters errors will still result in a - * resolved Promise. - * - * Errors from sub-fields of a NonNull type may propagate to the top level, - * at which point we still log the error and null the parent field, which - * in this case is the entire response. - */ -export function executeQueryOrMutationOrSubscriptionEvent( - validatedExecutionArgs: ValidatedExecutionArgs, -): PromiseOrValue { - const result = experimentalExecuteQueryOrMutationOrSubscriptionEvent( - validatedExecutionArgs, - ); - return ensureSinglePayload(result); -} - -export function experimentalExecuteQueryOrMutationOrSubscriptionEvent( +export function executeQueryOrMutationOrSubscriptionEventImpl( validatedExecutionArgs: ValidatedExecutionArgs, ): PromiseOrValue { const abortSignal = validatedExecutionArgs.abortSignal; @@ -418,136 +281,6 @@ function buildDataResponse( ); } -/** - * 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. - */ -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; -} - -/** - * Constructs a ExecutionContext object from the arguments passed to - * execute, which we will pass throughout the other execution methods. - * - * Throws a GraphQLError if a valid execution context cannot be created. - * - * TODO: consider no longer exporting this function - * @internal - */ -export function validateExecutionArgs( - args: ExecutionArgs, -): ReadonlyArray | ValidatedExecutionArgs { - const { - schema, - document, - rootValue, - contextValue, - variableValues: rawVariableValues, - operationName, - fieldResolver, - typeResolver, - subscribeFieldResolver, - perEventExecutor, - enableEarlyExecution, - abortSignal, - } = args; - - if (abortSignal?.aborted) { - return [locatedError(abortSignal.reason, undefined)]; - } - - // If the schema used for execution is invalid, throw an error. - assertValidSchema(schema); - - let operation: OperationDefinitionNode | undefined; - const fragmentDefinitions: ObjMap = - Object.create(null); - const fragments: ObjMap = 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: { - fragmentDefinitions[definition.name.value] = definition; - let variableSignatures; - if (definition.variableDefinitions) { - variableSignatures = Object.create(null); - for (const varDef of definition.variableDefinitions) { - const signature = getVariableSignature(schema, varDef); - variableSignatures[signature.name] = signature; - } - } - 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.')]; - } - - const variableDefinitions = operation.variableDefinitions ?? []; - const hideSuggestions = args.hideSuggestions ?? false; - - const variableValuesOrErrors = getVariableValues( - schema, - variableDefinitions, - rawVariableValues ?? {}, - { - maxErrors: 50, - hideSuggestions, - }, - ); - - if (variableValuesOrErrors.errors) { - return variableValuesOrErrors.errors; - } - - return { - schema, - fragmentDefinitions, - fragments, - rootValue, - contextValue, - operation, - variableValues: variableValuesOrErrors.variableValues, - fieldResolver: fieldResolver ?? defaultFieldResolver, - typeResolver: typeResolver ?? defaultTypeResolver, - subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, - perEventExecutor: perEventExecutor ?? executeSubscriptionEvent, - enableEarlyExecution: enableEarlyExecution === true, - hideSuggestions, - abortSignal: args.abortSignal ?? undefined, - }; -} - function executeRootExecutionPlan( exeContext: ExecutionContext, operation: OperationTypeNode, @@ -2000,346 +1733,6 @@ function buildSubExecutionPlan( return executionPlan; } -/** - * 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 = - 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 = []; - - 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) { - if (promisedIsTypeOfResults.length > 0) { - Promise.all(promisedIsTypeOfResults).then(undefined, () => { - /* ignore errors */ - }); - } - - 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; - } - } - }); - } - }; - -/** - * 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 = - function (source: any, args, contextValue, info, abortSignal) { - // 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, abortSignal); - } - return property; - } - }; - -/** - * 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. - * - * 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. - */ -export function subscribe( - args: ExecutionArgs, -): PromiseOrValue< - AsyncGenerator | ExecutionResult -> { - // If a valid execution context cannot be created due to incorrect arguments, - // a "Response" with only errors is returned. - const validatedExecutionArgs = validateExecutionArgs(args); - - // Return early errors if execution context failed. - if (!('schema' in validatedExecutionArgs)) { - return { errors: validatedExecutionArgs }; - } - - const resultOrStream = createSourceEventStreamImpl(validatedExecutionArgs); - - if (isPromise(resultOrStream)) { - return resultOrStream.then((resolvedResultOrStream) => - mapSourceToResponse(validatedExecutionArgs, resolvedResultOrStream), - ); - } - - return mapSourceToResponse(validatedExecutionArgs, resultOrStream); -} - -function mapSourceToResponse( - validatedExecutionArgs: ValidatedExecutionArgs, - resultOrStream: ExecutionResult | AsyncIterable, -): AsyncGenerator | ExecutionResult { - if (!isAsyncIterable(resultOrStream)) { - return resultOrStream; - } - - const abortSignal = validatedExecutionArgs.abortSignal; - const abortSignalListener = abortSignal - ? new AbortSignalListener(abortSignal) - : undefined; - - // For each payload yielded from a subscription, map it over the normal - // GraphQL `execute` function, with `payload` as the rootValue. - // This implements the "MapSourceToResponseEvent" algorithm described in - // the GraphQL specification.. - return mapAsyncIterable( - abortSignalListener - ? cancellableIterable(resultOrStream, abortSignalListener) - : resultOrStream, - (payload: unknown) => { - const perEventExecutionArgs: ValidatedExecutionArgs = { - ...validatedExecutionArgs, - rootValue: payload, - }; - return validatedExecutionArgs.perEventExecutor(perEventExecutionArgs); - }, - () => abortSignalListener?.disconnect(), - ); -} - -export function executeSubscriptionEvent( - validatedExecutionArgs: ValidatedExecutionArgs, -): PromiseOrValue { - return executeQueryOrMutationOrSubscriptionEvent(validatedExecutionArgs); -} - -/** - * Implements the "CreateSourceEventStream" algorithm described in the - * GraphQL specification, resolving the subscription source event stream. - * - * Returns a Promise which resolves to either an AsyncIterable (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 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 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. - */ -export function createSourceEventStream( - args: ExecutionArgs, -): PromiseOrValue | ExecutionResult> { - // If a valid execution context cannot be created due to incorrect arguments, - // a "Response" with only errors is returned. - const validatedExecutionArgs = validateExecutionArgs(args); - - // Return early errors if execution context failed. - if (!('schema' in validatedExecutionArgs)) { - return { errors: validatedExecutionArgs }; - } - - return createSourceEventStreamImpl(validatedExecutionArgs); -} - -function createSourceEventStreamImpl( - validatedExecutionArgs: ValidatedExecutionArgs, -): PromiseOrValue | ExecutionResult> { - try { - const eventStream = executeSubscription(validatedExecutionArgs); - if (isPromise(eventStream)) { - return eventStream.then(undefined, (error: unknown) => ({ - errors: [error as GraphQLError], - })); - } - - return eventStream; - } catch (error) { - return { errors: [error] }; - } -} - -function executeSubscription( - validatedExecutionArgs: ValidatedExecutionArgs, -): PromiseOrValue> { - const { - schema, - fragments, - rootValue, - contextValue, - operation, - variableValues, - hideSuggestions, - abortSignal, - } = 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 fieldName = fieldDetailsList[0].node.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 path = addPath(undefined, responseName, rootType.name); - const info = buildResolveInfo( - validatedExecutionArgs, - fieldDef, - fieldNodes, - rootType, - path, - ); - - 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, - fieldNodes[0], - variableValues, - 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, abortSignal); - - if (isPromise(result)) { - const abortSignalListener = abortSignal - ? new AbortSignalListener(abortSignal) - : undefined; - - const promise = abortSignalListener - ? cancellablePromise(result, abortSignalListener) - : result; - return promise.then(assertEventStream).then( - (resolved) => { - abortSignalListener?.disconnect(); - return resolved; - }, - (error: unknown) => { - abortSignalListener?.disconnect(); - throw locatedError(error, fieldNodes, pathToArray(path)); - }, - ); - } - - return assertEventStream(result); - } catch (error) { - throw locatedError(error, fieldNodes, pathToArray(path)); - } -} - -function assertEventStream(result: unknown): AsyncIterable { - 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 collectExecutionGroups( exeContext: ExecutionContext, parentType: GraphQLObjectType, diff --git a/src/execution/__tests__/abstract-test.ts b/src/execution/__tests__/abstract-test.ts index 52b85f0952..422f99c0c2 100644 --- a/src/execution/__tests__/abstract-test.ts +++ b/src/execution/__tests__/abstract-test.ts @@ -17,7 +17,7 @@ import { GraphQLSchema } from '../../type/schema.js'; import { buildSchema } from '../../utilities/buildASTSchema.js'; -import { execute, executeSync } from '../Executor.js'; +import { execute, executeSync } from '../execute.js'; interface Context { async: boolean; diff --git a/src/execution/__tests__/cancellation-test.ts b/src/execution/__tests__/cancellation-test.ts index 050e6022f4..3c2f41553f 100644 --- a/src/execution/__tests__/cancellation-test.ts +++ b/src/execution/__tests__/cancellation-test.ts @@ -16,7 +16,7 @@ import { execute, experimentalExecuteIncrementally, subscribe, -} from '../Executor.js'; +} from '../execute.js'; import type { InitialIncrementalExecutionResult, SubsequentIncrementalExecutionResult, diff --git a/src/execution/__tests__/defer-test.ts b/src/execution/__tests__/defer-test.ts index 40d6777cd8..97dcfeceb6 100644 --- a/src/execution/__tests__/defer-test.ts +++ b/src/execution/__tests__/defer-test.ts @@ -18,7 +18,7 @@ import { import { GraphQLID, GraphQLString } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import { execute, experimentalExecuteIncrementally } from '../Executor.js'; +import { execute, experimentalExecuteIncrementally } from '../execute.js'; import type { InitialIncrementalExecutionResult, SubsequentIncrementalExecutionResult, diff --git a/src/execution/__tests__/directives-test.ts b/src/execution/__tests__/directives-test.ts index fee9a1cc4d..2a89f07b6f 100644 --- a/src/execution/__tests__/directives-test.ts +++ b/src/execution/__tests__/directives-test.ts @@ -7,7 +7,7 @@ import { GraphQLObjectType } from '../../type/definition.js'; import { GraphQLString } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import { executeSync } from '../Executor.js'; +import { executeSync } from '../execute.js'; const schema = new GraphQLSchema({ query: new GraphQLObjectType({ diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 6cb6c5f449..6e72609fec 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -28,7 +28,7 @@ import { } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import { execute, executeSync } from '../Executor.js'; +import { execute, executeSync } from '../execute.js'; describe('Execute: Handles basic execution tasks', () => { it('executes arbitrary code', async () => { diff --git a/src/execution/__tests__/lists-test.ts b/src/execution/__tests__/lists-test.ts index d3c7427d0b..147520baca 100644 --- a/src/execution/__tests__/lists-test.ts +++ b/src/execution/__tests__/lists-test.ts @@ -18,7 +18,7 @@ import { GraphQLSchema } from '../../type/schema.js'; import { buildSchema } from '../../utilities/buildASTSchema.js'; -import { execute, executeSync } from '../Executor.js'; +import { execute, executeSync } from '../execute.js'; import type { ExecutionResult } from '../types.js'; describe('Execute: Accepts any iterable as list value', () => { diff --git a/src/execution/__tests__/mutations-test.ts b/src/execution/__tests__/mutations-test.ts index 8bc1619847..5697bf5250 100644 --- a/src/execution/__tests__/mutations-test.ts +++ b/src/execution/__tests__/mutations-test.ts @@ -14,7 +14,7 @@ import { execute, executeSync, experimentalExecuteIncrementally, -} from '../Executor.js'; +} from '../execute.js'; class NumberHolder { theNumber: number; diff --git a/src/execution/__tests__/nonnull-test.ts b/src/execution/__tests__/nonnull-test.ts index 21cb300568..ac92de1a30 100644 --- a/src/execution/__tests__/nonnull-test.ts +++ b/src/execution/__tests__/nonnull-test.ts @@ -14,7 +14,7 @@ import { GraphQLSchema } from '../../type/schema.js'; import { buildSchema } from '../../utilities/buildASTSchema.js'; -import { execute, executeSync } from '../Executor.js'; +import { execute, executeSync } from '../execute.js'; import type { ExecutionResult } from '../types.js'; const syncError = new Error('sync'); diff --git a/src/execution/__tests__/oneof-test.ts b/src/execution/__tests__/oneof-test.ts index 50a35d087f..c010a21b72 100644 --- a/src/execution/__tests__/oneof-test.ts +++ b/src/execution/__tests__/oneof-test.ts @@ -6,7 +6,7 @@ import { parse } from '../../language/parser.js'; import { buildSchema } from '../../utilities/buildASTSchema.js'; -import { execute } from '../Executor.js'; +import { execute } from '../execute.js'; import type { ExecutionResult } from '../types.js'; const schema = buildSchema(` diff --git a/src/execution/__tests__/resolve-test.ts b/src/execution/__tests__/resolve-test.ts index e34384ff0d..b13a4266f0 100644 --- a/src/execution/__tests__/resolve-test.ts +++ b/src/execution/__tests__/resolve-test.ts @@ -8,7 +8,7 @@ import { GraphQLObjectType } from '../../type/definition.js'; import { GraphQLInt, GraphQLString } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import { executeSync } from '../Executor.js'; +import { executeSync } from '../execute.js'; describe('Execute: resolve function', () => { function testSchema(testField: GraphQLFieldConfig) { diff --git a/src/execution/__tests__/schema-test.ts b/src/execution/__tests__/schema-test.ts index f5c949fcc5..3e94ecf59a 100644 --- a/src/execution/__tests__/schema-test.ts +++ b/src/execution/__tests__/schema-test.ts @@ -16,7 +16,7 @@ import { } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import { executeSync } from '../Executor.js'; +import { executeSync } from '../execute.js'; describe('Execute: Handles execution with a complex schema', () => { it('executes using a schema', () => { diff --git a/src/execution/__tests__/stream-test.ts b/src/execution/__tests__/stream-test.ts index dcf90ed3b8..d7bd7a6b48 100644 --- a/src/execution/__tests__/stream-test.ts +++ b/src/execution/__tests__/stream-test.ts @@ -19,7 +19,7 @@ import { import { GraphQLID, GraphQLString } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import { experimentalExecuteIncrementally } from '../Executor.js'; +import { experimentalExecuteIncrementally } from '../execute.js'; import type { InitialIncrementalExecutionResult, SubsequentIncrementalExecutionResult, diff --git a/src/execution/__tests__/subscribe-test.ts b/src/execution/__tests__/subscribe-test.ts index 2c78d7d1b9..ffa1c85276 100644 --- a/src/execution/__tests__/subscribe-test.ts +++ b/src/execution/__tests__/subscribe-test.ts @@ -20,12 +20,12 @@ import { } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import type { ExecutionArgs } from '../Executor.js'; +import type { ExecutionArgs } from '../execute.js'; import { createSourceEventStream, executeSubscriptionEvent, subscribe, -} from '../Executor.js'; +} from '../execute.js'; import type { ExecutionResult } from '../types.js'; import { SimplePubSub } from './simplePubSub.js'; diff --git a/src/execution/__tests__/sync-test.ts b/src/execution/__tests__/sync-test.ts index ab242d2057..f5efa4097c 100644 --- a/src/execution/__tests__/sync-test.ts +++ b/src/execution/__tests__/sync-test.ts @@ -13,7 +13,7 @@ import { validate } from '../../validation/validate.js'; import { graphqlSync } from '../../graphql.js'; -import { execute, executeSync } from '../Executor.js'; +import { execute, executeSync } from '../execute.js'; describe('Execute: synchronously when possible', () => { const schema = new GraphQLSchema({ diff --git a/src/execution/__tests__/union-interface-test.ts b/src/execution/__tests__/union-interface-test.ts index 2d0f54b647..6f8408c487 100644 --- a/src/execution/__tests__/union-interface-test.ts +++ b/src/execution/__tests__/union-interface-test.ts @@ -14,7 +14,7 @@ import { import { GraphQLBoolean, GraphQLString } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import { execute, executeSync } from '../Executor.js'; +import { execute, executeSync } from '../execute.js'; class Dog { name: string; diff --git a/src/execution/__tests__/variables-test.ts b/src/execution/__tests__/variables-test.ts index c3ce5ab3f6..eec35c99bc 100644 --- a/src/execution/__tests__/variables-test.ts +++ b/src/execution/__tests__/variables-test.ts @@ -30,7 +30,7 @@ import { import { GraphQLBoolean, GraphQLString } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import { executeSync, experimentalExecuteIncrementally } from '../Executor.js'; +import { executeSync, experimentalExecuteIncrementally } from '../execute.js'; import { getVariableValues } from '../values.js'; const TestFaultyScalarGraphQLError = new GraphQLError( diff --git a/src/execution/execute.ts b/src/execution/execute.ts new file mode 100644 index 0000000000..8fde88f98d --- /dev/null +++ b/src/execution/execute.ts @@ -0,0 +1,646 @@ +import { inspect } from '../jsutils/inspect.js'; +import { isAsyncIterable } from '../jsutils/isAsyncIterable.js'; +import { isObjectLike } from '../jsutils/isObjectLike.js'; +import { isPromise } from '../jsutils/isPromise.js'; +import type { Maybe } from '../jsutils/Maybe.js'; +import type { ObjMap } from '../jsutils/ObjMap.js'; +import { addPath, pathToArray } from '../jsutils/Path.js'; +import type { PromiseOrValue } from '../jsutils/PromiseOrValue.js'; + +import { GraphQLError } from '../error/GraphQLError.js'; +import { locatedError } from '../error/locatedError.js'; + +import type { + DocumentNode, + FragmentDefinitionNode, + OperationDefinitionNode, +} from '../language/ast.js'; +import { Kind } from '../language/kinds.js'; + +import type { + GraphQLFieldResolver, + GraphQLSchema, + GraphQLTypeResolver, +} from '../type/index.js'; +import { assertValidSchema } from '../type/index.js'; + +import { + AbortSignalListener, + cancellableIterable, + cancellablePromise, +} from './AbortSignalListener.js'; +import type { FieldDetailsList, FragmentDetails } from './collectFields.js'; +import { collectFields } from './collectFields.js'; +import type { ValidatedExecutionArgs } from './Executor.js'; +import { + buildResolveInfo, + executeQueryOrMutationOrSubscriptionEventImpl, +} from './Executor.js'; +import { getVariableSignature } from './getVariableSignature.js'; +import { mapAsyncIterable } from './mapAsyncIterable.js'; +import type { + ExecutionResult, + ExperimentalIncrementalExecutionResults, +} from './types.js'; +import { getArgumentValues, getVariableValues } from './values.js'; + +export interface ExecutionArgs { + schema: GraphQLSchema; + document: DocumentNode; + rootValue?: unknown; + contextValue?: unknown; + variableValues?: Maybe<{ readonly [variable: string]: unknown }>; + operationName?: Maybe; + fieldResolver?: Maybe>; + typeResolver?: Maybe>; + subscribeFieldResolver?: Maybe>; + perEventExecutor?: Maybe< + ( + validatedExecutionArgs: ValidatedExecutionArgs, + ) => PromiseOrValue + >; + enableEarlyExecution?: Maybe; + hideSuggestions?: Maybe; + abortSignal?: Maybe; +} + +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)'; + +/** + * 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 arguments to this function do not result in a legal execution context, + * a GraphQLError will be thrown immediately explaining the invalid input. + * + * This function does not support incremental delivery (`@defer` and `@stream`). + * If an operation which would defer or stream data is executed with this + * function, it will throw or return a rejected promise. + * Use `experimentalExecuteIncrementally` if you want to support incremental + * delivery. + */ +export function execute(args: ExecutionArgs): PromiseOrValue { + if (args.schema.getDirective('defer') || args.schema.getDirective('stream')) { + throw new Error(UNEXPECTED_EXPERIMENTAL_DIRECTIVES); + } + + const result = experimentalExecuteIncrementally(args); + // Multiple payloads could be encountered if the operation contains @defer or + // @stream directives and is not validated prior to execution + return ensureSinglePayload(result); +} + +function ensureSinglePayload( + result: PromiseOrValue< + ExecutionResult | ExperimentalIncrementalExecutionResults + >, +): PromiseOrValue { + if (isPromise(result)) { + return result.then((resolved) => { + if ('initialResult' in resolved) { + throw new Error(UNEXPECTED_MULTIPLE_PAYLOADS); + } + return resolved; + }); + } + if ('initialResult' in result) { + throw new Error(UNEXPECTED_MULTIPLE_PAYLOADS); + } + return result; +} + +/** + * 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 a Promise of an ExperimentalIncrementalExecutionResults + * object. This object either consists of a single ExecutionResult, or an + * object containing an `initialResult` and a stream of `subsequentResults`. + * + * If the arguments to this function do not result in a legal execution context, + * a GraphQLError will be thrown immediately explaining the invalid input. + */ +export function experimentalExecuteIncrementally( + args: ExecutionArgs, +): PromiseOrValue { + // If a valid execution context cannot be created due to incorrect arguments, + // a "Response" with only errors is returned. + const validatedExecutionArgs = validateExecutionArgs(args); + + // Return early errors if execution context failed. + if (!('schema' in validatedExecutionArgs)) { + return { errors: validatedExecutionArgs }; + } + + return experimentalExecuteQueryOrMutationOrSubscriptionEvent( + validatedExecutionArgs, + ); +} + +/** + * Implements the "Executing operations" section of the spec. + * + * Returns a Promise that will eventually resolve to the data 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. An execution which encounters errors will still result in a + * resolved Promise. + * + * Errors from sub-fields of a NonNull type may propagate to the top level, + * at which point we still log the error and null the parent field, which + * in this case is the entire response. + */ +export function executeQueryOrMutationOrSubscriptionEvent( + validatedExecutionArgs: ValidatedExecutionArgs, +): PromiseOrValue { + const result = experimentalExecuteQueryOrMutationOrSubscriptionEvent( + validatedExecutionArgs, + ); + return ensureSinglePayload(result); +} + +export function experimentalExecuteQueryOrMutationOrSubscriptionEvent( + validatedExecutionArgs: ValidatedExecutionArgs, +): PromiseOrValue { + return executeQueryOrMutationOrSubscriptionEventImpl(validatedExecutionArgs); +} + +/** + * 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. + */ +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; +} + +/** + * Constructs a ExecutionContext object from the arguments passed to + * execute, which we will pass throughout the other execution methods. + * + * Throws a GraphQLError if a valid execution context cannot be created. + * + * TODO: consider no longer exporting this function + * @internal + */ +export function validateExecutionArgs( + args: ExecutionArgs, +): ReadonlyArray | ValidatedExecutionArgs { + const { + schema, + document, + rootValue, + contextValue, + variableValues: rawVariableValues, + operationName, + fieldResolver, + typeResolver, + subscribeFieldResolver, + perEventExecutor, + enableEarlyExecution, + abortSignal, + } = args; + + if (abortSignal?.aborted) { + return [locatedError(abortSignal.reason, undefined)]; + } + + // If the schema used for execution is invalid, throw an error. + assertValidSchema(schema); + + let operation: OperationDefinitionNode | undefined; + const fragmentDefinitions: ObjMap = + Object.create(null); + const fragments: ObjMap = 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: { + fragmentDefinitions[definition.name.value] = definition; + let variableSignatures; + if (definition.variableDefinitions) { + variableSignatures = Object.create(null); + for (const varDef of definition.variableDefinitions) { + const signature = getVariableSignature(schema, varDef); + variableSignatures[signature.name] = signature; + } + } + 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.')]; + } + + const variableDefinitions = operation.variableDefinitions ?? []; + const hideSuggestions = args.hideSuggestions ?? false; + + const variableValuesOrErrors = getVariableValues( + schema, + variableDefinitions, + rawVariableValues ?? {}, + { + maxErrors: 50, + hideSuggestions, + }, + ); + + if (variableValuesOrErrors.errors) { + return variableValuesOrErrors.errors; + } + + return { + schema, + fragmentDefinitions, + fragments, + rootValue, + contextValue, + operation, + variableValues: variableValuesOrErrors.variableValues, + fieldResolver: fieldResolver ?? defaultFieldResolver, + typeResolver: typeResolver ?? defaultTypeResolver, + subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, + perEventExecutor: perEventExecutor ?? executeSubscriptionEvent, + enableEarlyExecution: enableEarlyExecution === true, + hideSuggestions, + abortSignal: args.abortSignal ?? undefined, + }; +} + +/** + * 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 = + 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 = []; + + 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) { + if (promisedIsTypeOfResults.length > 0) { + Promise.all(promisedIsTypeOfResults).then(undefined, () => { + /* ignore errors */ + }); + } + + 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; + } + } + }); + } + }; + +/** + * 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 = + function (source: any, args, contextValue, info, abortSignal) { + // 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, abortSignal); + } + return property; + } + }; + +/** + * 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. + * + * 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. + */ +export function subscribe( + args: ExecutionArgs, +): PromiseOrValue< + AsyncGenerator | ExecutionResult +> { + // If a valid execution context cannot be created due to incorrect arguments, + // a "Response" with only errors is returned. + const validatedExecutionArgs = validateExecutionArgs(args); + + // Return early errors if execution context failed. + if (!('schema' in validatedExecutionArgs)) { + return { errors: validatedExecutionArgs }; + } + + const resultOrStream = createSourceEventStreamImpl(validatedExecutionArgs); + + if (isPromise(resultOrStream)) { + return resultOrStream.then((resolvedResultOrStream) => + mapSourceToResponse(validatedExecutionArgs, resolvedResultOrStream), + ); + } + + return mapSourceToResponse(validatedExecutionArgs, resultOrStream); +} + +function mapSourceToResponse( + validatedExecutionArgs: ValidatedExecutionArgs, + resultOrStream: ExecutionResult | AsyncIterable, +): AsyncGenerator | ExecutionResult { + if (!isAsyncIterable(resultOrStream)) { + return resultOrStream; + } + + const abortSignal = validatedExecutionArgs.abortSignal; + const abortSignalListener = abortSignal + ? new AbortSignalListener(abortSignal) + : undefined; + + // For each payload yielded from a subscription, map it over the normal + // GraphQL `execute` function, with `payload` as the rootValue. + // This implements the "MapSourceToResponseEvent" algorithm described in + // the GraphQL specification.. + return mapAsyncIterable( + abortSignalListener + ? cancellableIterable(resultOrStream, abortSignalListener) + : resultOrStream, + (payload: unknown) => { + const perEventExecutionArgs: ValidatedExecutionArgs = { + ...validatedExecutionArgs, + rootValue: payload, + }; + return validatedExecutionArgs.perEventExecutor(perEventExecutionArgs); + }, + () => abortSignalListener?.disconnect(), + ); +} + +export function executeSubscriptionEvent( + validatedExecutionArgs: ValidatedExecutionArgs, +): PromiseOrValue { + return executeQueryOrMutationOrSubscriptionEvent(validatedExecutionArgs); +} + +/** + * Implements the "CreateSourceEventStream" algorithm described in the + * GraphQL specification, resolving the subscription source event stream. + * + * Returns a Promise which resolves to either an AsyncIterable (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 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 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. + */ +export function createSourceEventStream( + args: ExecutionArgs, +): PromiseOrValue | ExecutionResult> { + // If a valid execution context cannot be created due to incorrect arguments, + // a "Response" with only errors is returned. + const validatedExecutionArgs = validateExecutionArgs(args); + + // Return early errors if execution context failed. + if (!('schema' in validatedExecutionArgs)) { + return { errors: validatedExecutionArgs }; + } + + return createSourceEventStreamImpl(validatedExecutionArgs); +} + +function createSourceEventStreamImpl( + validatedExecutionArgs: ValidatedExecutionArgs, +): PromiseOrValue | ExecutionResult> { + try { + const eventStream = executeSubscription(validatedExecutionArgs); + if (isPromise(eventStream)) { + return eventStream.then(undefined, (error: unknown) => ({ + errors: [error as GraphQLError], + })); + } + + return eventStream; + } catch (error) { + return { errors: [error] }; + } +} + +export function executeSubscription( + validatedExecutionArgs: ValidatedExecutionArgs, +): PromiseOrValue> { + const { + schema, + fragments, + rootValue, + contextValue, + operation, + variableValues, + hideSuggestions, + abortSignal, + } = 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 fieldName = fieldDetailsList[0].node.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 path = addPath(undefined, responseName, rootType.name); + const info = buildResolveInfo( + validatedExecutionArgs, + fieldDef, + fieldNodes, + rootType, + path, + ); + + 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, + fieldNodes[0], + variableValues, + 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, abortSignal); + + if (isPromise(result)) { + const abortSignalListener = abortSignal + ? new AbortSignalListener(abortSignal) + : undefined; + + const promise = abortSignalListener + ? cancellablePromise(result, abortSignalListener) + : result; + return promise.then(assertEventStream).then( + (resolved) => { + abortSignalListener?.disconnect(); + return resolved; + }, + (error: unknown) => { + abortSignalListener?.disconnect(); + throw locatedError(error, fieldNodes, pathToArray(path)); + }, + ); + } + + return assertEventStream(result); + } catch (error) { + throw locatedError(error, fieldNodes, pathToArray(path)); + } +} + +function assertEventStream(result: unknown): AsyncIterable { + 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; +} diff --git a/src/execution/index.ts b/src/execution/index.ts index cdbda26d7b..ff339deb88 100644 --- a/src/execution/index.ts +++ b/src/execution/index.ts @@ -11,9 +11,11 @@ export { defaultFieldResolver, defaultTypeResolver, subscribe, -} from './Executor.js'; +} from './execute.js'; -export type { ExecutionArgs, ValidatedExecutionArgs } from './Executor.js'; +export type { ExecutionArgs } from './execute.js'; + +export type { ValidatedExecutionArgs } from './Executor.js'; export type { ExecutionResult, diff --git a/src/graphql.ts b/src/graphql.ts index 13c840c0f3..ff11025968 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -14,7 +14,7 @@ import { validateSchema } from './type/validate.js'; import { validate } from './validation/validate.js'; -import { execute } from './execution/Executor.js'; +import { execute } from './execution/execute.js'; import type { ExecutionResult } from './execution/types.js'; /** diff --git a/src/utilities/introspectionFromSchema.ts b/src/utilities/introspectionFromSchema.ts index f30d4273af..375d53f119 100644 --- a/src/utilities/introspectionFromSchema.ts +++ b/src/utilities/introspectionFromSchema.ts @@ -4,7 +4,7 @@ import { parse } from '../language/parser.js'; import type { GraphQLSchema } from '../type/schema.js'; -import { executeSync } from '../execution/Executor.js'; +import { executeSync } from '../execution/execute.js'; import type { IntrospectionOptions, From 128b790140dc5445d41195f5a1ab0db3f42239f2 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Thu, 19 Dec 2024 16:41:18 +0200 Subject: [PATCH 3/6] create internal Executor class --- src/execution/Executor.ts | 3377 ++++++++++++++--------------- src/execution/buildResolveInfo.ts | 40 + src/execution/execute.ts | 9 +- 3 files changed, 1704 insertions(+), 1722 deletions(-) create mode 100644 src/execution/buildResolveInfo.ts diff --git a/src/execution/Executor.ts b/src/execution/Executor.ts index d7195437e4..54b5ba6973 100644 --- a/src/execution/Executor.ts +++ b/src/execution/Executor.ts @@ -24,7 +24,6 @@ import { OperationTypeNode } from '../language/ast.js'; import type { GraphQLAbstractType, - GraphQLField, GraphQLFieldResolver, GraphQLLeafType, GraphQLList, @@ -50,6 +49,7 @@ import { } from './AbortSignalListener.js'; import type { DeferUsageSet, ExecutionPlan } from './buildExecutionPlan.js'; import { buildExecutionPlan } from './buildExecutionPlan.js'; +import { buildResolveInfo } from './buildResolveInfo.js'; import type { DeferUsage, FieldDetailsList, @@ -153,7 +153,6 @@ export interface ValidatedExecutionArgs { } export interface ExecutionContext { - validatedExecutionArgs: ValidatedExecutionArgs; errors: Array | undefined; abortSignalListener: AbortSignalListener | undefined; completed: boolean; @@ -177,889 +176,1003 @@ interface GraphQLWrappedResult { incrementalDataRecords: Array | undefined; } -export function executeQueryOrMutationOrSubscriptionEventImpl( - validatedExecutionArgs: ValidatedExecutionArgs, -): PromiseOrValue { - const abortSignal = validatedExecutionArgs.abortSignal; - const exeContext: ExecutionContext = { - validatedExecutionArgs, - errors: undefined, - abortSignalListener: abortSignal - ? new AbortSignalListener(abortSignal) - : undefined, - completed: false, - cancellableStreams: undefined, - }; - try { - const { - schema, - fragments, - rootValue, - operation, - variableValues, - hideSuggestions, - } = validatedExecutionArgs; +/** @internal */ +export class Executor { + validatedExecutionArgs: ValidatedExecutionArgs; + exeContext: ExecutionContext; + + constructor(validatedExecutionArgs: ValidatedExecutionArgs) { + this.validatedExecutionArgs = validatedExecutionArgs; + const abortSignal = validatedExecutionArgs.abortSignal; + this.exeContext = { + errors: undefined, + abortSignalListener: abortSignal + ? new AbortSignalListener(abortSignal) + : undefined, + completed: false, + cancellableStreams: undefined, + }; + } - const { operation: operationType, selectionSet } = operation; + executeQueryOrMutationOrSubscriptionEvent(): PromiseOrValue< + ExecutionResult | ExperimentalIncrementalExecutionResults + > { + try { + const { + schema, + fragments, + rootValue, + operation, + variableValues, + hideSuggestions, + } = this.validatedExecutionArgs; + + const { operation: operationType, selectionSet } = operation; + + const rootType = schema.getRootType(operationType); + if (rootType == null) { + throw new GraphQLError( + `Schema is not configured to execute ${operationType} operation.`, + { nodes: operation }, + ); + } - const rootType = schema.getRootType(operationType); - if (rootType == null) { - throw new GraphQLError( - `Schema is not configured to execute ${operationType} operation.`, - { nodes: operation }, + const { groupedFieldSet, newDeferUsages } = collectFields( + schema, + fragments, + variableValues, + rootType, + selectionSet, + hideSuggestions, ); - } - - const { groupedFieldSet, newDeferUsages } = collectFields( - schema, - fragments, - variableValues, - rootType, - selectionSet, - hideSuggestions, - ); - - const graphqlWrappedResult = executeRootExecutionPlan( - exeContext, - operation.operation, - rootType, - rootValue, - groupedFieldSet, - newDeferUsages, - ); - if (isPromise(graphqlWrappedResult)) { - return graphqlWrappedResult.then( - (resolved) => { - exeContext.completed = true; - return buildDataResponse(exeContext, resolved); - }, - (error: unknown) => { - exeContext.completed = true; - exeContext.abortSignalListener?.disconnect(); - return { - data: null, - errors: withError(exeContext.errors, error as GraphQLError), - }; - }, + const graphqlWrappedResult = this.executeRootExecutionPlan( + operation.operation, + rootType, + rootValue, + groupedFieldSet, + newDeferUsages, ); + + if (isPromise(graphqlWrappedResult)) { + return graphqlWrappedResult.then( + (resolved) => { + this.exeContext.completed = true; + return this.buildDataResponse(resolved); + }, + (error: unknown) => { + this.exeContext.completed = true; + this.exeContext.abortSignalListener?.disconnect(); + return { + data: null, + errors: this.withError( + this.exeContext.errors, + error as GraphQLError, + ), + }; + }, + ); + } + this.exeContext.completed = true; + return this.buildDataResponse(graphqlWrappedResult); + } catch (error) { + this.exeContext.completed = true; + // TODO: add test case for synchronous null bubbling to root with cancellation + /* c8 ignore next */ + this.exeContext.abortSignalListener?.disconnect(); + return { + data: null, + errors: this.withError(this.exeContext.errors, error), + }; } - exeContext.completed = true; - return buildDataResponse(exeContext, graphqlWrappedResult); - } catch (error) { - exeContext.completed = true; - // TODO: add test case for synchronous null bubbling to root with cancellation - /* c8 ignore next */ - exeContext.abortSignalListener?.disconnect(); - return { data: null, errors: withError(exeContext.errors, error) }; } -} -function withError( - errors: Array | undefined, - error: GraphQLError, -): ReadonlyArray { - return errors === undefined ? [error] : [...errors, error]; -} - -function buildDataResponse( - exeContext: ExecutionContext, - graphqlWrappedResult: GraphQLWrappedResult>, -): ExecutionResult | ExperimentalIncrementalExecutionResults { - const { rawResult: data, incrementalDataRecords } = graphqlWrappedResult; - const errors = exeContext.errors; - if (incrementalDataRecords === undefined) { - exeContext.abortSignalListener?.disconnect(); - return errors !== undefined ? { errors, data } : { data }; + withError( + errors: Array | undefined, + error: GraphQLError, + ): ReadonlyArray { + return errors === undefined ? [error] : [...errors, error]; } - return buildIncrementalResponse( - exeContext, - data, - errors, - incrementalDataRecords, - ); -} + buildDataResponse( + graphqlWrappedResult: GraphQLWrappedResult>, + ): ExecutionResult | ExperimentalIncrementalExecutionResults { + const { rawResult: data, incrementalDataRecords } = graphqlWrappedResult; + const errors = this.exeContext.errors; + if (incrementalDataRecords === undefined) { + this.exeContext.abortSignalListener?.disconnect(); + return errors !== undefined ? { errors, data } : { data }; + } -function executeRootExecutionPlan( - exeContext: ExecutionContext, - operation: OperationTypeNode, - rootType: GraphQLObjectType, - rootValue: unknown, - originalGroupedFieldSet: GroupedFieldSet, - newDeferUsages: ReadonlyArray, -): PromiseOrValue>> { - if (newDeferUsages.length === 0) { - return executeRootGroupedFieldSet( - exeContext, - operation, - rootType, - rootValue, - originalGroupedFieldSet, - undefined, + return buildIncrementalResponse( + this.exeContext, + data, + errors, + incrementalDataRecords, ); } - const newDeferMap = getNewDeferMap(newDeferUsages, undefined, undefined); - - const { groupedFieldSet, newGroupedFieldSets } = buildExecutionPlan( - originalGroupedFieldSet, - ); - - const graphqlWrappedResult = executeRootGroupedFieldSet( - exeContext, - operation, - rootType, - rootValue, - groupedFieldSet, - newDeferMap, - ); - - if (newGroupedFieldSets.size > 0) { - const newPendingExecutionGroups = collectExecutionGroups( - exeContext, - rootType, - rootValue, + + executeRootExecutionPlan( + operation: OperationTypeNode, + rootType: GraphQLObjectType, + rootValue: unknown, + originalGroupedFieldSet: GroupedFieldSet, + newDeferUsages: ReadonlyArray, + ): PromiseOrValue>> { + if (newDeferUsages.length === 0) { + return this.executeRootGroupedFieldSet( + operation, + rootType, + rootValue, + originalGroupedFieldSet, + undefined, + ); + } + const newDeferMap = this.getNewDeferMap( + newDeferUsages, undefined, undefined, - newGroupedFieldSets, - newDeferMap, ); - return withNewExecutionGroups( - graphqlWrappedResult, - newPendingExecutionGroups, + const { groupedFieldSet, newGroupedFieldSets } = buildExecutionPlan( + originalGroupedFieldSet, ); - } - return graphqlWrappedResult; -} - -function withNewExecutionGroups( - result: PromiseOrValue>>, - newPendingExecutionGroups: ReadonlyArray, -): PromiseOrValue>> { - if (isPromise(result)) { - return result.then((resolved) => { - addIncrementalDataRecords(resolved, newPendingExecutionGroups); - return resolved; - }); - } - addIncrementalDataRecords(result, newPendingExecutionGroups); - return result; -} + const graphqlWrappedResult = this.executeRootGroupedFieldSet( + operation, + rootType, + rootValue, + groupedFieldSet, + newDeferMap, + ); -function executeRootGroupedFieldSet( - exeContext: ExecutionContext, - operation: OperationTypeNode, - rootType: GraphQLObjectType, - rootValue: unknown, - groupedFieldSet: GroupedFieldSet, - deferMap: ReadonlyMap | undefined, -): PromiseOrValue>> { - switch (operation) { - case OperationTypeNode.QUERY: - return executeFields( - exeContext, - rootType, - rootValue, - undefined, - groupedFieldSet, - undefined, - deferMap, - ); - case OperationTypeNode.MUTATION: - return executeFieldsSerially( - exeContext, + if (newGroupedFieldSets.size > 0) { + const newPendingExecutionGroups = this.collectExecutionGroups( rootType, rootValue, undefined, - groupedFieldSet, undefined, - deferMap, + newGroupedFieldSets, + newDeferMap, ); - case OperationTypeNode.SUBSCRIPTION: - // TODO: deprecate `subscribe` and move all logic here - // Temporary solution until we finish merging execute and subscribe together - return executeFields( - exeContext, - rootType, - rootValue, - undefined, - groupedFieldSet, - undefined, - deferMap, + + return this.withNewExecutionGroups( + graphqlWrappedResult, + newPendingExecutionGroups, ); + } + return graphqlWrappedResult; } -} -/** - * Implements the "Executing selection sets" section of the spec - * for fields that must be executed serially. - */ -function executeFieldsSerially( - exeContext: ExecutionContext, - parentType: GraphQLObjectType, - sourceValue: unknown, - path: Path | undefined, - groupedFieldSet: GroupedFieldSet, - incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, -): PromiseOrValue>> { - const abortSignal = exeContext.validatedExecutionArgs.abortSignal; - return promiseReduce( - groupedFieldSet, - (graphqlWrappedResult, [responseName, fieldDetailsList]) => { - const fieldPath = addPath(path, responseName, parentType.name); - - if (abortSignal?.aborted) { - handleFieldError( - abortSignal.reason, - exeContext, + withNewExecutionGroups( + result: PromiseOrValue>>, + newPendingExecutionGroups: ReadonlyArray, + ): PromiseOrValue>> { + if (isPromise(result)) { + return result.then((resolved) => { + this.addIncrementalDataRecords(resolved, newPendingExecutionGroups); + return resolved; + }); + } + + this.addIncrementalDataRecords(result, newPendingExecutionGroups); + return result; + } + + executeRootGroupedFieldSet( + operation: OperationTypeNode, + rootType: GraphQLObjectType, + rootValue: unknown, + groupedFieldSet: GroupedFieldSet, + deferMap: ReadonlyMap | undefined, + ): PromiseOrValue>> { + switch (operation) { + case OperationTypeNode.QUERY: + return this.executeFields( + rootType, + rootValue, + undefined, + groupedFieldSet, + undefined, + deferMap, + ); + case OperationTypeNode.MUTATION: + return this.executeFieldsSerially( + rootType, + rootValue, + undefined, + groupedFieldSet, + undefined, + deferMap, + ); + case OperationTypeNode.SUBSCRIPTION: + // TODO: deprecate `subscribe` and move all logic here + // Temporary solution until we finish merging execute and subscribe together + return this.executeFields( + rootType, + rootValue, + undefined, + groupedFieldSet, + undefined, + deferMap, + ); + } + } + + /** + * Implements the "Executing selection sets" section of the spec + * for fields that must be executed serially. + */ + executeFieldsSerially( + parentType: GraphQLObjectType, + sourceValue: unknown, + path: Path | undefined, + groupedFieldSet: GroupedFieldSet, + incrementalContext: IncrementalContext | undefined, + deferMap: ReadonlyMap | undefined, + ): PromiseOrValue>> { + const abortSignal = this.validatedExecutionArgs.abortSignal; + return promiseReduce( + groupedFieldSet, + (graphqlWrappedResult, [responseName, fieldDetailsList]) => { + const fieldPath = addPath(path, responseName, parentType.name); + + if (abortSignal?.aborted) { + this.handleFieldError( + abortSignal.reason, + parentType, + fieldDetailsList, + fieldPath, + incrementalContext, + ); + graphqlWrappedResult.rawResult[responseName] = null; + return graphqlWrappedResult; + } + + const result = this.executeField( parentType, + sourceValue, fieldDetailsList, fieldPath, incrementalContext, + deferMap, + ); + if (result === undefined) { + return graphqlWrappedResult; + } + if (isPromise(result)) { + return result.then((resolved) => { + graphqlWrappedResult.rawResult[responseName] = resolved.rawResult; + this.addIncrementalDataRecords( + graphqlWrappedResult, + resolved.incrementalDataRecords, + ); + return graphqlWrappedResult; + }); + } + graphqlWrappedResult.rawResult[responseName] = result.rawResult; + this.addIncrementalDataRecords( + graphqlWrappedResult, + result.incrementalDataRecords, ); - graphqlWrappedResult.rawResult[responseName] = null; return graphqlWrappedResult; - } + }, + { + rawResult: Object.create(null), + incrementalDataRecords: undefined, + }, + ); + } - const result = executeField( - exeContext, - parentType, - sourceValue, - fieldDetailsList, - fieldPath, - incrementalContext, - deferMap, - ); - if (result === undefined) { - return graphqlWrappedResult; - } - if (isPromise(result)) { - return result.then((resolved) => { - graphqlWrappedResult.rawResult[responseName] = resolved.rawResult; - addIncrementalDataRecords( - graphqlWrappedResult, - resolved.incrementalDataRecords, - ); - return graphqlWrappedResult; - }); - } - graphqlWrappedResult.rawResult[responseName] = result.rawResult; - addIncrementalDataRecords( - graphqlWrappedResult, - result.incrementalDataRecords, + addIncrementalDataRecords( + graphqlWrappedResult: GraphQLWrappedResult, + incrementalDataRecords: ReadonlyArray | undefined, + ): void { + if (incrementalDataRecords === undefined) { + return; + } + if (graphqlWrappedResult.incrementalDataRecords === undefined) { + graphqlWrappedResult.incrementalDataRecords = [...incrementalDataRecords]; + } else { + graphqlWrappedResult.incrementalDataRecords.push( + ...incrementalDataRecords, ); - return graphqlWrappedResult; - }, - { - rawResult: Object.create(null), - incrementalDataRecords: undefined, - }, - ); -} - -function addIncrementalDataRecords( - graphqlWrappedResult: GraphQLWrappedResult, - incrementalDataRecords: ReadonlyArray | undefined, -): void { - if (incrementalDataRecords === undefined) { - return; - } - if (graphqlWrappedResult.incrementalDataRecords === undefined) { - graphqlWrappedResult.incrementalDataRecords = [...incrementalDataRecords]; - } else { - graphqlWrappedResult.incrementalDataRecords.push(...incrementalDataRecords); + } } -} -/** - * Implements the "Executing selection sets" section of the spec - * for fields that may be executed in parallel. - */ -function executeFields( - exeContext: ExecutionContext, - parentType: GraphQLObjectType, - sourceValue: unknown, - path: Path | undefined, - groupedFieldSet: GroupedFieldSet, - incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, -): PromiseOrValue>> { - const results = Object.create(null); - const graphqlWrappedResult: GraphQLWrappedResult> = { - rawResult: results, - incrementalDataRecords: undefined, - }; - let containsPromise = false; - - try { - for (const [responseName, fieldDetailsList] of groupedFieldSet) { - const fieldPath = addPath(path, responseName, parentType.name); - const result = executeField( - exeContext, - parentType, - sourceValue, - fieldDetailsList, - fieldPath, - incrementalContext, - deferMap, - ); + /** + * Implements the "Executing selection sets" section of the spec + * for fields that may be executed in parallel. + */ + executeFields( + parentType: GraphQLObjectType, + sourceValue: unknown, + path: Path | undefined, + groupedFieldSet: GroupedFieldSet, + incrementalContext: IncrementalContext | undefined, + deferMap: ReadonlyMap | undefined, + ): PromiseOrValue>> { + const results = Object.create(null); + const graphqlWrappedResult: GraphQLWrappedResult> = { + rawResult: results, + incrementalDataRecords: undefined, + }; + let containsPromise = false; - if (result !== undefined) { - if (isPromise(result)) { - results[responseName] = result.then((resolved) => { - addIncrementalDataRecords( + try { + for (const [responseName, fieldDetailsList] of groupedFieldSet) { + const fieldPath = addPath(path, responseName, parentType.name); + const result = this.executeField( + parentType, + sourceValue, + fieldDetailsList, + fieldPath, + incrementalContext, + deferMap, + ); + + if (result !== undefined) { + if (isPromise(result)) { + results[responseName] = result.then((resolved) => { + this.addIncrementalDataRecords( + graphqlWrappedResult, + resolved.incrementalDataRecords, + ); + return resolved.rawResult; + }); + containsPromise = true; + } else { + results[responseName] = result.rawResult; + this.addIncrementalDataRecords( graphqlWrappedResult, - resolved.incrementalDataRecords, + result.incrementalDataRecords, ); - return resolved.rawResult; - }); - containsPromise = true; - } else { - results[responseName] = result.rawResult; - addIncrementalDataRecords( - graphqlWrappedResult, - result.incrementalDataRecords, - ); + } } } + } catch (error) { + if (containsPromise) { + // Ensure that any promises returned by other fields are handled, as they may also reject. + return promiseForObject(results, () => { + /* noop */ + }).finally(() => { + throw error; + }) as never; + } + throw error; } - } catch (error) { - if (containsPromise) { - // Ensure that any promises returned by other fields are handled, as they may also reject. - return promiseForObject(results, () => { - /* noop */ - }).finally(() => { - throw error; - }) as never; + + // If there are no promises, we can just return the object and any incrementalDataRecords + if (!containsPromise) { + return graphqlWrappedResult; } - throw error; - } - // If there are no promises, we can just return the object and any incrementalDataRecords - if (!containsPromise) { - return graphqlWrappedResult; + // Otherwise, results is a map from field name to the result of resolving that + // field, which is possibly a promise. Return a promise that will return this + // same map, but with any promises replaced with the values they resolved to. + return promiseForObject(results, (resolved) => ({ + rawResult: resolved, + incrementalDataRecords: graphqlWrappedResult.incrementalDataRecords, + })); } - // Otherwise, results is a map from field name to the result of resolving that - // field, which is possibly a promise. Return a promise that will return this - // same map, but with any promises replaced with the values they resolved to. - return promiseForObject(results, (resolved) => ({ - rawResult: resolved, - incrementalDataRecords: graphqlWrappedResult.incrementalDataRecords, - })); -} - -function toNodes(fieldDetailsList: FieldDetailsList): ReadonlyArray { - return fieldDetailsList.map((fieldDetails) => fieldDetails.node); -} - -/** - * Implements the "Executing fields" section of the spec - * In particular, this function figures out the value that the field returns by - * calling its resolve function, then calls completeValue to complete promises, - * coercing scalars, or execute the sub-selection-set for objects. - */ -function executeField( - exeContext: ExecutionContext, - parentType: GraphQLObjectType, - source: unknown, - fieldDetailsList: FieldDetailsList, - path: Path, - incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, -): PromiseOrValue> | undefined { - const { validatedExecutionArgs, abortSignalListener } = exeContext; - const { schema, contextValue, variableValues, hideSuggestions, abortSignal } = - validatedExecutionArgs; - const fieldName = fieldDetailsList[0].node.name.value; - const fieldDef = schema.getField(parentType, fieldName); - if (!fieldDef) { - return; + toNodes(fieldDetailsList: FieldDetailsList): ReadonlyArray { + return fieldDetailsList.map((fieldDetails) => fieldDetails.node); } - const returnType = fieldDef.type; - const resolveFn = fieldDef.resolve ?? validatedExecutionArgs.fieldResolver; - - const info = buildResolveInfo( - validatedExecutionArgs, - fieldDef, - toNodes(fieldDetailsList), - parentType, - path, - ); - - // Get the resolve function, regardless of if its result is normal or abrupt (error). - try { - // Build a JS object of arguments from the field.arguments AST, using the - // variables scope to fulfill any variable references. - // TODO: find a way to memoize, in case this field is within a List type. - const args = experimentalGetArgumentValues( - fieldDetailsList[0].node, - fieldDef.args, + /** + * Implements the "Executing fields" section of the spec + * In particular, this function figures out the value that the field returns by + * calling its resolve function, then calls completeValue to complete promises, + * coercing scalars, or execute the sub-selection-set for objects. + */ + executeField( + parentType: GraphQLObjectType, + source: unknown, + fieldDetailsList: FieldDetailsList, + path: Path, + incrementalContext: IncrementalContext | undefined, + deferMap: ReadonlyMap | undefined, + ): PromiseOrValue> | undefined { + const { + schema, + contextValue, variableValues, - fieldDetailsList[0].fragmentVariableValues, hideSuggestions, + abortSignal, + } = this.validatedExecutionArgs; + const fieldName = fieldDetailsList[0].node.name.value; + const fieldDef = schema.getField(parentType, fieldName); + if (!fieldDef) { + return; + } + + const returnType = fieldDef.type; + const resolveFn = + fieldDef.resolve ?? this.validatedExecutionArgs.fieldResolver; + + const info = buildResolveInfo( + this.validatedExecutionArgs, + fieldDef, + this.toNodes(fieldDetailsList), + parentType, + path, ); - // 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(source, args, contextValue, info, abortSignal); + // Get the resolve function, regardless of if its result is normal or abrupt (error). + try { + // Build a JS object of arguments from the field.arguments AST, using the + // variables scope to fulfill any variable references. + // TODO: find a way to memoize, in case this field is within a List type. + const args = experimentalGetArgumentValues( + fieldDetailsList[0].node, + fieldDef.args, + variableValues, + fieldDetailsList[0].fragmentVariableValues, + hideSuggestions, + ); + + // 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(source, args, contextValue, info, abortSignal); - if (isPromise(result)) { - return completePromisedValue( - exeContext, + if (isPromise(result)) { + return this.completePromisedValue( + returnType, + fieldDetailsList, + info, + path, + this.exeContext.abortSignalListener + ? cancellablePromise(result, this.exeContext.abortSignalListener) + : result, + incrementalContext, + deferMap, + ); + } + + const completed = this.completeValue( returnType, fieldDetailsList, info, path, - abortSignalListener - ? cancellablePromise(result, abortSignalListener) - : result, + result, incrementalContext, deferMap, ); + + if (isPromise(completed)) { + // Note: we don't rely on a `catch` method, but we do expect "thenable" + // to take a second callback for the error case. + return completed.then(undefined, (rawError: unknown) => { + this.handleFieldError( + rawError, + returnType, + fieldDetailsList, + path, + incrementalContext, + ); + return { rawResult: null, incrementalDataRecords: undefined }; + }); + } + return completed; + } catch (rawError) { + this.handleFieldError( + rawError, + returnType, + fieldDetailsList, + path, + incrementalContext, + ); + return { rawResult: null, incrementalDataRecords: undefined }; } + } - const completed = completeValue( - exeContext, - returnType, - fieldDetailsList, - info, - path, - result, - incrementalContext, - deferMap, + handleFieldError( + rawError: unknown, + returnType: GraphQLOutputType, + fieldDetailsList: FieldDetailsList, + path: Path, + incrementalContext: IncrementalContext | undefined, + ): void { + const error = locatedError( + rawError, + this.toNodes(fieldDetailsList), + pathToArray(path), ); - if (isPromise(completed)) { - // Note: we don't rely on a `catch` method, but we do expect "thenable" - // to take a second callback for the error case. - return completed.then(undefined, (rawError: unknown) => { - handleFieldError( - rawError, - exeContext, - returnType, - fieldDetailsList, - path, - incrementalContext, - ); - return { rawResult: null, incrementalDataRecords: undefined }; - }); + // If the field type is non-nullable, then it is resolved without any + // protection from errors, however it still properly locates the error. + if (isNonNullType(returnType)) { + throw error; } - return completed; - } catch (rawError) { - handleFieldError( - rawError, - exeContext, - returnType, - fieldDetailsList, - path, - incrementalContext, - ); - return { rawResult: null, incrementalDataRecords: undefined }; - } -} -/** - * TODO: consider no longer exporting this function - * @internal - */ -export function buildResolveInfo( - validatedExecutionArgs: ValidatedExecutionArgs, - fieldDef: GraphQLField, - fieldNodes: ReadonlyArray, - parentType: GraphQLObjectType, - path: Path, -): GraphQLResolveInfo { - const { schema, fragmentDefinitions, rootValue, operation, variableValues } = - validatedExecutionArgs; - // The resolve function's optional fourth argument is a collection of - // information about the current execution state. - return { - fieldName: fieldDef.name, - fieldNodes, - returnType: fieldDef.type, - parentType, - path, - schema, - fragments: fragmentDefinitions, - rootValue, - operation, - variableValues, - }; -} - -function handleFieldError( - rawError: unknown, - exeContext: ExecutionContext, - returnType: GraphQLOutputType, - fieldDetailsList: FieldDetailsList, - path: Path, - incrementalContext: IncrementalContext | undefined, -): void { - const error = locatedError( - rawError, - toNodes(fieldDetailsList), - pathToArray(path), - ); - - // If the field type is non-nullable, then it is resolved without any - // protection from errors, however it still properly locates the error. - if (isNonNullType(returnType)) { - throw error; + // Otherwise, error protection is applied, logging the error and resolving + // a null value for this field if one is encountered. + const context = incrementalContext ?? this.exeContext; + let errors = context.errors; + if (errors === undefined) { + errors = []; + context.errors = errors; + } + errors.push(error); } - // Otherwise, error protection is applied, logging the error and resolving - // a null value for this field if one is encountered. - const context = incrementalContext ?? exeContext; - let errors = context.errors; - if (errors === undefined) { - errors = []; - context.errors = errors; - } - errors.push(error); -} + /** + * Implements the instructions for completeValue as defined in the + * "Value Completion" section of the spec. + * + * If the field type is Non-Null, then this recursively completes the value + * for the inner type. It throws a field error if that completion returns null, + * as per the "Nullability" section of the spec. + * + * If the field type is a List, then this recursively completes the value + * for the inner type on each item in the list. + * + * If the field type is a Scalar or Enum, ensures the completed value is a legal + * value of the type by calling the `coerceOutputValue` method of GraphQL type + * definition. + * + * If the field is an abstract type, determine the runtime type of the value + * and then complete based on that type + * + * Otherwise, the field type expects a sub-selection set, and will complete the + * value by executing all sub-selections. + */ + completeValue( + returnType: GraphQLOutputType, + fieldDetailsList: FieldDetailsList, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + incrementalContext: IncrementalContext | undefined, + deferMap: ReadonlyMap | undefined, + ): PromiseOrValue> { + // If result is an Error, throw a located error. + if (result instanceof Error) { + throw result; + } -/** - * Implements the instructions for completeValue as defined in the - * "Value Completion" section of the spec. - * - * If the field type is Non-Null, then this recursively completes the value - * for the inner type. It throws a field error if that completion returns null, - * as per the "Nullability" section of the spec. - * - * If the field type is a List, then this recursively completes the value - * for the inner type on each item in the list. - * - * If the field type is a Scalar or Enum, ensures the completed value is a legal - * value of the type by calling the `coerceOutputValue` method of GraphQL type - * definition. - * - * If the field is an abstract type, determine the runtime type of the value - * and then complete based on that type - * - * Otherwise, the field type expects a sub-selection set, and will complete the - * value by executing all sub-selections. - */ -function completeValue( - exeContext: ExecutionContext, - returnType: GraphQLOutputType, - fieldDetailsList: FieldDetailsList, - info: GraphQLResolveInfo, - path: Path, - result: unknown, - incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, -): PromiseOrValue> { - // If result is an Error, throw a located error. - if (result instanceof Error) { - throw result; - } + // If field type is NonNull, complete for inner type, and throw field error + // if result is null. + if (isNonNullType(returnType)) { + const completed = this.completeValue( + returnType.ofType, + fieldDetailsList, + info, + path, + result, + incrementalContext, + deferMap, + ); + if ((completed as GraphQLWrappedResult).rawResult === null) { + throw new Error( + `Cannot return null for non-nullable field ${info.parentType}.${info.fieldName}.`, + ); + } + return completed; + } - // If field type is NonNull, complete for inner type, and throw field error - // if result is null. - if (isNonNullType(returnType)) { - const completed = completeValue( - exeContext, - returnType.ofType, - fieldDetailsList, - info, - path, - result, - incrementalContext, - deferMap, - ); - if ((completed as GraphQLWrappedResult).rawResult === null) { - throw new Error( - `Cannot return null for non-nullable field ${info.parentType}.${info.fieldName}.`, + // If result value is null or undefined then return null. + if (result == null) { + return { rawResult: null, incrementalDataRecords: undefined }; + } + + // If field type is List, complete each item in the list with the inner type + if (isListType(returnType)) { + return this.completeListValue( + returnType, + fieldDetailsList, + info, + path, + result, + incrementalContext, + deferMap, ); } - return completed; - } - // If result value is null or undefined then return null. - if (result == null) { - return { rawResult: null, incrementalDataRecords: undefined }; - } + // If field type is a leaf type, Scalar or Enum, coerce to a valid value, + // returning null if coercion is not possible. + if (isLeafType(returnType)) { + return { + rawResult: this.completeLeafValue(returnType, result), + incrementalDataRecords: undefined, + }; + } - // If field type is List, complete each item in the list with the inner type - if (isListType(returnType)) { - return completeListValue( - exeContext, - returnType, - fieldDetailsList, - info, - path, - result, - incrementalContext, - deferMap, + // If field type is an abstract type, Interface or Union, determine the + // runtime Object type and complete for that type. + if (isAbstractType(returnType)) { + return this.completeAbstractValue( + returnType, + fieldDetailsList, + info, + path, + result, + incrementalContext, + deferMap, + ); + } + + // If field type is Object, execute and complete all sub-selections. + if (isObjectType(returnType)) { + return this.completeObjectValue( + returnType, + fieldDetailsList, + info, + path, + result, + incrementalContext, + deferMap, + ); + } + /* c8 ignore next 6 */ + // Not reachable, all possible output types have been considered. + invariant( + false, + 'Cannot complete value of unexpected output type: ' + inspect(returnType), ); } - // If field type is a leaf type, Scalar or Enum, coerce to a valid value, - // returning null if coercion is not possible. - if (isLeafType(returnType)) { - return { - rawResult: completeLeafValue(returnType, result), - incrementalDataRecords: undefined, - }; + async completePromisedValue( + returnType: GraphQLOutputType, + fieldDetailsList: FieldDetailsList, + info: GraphQLResolveInfo, + path: Path, + result: Promise, + incrementalContext: IncrementalContext | undefined, + deferMap: ReadonlyMap | undefined, + ): Promise> { + try { + const resolved = await result; + let completed = this.completeValue( + returnType, + fieldDetailsList, + info, + path, + resolved, + incrementalContext, + deferMap, + ); + + if (isPromise(completed)) { + completed = await completed; + } + + return completed; + } catch (rawError) { + this.handleFieldError( + rawError, + returnType, + fieldDetailsList, + path, + incrementalContext, + ); + return { rawResult: null, incrementalDataRecords: undefined }; + } } - // If field type is an abstract type, Interface or Union, determine the - // runtime Object type and complete for that type. - if (isAbstractType(returnType)) { - return completeAbstractValue( - exeContext, - returnType, - fieldDetailsList, - info, - path, - result, - incrementalContext, - deferMap, + /** + * Returns an object containing info for streaming if a field should be + * streamed based on the experimental flag, stream directive present and + * not disabled by the "if" argument. + */ + getStreamUsage( + fieldDetailsList: FieldDetailsList, + path: Path, + ): StreamUsage | undefined { + // do not stream inner lists of multi-dimensional lists + if (typeof path.key === 'number') { + return; + } + + // TODO: add test for this case (a streamed list nested under a list). + /* c8 ignore next 7 */ + if ( + (fieldDetailsList as unknown as { _streamUsage: StreamUsage }) + ._streamUsage !== undefined + ) { + return (fieldDetailsList as unknown as { _streamUsage: StreamUsage }) + ._streamUsage; + } + + const { operation, variableValues } = this.validatedExecutionArgs; + // validation only allows equivalent streams on multiple fields, so it is + // safe to only check the first fieldNode for the stream directive + const stream = getDirectiveValues( + GraphQLStreamDirective, + fieldDetailsList[0].node, + variableValues, + fieldDetailsList[0].fragmentVariableValues, ); - } - // If field type is Object, execute and complete all sub-selections. - if (isObjectType(returnType)) { - return completeObjectValue( - exeContext, - returnType, - fieldDetailsList, - info, - path, - result, - incrementalContext, - deferMap, + if (!stream) { + return; + } + + if (stream.if === false) { + return; + } + + invariant( + typeof stream.initialCount === 'number', + 'initialCount must be a number', ); - } - /* c8 ignore next 6 */ - // Not reachable, all possible output types have been considered. - invariant( - false, - 'Cannot complete value of unexpected output type: ' + inspect(returnType), - ); -} -async function completePromisedValue( - exeContext: ExecutionContext, - returnType: GraphQLOutputType, - fieldDetailsList: FieldDetailsList, - info: GraphQLResolveInfo, - path: Path, - result: Promise, - incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, -): Promise> { - try { - const resolved = await result; - let completed = completeValue( - exeContext, - returnType, - fieldDetailsList, - info, - path, - resolved, - incrementalContext, - deferMap, + invariant( + stream.initialCount >= 0, + 'initialCount must be a positive integer', ); - if (isPromise(completed)) { - completed = await completed; - } + invariant( + operation.operation !== OperationTypeNode.SUBSCRIPTION, + '`@stream` directive not supported on subscription operations. Disable `@stream` by setting the `if` argument to `false`.', + ); - return completed; - } catch (rawError) { - handleFieldError( - rawError, - exeContext, - returnType, - fieldDetailsList, - path, - incrementalContext, + const streamedFieldDetailsList: FieldDetailsList = fieldDetailsList.map( + (fieldDetails) => ({ + node: fieldDetails.node, + deferUsage: undefined, + fragmentVariableValues: fieldDetails.fragmentVariableValues, + }), ); - return { rawResult: null, incrementalDataRecords: undefined }; - } -} -/** - * Returns an object containing info for streaming if a field should be - * streamed based on the experimental flag, stream directive present and - * not disabled by the "if" argument. - */ -function getStreamUsage( - validatedExecutionArgs: ValidatedExecutionArgs, - fieldDetailsList: FieldDetailsList, - path: Path, -): StreamUsage | undefined { - // do not stream inner lists of multi-dimensional lists - if (typeof path.key === 'number') { - return; - } + const streamUsage = { + initialCount: stream.initialCount, + label: typeof stream.label === 'string' ? stream.label : undefined, + fieldDetailsList: streamedFieldDetailsList, + }; - // TODO: add test for this case (a streamed list nested under a list). - /* c8 ignore next 7 */ - if ( - (fieldDetailsList as unknown as { _streamUsage: StreamUsage }) - ._streamUsage !== undefined - ) { - return (fieldDetailsList as unknown as { _streamUsage: StreamUsage }) - ._streamUsage; + ( + fieldDetailsList as unknown as { _streamUsage: StreamUsage } + )._streamUsage = streamUsage; + + return streamUsage; } - const { operation, variableValues } = validatedExecutionArgs; - // validation only allows equivalent streams on multiple fields, so it is - // safe to only check the first fieldNode for the stream directive - const stream = getDirectiveValues( - GraphQLStreamDirective, - fieldDetailsList[0].node, - variableValues, - fieldDetailsList[0].fragmentVariableValues, - ); - - if (!stream) { - return; + /** + * Complete a async iterator value by completing the result and calling + * recursively until all the results are completed. + */ + async completeAsyncIteratorValue( + itemType: GraphQLOutputType, + fieldDetailsList: FieldDetailsList, + info: GraphQLResolveInfo, + path: Path, + asyncIterator: AsyncIterator, + incrementalContext: IncrementalContext | undefined, + deferMap: ReadonlyMap | undefined, + ): Promise>> { + let containsPromise = false; + const completedResults: Array = []; + const graphqlWrappedResult: GraphQLWrappedResult> = { + rawResult: completedResults, + incrementalDataRecords: undefined, + }; + let index = 0; + const streamUsage = this.getStreamUsage(fieldDetailsList, path); + const earlyReturn = + asyncIterator.return === undefined + ? undefined + : asyncIterator.return.bind(asyncIterator); + try { + // eslint-disable-next-line no-constant-condition + while (true) { + if (streamUsage && index >= streamUsage.initialCount) { + const streamItemQueue = this.buildAsyncStreamItemQueue( + index, + path, + asyncIterator, + streamUsage.fieldDetailsList, + info, + itemType, + ); + + let streamRecord: StreamRecord | CancellableStreamRecord; + if (earlyReturn === undefined) { + streamRecord = { + label: streamUsage.label, + path, + streamItemQueue, + }; + } else { + streamRecord = { + label: streamUsage.label, + path, + earlyReturn, + streamItemQueue, + }; + if (this.exeContext.cancellableStreams === undefined) { + this.exeContext.cancellableStreams = new Set(); + } + this.exeContext.cancellableStreams.add(streamRecord); + } + + this.addIncrementalDataRecords(graphqlWrappedResult, [streamRecord]); + break; + } + + const itemPath = addPath(path, index, undefined); + let iteration; + try { + // eslint-disable-next-line no-await-in-loop + iteration = await asyncIterator.next(); + } catch (rawError) { + throw locatedError( + rawError, + this.toNodes(fieldDetailsList), + pathToArray(path), + ); + } + + // TODO: add test case for stream returning done before initialCount + /* c8 ignore next 3 */ + if (iteration.done) { + break; + } + + const item = iteration.value; + // TODO: add tests for stream backed by asyncIterator that returns a promise + /* c8 ignore start */ + if (isPromise(item)) { + completedResults.push( + this.completePromisedListItemValue( + item, + graphqlWrappedResult, + itemType, + fieldDetailsList, + info, + itemPath, + incrementalContext, + deferMap, + ), + ); + containsPromise = true; + } else if ( + /* c8 ignore stop */ + this.completeListItemValue( + item, + completedResults, + graphqlWrappedResult, + itemType, + fieldDetailsList, + info, + itemPath, + incrementalContext, + deferMap, + ) + // TODO: add tests for stream backed by asyncIterator that completes to a promise + /* c8 ignore start */ + ) { + containsPromise = true; + } + /* c8 ignore stop */ + index++; + } + } catch (error) { + if (earlyReturn !== undefined) { + earlyReturn().catch(() => { + /* c8 ignore next 1 */ + // ignore error + }); + } + throw error; + } + + return containsPromise + ? /* c8 ignore start */ Promise.all(completedResults).then( + (resolved) => ({ + rawResult: resolved, + incrementalDataRecords: graphqlWrappedResult.incrementalDataRecords, + }), + ) + : /* c8 ignore stop */ graphqlWrappedResult; } - if (stream.if === false) { - return; + /** + * Complete a list value by completing each item in the list with the + * inner type + */ + completeListValue( + returnType: GraphQLList, + fieldDetailsList: FieldDetailsList, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + incrementalContext: IncrementalContext | undefined, + deferMap: ReadonlyMap | undefined, + ): PromiseOrValue>> { + const itemType = returnType.ofType; + + if (isAsyncIterable(result)) { + const abortSignalListener = this.exeContext.abortSignalListener; + const maybeCancellableIterable = abortSignalListener + ? cancellableIterable(result, abortSignalListener) + : result; + const asyncIterator = maybeCancellableIterable[Symbol.asyncIterator](); + + return this.completeAsyncIteratorValue( + itemType, + fieldDetailsList, + info, + path, + asyncIterator, + incrementalContext, + deferMap, + ); + } + + if (!isIterableObject(result)) { + throw new GraphQLError( + `Expected Iterable, but did not find one for field "${info.parentType}.${info.fieldName}".`, + ); + } + + return this.completeIterableValue( + itemType, + fieldDetailsList, + info, + path, + result, + incrementalContext, + deferMap, + ); } - invariant( - typeof stream.initialCount === 'number', - 'initialCount must be a number', - ); - - invariant( - stream.initialCount >= 0, - 'initialCount must be a positive integer', - ); - - invariant( - operation.operation !== OperationTypeNode.SUBSCRIPTION, - '`@stream` directive not supported on subscription operations. Disable `@stream` by setting the `if` argument to `false`.', - ); - - const streamedFieldDetailsList: FieldDetailsList = fieldDetailsList.map( - (fieldDetails) => ({ - node: fieldDetails.node, - deferUsage: undefined, - fragmentVariableValues: fieldDetails.fragmentVariableValues, - }), - ); - - const streamUsage = { - initialCount: stream.initialCount, - label: typeof stream.label === 'string' ? stream.label : undefined, - fieldDetailsList: streamedFieldDetailsList, - }; - - (fieldDetailsList as unknown as { _streamUsage: StreamUsage })._streamUsage = - streamUsage; - - return streamUsage; -} + completeIterableValue( + itemType: GraphQLOutputType, + fieldDetailsList: FieldDetailsList, + info: GraphQLResolveInfo, + path: Path, + items: Iterable, + incrementalContext: IncrementalContext | undefined, + deferMap: ReadonlyMap | undefined, + ): PromiseOrValue>> { + // This is specified as a simple map, however we're optimizing the path + // where the list contains no Promises by avoiding creating another Promise. + let containsPromise = false; + const completedResults: Array = []; + const graphqlWrappedResult: GraphQLWrappedResult> = { + rawResult: completedResults, + incrementalDataRecords: undefined, + }; + let index = 0; + const streamUsage = this.getStreamUsage(fieldDetailsList, path); + const iterator = items[Symbol.iterator](); + let iteration = iterator.next(); + while (!iteration.done) { + const item = iteration.value; -/** - * Complete a async iterator value by completing the result and calling - * recursively until all the results are completed. - */ -async function completeAsyncIteratorValue( - exeContext: ExecutionContext, - itemType: GraphQLOutputType, - fieldDetailsList: FieldDetailsList, - info: GraphQLResolveInfo, - path: Path, - asyncIterator: AsyncIterator, - incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, -): Promise>> { - let containsPromise = false; - const completedResults: Array = []; - const graphqlWrappedResult: GraphQLWrappedResult> = { - rawResult: completedResults, - incrementalDataRecords: undefined, - }; - let index = 0; - const streamUsage = getStreamUsage( - exeContext.validatedExecutionArgs, - fieldDetailsList, - path, - ); - const earlyReturn = - asyncIterator.return === undefined - ? undefined - : asyncIterator.return.bind(asyncIterator); - try { - // eslint-disable-next-line no-constant-condition - while (true) { if (streamUsage && index >= streamUsage.initialCount) { - const streamItemQueue = buildAsyncStreamItemQueue( - index, + const syncStreamRecord: StreamRecord = { + label: streamUsage.label, path, - asyncIterator, - exeContext, - streamUsage.fieldDetailsList, - info, - itemType, - ); - - let streamRecord: StreamRecord | CancellableStreamRecord; - if (earlyReturn === undefined) { - streamRecord = { - label: streamUsage.label, - path, - streamItemQueue, - }; - } else { - streamRecord = { - label: streamUsage.label, + streamItemQueue: this.buildSyncStreamItemQueue( + item, + index, path, - earlyReturn, - streamItemQueue, - }; - if (exeContext.cancellableStreams === undefined) { - exeContext.cancellableStreams = new Set(); - } - exeContext.cancellableStreams.add(streamRecord); - } + iterator, + streamUsage.fieldDetailsList, + info, + itemType, + ), + }; - addIncrementalDataRecords(graphqlWrappedResult, [streamRecord]); + this.addIncrementalDataRecords(graphqlWrappedResult, [ + syncStreamRecord, + ]); break; } + // No need to modify the info object containing the path, + // since from here on it is not ever accessed by resolver functions. const itemPath = addPath(path, index, undefined); - let iteration; - try { - // eslint-disable-next-line no-await-in-loop - iteration = await asyncIterator.next(); - } catch (rawError) { - throw locatedError( - rawError, - toNodes(fieldDetailsList), - pathToArray(path), - ); - } - // TODO: add test case for stream returning done before initialCount - /* c8 ignore next 3 */ - if (iteration.done) { - break; - } - - const item = iteration.value; - // TODO: add tests for stream backed by asyncIterator that returns a promise - /* c8 ignore start */ if (isPromise(item)) { completedResults.push( - completePromisedListItemValue( + this.completePromisedListItemValue( item, graphqlWrappedResult, - exeContext, itemType, fieldDetailsList, info, @@ -1070,12 +1183,10 @@ async function completeAsyncIteratorValue( ); containsPromise = true; } else if ( - /* c8 ignore stop */ - completeListItemValue( + this.completeListItemValue( item, completedResults, graphqlWrappedResult, - exeContext, itemType, fieldDetailsList, info, @@ -1083,1091 +1194,923 @@ async function completeAsyncIteratorValue( incrementalContext, deferMap, ) - // TODO: add tests for stream backed by asyncIterator that completes to a promise - /* c8 ignore start */ ) { containsPromise = true; } - /* c8 ignore stop */ index++; - } - } catch (error) { - if (earlyReturn !== undefined) { - earlyReturn().catch(() => { - /* c8 ignore next 1 */ - // ignore error - }); - } - throw error; - } - - return containsPromise - ? /* c8 ignore start */ Promise.all(completedResults).then((resolved) => ({ - rawResult: resolved, - incrementalDataRecords: graphqlWrappedResult.incrementalDataRecords, - })) - : /* c8 ignore stop */ graphqlWrappedResult; -} - -/** - * Complete a list value by completing each item in the list with the - * inner type - */ -function completeListValue( - exeContext: ExecutionContext, - returnType: GraphQLList, - fieldDetailsList: FieldDetailsList, - info: GraphQLResolveInfo, - path: Path, - result: unknown, - incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, -): PromiseOrValue>> { - const itemType = returnType.ofType; - - if (isAsyncIterable(result)) { - const abortSignalListener = exeContext.abortSignalListener; - const maybeCancellableIterable = abortSignalListener - ? cancellableIterable(result, abortSignalListener) - : result; - const asyncIterator = maybeCancellableIterable[Symbol.asyncIterator](); - - return completeAsyncIteratorValue( - exeContext, - itemType, - fieldDetailsList, - info, - path, - asyncIterator, - incrementalContext, - deferMap, - ); - } - - if (!isIterableObject(result)) { - throw new GraphQLError( - `Expected Iterable, but did not find one for field "${info.parentType}.${info.fieldName}".`, - ); - } - - return completeIterableValue( - exeContext, - itemType, - fieldDetailsList, - info, - path, - result, - incrementalContext, - deferMap, - ); -} - -function completeIterableValue( - exeContext: ExecutionContext, - itemType: GraphQLOutputType, - fieldDetailsList: FieldDetailsList, - info: GraphQLResolveInfo, - path: Path, - items: Iterable, - incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, -): PromiseOrValue>> { - // This is specified as a simple map, however we're optimizing the path - // where the list contains no Promises by avoiding creating another Promise. - let containsPromise = false; - const completedResults: Array = []; - const graphqlWrappedResult: GraphQLWrappedResult> = { - rawResult: completedResults, - incrementalDataRecords: undefined, - }; - let index = 0; - const streamUsage = getStreamUsage( - exeContext.validatedExecutionArgs, - fieldDetailsList, - path, - ); - const iterator = items[Symbol.iterator](); - let iteration = iterator.next(); - while (!iteration.done) { - const item = iteration.value; - - if (streamUsage && index >= streamUsage.initialCount) { - const syncStreamRecord: StreamRecord = { - label: streamUsage.label, - path, - streamItemQueue: buildSyncStreamItemQueue( - item, - index, - path, - iterator, - exeContext, - streamUsage.fieldDetailsList, - info, - itemType, - ), - }; - addIncrementalDataRecords(graphqlWrappedResult, [syncStreamRecord]); - break; + iteration = iterator.next(); } - // No need to modify the info object containing the path, - // since from here on it is not ever accessed by resolver functions. - const itemPath = addPath(path, index, undefined); + return containsPromise + ? Promise.all(completedResults).then((resolved) => ({ + rawResult: resolved, + incrementalDataRecords: graphqlWrappedResult.incrementalDataRecords, + })) + : graphqlWrappedResult; + } - if (isPromise(item)) { - completedResults.push( - completePromisedListItemValue( - item, - graphqlWrappedResult, - exeContext, - itemType, - fieldDetailsList, - info, - itemPath, - incrementalContext, - deferMap, - ), - ); - containsPromise = true; - } else if ( - completeListItemValue( - item, - completedResults, - graphqlWrappedResult, - exeContext, + /** + * Complete a list item value by adding it to the completed results. + * + * Returns true if the value is a Promise. + */ + completeListItemValue( + item: unknown, + completedResults: Array, + parent: GraphQLWrappedResult>, + itemType: GraphQLOutputType, + fieldDetailsList: FieldDetailsList, + info: GraphQLResolveInfo, + itemPath: Path, + incrementalContext: IncrementalContext | undefined, + deferMap: ReadonlyMap | undefined, + ): boolean { + try { + const completedItem = this.completeValue( itemType, fieldDetailsList, info, itemPath, + item, incrementalContext, deferMap, - ) - ) { - containsPromise = true; - } - index++; - - iteration = iterator.next(); - } - - return containsPromise - ? Promise.all(completedResults).then((resolved) => ({ - rawResult: resolved, - incrementalDataRecords: graphqlWrappedResult.incrementalDataRecords, - })) - : graphqlWrappedResult; -} + ); -/** - * Complete a list item value by adding it to the completed results. - * - * Returns true if the value is a Promise. - */ -function completeListItemValue( - item: unknown, - completedResults: Array, - parent: GraphQLWrappedResult>, - exeContext: ExecutionContext, - itemType: GraphQLOutputType, - fieldDetailsList: FieldDetailsList, - info: GraphQLResolveInfo, - itemPath: Path, - incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, -): boolean { - try { - const completedItem = completeValue( - exeContext, - itemType, - fieldDetailsList, - info, - itemPath, - item, - incrementalContext, - deferMap, - ); + if (isPromise(completedItem)) { + // Note: we don't rely on a `catch` method, but we do expect "thenable" + // to take a second callback for the error case. + completedResults.push( + completedItem.then( + (resolved) => { + this.addIncrementalDataRecords( + parent, + resolved.incrementalDataRecords, + ); + return resolved.rawResult; + }, + (rawError: unknown) => { + this.handleFieldError( + rawError, + itemType, + fieldDetailsList, + itemPath, + incrementalContext, + ); + return null; + }, + ), + ); + return true; + } - if (isPromise(completedItem)) { - // Note: we don't rely on a `catch` method, but we do expect "thenable" - // to take a second callback for the error case. - completedResults.push( - completedItem.then( - (resolved) => { - addIncrementalDataRecords(parent, resolved.incrementalDataRecords); - return resolved.rawResult; - }, - (rawError: unknown) => { - handleFieldError( - rawError, - exeContext, - itemType, - fieldDetailsList, - itemPath, - incrementalContext, - ); - return null; - }, - ), + completedResults.push(completedItem.rawResult); + this.addIncrementalDataRecords( + parent, + completedItem.incrementalDataRecords, + ); + } catch (rawError) { + this.handleFieldError( + rawError, + itemType, + fieldDetailsList, + itemPath, + incrementalContext, ); - return true; + completedResults.push(null); } - - completedResults.push(completedItem.rawResult); - addIncrementalDataRecords(parent, completedItem.incrementalDataRecords); - } catch (rawError) { - handleFieldError( - rawError, - exeContext, - itemType, - fieldDetailsList, - itemPath, - incrementalContext, - ); - completedResults.push(null); + return false; } - return false; -} -async function completePromisedListItemValue( - item: Promise, - parent: GraphQLWrappedResult>, - exeContext: ExecutionContext, - itemType: GraphQLOutputType, - fieldDetailsList: FieldDetailsList, - info: GraphQLResolveInfo, - itemPath: Path, - incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, -): Promise { - try { - const abortSignalListener = exeContext.abortSignalListener; - const maybeCancellableItem = abortSignalListener - ? cancellablePromise(item, abortSignalListener) - : item; - const resolved = await maybeCancellableItem; - let completed = completeValue( - exeContext, - itemType, - fieldDetailsList, - info, - itemPath, - resolved, - incrementalContext, - deferMap, - ); - if (isPromise(completed)) { - completed = await completed; + async completePromisedListItemValue( + item: Promise, + parent: GraphQLWrappedResult>, + itemType: GraphQLOutputType, + fieldDetailsList: FieldDetailsList, + info: GraphQLResolveInfo, + itemPath: Path, + incrementalContext: IncrementalContext | undefined, + deferMap: ReadonlyMap | undefined, + ): Promise { + try { + const abortSignalListener = this.exeContext.abortSignalListener; + const maybeCancellableItem = abortSignalListener + ? cancellablePromise(item, abortSignalListener) + : item; + const resolved = await maybeCancellableItem; + let completed = this.completeValue( + itemType, + fieldDetailsList, + info, + itemPath, + resolved, + incrementalContext, + deferMap, + ); + if (isPromise(completed)) { + completed = await completed; + } + this.addIncrementalDataRecords(parent, completed.incrementalDataRecords); + return completed.rawResult; + } catch (rawError) { + this.handleFieldError( + rawError, + itemType, + fieldDetailsList, + itemPath, + incrementalContext, + ); + return null; } - addIncrementalDataRecords(parent, completed.incrementalDataRecords); - return completed.rawResult; - } catch (rawError) { - handleFieldError( - rawError, - exeContext, - itemType, - fieldDetailsList, - itemPath, - incrementalContext, - ); - return null; } -} -/** - * Complete a Scalar or Enum by serializing to a valid value, returning - * null if serialization is not possible. - */ -function completeLeafValue( - returnType: GraphQLLeafType, - result: unknown, -): unknown { - const coerced = returnType.coerceOutputValue(result); - if (coerced == null) { - throw new Error( - `Expected \`${inspect(returnType)}.coerceOutputValue(${inspect(result)})\` to ` + - `return non-nullable value, returned: ${inspect(coerced)}`, - ); + /** + * Complete a Scalar or Enum by serializing to a valid value, returning + * null if serialization is not possible. + */ + completeLeafValue(returnType: GraphQLLeafType, result: unknown): unknown { + const coerced = returnType.coerceOutputValue(result); + if (coerced == null) { + throw new Error( + `Expected \`${inspect(returnType)}.coerceOutputValue(${inspect(result)})\` to ` + + `return non-nullable value, returned: ${inspect(coerced)}`, + ); + } + return coerced; } - return coerced; -} -/** - * Complete a value of an abstract type by determining the runtime object type - * of that value, then complete the value for that type. - */ -function completeAbstractValue( - exeContext: ExecutionContext, - returnType: GraphQLAbstractType, - fieldDetailsList: FieldDetailsList, - info: GraphQLResolveInfo, - path: Path, - result: unknown, - incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, -): PromiseOrValue>> { - const validatedExecutionArgs = exeContext.validatedExecutionArgs; - const { schema, contextValue } = validatedExecutionArgs; - const resolveTypeFn = - returnType.resolveType ?? validatedExecutionArgs.typeResolver; - const runtimeType = resolveTypeFn(result, contextValue, info, returnType); - - if (isPromise(runtimeType)) { - return runtimeType.then((resolvedRuntimeType) => - completeObjectValue( - exeContext, - ensureValidRuntimeType( - resolvedRuntimeType, - schema, - returnType, + /** + * Complete a value of an abstract type by determining the runtime object type + * of that value, then complete the value for that type. + */ + completeAbstractValue( + returnType: GraphQLAbstractType, + fieldDetailsList: FieldDetailsList, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + incrementalContext: IncrementalContext | undefined, + deferMap: ReadonlyMap | undefined, + ): PromiseOrValue>> { + const { schema, contextValue } = this.validatedExecutionArgs; + const resolveTypeFn = + returnType.resolveType ?? this.validatedExecutionArgs.typeResolver; + const runtimeType = resolveTypeFn(result, contextValue, info, returnType); + + if (isPromise(runtimeType)) { + return runtimeType.then((resolvedRuntimeType) => + this.completeObjectValue( + this.ensureValidRuntimeType( + resolvedRuntimeType, + schema, + returnType, + fieldDetailsList, + info, + result, + ), fieldDetailsList, info, + path, result, + incrementalContext, + deferMap, ), + ); + } + + return this.completeObjectValue( + this.ensureValidRuntimeType( + runtimeType, + schema, + returnType, fieldDetailsList, info, - path, result, - incrementalContext, - deferMap, ), - ); - } - - return completeObjectValue( - exeContext, - ensureValidRuntimeType( - runtimeType, - schema, - returnType, fieldDetailsList, info, + path, result, - ), - fieldDetailsList, - info, - path, - result, - incrementalContext, - deferMap, - ); -} - -function ensureValidRuntimeType( - runtimeTypeName: unknown, - schema: GraphQLSchema, - returnType: GraphQLAbstractType, - fieldDetailsList: FieldDetailsList, - info: GraphQLResolveInfo, - result: unknown, -): GraphQLObjectType { - if (runtimeTypeName == null) { - throw new GraphQLError( - `Abstract type "${returnType}" must resolve to an Object type at runtime for field "${info.parentType}.${info.fieldName}". Either the "${returnType}" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`, - { nodes: toNodes(fieldDetailsList) }, + incrementalContext, + deferMap, ); } - if (typeof runtimeTypeName !== 'string') { - throw new GraphQLError( - `Abstract type "${returnType}" must resolve to an Object type at runtime for field "${info.parentType}.${info.fieldName}" with ` + - `value ${inspect(result)}, received "${inspect( - runtimeTypeName, - )}", which is not a valid Object type name.`, - ); - } + ensureValidRuntimeType( + runtimeTypeName: unknown, + schema: GraphQLSchema, + returnType: GraphQLAbstractType, + fieldDetailsList: FieldDetailsList, + info: GraphQLResolveInfo, + result: unknown, + ): GraphQLObjectType { + if (runtimeTypeName == null) { + throw new GraphQLError( + `Abstract type "${returnType}" must resolve to an Object type at runtime for field "${info.parentType}.${info.fieldName}". Either the "${returnType}" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`, + { nodes: this.toNodes(fieldDetailsList) }, + ); + } - const runtimeType = schema.getType(runtimeTypeName); - if (runtimeType == null) { - throw new GraphQLError( - `Abstract type "${returnType}" was resolved to a type "${runtimeTypeName}" that does not exist inside the schema.`, - { nodes: toNodes(fieldDetailsList) }, - ); - } + if (typeof runtimeTypeName !== 'string') { + throw new GraphQLError( + `Abstract type "${returnType}" must resolve to an Object type at runtime for field "${info.parentType}.${info.fieldName}" with ` + + `value ${inspect(result)}, received "${inspect( + runtimeTypeName, + )}", which is not a valid Object type name.`, + ); + } - if (!isObjectType(runtimeType)) { - throw new GraphQLError( - `Abstract type "${returnType}" was resolved to a non-object type "${runtimeTypeName}".`, - { nodes: toNodes(fieldDetailsList) }, - ); - } + const runtimeType = schema.getType(runtimeTypeName); + if (runtimeType == null) { + throw new GraphQLError( + `Abstract type "${returnType}" was resolved to a type "${runtimeTypeName}" that does not exist inside the schema.`, + { nodes: this.toNodes(fieldDetailsList) }, + ); + } - if (!schema.isSubType(returnType, runtimeType)) { - throw new GraphQLError( - `Runtime Object type "${runtimeType}" is not a possible type for "${returnType}".`, - { nodes: toNodes(fieldDetailsList) }, - ); - } + if (!isObjectType(runtimeType)) { + throw new GraphQLError( + `Abstract type "${returnType}" was resolved to a non-object type "${runtimeTypeName}".`, + { nodes: this.toNodes(fieldDetailsList) }, + ); + } - return runtimeType; -} + if (!schema.isSubType(returnType, runtimeType)) { + throw new GraphQLError( + `Runtime Object type "${runtimeType}" is not a possible type for "${returnType}".`, + { nodes: this.toNodes(fieldDetailsList) }, + ); + } -/** - * Complete an Object value by executing all sub-selections. - */ -function completeObjectValue( - exeContext: ExecutionContext, - returnType: GraphQLObjectType, - fieldDetailsList: FieldDetailsList, - info: GraphQLResolveInfo, - path: Path, - result: unknown, - incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, -): PromiseOrValue>> { - if ((incrementalContext ?? exeContext).completed) { - throw new Error('Completed, aborting.'); + return runtimeType; } - // If there is an isTypeOf predicate function, call it with the - // current result. If isTypeOf returns false, then raise an error rather - // than continuing execution. - if (returnType.isTypeOf) { - const isTypeOf = returnType.isTypeOf( - result, - exeContext.validatedExecutionArgs.contextValue, - info, - ); - - if (isPromise(isTypeOf)) { - return isTypeOf.then((resolvedIsTypeOf) => { - if (!resolvedIsTypeOf) { - throw invalidReturnTypeError(returnType, result, fieldDetailsList); - } - return collectAndExecuteSubfields( - exeContext, - returnType, - fieldDetailsList, - path, - result, - incrementalContext, - deferMap, - ); - }); - } - - if (!isTypeOf) { - throw invalidReturnTypeError(returnType, result, fieldDetailsList); + /** + * Complete an Object value by executing all sub-selections. + */ + completeObjectValue( + returnType: GraphQLObjectType, + fieldDetailsList: FieldDetailsList, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + incrementalContext: IncrementalContext | undefined, + deferMap: ReadonlyMap | undefined, + ): PromiseOrValue>> { + if ((incrementalContext ?? this.exeContext).completed) { + throw new Error('Completed, aborting.'); } - } - return collectAndExecuteSubfields( - exeContext, - returnType, - fieldDetailsList, - path, - result, - incrementalContext, - deferMap, - ); -} + // If there is an isTypeOf predicate function, call it with the + // current result. If isTypeOf returns false, then raise an error rather + // than continuing execution. + if (returnType.isTypeOf) { + const isTypeOf = returnType.isTypeOf( + result, + this.validatedExecutionArgs.contextValue, + info, + ); -function invalidReturnTypeError( - returnType: GraphQLObjectType, - result: unknown, - fieldDetailsList: FieldDetailsList, -): GraphQLError { - return new GraphQLError( - `Expected value of type "${returnType}" but got: ${inspect(result)}.`, - { nodes: toNodes(fieldDetailsList) }, - ); -} + if (isPromise(isTypeOf)) { + return isTypeOf.then((resolvedIsTypeOf) => { + if (!resolvedIsTypeOf) { + throw this.invalidReturnTypeError( + returnType, + result, + fieldDetailsList, + ); + } + return this.collectAndExecuteSubfields( + returnType, + fieldDetailsList, + path, + result, + incrementalContext, + deferMap, + ); + }); + } -/** - * Instantiates new DeferredFragmentRecords for the given path within an - * incremental data record, returning an updated map of DeferUsage - * objects to DeferredFragmentRecords. - * - * Note: As defer directives may be used with operations returning lists, - * a DeferUsage object may correspond to many DeferredFragmentRecords. - */ -function getNewDeferMap( - newDeferUsages: ReadonlyArray, - deferMap?: ReadonlyMap | undefined, - path?: Path | undefined, -): ReadonlyMap { - const newDeferMap = new Map(deferMap); - - // For each new deferUsage object: - for (const newDeferUsage of newDeferUsages) { - const parentDeferUsage = newDeferUsage.parentDeferUsage; - - const parent = - parentDeferUsage === undefined - ? undefined - : deferredFragmentRecordFromDeferUsage(parentDeferUsage, newDeferMap); + if (!isTypeOf) { + throw this.invalidReturnTypeError(returnType, result, fieldDetailsList); + } + } - // Instantiate the new record. - const deferredFragmentRecord = new DeferredFragmentRecord( + return this.collectAndExecuteSubfields( + returnType, + fieldDetailsList, path, - newDeferUsage.label, - parent, + result, + incrementalContext, + deferMap, ); + } - // Update the map. - newDeferMap.set(newDeferUsage, deferredFragmentRecord); + invalidReturnTypeError( + returnType: GraphQLObjectType, + result: unknown, + fieldDetailsList: FieldDetailsList, + ): GraphQLError { + return new GraphQLError( + `Expected value of type "${returnType}" but got: ${inspect(result)}.`, + { nodes: this.toNodes(fieldDetailsList) }, + ); } - return newDeferMap; -} + /** + * Instantiates new DeferredFragmentRecords for the given path within an + * incremental data record, returning an updated map of DeferUsage + * objects to DeferredFragmentRecords. + * + * Note: As defer directives may be used with operations returning lists, + * a DeferUsage object may correspond to many DeferredFragmentRecords. + */ + getNewDeferMap( + newDeferUsages: ReadonlyArray, + deferMap?: ReadonlyMap | undefined, + path?: Path | undefined, + ): ReadonlyMap { + const newDeferMap = new Map(deferMap); + + // For each new deferUsage object: + for (const newDeferUsage of newDeferUsages) { + const parentDeferUsage = newDeferUsage.parentDeferUsage; + + const parent = + parentDeferUsage === undefined + ? undefined + : this.deferredFragmentRecordFromDeferUsage( + parentDeferUsage, + newDeferMap, + ); -function deferredFragmentRecordFromDeferUsage( - deferUsage: DeferUsage, - deferMap: ReadonlyMap, -): DeferredFragmentRecord { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return deferMap.get(deferUsage)!; -} + // Instantiate the new record. + const deferredFragmentRecord = new DeferredFragmentRecord( + path, + newDeferUsage.label, + parent, + ); -function collectAndExecuteSubfields( - exeContext: ExecutionContext, - returnType: GraphQLObjectType, - fieldDetailsList: FieldDetailsList, - path: Path, - result: unknown, - incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, -): PromiseOrValue>> { - const validatedExecutionArgs = exeContext.validatedExecutionArgs; - - // Collect sub-fields to execute to complete this value. - const collectedSubfields = collectSubfields( - validatedExecutionArgs, - returnType, - fieldDetailsList, - ); - const { groupedFieldSet, newDeferUsages } = collectedSubfields; - - if (newDeferUsages.length > 0) { - invariant( - validatedExecutionArgs.operation.operation !== - OperationTypeNode.SUBSCRIPTION, - '`@defer` directive not supported on subscription operations. Disable `@defer` by setting the `if` argument to `false`.', + // Update the map. + newDeferMap.set(newDeferUsage, deferredFragmentRecord); + } + + return newDeferMap; + } + + deferredFragmentRecordFromDeferUsage( + deferUsage: DeferUsage, + deferMap: ReadonlyMap, + ): DeferredFragmentRecord { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return deferMap.get(deferUsage)!; + } + + collectAndExecuteSubfields( + returnType: GraphQLObjectType, + fieldDetailsList: FieldDetailsList, + path: Path, + result: unknown, + incrementalContext: IncrementalContext | undefined, + deferMap: ReadonlyMap | undefined, + ): PromiseOrValue>> { + // Collect sub-fields to execute to complete this value. + const collectedSubfields = collectSubfields( + this.validatedExecutionArgs, + returnType, + fieldDetailsList, ); - } + const { groupedFieldSet, newDeferUsages } = collectedSubfields; - return executeSubExecutionPlan( - exeContext, - returnType, - result, - groupedFieldSet, - newDeferUsages, - path, - incrementalContext, - deferMap, - ); -} + if (newDeferUsages.length > 0) { + invariant( + this.validatedExecutionArgs.operation.operation !== + OperationTypeNode.SUBSCRIPTION, + '`@defer` directive not supported on subscription operations. Disable `@defer` by setting the `if` argument to `false`.', + ); + } -function executeSubExecutionPlan( - exeContext: ExecutionContext, - returnType: GraphQLObjectType, - sourceValue: unknown, - originalGroupedFieldSet: GroupedFieldSet, - newDeferUsages: ReadonlyArray, - path?: Path | undefined, - incrementalContext?: IncrementalContext | undefined, - deferMap?: ReadonlyMap | undefined, -): PromiseOrValue>> { - if (deferMap === undefined && newDeferUsages.length === 0) { - return executeFields( - exeContext, + return this.executeSubExecutionPlan( returnType, - sourceValue, + result, + groupedFieldSet, + newDeferUsages, path, - originalGroupedFieldSet, incrementalContext, deferMap, ); } - const newDeferMap = getNewDeferMap(newDeferUsages, deferMap, path); - - const { groupedFieldSet, newGroupedFieldSets } = buildSubExecutionPlan( - originalGroupedFieldSet, - incrementalContext?.deferUsageSet, - ); - - const graphqlWrappedResult = executeFields( - exeContext, - returnType, - sourceValue, - path, - groupedFieldSet, - incrementalContext, - newDeferMap, - ); - - if (newGroupedFieldSets.size > 0) { - const newPendingExecutionGroups = collectExecutionGroups( - exeContext, + executeSubExecutionPlan( + returnType: GraphQLObjectType, + sourceValue: unknown, + originalGroupedFieldSet: GroupedFieldSet, + newDeferUsages: ReadonlyArray, + path?: Path | undefined, + incrementalContext?: IncrementalContext | undefined, + deferMap?: ReadonlyMap | undefined, + ): PromiseOrValue>> { + if (deferMap === undefined && newDeferUsages.length === 0) { + return this.executeFields( + returnType, + sourceValue, + path, + originalGroupedFieldSet, + incrementalContext, + deferMap, + ); + } + + const newDeferMap = this.getNewDeferMap(newDeferUsages, deferMap, path); + + const { groupedFieldSet, newGroupedFieldSets } = this.buildSubExecutionPlan( + originalGroupedFieldSet, + incrementalContext?.deferUsageSet, + ); + + const graphqlWrappedResult = this.executeFields( returnType, sourceValue, path, - incrementalContext?.deferUsageSet, - newGroupedFieldSets, + groupedFieldSet, + incrementalContext, newDeferMap, ); - return withNewExecutionGroups( - graphqlWrappedResult, - newPendingExecutionGroups, - ); + if (newGroupedFieldSets.size > 0) { + const newPendingExecutionGroups = this.collectExecutionGroups( + returnType, + sourceValue, + path, + incrementalContext?.deferUsageSet, + newGroupedFieldSets, + newDeferMap, + ); + + return this.withNewExecutionGroups( + graphqlWrappedResult, + newPendingExecutionGroups, + ); + } + return graphqlWrappedResult; } - return graphqlWrappedResult; -} -function buildSubExecutionPlan( - originalGroupedFieldSet: GroupedFieldSet, - deferUsageSet: DeferUsageSet | undefined, -): ExecutionPlan { - let executionPlan = ( - originalGroupedFieldSet as unknown as { _executionPlan: ExecutionPlan } - )._executionPlan; - if (executionPlan !== undefined) { + buildSubExecutionPlan( + originalGroupedFieldSet: GroupedFieldSet, + deferUsageSet: DeferUsageSet | undefined, + ): ExecutionPlan { + let executionPlan = ( + originalGroupedFieldSet as unknown as { _executionPlan: ExecutionPlan } + )._executionPlan; + if (executionPlan !== undefined) { + return executionPlan; + } + executionPlan = buildExecutionPlan(originalGroupedFieldSet, deferUsageSet); + ( + originalGroupedFieldSet as unknown as { _executionPlan: ExecutionPlan } + )._executionPlan = executionPlan; return executionPlan; } - executionPlan = buildExecutionPlan(originalGroupedFieldSet, deferUsageSet); - ( - originalGroupedFieldSet as unknown as { _executionPlan: ExecutionPlan } - )._executionPlan = executionPlan; - return executionPlan; -} -function collectExecutionGroups( - exeContext: ExecutionContext, - parentType: GraphQLObjectType, - sourceValue: unknown, - path: Path | undefined, - parentDeferUsages: DeferUsageSet | undefined, - newGroupedFieldSets: Map, - deferMap: ReadonlyMap, -): ReadonlyArray { - const newPendingExecutionGroups: Array = []; - - for (const [deferUsageSet, groupedFieldSet] of newGroupedFieldSets) { - const deferredFragmentRecords = getDeferredFragmentRecords( - deferUsageSet, - deferMap, - ); + collectExecutionGroups( + parentType: GraphQLObjectType, + sourceValue: unknown, + path: Path | undefined, + parentDeferUsages: DeferUsageSet | undefined, + newGroupedFieldSets: Map, + deferMap: ReadonlyMap, + ): ReadonlyArray { + const newPendingExecutionGroups: Array = []; + + for (const [deferUsageSet, groupedFieldSet] of newGroupedFieldSets) { + const deferredFragmentRecords = this.getDeferredFragmentRecords( + deferUsageSet, + deferMap, + ); - const pendingExecutionGroup: PendingExecutionGroup = { - deferredFragmentRecords, - result: - undefined as unknown as BoxedPromiseOrValue, - }; + const pendingExecutionGroup: PendingExecutionGroup = { + deferredFragmentRecords, + result: + undefined as unknown as BoxedPromiseOrValue, + }; - const executor = () => - executeExecutionGroup( - pendingExecutionGroup, - exeContext, + const executor = () => + this.executeExecutionGroup( + pendingExecutionGroup, + parentType, + sourceValue, + path, + groupedFieldSet, + { + errors: undefined, + completed: false, + deferUsageSet, + }, + deferMap, + ); + + if (this.validatedExecutionArgs.enableEarlyExecution) { + pendingExecutionGroup.result = new BoxedPromiseOrValue( + this.shouldDefer(parentDeferUsages, deferUsageSet) + ? Promise.resolve().then(executor) + : executor(), + ); + } else { + pendingExecutionGroup.result = () => + new BoxedPromiseOrValue(executor()); + } + + newPendingExecutionGroups.push(pendingExecutionGroup); + } + + return newPendingExecutionGroups; + } + + shouldDefer( + parentDeferUsages: undefined | DeferUsageSet, + deferUsages: DeferUsageSet, + ): boolean { + // If we have a new child defer usage, defer. + // Otherwise, this defer usage was already deferred when it was initially + // encountered, and is now in the midst of executing early, so the new + // deferred grouped fields set can be executed immediately. + return ( + parentDeferUsages === undefined || + !Array.from(deferUsages).every((deferUsage) => + parentDeferUsages.has(deferUsage), + ) + ); + } + + executeExecutionGroup( + pendingExecutionGroup: PendingExecutionGroup, + parentType: GraphQLObjectType, + sourceValue: unknown, + path: Path | undefined, + groupedFieldSet: GroupedFieldSet, + incrementalContext: IncrementalContext, + deferMap: ReadonlyMap, + ): PromiseOrValue { + let result; + try { + result = this.executeFields( parentType, sourceValue, path, groupedFieldSet, - { - errors: undefined, - completed: false, - deferUsageSet, - }, + incrementalContext, deferMap, ); + } catch (error) { + incrementalContext.completed = true; + return { + pendingExecutionGroup, + path: pathToArray(path), + errors: this.withError(incrementalContext.errors, error), + }; + } - if (exeContext.validatedExecutionArgs.enableEarlyExecution) { - pendingExecutionGroup.result = new BoxedPromiseOrValue( - shouldDefer(parentDeferUsages, deferUsageSet) - ? Promise.resolve().then(executor) - : executor(), + if (isPromise(result)) { + return result.then( + (resolved) => { + incrementalContext.completed = true; + return this.buildCompletedExecutionGroup( + incrementalContext.errors, + pendingExecutionGroup, + path, + resolved, + ); + }, + (error: unknown) => { + incrementalContext.completed = true; + return { + pendingExecutionGroup, + path: pathToArray(path), + errors: this.withError( + incrementalContext.errors, + error as GraphQLError, + ), + }; + }, ); - } else { - pendingExecutionGroup.result = () => new BoxedPromiseOrValue(executor()); } - newPendingExecutionGroups.push(pendingExecutionGroup); - } - - return newPendingExecutionGroups; -} - -function shouldDefer( - parentDeferUsages: undefined | DeferUsageSet, - deferUsages: DeferUsageSet, -): boolean { - // If we have a new child defer usage, defer. - // Otherwise, this defer usage was already deferred when it was initially - // encountered, and is now in the midst of executing early, so the new - // deferred grouped fields set can be executed immediately. - return ( - parentDeferUsages === undefined || - !Array.from(deferUsages).every((deferUsage) => - parentDeferUsages.has(deferUsage), - ) - ); -} - -function executeExecutionGroup( - pendingExecutionGroup: PendingExecutionGroup, - exeContext: ExecutionContext, - parentType: GraphQLObjectType, - sourceValue: unknown, - path: Path | undefined, - groupedFieldSet: GroupedFieldSet, - incrementalContext: IncrementalContext, - deferMap: ReadonlyMap, -): PromiseOrValue { - let result; - try { - result = executeFields( - exeContext, - parentType, - sourceValue, + incrementalContext.completed = true; + return this.buildCompletedExecutionGroup( + incrementalContext.errors, + pendingExecutionGroup, path, - groupedFieldSet, - incrementalContext, - deferMap, + result, ); - } catch (error) { - incrementalContext.completed = true; + } + + buildCompletedExecutionGroup( + errors: ReadonlyArray | undefined, + pendingExecutionGroup: PendingExecutionGroup, + path: Path | undefined, + result: GraphQLWrappedResult>, + ): CompletedExecutionGroup { + const { rawResult: data, incrementalDataRecords } = result; return { pendingExecutionGroup, path: pathToArray(path), - errors: withError(incrementalContext.errors, error), + result: errors === undefined ? { data } : { data, errors }, + incrementalDataRecords, }; } - if (isPromise(result)) { - return result.then( - (resolved) => { - incrementalContext.completed = true; - return buildCompletedExecutionGroup( - incrementalContext.errors, - pendingExecutionGroup, - path, - resolved, - ); - }, - (error: unknown) => { - incrementalContext.completed = true; - return { - pendingExecutionGroup, - path: pathToArray(path), - errors: withError(incrementalContext.errors, error as GraphQLError), - }; - }, + getDeferredFragmentRecords( + deferUsages: DeferUsageSet, + deferMap: ReadonlyMap, + ): ReadonlyArray { + return Array.from(deferUsages).map((deferUsage) => + this.deferredFragmentRecordFromDeferUsage(deferUsage, deferMap), ); } - incrementalContext.completed = true; - return buildCompletedExecutionGroup( - incrementalContext.errors, - pendingExecutionGroup, - path, - result, - ); -} - -function buildCompletedExecutionGroup( - errors: ReadonlyArray | undefined, - pendingExecutionGroup: PendingExecutionGroup, - path: Path | undefined, - result: GraphQLWrappedResult>, -): CompletedExecutionGroup { - const { rawResult: data, incrementalDataRecords } = result; - return { - pendingExecutionGroup, - path: pathToArray(path), - result: errors === undefined ? { data } : { data, errors }, - incrementalDataRecords, - }; -} - -function getDeferredFragmentRecords( - deferUsages: DeferUsageSet, - deferMap: ReadonlyMap, -): ReadonlyArray { - return Array.from(deferUsages).map((deferUsage) => - deferredFragmentRecordFromDeferUsage(deferUsage, deferMap), - ); -} - -function buildSyncStreamItemQueue( - initialItem: PromiseOrValue, - initialIndex: number, - streamPath: Path, - iterator: Iterator, - exeContext: ExecutionContext, - fieldDetailsList: FieldDetailsList, - info: GraphQLResolveInfo, - itemType: GraphQLOutputType, -): Array { - const streamItemQueue: Array = []; - - const enableEarlyExecution = - exeContext.validatedExecutionArgs.enableEarlyExecution; - - const firstExecutor = () => { - const initialPath = addPath(streamPath, initialIndex, undefined); - const firstStreamItem = new BoxedPromiseOrValue( - completeStreamItem( - initialPath, - initialItem, - exeContext, - { errors: undefined, completed: false }, - fieldDetailsList, - info, - itemType, - ), - ); - - let iteration = iterator.next(); - let currentIndex = initialIndex + 1; - let currentStreamItem: - | BoxedPromiseOrValue - | (() => BoxedPromiseOrValue) = firstStreamItem; - while (!iteration.done) { - // TODO: add test case for early sync termination - /* c8 ignore next 6 */ - if (currentStreamItem instanceof BoxedPromiseOrValue) { - const result = currentStreamItem.value; - if (!isPromise(result) && result.errors !== undefined) { - break; - } - } - - const itemPath = addPath(streamPath, currentIndex, undefined); - - const value = iteration.value; - - const currentExecutor = () => - completeStreamItem( - itemPath, - value, - exeContext, + buildSyncStreamItemQueue( + initialItem: PromiseOrValue, + initialIndex: number, + streamPath: Path, + iterator: Iterator, + fieldDetailsList: FieldDetailsList, + info: GraphQLResolveInfo, + itemType: GraphQLOutputType, + ): Array { + const streamItemQueue: Array = []; + + const enableEarlyExecution = + this.validatedExecutionArgs.enableEarlyExecution; + + const firstExecutor = () => { + const initialPath = addPath(streamPath, initialIndex, undefined); + const firstStreamItem = new BoxedPromiseOrValue( + this.completeStreamItem( + initialPath, + initialItem, { errors: undefined, completed: false }, fieldDetailsList, info, itemType, - ); + ), + ); - currentStreamItem = enableEarlyExecution - ? new BoxedPromiseOrValue(currentExecutor()) - : () => new BoxedPromiseOrValue(currentExecutor()); + let iteration = iterator.next(); + let currentIndex = initialIndex + 1; + let currentStreamItem: + | BoxedPromiseOrValue + | (() => BoxedPromiseOrValue) = firstStreamItem; + while (!iteration.done) { + // TODO: add test case for early sync termination + /* c8 ignore next 6 */ + if (currentStreamItem instanceof BoxedPromiseOrValue) { + const result = currentStreamItem.value; + if (!isPromise(result) && result.errors !== undefined) { + break; + } + } - streamItemQueue.push(currentStreamItem); + const itemPath = addPath(streamPath, currentIndex, undefined); - iteration = iterator.next(); - currentIndex = initialIndex + 1; - } + const value = iteration.value; - streamItemQueue.push(new BoxedPromiseOrValue({})); + const currentExecutor = () => + this.completeStreamItem( + itemPath, + value, + { errors: undefined, completed: false }, + fieldDetailsList, + info, + itemType, + ); - return firstStreamItem.value; - }; + currentStreamItem = enableEarlyExecution + ? new BoxedPromiseOrValue(currentExecutor()) + : () => new BoxedPromiseOrValue(currentExecutor()); - streamItemQueue.push( - enableEarlyExecution - ? new BoxedPromiseOrValue(Promise.resolve().then(firstExecutor)) - : () => new BoxedPromiseOrValue(firstExecutor()), - ); + streamItemQueue.push(currentStreamItem); - return streamItemQueue; -} + iteration = iterator.next(); + currentIndex = initialIndex + 1; + } -function buildAsyncStreamItemQueue( - initialIndex: number, - streamPath: Path, - asyncIterator: AsyncIterator, - exeContext: ExecutionContext, - fieldDetailsList: FieldDetailsList, - info: GraphQLResolveInfo, - itemType: GraphQLOutputType, -): Array { - const streamItemQueue: Array = []; - const executor = () => - getNextAsyncStreamItemResult( - streamItemQueue, - streamPath, - initialIndex, - asyncIterator, - exeContext, - fieldDetailsList, - info, - itemType, - ); + streamItemQueue.push(new BoxedPromiseOrValue({})); - streamItemQueue.push( - exeContext.validatedExecutionArgs.enableEarlyExecution - ? new BoxedPromiseOrValue(executor()) - : () => new BoxedPromiseOrValue(executor()), - ); + return firstStreamItem.value; + }; - return streamItemQueue; -} + streamItemQueue.push( + enableEarlyExecution + ? new BoxedPromiseOrValue(Promise.resolve().then(firstExecutor)) + : () => new BoxedPromiseOrValue(firstExecutor()), + ); -async function getNextAsyncStreamItemResult( - streamItemQueue: Array, - streamPath: Path, - index: number, - asyncIterator: AsyncIterator, - exeContext: ExecutionContext, - fieldDetailsList: FieldDetailsList, - info: GraphQLResolveInfo, - itemType: GraphQLOutputType, -): Promise { - let iteration; - try { - iteration = await asyncIterator.next(); - } catch (error) { - return { - errors: [ - locatedError(error, toNodes(fieldDetailsList), pathToArray(streamPath)), - ], - }; + return streamItemQueue; } - if (iteration.done) { - return {}; + buildAsyncStreamItemQueue( + initialIndex: number, + streamPath: Path, + asyncIterator: AsyncIterator, + fieldDetailsList: FieldDetailsList, + info: GraphQLResolveInfo, + itemType: GraphQLOutputType, + ): Array { + const streamItemQueue: Array = []; + const executor = () => + this.getNextAsyncStreamItemResult( + streamItemQueue, + streamPath, + initialIndex, + asyncIterator, + fieldDetailsList, + info, + itemType, + ); + + streamItemQueue.push( + this.validatedExecutionArgs.enableEarlyExecution + ? new BoxedPromiseOrValue(executor()) + : () => new BoxedPromiseOrValue(executor()), + ); + + return streamItemQueue; } - const itemPath = addPath(streamPath, index, undefined); - - const result = completeStreamItem( - itemPath, - iteration.value, - exeContext, - { errors: undefined, completed: false }, - fieldDetailsList, - info, - itemType, - ); - - const executor = () => - getNextAsyncStreamItemResult( - streamItemQueue, - streamPath, - index + 1, - asyncIterator, - exeContext, + async getNextAsyncStreamItemResult( + streamItemQueue: Array, + streamPath: Path, + index: number, + asyncIterator: AsyncIterator, + fieldDetailsList: FieldDetailsList, + info: GraphQLResolveInfo, + itemType: GraphQLOutputType, + ): Promise { + let iteration; + try { + iteration = await asyncIterator.next(); + } catch (error) { + return { + errors: [ + locatedError( + error, + this.toNodes(fieldDetailsList), + pathToArray(streamPath), + ), + ], + }; + } + + if (iteration.done) { + return {}; + } + + const itemPath = addPath(streamPath, index, undefined); + + const result = this.completeStreamItem( + itemPath, + iteration.value, + { errors: undefined, completed: false }, fieldDetailsList, info, itemType, ); - streamItemQueue.push( - exeContext.validatedExecutionArgs.enableEarlyExecution - ? new BoxedPromiseOrValue(executor()) - : () => new BoxedPromiseOrValue(executor()), - ); - - return result; -} + const executor = () => + this.getNextAsyncStreamItemResult( + streamItemQueue, + streamPath, + index + 1, + asyncIterator, + fieldDetailsList, + info, + itemType, + ); -function completeStreamItem( - itemPath: Path, - item: unknown, - exeContext: ExecutionContext, - incrementalContext: IncrementalContext, - fieldDetailsList: FieldDetailsList, - info: GraphQLResolveInfo, - itemType: GraphQLOutputType, -): PromiseOrValue { - if (isPromise(item)) { - const abortSignalListener = exeContext.abortSignalListener; - const maybeCancellableItem = abortSignalListener - ? cancellablePromise(item, abortSignalListener) - : item; - return completePromisedValue( - exeContext, - itemType, - fieldDetailsList, - info, - itemPath, - maybeCancellableItem, - incrementalContext, - new Map(), - ).then( - (resolvedItem) => { - incrementalContext.completed = true; - return buildStreamItemResult(incrementalContext.errors, resolvedItem); - }, - (error: unknown) => { - incrementalContext.completed = true; - return { - errors: withError(incrementalContext.errors, error as GraphQLError), - }; - }, + streamItemQueue.push( + this.validatedExecutionArgs.enableEarlyExecution + ? new BoxedPromiseOrValue(executor()) + : () => new BoxedPromiseOrValue(executor()), ); + + return result; } - let result: PromiseOrValue>; - try { - try { - result = completeValue( - exeContext, + completeStreamItem( + itemPath: Path, + item: unknown, + incrementalContext: IncrementalContext, + fieldDetailsList: FieldDetailsList, + info: GraphQLResolveInfo, + itemType: GraphQLOutputType, + ): PromiseOrValue { + if (isPromise(item)) { + const abortSignalListener = this.exeContext.abortSignalListener; + const maybeCancellableItem = abortSignalListener + ? cancellablePromise(item, abortSignalListener) + : item; + return this.completePromisedValue( itemType, fieldDetailsList, info, itemPath, - item, + maybeCancellableItem, incrementalContext, new Map(), - ); - } catch (rawError) { - handleFieldError( - rawError, - exeContext, - itemType, - fieldDetailsList, - itemPath, - incrementalContext, - ); - result = { rawResult: null, incrementalDataRecords: undefined }; - } - } catch (error) { - incrementalContext.completed = true; - return { - errors: withError(incrementalContext.errors, error), - }; - } - - if (isPromise(result)) { - return result - .then(undefined, (rawError: unknown) => { - handleFieldError( - rawError, - exeContext, - itemType, - fieldDetailsList, - itemPath, - incrementalContext, - ); - return { rawResult: null, incrementalDataRecords: undefined }; - }) - .then( + ).then( (resolvedItem) => { incrementalContext.completed = true; - return buildStreamItemResult(incrementalContext.errors, resolvedItem); + return this.buildStreamItemResult( + incrementalContext.errors, + resolvedItem, + ); }, (error: unknown) => { incrementalContext.completed = true; return { - errors: withError(incrementalContext.errors, error as GraphQLError), + errors: this.withError( + incrementalContext.errors, + error as GraphQLError, + ), }; }, ); - } + } - incrementalContext.completed = true; - return buildStreamItemResult(incrementalContext.errors, result); -} + let result: PromiseOrValue>; + try { + try { + result = this.completeValue( + itemType, + fieldDetailsList, + info, + itemPath, + item, + incrementalContext, + new Map(), + ); + } catch (rawError) { + this.handleFieldError( + rawError, + itemType, + fieldDetailsList, + itemPath, + incrementalContext, + ); + result = { rawResult: null, incrementalDataRecords: undefined }; + } + } catch (error) { + incrementalContext.completed = true; + return { + errors: this.withError(incrementalContext.errors, error), + }; + } + + if (isPromise(result)) { + return result + .then(undefined, (rawError: unknown) => { + this.handleFieldError( + rawError, + itemType, + fieldDetailsList, + itemPath, + incrementalContext, + ); + return { rawResult: null, incrementalDataRecords: undefined }; + }) + .then( + (resolvedItem) => { + incrementalContext.completed = true; + return this.buildStreamItemResult( + incrementalContext.errors, + resolvedItem, + ); + }, + (error: unknown) => { + incrementalContext.completed = true; + return { + errors: this.withError( + incrementalContext.errors, + error as GraphQLError, + ), + }; + }, + ); + } + + incrementalContext.completed = true; + return this.buildStreamItemResult(incrementalContext.errors, result); + } -function buildStreamItemResult( - errors: ReadonlyArray | undefined, - result: GraphQLWrappedResult, -): StreamItemResult { - const { rawResult: item, incrementalDataRecords } = result; - return { - item, - errors, - incrementalDataRecords, - }; + buildStreamItemResult( + errors: ReadonlyArray | undefined, + result: GraphQLWrappedResult, + ): StreamItemResult { + const { rawResult: item, incrementalDataRecords } = result; + return { + item, + errors, + incrementalDataRecords, + }; + } } diff --git a/src/execution/buildResolveInfo.ts b/src/execution/buildResolveInfo.ts new file mode 100644 index 0000000000..5dcd6a46a4 --- /dev/null +++ b/src/execution/buildResolveInfo.ts @@ -0,0 +1,40 @@ +import type { Path } from '../jsutils/Path.js'; + +import type { FieldNode } from '../language/ast.js'; + +import type { + GraphQLField, + GraphQLObjectType, + GraphQLResolveInfo, +} from '../type/definition.js'; + +import type { ValidatedExecutionArgs } from './Executor.js'; + +/** + * TODO: consider no longer exporting this function + * @internal + */ +export function buildResolveInfo( + validatedExecutionArgs: ValidatedExecutionArgs, + fieldDef: GraphQLField, + fieldNodes: ReadonlyArray, + parentType: GraphQLObjectType, + path: Path, +): GraphQLResolveInfo { + const { schema, fragmentDefinitions, rootValue, operation, variableValues } = + validatedExecutionArgs; + // The resolve function's optional fourth argument is a collection of + // information about the current execution state. + return { + fieldName: fieldDef.name, + fieldNodes, + returnType: fieldDef.type, + parentType, + path, + schema, + fragments: fragmentDefinitions, + rootValue, + operation, + variableValues, + }; +} diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 8fde88f98d..9a423de503 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -29,13 +29,11 @@ import { cancellableIterable, cancellablePromise, } from './AbortSignalListener.js'; +import { buildResolveInfo } from './buildResolveInfo.js'; import type { FieldDetailsList, FragmentDetails } from './collectFields.js'; import { collectFields } from './collectFields.js'; import type { ValidatedExecutionArgs } from './Executor.js'; -import { - buildResolveInfo, - executeQueryOrMutationOrSubscriptionEventImpl, -} from './Executor.js'; +import { Executor } from './Executor.js'; import { getVariableSignature } from './getVariableSignature.js'; import { mapAsyncIterable } from './mapAsyncIterable.js'; import type { @@ -172,7 +170,8 @@ export function executeQueryOrMutationOrSubscriptionEvent( export function experimentalExecuteQueryOrMutationOrSubscriptionEvent( validatedExecutionArgs: ValidatedExecutionArgs, ): PromiseOrValue { - return executeQueryOrMutationOrSubscriptionEventImpl(validatedExecutionArgs); + const executor = new Executor(validatedExecutionArgs); + return executor.executeQueryOrMutationOrSubscriptionEvent(); } /** From 4612e99d4533b41bb0379e9cf0a53e9bec905180 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 17 Dec 2024 14:22:48 +0200 Subject: [PATCH 4/6] bring the memoized subfield collector into the class --- src/execution/Executor.ts | 62 +++++++++++++++++++-------------------- src/execution/execute.ts | 6 ---- src/jsutils/memoize2.ts | 28 ++++++++++++++++++ src/jsutils/memoize3.ts | 37 ----------------------- 4 files changed, 59 insertions(+), 74 deletions(-) create mode 100644 src/jsutils/memoize2.ts delete mode 100644 src/jsutils/memoize3.ts diff --git a/src/execution/Executor.ts b/src/execution/Executor.ts index 54b5ba6973..a2a693c6dd 100644 --- a/src/execution/Executor.ts +++ b/src/execution/Executor.ts @@ -4,7 +4,7 @@ import { invariant } from '../jsutils/invariant.js'; import { isAsyncIterable } from '../jsutils/isAsyncIterable.js'; import { isIterableObject } from '../jsutils/isIterableObject.js'; import { isPromise } from '../jsutils/isPromise.js'; -import { memoize3 } from '../jsutils/memoize3.js'; +import { memoize2 } from '../jsutils/memoize2.js'; import type { ObjMap } from '../jsutils/ObjMap.js'; import type { Path } from '../jsutils/Path.js'; import { addPath, pathToArray } from '../jsutils/Path.js'; @@ -56,10 +56,7 @@ import type { FragmentDetails, GroupedFieldSet, } from './collectFields.js'; -import { - collectFields, - collectSubfields as _collectSubfields, -} from './collectFields.js'; +import { collectFields, collectSubfields } from './collectFields.js'; import { buildIncrementalResponse } from './IncrementalPublisher.js'; import type { CancellableStreamRecord, @@ -80,30 +77,6 @@ import { experimentalGetArgumentValues, getDirectiveValues } from './values.js'; // This file contains a lot of such errors but we plan to refactor it anyway // so just disable it for entire file. -/** - * A memoized collection of relevant subfields with regard to the return - * type. Memoizing ensures the subfields are not repeatedly calculated, which - * saves overhead when resolving lists of values. - */ -const collectSubfields = memoize3( - ( - validatedExecutionArgs: ValidatedExecutionArgs, - returnType: GraphQLObjectType, - fieldDetailsList: FieldDetailsList, - ) => { - const { schema, fragments, variableValues, hideSuggestions } = - validatedExecutionArgs; - return _collectSubfields( - schema, - fragments, - variableValues, - returnType, - fieldDetailsList, - hideSuggestions, - ); - }, -); - /** * Terminology * @@ -181,6 +154,19 @@ export class Executor { validatedExecutionArgs: ValidatedExecutionArgs; exeContext: ExecutionContext; + /** + * A memoized collection of relevant subfields with regard to the return + * type. Memoizing ensures the subfields are not repeatedly calculated, which + * saves overhead when resolving lists of values. + */ + collectSubfields: ( + returnType: GraphQLObjectType, + fieldDetailsList: FieldDetailsList, + ) => { + groupedFieldSet: GroupedFieldSet; + newDeferUsages: ReadonlyArray; + }; + constructor(validatedExecutionArgs: ValidatedExecutionArgs) { this.validatedExecutionArgs = validatedExecutionArgs; const abortSignal = validatedExecutionArgs.abortSignal; @@ -192,6 +178,21 @@ export class Executor { completed: false, cancellableStreams: undefined, }; + + const { schema, fragments, variableValues, hideSuggestions } = + validatedExecutionArgs; + + this.collectSubfields = memoize2( + (returnType: GraphQLObjectType, fieldDetailsList: FieldDetailsList) => + collectSubfields( + schema, + fragments, + variableValues, + returnType, + fieldDetailsList, + hideSuggestions, + ), + ); } executeQueryOrMutationOrSubscriptionEvent(): PromiseOrValue< @@ -1575,8 +1576,7 @@ export class Executor { deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { // Collect sub-fields to execute to complete this value. - const collectedSubfields = collectSubfields( - this.validatedExecutionArgs, + const collectedSubfields = this.collectSubfields( returnType, fieldDetailsList, ); diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 9a423de503..46e125a688 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -191,12 +191,6 @@ export function executeSync(args: ExecutionArgs): ExecutionResult { } /** - * Constructs a ExecutionContext object from the arguments passed to - * execute, which we will pass throughout the other execution methods. - * - * Throws a GraphQLError if a valid execution context cannot be created. - * - * TODO: consider no longer exporting this function * @internal */ export function validateExecutionArgs( diff --git a/src/jsutils/memoize2.ts b/src/jsutils/memoize2.ts new file mode 100644 index 0000000000..d15d400159 --- /dev/null +++ b/src/jsutils/memoize2.ts @@ -0,0 +1,28 @@ +/** + * Memoizes the provided two-argument function. + */ +export function memoize2( + fn: (a1: A1, a2: A2) => R, +): (a1: A1, a2: A2) => R { + let cache0: WeakMap>; + + return function memoized(a1, a2) { + if (cache0 === undefined) { + cache0 = new WeakMap(); + } + + let cache1 = cache0.get(a1); + if (cache1 === undefined) { + cache1 = new WeakMap(); + cache0.set(a1, cache1); + } + + let fnResult = cache1.get(a2); + if (fnResult === undefined) { + fnResult = fn(a1, a2); + cache1.set(a2, fnResult); + } + + return fnResult; + }; +} diff --git a/src/jsutils/memoize3.ts b/src/jsutils/memoize3.ts deleted file mode 100644 index 213cb95d10..0000000000 --- a/src/jsutils/memoize3.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Memoizes the provided three-argument function. - */ -export function memoize3< - A1 extends object, - A2 extends object, - A3 extends object, - R, ->(fn: (a1: A1, a2: A2, a3: A3) => R): (a1: A1, a2: A2, a3: A3) => R { - let cache0: WeakMap>>; - - return function memoized(a1, a2, a3) { - if (cache0 === undefined) { - cache0 = new WeakMap(); - } - - let cache1 = cache0.get(a1); - if (cache1 === undefined) { - cache1 = new WeakMap(); - cache0.set(a1, cache1); - } - - let cache2 = cache1.get(a2); - if (cache2 === undefined) { - cache2 = new WeakMap(); - cache1.set(a2, cache2); - } - - let fnResult = cache2.get(a3); - if (fnResult === undefined) { - fnResult = fn(a1, a2, a3); - cache2.set(a3, fnResult); - } - - return fnResult; - }; -} From d6e6ac151d5f997caf8011273bd6c3d14c2d9be3 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Wed, 11 Dec 2024 22:16:22 +0200 Subject: [PATCH 5/6] extract PayloadPublisher --- src/execution/Executor.ts | 89 +++++++-- src/execution/IncrementalPublisher.ts | 260 +++++++++----------------- src/execution/PayloadPublisher.ts | 242 ++++++++++++++++++++++++ src/execution/execute.ts | 8 +- src/execution/types.ts | 6 +- 5 files changed, 414 insertions(+), 191 deletions(-) create mode 100644 src/execution/PayloadPublisher.ts diff --git a/src/execution/Executor.ts b/src/execution/Executor.ts index a2a693c6dd..767535f0e0 100644 --- a/src/execution/Executor.ts +++ b/src/execution/Executor.ts @@ -48,7 +48,6 @@ import { cancellablePromise, } from './AbortSignalListener.js'; import type { DeferUsageSet, ExecutionPlan } from './buildExecutionPlan.js'; -import { buildExecutionPlan } from './buildExecutionPlan.js'; import { buildResolveInfo } from './buildResolveInfo.js'; import type { DeferUsage, @@ -57,17 +56,20 @@ import type { GroupedFieldSet, } from './collectFields.js'; import { collectFields, collectSubfields } from './collectFields.js'; +import type { ExperimentalIncrementalExecutionResults } from './IncrementalPublisher.js'; import { buildIncrementalResponse } from './IncrementalPublisher.js'; +import type { PayloadPublisher } from './PayloadPublisher.js'; import type { CancellableStreamRecord, CompletedExecutionGroup, ExecutionResult, - ExperimentalIncrementalExecutionResults, IncrementalDataRecord, + InitialIncrementalExecutionResult, PendingExecutionGroup, StreamItemRecord, StreamItemResult, StreamRecord, + SubsequentIncrementalExecutionResult, } from './types.js'; import { DeferredFragmentRecord } from './types.js'; import type { VariableValues } from './values.js'; @@ -150,8 +152,22 @@ interface GraphQLWrappedResult { } /** @internal */ -export class Executor { +export class Executor< + TInitialPayload = InitialIncrementalExecutionResult, + TSubsequentPayload = SubsequentIncrementalExecutionResult, +> { validatedExecutionArgs: ValidatedExecutionArgs; + + buildExecutionPlan: ( + originalGroupedFieldSet: GroupedFieldSet, + parentDeferUsages?: DeferUsageSet | undefined, + ) => ExecutionPlan; + + getPayloadPublisher: () => PayloadPublisher< + TInitialPayload, + TSubsequentPayload + >; + exeContext: ExecutionContext; /** @@ -167,8 +183,21 @@ export class Executor { newDeferUsages: ReadonlyArray; }; - constructor(validatedExecutionArgs: ValidatedExecutionArgs) { + constructor( + validatedExecutionArgs: ValidatedExecutionArgs, + buildExecutionPlan: ( + originalGroupedFieldSet: GroupedFieldSet, + parentDeferUsages?: DeferUsageSet | undefined, + ) => ExecutionPlan, + getPayloadPublisher: () => PayloadPublisher< + TInitialPayload, + TSubsequentPayload + >, + ) { this.validatedExecutionArgs = validatedExecutionArgs; + this.buildExecutionPlan = buildExecutionPlan; + this.getPayloadPublisher = getPayloadPublisher; + const abortSignal = validatedExecutionArgs.abortSignal; this.exeContext = { errors: undefined, @@ -196,7 +225,11 @@ export class Executor { } executeQueryOrMutationOrSubscriptionEvent(): PromiseOrValue< - ExecutionResult | ExperimentalIncrementalExecutionResults + | ExecutionResult + | ExperimentalIncrementalExecutionResults< + TInitialPayload, + TSubsequentPayload + > > { try { const { @@ -277,7 +310,12 @@ export class Executor { buildDataResponse( graphqlWrappedResult: GraphQLWrappedResult>, - ): ExecutionResult | ExperimentalIncrementalExecutionResults { + ): + | ExecutionResult + | ExperimentalIncrementalExecutionResults< + TInitialPayload, + TSubsequentPayload + > { const { rawResult: data, incrementalDataRecords } = graphqlWrappedResult; const errors = this.exeContext.errors; if (incrementalDataRecords === undefined) { @@ -285,11 +323,23 @@ export class Executor { return errors !== undefined ? { errors, data } : { data }; } + return this.buildIncrementalResponse(data, errors, incrementalDataRecords); + } + + buildIncrementalResponse( + data: ObjMap, + errors: ReadonlyArray | undefined, + incrementalDataRecords: ReadonlyArray, + ): ExperimentalIncrementalExecutionResults< + TInitialPayload, + TSubsequentPayload + > { return buildIncrementalResponse( this.exeContext, data, errors, incrementalDataRecords, + this.getPayloadPublisher(), ); } @@ -315,7 +365,7 @@ export class Executor { undefined, ); - const { groupedFieldSet, newGroupedFieldSets } = buildExecutionPlan( + const { groupedFieldSet, newGroupedFieldSets } = this.buildExecutionPlan( originalGroupedFieldSet, ); @@ -1582,13 +1632,7 @@ export class Executor { ); const { groupedFieldSet, newDeferUsages } = collectedSubfields; - if (newDeferUsages.length > 0) { - invariant( - this.validatedExecutionArgs.operation.operation !== - OperationTypeNode.SUBSCRIPTION, - '`@defer` directive not supported on subscription operations. Disable `@defer` by setting the `if` argument to `false`.', - ); - } + this.assertValidOperationTypeForDefer(newDeferUsages); return this.executeSubExecutionPlan( returnType, @@ -1601,6 +1645,18 @@ export class Executor { ); } + assertValidOperationTypeForDefer( + newDeferUsages: ReadonlyArray, + ): void { + if (newDeferUsages.length > 0) { + invariant( + this.validatedExecutionArgs.operation.operation !== + OperationTypeNode.SUBSCRIPTION, + '`@defer` directive not supported on subscription operations. Disable `@defer` by setting the `if` argument to `false`.', + ); + } + } + executeSubExecutionPlan( returnType: GraphQLObjectType, sourceValue: unknown, @@ -1665,7 +1721,10 @@ export class Executor { if (executionPlan !== undefined) { return executionPlan; } - executionPlan = buildExecutionPlan(originalGroupedFieldSet, deferUsageSet); + executionPlan = this.buildExecutionPlan( + originalGroupedFieldSet, + deferUsageSet, + ); ( originalGroupedFieldSet as unknown as { _executionPlan: ExecutionPlan } )._executionPlan = executionPlan; diff --git a/src/execution/IncrementalPublisher.ts b/src/execution/IncrementalPublisher.ts index b826a9b9c5..d2b90f7b33 100644 --- a/src/execution/IncrementalPublisher.ts +++ b/src/execution/IncrementalPublisher.ts @@ -1,27 +1,20 @@ import { invariant } from '../jsutils/invariant.js'; import type { ObjMap } from '../jsutils/ObjMap.js'; -import { pathToArray } from '../jsutils/Path.js'; import type { GraphQLError } from '../error/GraphQLError.js'; import type { AbortSignalListener } from './AbortSignalListener.js'; import { IncrementalGraph } from './IncrementalGraph.js'; +import type { + PayloadPublisher, + SubsequentPayloadPublisher, +} from './PayloadPublisher.js'; import type { CancellableStreamRecord, CompletedExecutionGroup, - CompletedResult, - DeferredFragmentRecord, - DeliveryGroup, - ExperimentalIncrementalExecutionResults, IncrementalDataRecord, IncrementalDataRecordResult, - IncrementalDeferResult, - IncrementalResult, - IncrementalStreamResult, - InitialIncrementalExecutionResult, - PendingResult, StreamItemsResult, - SubsequentIncrementalExecutionResult, } from './types.js'; import { isCancellableStreamRecord, @@ -29,13 +22,28 @@ import { isFailedExecutionGroup, } from './types.js'; -export function buildIncrementalResponse( +export interface ExperimentalIncrementalExecutionResults< + TInitialPayload, + TSubsequentPayload, +> { + initialResult: TInitialPayload; + subsequentResults: AsyncGenerator; +} + +export function buildIncrementalResponse( context: IncrementalPublisherContext, result: ObjMap, errors: ReadonlyArray | undefined, incrementalDataRecords: ReadonlyArray, -): ExperimentalIncrementalExecutionResults { - const incrementalPublisher = new IncrementalPublisher(context); + payloadPublisher: PayloadPublisher, +): ExperimentalIncrementalExecutionResults< + TInitialPayload, + TSubsequentPayload +> { + const incrementalPublisher = new IncrementalPublisher< + TInitialPayload, + TSubsequentPayload + >(context, payloadPublisher); return incrementalPublisher.buildResponse( result, errors, @@ -48,26 +56,26 @@ interface IncrementalPublisherContext { cancellableStreams: Set | undefined; } -interface SubsequentIncrementalExecutionResultContext { - pending: Array; - incremental: Array; - completed: Array; -} - /** * This class is used to publish incremental results to the client, enabling semi-concurrent * execution while preserving result order. * * @internal */ -class IncrementalPublisher { +class IncrementalPublisher { private _context: IncrementalPublisherContext; - private _nextId: number; + private _payloadPublisher: PayloadPublisher< + TInitialPayload, + TSubsequentPayload + >; private _incrementalGraph: IncrementalGraph; - constructor(context: IncrementalPublisherContext) { - this._context = context; - this._nextId = 0; + constructor( + publisherContext: IncrementalPublisherContext, + payloadPublisher: PayloadPublisher, + ) { + this._context = publisherContext; + this._payloadPublisher = payloadPublisher; this._incrementalGraph = new IncrementalGraph(); } @@ -75,17 +83,19 @@ class IncrementalPublisher { data: ObjMap, errors: ReadonlyArray | undefined, incrementalDataRecords: ReadonlyArray, - ): ExperimentalIncrementalExecutionResults { + ): ExperimentalIncrementalExecutionResults< + TInitialPayload, + TSubsequentPayload + > { const newRootNodes = this._incrementalGraph.getNewRootNodes( incrementalDataRecords, ); - const pending = this._toPendingResults(newRootNodes); - - const initialResult: InitialIncrementalExecutionResult = - errors === undefined - ? { data, pending, hasNext: true } - : { errors, data, pending, hasNext: true }; + const initialResult = this._payloadPublisher.getInitialPayload( + data, + errors, + newRootNodes, + ); return { initialResult, @@ -93,38 +103,11 @@ class IncrementalPublisher { }; } - private _toPendingResults( - newRootNodes: ReadonlyArray, - ): Array { - const pendingResults: Array = []; - for (const node of newRootNodes) { - const id = String(this._getNextId()); - node.id = id; - const pendingResult: PendingResult = { - id, - path: pathToArray(node.path), - }; - if (node.label !== undefined) { - pendingResult.label = node.label; - } - pendingResults.push(pendingResult); - } - return pendingResults; - } - - private _getNextId(): string { - return String(this._nextId++); - } - - private _subscribe(): AsyncGenerator< - SubsequentIncrementalExecutionResult, - void, - void - > { + private _subscribe(): AsyncGenerator { let isDone = false; const _next = async (): Promise< - IteratorResult + IteratorResult > => { if (isDone) { this._context.abortSignalListener?.disconnect(); @@ -132,42 +115,29 @@ class IncrementalPublisher { return { value: undefined, done: true }; } - const context: SubsequentIncrementalExecutionResultContext = { - pending: [], - incremental: [], - completed: [], - }; + const payloadPublisher = + this._payloadPublisher.getSubsequentPayloadPublisher(); let batch: Iterable | undefined = this._incrementalGraph.currentCompletedBatch(); do { for (const completedResult of batch) { - this._handleCompletedIncrementalData(completedResult, context); + this._handleCompletedIncrementalData( + completedResult, + payloadPublisher, + ); } - const { incremental, completed } = context; - if (incremental.length > 0 || completed.length > 0) { - const hasNext = this._incrementalGraph.hasNext(); + const hasNext = this._incrementalGraph.hasNext(); + const subsequentPayload = + payloadPublisher.getSubsequentPayload(hasNext); + if (subsequentPayload !== undefined) { if (!hasNext) { isDone = true; } - const subsequentIncrementalExecutionResult: SubsequentIncrementalExecutionResult = - { hasNext }; - - const pending = context.pending; - if (pending.length > 0) { - subsequentIncrementalExecutionResult.pending = pending; - } - if (incremental.length > 0) { - subsequentIncrementalExecutionResult.incremental = incremental; - } - if (completed.length > 0) { - subsequentIncrementalExecutionResult.completed = completed; - } - - return { value: subsequentIncrementalExecutionResult, done: false }; + return { value: subsequentPayload, done: false }; } // eslint-disable-next-line no-await-in-loop @@ -182,7 +152,7 @@ class IncrementalPublisher { }; const _return = async (): Promise< - IteratorResult + IteratorResult > => { isDone = true; this._incrementalGraph.abort(); @@ -192,7 +162,7 @@ class IncrementalPublisher { const _throw = async ( error?: unknown, - ): Promise> => { + ): Promise> => { isDone = true; this._incrementalGraph.abort(); await this._returnAsyncIterators(); @@ -212,34 +182,39 @@ class IncrementalPublisher { private _handleCompletedIncrementalData( completedIncrementalData: IncrementalDataRecordResult, - context: SubsequentIncrementalExecutionResultContext, + payloadPublisher: SubsequentPayloadPublisher, ): void { if (isCompletedExecutionGroup(completedIncrementalData)) { - this._handleCompletedExecutionGroup(completedIncrementalData, context); + this._handleCompletedExecutionGroup( + completedIncrementalData, + payloadPublisher, + ); } else { - this._handleCompletedStreamItems(completedIncrementalData, context); + this._handleCompletedStreamItems( + completedIncrementalData, + payloadPublisher, + ); } } private _handleCompletedExecutionGroup( completedExecutionGroup: CompletedExecutionGroup, - context: SubsequentIncrementalExecutionResultContext, + payloadPublisher: SubsequentPayloadPublisher, ): void { if (isFailedExecutionGroup(completedExecutionGroup)) { for (const deferredFragmentRecord of completedExecutionGroup .pendingExecutionGroup.deferredFragmentRecords) { - const id = deferredFragmentRecord.id; if ( !this._incrementalGraph.removeDeferredFragment(deferredFragmentRecord) ) { // This can occur if multiple deferred grouped field sets error for a fragment. continue; } - invariant(id !== undefined); - context.completed.push({ - id, - errors: completedExecutionGroup.errors, - }); + + payloadPublisher.addFailedDeferredFragmentRecord( + deferredFragmentRecord, + completedExecutionGroup, + ); } return; } @@ -256,42 +231,25 @@ class IncrementalPublisher { if (completion === undefined) { continue; } - const id = deferredFragmentRecord.id; - invariant(id !== undefined); - const incremental = context.incremental; - const { newRootNodes, successfulExecutionGroups } = completion; - context.pending.push(...this._toPendingResults(newRootNodes)); - for (const successfulExecutionGroup of successfulExecutionGroups) { - const { bestId, subPath } = this._getBestIdAndSubPath( - id, - deferredFragmentRecord, - successfulExecutionGroup, - ); - const incrementalEntry: IncrementalDeferResult = { - ...successfulExecutionGroup.result, - id: bestId, - }; - if (subPath !== undefined) { - incrementalEntry.subPath = subPath; - } - incremental.push(incrementalEntry); - } - context.completed.push({ id }); + + payloadPublisher.addSuccessfulDeferredFragmentRecord( + deferredFragmentRecord, + completion.newRootNodes, + completion.successfulExecutionGroups, + ); } } private _handleCompletedStreamItems( streamItemsResult: StreamItemsResult, - context: SubsequentIncrementalExecutionResultContext, + payloadPublisher: SubsequentPayloadPublisher, ): void { const streamRecord = streamItemsResult.streamRecord; - const id = streamRecord.id; - invariant(id !== undefined); if (streamItemsResult.errors !== undefined) { - context.completed.push({ - id, - errors: streamItemsResult.errors, - }); + payloadPublisher.addFailedStreamRecord( + streamRecord, + streamItemsResult.errors, + ); this._incrementalGraph.removeStream(streamRecord); if (isCancellableStreamRecord(streamRecord)) { invariant(this._context.cancellableStreams !== undefined); @@ -302,63 +260,23 @@ class IncrementalPublisher { }); } } else if (streamItemsResult.result === undefined) { - context.completed.push({ id }); + payloadPublisher.addSuccessfulStreamRecord(streamRecord); this._incrementalGraph.removeStream(streamRecord); if (isCancellableStreamRecord(streamRecord)) { invariant(this._context.cancellableStreams !== undefined); this._context.cancellableStreams.delete(streamRecord); } } else { - const incrementalEntry: IncrementalStreamResult = { - id, - ...streamItemsResult.result, - }; + const { result, incrementalDataRecords } = streamItemsResult; - context.incremental.push(incrementalEntry); + const newRootNodes = + incrementalDataRecords && + this._incrementalGraph.getNewRootNodes(incrementalDataRecords); - const incrementalDataRecords = streamItemsResult.incrementalDataRecords; - if (incrementalDataRecords !== undefined) { - const newRootNodes = this._incrementalGraph.getNewRootNodes( - incrementalDataRecords, - ); - context.pending.push(...this._toPendingResults(newRootNodes)); - } + payloadPublisher.addStreamItems(streamRecord, newRootNodes, result); } } - private _getBestIdAndSubPath( - initialId: string, - initialDeferredFragmentRecord: DeferredFragmentRecord, - completedExecutionGroup: CompletedExecutionGroup, - ): { bestId: string; subPath: ReadonlyArray | undefined } { - let maxLength = pathToArray(initialDeferredFragmentRecord.path).length; - let bestId = initialId; - - for (const deferredFragmentRecord of completedExecutionGroup - .pendingExecutionGroup.deferredFragmentRecords) { - if (deferredFragmentRecord === initialDeferredFragmentRecord) { - continue; - } - const id = deferredFragmentRecord.id; - // TODO: add test case for when an fragment has not been released, but might be processed for the shortest path. - /* c8 ignore next 3 */ - if (id === undefined) { - continue; - } - const fragmentPath = pathToArray(deferredFragmentRecord.path); - const length = fragmentPath.length; - if (length > maxLength) { - maxLength = length; - bestId = id; - } - } - const subPath = completedExecutionGroup.path.slice(maxLength); - return { - bestId, - subPath: subPath.length > 0 ? subPath : undefined, - }; - } - private async _returnAsyncIterators(): Promise { const cancellableStreams = this._context.cancellableStreams; if (cancellableStreams === undefined) { diff --git a/src/execution/PayloadPublisher.ts b/src/execution/PayloadPublisher.ts new file mode 100644 index 0000000000..7061afb4a1 --- /dev/null +++ b/src/execution/PayloadPublisher.ts @@ -0,0 +1,242 @@ +import { invariant } from '../jsutils/invariant.js'; +import type { ObjMap } from '../jsutils/ObjMap.js'; +import { pathToArray } from '../jsutils/Path.js'; + +import type { GraphQLError } from '../error/GraphQLError.js'; + +import type { + CompletedExecutionGroup, + CompletedResult, + DeferredFragmentRecord, + DeliveryGroup, + FailedExecutionGroup, + IncrementalDeferResult, + IncrementalResult, + IncrementalStreamResult, + InitialIncrementalExecutionResult, + PendingResult, + StreamItemsRecordResult, + StreamRecord, + SubsequentIncrementalExecutionResult, + SuccessfulExecutionGroup, +} from './types.js'; + +export interface PayloadPublisher { + getInitialPayload: ( + data: ObjMap, + errors: ReadonlyArray | undefined, + newRootNodes: ReadonlyArray, + ) => TInitialPayload; + getSubsequentPayloadPublisher: () => SubsequentPayloadPublisher; +} + +export interface SubsequentPayloadPublisher { + addFailedDeferredFragmentRecord: ( + deferredFragmentRecord: DeferredFragmentRecord, + failedExecutionGroup: FailedExecutionGroup, + ) => void; + addSuccessfulDeferredFragmentRecord: ( + deferredFragmentRecord: DeferredFragmentRecord, + newRootNodes: ReadonlyArray, + successfulExecutionGroups: ReadonlyArray, + ) => void; + addFailedStreamRecord: ( + streamRecord: StreamRecord, + errors: ReadonlyArray, + ) => void; + addSuccessfulStreamRecord: (streamRecord: StreamRecord) => void; + addStreamItems: ( + streamRecord: StreamRecord, + newRootNodes: ReadonlyArray | undefined, + result: StreamItemsRecordResult, + ) => void; + getSubsequentPayload: (hasNext: boolean) => TSubsequentPayload | undefined; +} + +export function getPayloadPublisher(): PayloadPublisher< + InitialIncrementalExecutionResult, + SubsequentIncrementalExecutionResult +> { + const ids = new Map(); + let nextId = 0; + + return { + getInitialPayload, + getSubsequentPayloadPublisher, + }; + + function getInitialPayload( + data: ObjMap, + errors: ReadonlyArray | undefined, + newRootNodes: ReadonlyArray, + ): InitialIncrementalExecutionResult { + const pending: Array = []; + addPendingResults(newRootNodes, pending); + + return errors === undefined + ? { data, pending, hasNext: true } + : { errors, data, pending, hasNext: true }; + } + + function addPendingResults( + newRootNodes: ReadonlyArray, + pending: Array, + ): void { + for (const node of newRootNodes) { + const id = String(nextId++); + ids.set(node, id); + const pendingResult: PendingResult = { + id, + path: pathToArray(node.path), + }; + if (node.label !== undefined) { + pendingResult.label = node.label; + } + pending.push(pendingResult); + } + } + + function getSubsequentPayloadPublisher(): SubsequentPayloadPublisher { + const pending: Array = []; + const incremental: Array = []; + const completed: Array = []; + + return { + addFailedDeferredFragmentRecord, + addSuccessfulDeferredFragmentRecord, + addFailedStreamRecord, + addSuccessfulStreamRecord, + addStreamItems, + getSubsequentPayload, + }; + + function addFailedDeferredFragmentRecord( + deferredFragmentRecord: DeferredFragmentRecord, + failedExecutionGroup: FailedExecutionGroup, + ): void { + const id = ids.get(deferredFragmentRecord); + invariant(id !== undefined); + completed.push({ + id, + errors: failedExecutionGroup.errors, + }); + } + + function addSuccessfulDeferredFragmentRecord( + deferredFragmentRecord: DeferredFragmentRecord, + newRootNodes: ReadonlyArray, + successfulExecutionGroups: ReadonlyArray, + ): void { + const id = ids.get(deferredFragmentRecord); + invariant(id !== undefined); + addPendingResults(newRootNodes, pending); + for (const successfulExecutionGroup of successfulExecutionGroups) { + const { bestId, subPath } = getBestIdAndSubPath( + id, + deferredFragmentRecord, + successfulExecutionGroup, + ); + const incrementalEntry: IncrementalDeferResult = { + ...successfulExecutionGroup.result, + id: bestId, + }; + if (subPath !== undefined) { + incrementalEntry.subPath = subPath; + } + incremental.push(incrementalEntry); + } + completed.push({ id }); + } + + function addFailedStreamRecord( + streamRecord: StreamRecord, + errors: ReadonlyArray, + ): void { + const id = ids.get(streamRecord); + invariant(id !== undefined); + completed.push({ + id, + errors, + }); + } + + function addSuccessfulStreamRecord(streamRecord: StreamRecord): void { + const id = ids.get(streamRecord); + invariant(id !== undefined); + completed.push({ id }); + } + + function addStreamItems( + streamRecord: StreamRecord, + newRootNodes: ReadonlyArray | undefined, + result: StreamItemsRecordResult, + ): void { + const id = ids.get(streamRecord); + invariant(id !== undefined); + const incrementalEntry: IncrementalStreamResult = { + id, + ...result, + }; + + incremental.push(incrementalEntry); + + if (newRootNodes) { + addPendingResults(newRootNodes, pending); + } + } + + function getSubsequentPayload( + hasNext: boolean, + ): SubsequentIncrementalExecutionResult | undefined { + if (incremental.length > 0 || completed.length > 0) { + const subsequentIncrementalExecutionResult: SubsequentIncrementalExecutionResult = + { hasNext }; + + if (pending.length > 0) { + subsequentIncrementalExecutionResult.pending = pending; + } + if (incremental.length > 0) { + subsequentIncrementalExecutionResult.incremental = incremental; + } + if (completed.length > 0) { + subsequentIncrementalExecutionResult.completed = completed; + } + + return subsequentIncrementalExecutionResult; + } + } + } + + function getBestIdAndSubPath( + initialId: string, + initialDeferredFragmentRecord: DeferredFragmentRecord, + completedExecutionGroup: CompletedExecutionGroup, + ): { bestId: string; subPath: ReadonlyArray | undefined } { + let maxLength = pathToArray(initialDeferredFragmentRecord.path).length; + let bestId = initialId; + + for (const deferredFragmentRecord of completedExecutionGroup + .pendingExecutionGroup.deferredFragmentRecords) { + if (deferredFragmentRecord === initialDeferredFragmentRecord) { + continue; + } + const id = ids.get(deferredFragmentRecord); + // TODO: add test case for when an fragment has not been released, but might be processed for the shortest path. + /* c8 ignore next 3 */ + if (id === undefined) { + continue; + } + const fragmentPath = pathToArray(deferredFragmentRecord.path); + const length = fragmentPath.length; + if (length > maxLength) { + maxLength = length; + bestId = id; + } + } + const subPath = completedExecutionGroup.path.slice(maxLength); + return { + bestId, + subPath: subPath.length > 0 ? subPath : undefined, + }; + } +} diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 46e125a688..7fa306d640 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -29,6 +29,7 @@ import { cancellableIterable, cancellablePromise, } from './AbortSignalListener.js'; +import { buildExecutionPlan } from './buildExecutionPlan.js'; import { buildResolveInfo } from './buildResolveInfo.js'; import type { FieldDetailsList, FragmentDetails } from './collectFields.js'; import { collectFields } from './collectFields.js'; @@ -36,6 +37,7 @@ import type { ValidatedExecutionArgs } from './Executor.js'; import { Executor } from './Executor.js'; import { getVariableSignature } from './getVariableSignature.js'; import { mapAsyncIterable } from './mapAsyncIterable.js'; +import { getPayloadPublisher } from './PayloadPublisher.js'; import type { ExecutionResult, ExperimentalIncrementalExecutionResults, @@ -170,7 +172,11 @@ export function executeQueryOrMutationOrSubscriptionEvent( export function experimentalExecuteQueryOrMutationOrSubscriptionEvent( validatedExecutionArgs: ValidatedExecutionArgs, ): PromiseOrValue { - const executor = new Executor(validatedExecutionArgs); + const executor = new Executor( + validatedExecutionArgs, + buildExecutionPlan, + getPayloadPublisher, + ); return executor.executeQueryOrMutationOrSubscriptionEvent(); } diff --git a/src/execution/types.ts b/src/execution/types.ts index 87d47e3efc..3d6443de0d 100644 --- a/src/execution/types.ts +++ b/src/execution/types.ts @@ -114,7 +114,7 @@ export interface FormattedIncrementalDeferResult< extensions?: TExtensions; } -interface StreamItemsRecordResult> { +export interface StreamItemsRecordResult> { errors?: ReadonlyArray; items: TData; } @@ -191,7 +191,7 @@ export interface SuccessfulExecutionGroup { errors?: never; } -interface FailedExecutionGroup { +export interface FailedExecutionGroup { pendingExecutionGroup: PendingExecutionGroup; path: Array; errors: ReadonlyArray; @@ -219,7 +219,6 @@ export type DeliveryGroup = DeferredFragmentRecord | StreamRecord; export class DeferredFragmentRecord { path: Path | undefined; label: string | undefined; - id?: string | undefined; parent: DeferredFragmentRecord | undefined; pendingExecutionGroups: Set; successfulExecutionGroups: Set; @@ -256,7 +255,6 @@ export type StreamItemRecord = ThunkIncrementalResult; export interface StreamRecord { path: Path; label: string | undefined; - id?: string | undefined; streamItemQueue: Array; } From 54b215fe1c06d06c28b547591c2219ab383569d7 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Thu, 19 Dec 2024 23:01:05 +0200 Subject: [PATCH 6/6] add legacy incremental delivery entrypoints --- src/execution/index.ts | 5 + src/execution/legacyExecuteIncrementally.ts | 299 ++++++++++++++++++++ 2 files changed, 304 insertions(+) create mode 100644 src/execution/legacyExecuteIncrementally.ts diff --git a/src/execution/index.ts b/src/execution/index.ts index ff339deb88..f3e63b7858 100644 --- a/src/execution/index.ts +++ b/src/execution/index.ts @@ -13,6 +13,11 @@ export { subscribe, } from './execute.js'; +export { + legacyExecuteIncrementally, + legacyExecuteQueryOrMutationOrSubscriptionEvent, +} from './legacyExecuteIncrementally.js'; + export type { ExecutionArgs } from './execute.js'; export type { ValidatedExecutionArgs } from './Executor.js'; diff --git a/src/execution/legacyExecuteIncrementally.ts b/src/execution/legacyExecuteIncrementally.ts new file mode 100644 index 0000000000..a3b0856bad --- /dev/null +++ b/src/execution/legacyExecuteIncrementally.ts @@ -0,0 +1,299 @@ +import { AccumulatorMap } from '../jsutils/AccumulatorMap.js'; +import { getBySet } from '../jsutils/getBySet.js'; +import { invariant } from '../jsutils/invariant.js'; +import { isSameSet } from '../jsutils/isSameSet.js'; +import type { ObjMap } from '../jsutils/ObjMap.js'; +import { addPath, pathToArray } from '../jsutils/Path.js'; +import type { PromiseOrValue } from '../jsutils/PromiseOrValue.js'; + +import type { GraphQLError } from '../error/GraphQLError.js'; + +import type { DeferUsageSet, ExecutionPlan } from './buildExecutionPlan.js'; +import type { + DeferUsage, + FieldDetails, + GroupedFieldSet, +} from './collectFields.js'; +import type { ExecutionArgs } from './execute.js'; +import { validateExecutionArgs } from './execute.js'; +import type { ValidatedExecutionArgs } from './Executor.js'; +import { Executor } from './Executor.js'; +import type { ExperimentalIncrementalExecutionResults } from './IncrementalPublisher.js'; +import type { + PayloadPublisher, + SubsequentPayloadPublisher, +} from './PayloadPublisher.js'; +import type { + DeferredFragmentRecord, + DeliveryGroup, + ExecutionResult, + FailedExecutionGroup, + StreamItemsRecordResult, + StreamRecord, + SuccessfulExecutionGroup, +} from './types.js'; +import { isDeferredFragmentRecord } from './types.js'; + +interface InitialIncrementalExecutionResult< + TData = ObjMap, + TExtensions = ObjMap, +> extends ExecutionResult { + data: TData; + hasNext: true; + extensions?: TExtensions; +} + +interface SubsequentIncrementalExecutionResult< + TData = unknown, + TExtensions = ObjMap, +> { + incremental?: ReadonlyArray>; + hasNext: boolean; + extensions?: TExtensions; +} + +type IncrementalResult> = + | IncrementalDeferResult + | IncrementalStreamResult; + +interface IncrementalDeferResult< + TData = ObjMap, + TExtensions = ObjMap, +> extends ExecutionResult { + path: ReadonlyArray; + label?: string; +} + +interface IncrementalStreamResult< + TData = ReadonlyArray, + TExtensions = ObjMap, +> { + errors?: ReadonlyArray; + items: TData | null; + path: ReadonlyArray; + label?: string; + extensions?: TExtensions; +} + +export function legacyExecuteIncrementally( + args: ExecutionArgs, +): PromiseOrValue< + | ExecutionResult + | ExperimentalIncrementalExecutionResults< + InitialIncrementalExecutionResult, + SubsequentIncrementalExecutionResult + > +> { + // If a valid execution context cannot be created due to incorrect arguments, + // a "Response" with only errors is returned. + const validatedExecutionArgs = validateExecutionArgs(args); + + // Return early errors if execution context failed. + if (!('schema' in validatedExecutionArgs)) { + return { errors: validatedExecutionArgs }; + } + + return legacyExecuteQueryOrMutationOrSubscriptionEvent( + validatedExecutionArgs, + ); +} + +export function legacyExecuteQueryOrMutationOrSubscriptionEvent( + validatedExecutionArgs: ValidatedExecutionArgs, +): PromiseOrValue< + | ExecutionResult + | ExperimentalIncrementalExecutionResults< + InitialIncrementalExecutionResult, + SubsequentIncrementalExecutionResult + > +> { + const executor = new Executor( + validatedExecutionArgs, + buildBranchingExecutionPlan, + getBranchingPayloadPublisher, + ); + return executor.executeQueryOrMutationOrSubscriptionEvent(); +} + +function buildBranchingExecutionPlan( + originalGroupedFieldSet: GroupedFieldSet, + parentDeferUsages: DeferUsageSet = new Set(), +): ExecutionPlan { + const groupedFieldSet = new AccumulatorMap(); + + const newGroupedFieldSets = new Map< + DeferUsageSet, + AccumulatorMap + >(); + + for (const [responseKey, fieldGroup] of originalGroupedFieldSet) { + for (const fieldDetails of fieldGroup) { + const deferUsage = fieldDetails.deferUsage; + const deferUsageSet = + deferUsage === undefined + ? new Set() + : new Set([deferUsage]); + if (isSameSet(parentDeferUsages, deferUsageSet)) { + groupedFieldSet.add(responseKey, fieldDetails); + } else { + let newGroupedFieldSet = getBySet(newGroupedFieldSets, deferUsageSet); + if (newGroupedFieldSet === undefined) { + newGroupedFieldSet = new AccumulatorMap(); + newGroupedFieldSets.set(deferUsageSet, newGroupedFieldSet); + } + newGroupedFieldSet.add(responseKey, fieldDetails); + } + } + } + + return { + groupedFieldSet, + newGroupedFieldSets, + }; +} + +function getBranchingPayloadPublisher(): PayloadPublisher< + InitialIncrementalExecutionResult, + SubsequentIncrementalExecutionResult +> { + const indices = new Map(); + + return { + getInitialPayload, + getSubsequentPayloadPublisher, + }; + + function getInitialPayload( + data: ObjMap, + errors: ReadonlyArray | undefined, + newRootNodes: ReadonlyArray, + ): InitialIncrementalExecutionResult { + for (const node of newRootNodes) { + if (!isDeferredFragmentRecord(node)) { + indices.set(node, 0); + } + } + + return errors === undefined + ? { data, hasNext: true } + : { errors, data, hasNext: true }; + } + + function getSubsequentPayloadPublisher(): SubsequentPayloadPublisher { + const incremental: Array = []; + + return { + addFailedDeferredFragmentRecord, + addSuccessfulDeferredFragmentRecord, + addFailedStreamRecord, + addSuccessfulStreamRecord, + addStreamItems, + getSubsequentPayload, + }; + + function addFailedDeferredFragmentRecord( + deferredFragmentRecord: DeferredFragmentRecord, + failedExecutionGroup: FailedExecutionGroup, + ): void { + const { path, label } = deferredFragmentRecord; + const incrementalEntry: IncrementalDeferResult = { + errors: failedExecutionGroup.errors, + data: null, + path: pathToArray(path), + }; + incrementalEntry.path = pathToArray(path); + if (label !== undefined) { + incrementalEntry.label = label; + } + incremental.push(incrementalEntry); + } + + function addSuccessfulDeferredFragmentRecord( + deferredFragmentRecord: DeferredFragmentRecord, + newRootNodes: ReadonlyArray, + successfulExecutionGroups: ReadonlyArray, + ): void { + for (const node of newRootNodes) { + if (!isDeferredFragmentRecord(node)) { + indices.set(node, 0); + } + } + + for (const successfulExecutionGroup of successfulExecutionGroups) { + const { path, label } = deferredFragmentRecord; + const incrementalEntry: IncrementalDeferResult = { + ...successfulExecutionGroup.result, + path: pathToArray(path), + }; + if (label !== undefined) { + incrementalEntry.label = label; + } + incremental.push(incrementalEntry); + } + } + + function addFailedStreamRecord( + streamRecord: StreamRecord, + errors: ReadonlyArray, + ): void { + const { path, label } = streamRecord; + const index = indices.get(streamRecord); + invariant(index !== undefined); + const incrementalEntry: IncrementalStreamResult = { + errors, + items: null, + path: pathToArray(addPath(path, index, undefined)), + }; + if (label !== undefined) { + incrementalEntry.label = label; + } + incremental.push(incrementalEntry); + indices.delete(streamRecord); + } + + function addSuccessfulStreamRecord(streamRecord: StreamRecord): void { + indices.delete(streamRecord); + } + + function addStreamItems( + streamRecord: StreamRecord, + newRootNodes: ReadonlyArray | undefined, + result: StreamItemsRecordResult, + ): void { + if (newRootNodes !== undefined) { + for (const node of newRootNodes) { + if (!isDeferredFragmentRecord(node)) { + indices.set(node, 0); + } + } + } + + const { path, label } = streamRecord; + const index = indices.get(streamRecord); + invariant(index !== undefined); + const incrementalEntry: IncrementalStreamResult = { + ...result, + path: pathToArray(addPath(path, index, undefined)), + }; + if (label !== undefined) { + incrementalEntry.label = label; + } + incremental.push(incrementalEntry); + } + + function getSubsequentPayload( + hasNext: boolean, + ): SubsequentIncrementalExecutionResult | undefined { + if (incremental.length > 0) { + const subsequentIncrementalExecutionResult: SubsequentIncrementalExecutionResult = + { hasNext }; + + if (incremental.length > 0) { + subsequentIncrementalExecutionResult.incremental = incremental; + } + + return subsequentIncrementalExecutionResult; + } + } + } +}