diff --git a/grafast/grafast/README.md b/grafast/grafast/README.md index 63c3b062c6..8d1654d3ba 100644 --- a/grafast/grafast/README.md +++ b/grafast/grafast/README.md @@ -82,7 +82,6 @@ requirements: - for every request: - `context` must be an object (anything suitable to be used as the key to a `WeakMap`); if you do not need a context then `{}` is perfectly acceptable - - `rootValue` must be an object or `null`/`undefined` - resolver limitations: - only explicit field resolvers (baked into the GraphQL schema) are supported, i.e. resolvers passed via `rootValue` are not (currently) supported diff --git a/grafast/grafast/src/bucket.ts b/grafast/grafast/src/bucket.ts index feeb3ee9fc..b20883a8f6 100644 --- a/grafast/grafast/src/bucket.ts +++ b/grafast/grafast/src/bucket.ts @@ -16,6 +16,8 @@ export interface RequestTools { /** @internal */ args: GrafastExecutionArgs; /** @internal */ + currentRootValue: any; + /** @internal */ onError: ErrorBehavior; /** The `timeSource.now()` at which the request started executing */ startTime: number; diff --git a/grafast/grafast/src/constants.ts b/grafast/grafast/src/constants.ts index a11dc257e1..126771397a 100644 --- a/grafast/grafast/src/constants.ts +++ b/grafast/grafast/src/constants.ts @@ -29,6 +29,7 @@ export const $$idempotent = Symbol("idempotent"); * The event emitter used for outputting execution events. */ export const $$eventEmitter = Symbol("executionEventEmitter"); +export const $$originalRootValue = Symbol("originalRootValue"); /** * Used to indicate that an array has more results available via a stream. diff --git a/grafast/grafast/src/engine/OperationPlan.ts b/grafast/grafast/src/engine/OperationPlan.ts index cd064b8a7f..f8de451a81 100644 --- a/grafast/grafast/src/engine/OperationPlan.ts +++ b/grafast/grafast/src/engine/OperationPlan.ts @@ -1503,7 +1503,7 @@ export class OperationPlan { deferredLayerPlan, outputPlan.rootStep, { - mode: "object", + mode: outputPlan.type.mode === "root" ? "root" : "object", deferLabel: deferred.label, typeName: objectType.name, }, diff --git a/grafast/grafast/src/engine/OutputPlan.ts b/grafast/grafast/src/engine/OutputPlan.ts index 9938537fa2..03a0d2c3a9 100644 --- a/grafast/grafast/src/engine/OutputPlan.ts +++ b/grafast/grafast/src/engine/OutputPlan.ts @@ -74,6 +74,7 @@ export type OutputPlanTypeRoot = { */ mode: "root"; typeName: string; + deferLabel?: string | undefined; }; export type OutputPlanTypeObject = { /** @@ -214,10 +215,10 @@ export class OutputPlan { | undefined; /** - * For object output plan types only. + * For root and object output plan types only. */ public deferredOutputPlans: OutputPlan< - OutputPlanTypeObject | OutputPlanTypePolymorphicObject + OutputPlanTypeRoot | OutputPlanTypeObject | OutputPlanTypePolymorphicObject >[] = []; public layerPlan: LayerPlan; diff --git a/grafast/grafast/src/execute.ts b/grafast/grafast/src/execute.ts index 1db4fc18b3..ec78a244e4 100644 --- a/grafast/grafast/src/execute.ts +++ b/grafast/grafast/src/execute.ts @@ -6,9 +6,11 @@ import type { } from "graphql"; import type { PromiseOrValue } from "graphql/jsutils/PromiseOrValue.js"; -import { $$eventEmitter, $$extensions } from "./constants.ts"; -import { isDev } from "./dev.ts"; -import { inspect } from "./inspect.ts"; +import { + $$eventEmitter, + $$extensions, + $$originalRootValue, +} from "./constants.ts"; import type { ExecuteEvent, ExecutionEventEmitter, @@ -30,25 +32,10 @@ export function withGrafastArgs( ExecutionResult | AsyncGenerator > { const options = args.resolvedPreset?.grafast; - if (isDev) { - if ( - args.rootValue != null && - (typeof args.rootValue !== "object" || - Object.keys(args.rootValue).length > 0) - ) { - throw new Error( - `Grafast executor doesn't support there being a rootValue (found ${inspect( - args.rootValue, - )})`, - ); - } - } + args[$$originalRootValue] = args.rootValue; if (args.rootValue == null) { args.rootValue = Object.create(null); } - if (typeof args.rootValue !== "object" || args.rootValue == null) { - throw new Error("Grafast requires that the 'rootValue' be an object"); - } const explain = options?.explain; const shouldExplain = !!explain; @@ -56,14 +43,12 @@ export function withGrafastArgs( if (shouldExplain) { const eventEmitter: ExecutionEventEmitter | undefined = new EventEmitter(); const explainOperations: any[] = []; - args.rootValue = Object.assign(Object.create(null), args.rootValue, { - [$$eventEmitter]: eventEmitter, - [$$extensions]: { - explain: { - operations: explainOperations, - }, + args[$$eventEmitter] = eventEmitter; + args[$$extensions] = { + explain: { + operations: explainOperations, }, - }); + }; const handleExplainOperation = ({ operation, }: ExecutionEventMap["explainOperation"]) => { diff --git a/grafast/grafast/src/interfaces.ts b/grafast/grafast/src/interfaces.ts index 1b87ed4398..8e4cd8fb5b 100644 --- a/grafast/grafast/src/interfaces.ts +++ b/grafast/grafast/src/interfaces.ts @@ -28,11 +28,14 @@ import type { import type { ObjMap } from "graphql/jsutils/ObjMap.js"; import type { Bucket, RequestTools } from "./bucket.ts"; -import type { - $$streamMore, - $$timeout, - $$ts, - ExecutionEntryFlags, +import { + $$eventEmitter, + $$extensions, + $$originalRootValue, + type $$streamMore, + type $$timeout, + type $$ts, + type ExecutionEntryFlags, } from "./constants.ts"; import type { Constraint } from "./constraints.ts"; import type { LayerPlanReasonListItemStream } from "./engine/LayerPlan.ts"; @@ -825,6 +828,9 @@ export interface GrafastExecutionArgs extends ExecutionArgs { middleware?: Middleware | null; requestContext?: Partial; outputDataAsString?: boolean; + [$$eventEmitter]?: ExecutionEventEmitter; + [$$extensions]?: Record; + [$$originalRootValue]?: any; } export interface ValidateSchemaEvent { diff --git a/grafast/grafast/src/prepare.ts b/grafast/grafast/src/prepare.ts index 63a2029186..fb7c8b5ba7 100644 --- a/grafast/grafast/src/prepare.ts +++ b/grafast/grafast/src/prepare.ts @@ -12,6 +12,7 @@ import { $$contextPlanCache, $$eventEmitter, $$extensions, + $$originalRootValue, $$streamMore, FLAG_ERROR, NO_FLAGS, @@ -359,11 +360,12 @@ function executePreemptive( executionTimeout !== null ? startTime + executionTimeout : null; const requestContext: RequestTools = { args, + currentRootValue: args[$$originalRootValue], onError, startTime, stopTime, // toSerialize: [], - eventEmitter: rootValue?.[$$eventEmitter], + eventEmitter: args[$$eventEmitter], abortSignal, insideGraphQL: false, }; @@ -404,14 +406,21 @@ function executePreemptive( iterators: [new Set()], size: 1, //store.size }); - const bucketPromise = executeBucket(subscriptionBucket, requestContext); + const subscriptionRequestContext: RequestTools = { + ...requestContext, + currentRootValue: payload, + }; + const bucketPromise = executeBucket( + subscriptionBucket, + subscriptionRequestContext, + ); function outputStreamBucket() { // NOTE: this is the root output plan for a subscription operation. const [ctx, result] = outputBucket( operationPlan.rootOutputPlan, subscriptionBucket, ZERO, - requestContext, + subscriptionRequestContext, [], rootBucket.store .get(operationPlan.variableValuesStep.id)! @@ -421,7 +430,7 @@ function executePreemptive( return finalize( result, ctx, - index === 0 ? (rootValue[$$extensions] ?? undefined) : undefined, + index === 0 ? (args[$$extensions] ?? undefined) : undefined, outputDataAsString, ); } @@ -460,7 +469,7 @@ function executePreemptive( ]; const payload = Object.create(null) as ExecutionResult; payload.errors = errors; - const extensions = bucketRootValue[$$extensions]; + const extensions = args[$$extensions]; if (extensions != null) { payload.extensions = extensions; } @@ -564,7 +573,7 @@ function executePreemptive( return finalize( result, ctx, - rootValue[$$extensions] ?? undefined, + args[$$extensions] ?? undefined, outputDataAsString, ); } @@ -601,7 +610,7 @@ export function grafastPrepare( const { schema, contextValue: context, - rootValue = Object.create(null), + rootValue, // operationName, // document, middleware, @@ -612,7 +621,7 @@ export function grafastPrepare( if (Array.isArray(exeContext) || "length" in exeContext) { return Object.assign(Object.create(bypassGraphQLObj), { errors: exeContext, - extensions: rootValue[$$extensions], + extensions: args[$$extensions], }); } @@ -674,7 +683,7 @@ export function grafastPrepare( if (operationPlan[$$contextPlanCache] == null) { operationPlan[$$contextPlanCache] = operationPlan.generatePlanJSON(); } - rootValue[$$extensions]?.explain?.operations.push({ + (args[$$extensions] as any)?.explain?.operations.push({ type: "plan", title: "Plan", plan: operationPlan[$$contextPlanCache], diff --git a/grafast/grafast/src/steps/graphqlResolver.ts b/grafast/grafast/src/steps/graphqlResolver.ts index 3c2f975609..b450743576 100644 --- a/grafast/grafast/src/steps/graphqlResolver.ts +++ b/grafast/grafast/src/steps/graphqlResolver.ts @@ -97,15 +97,17 @@ export class GraphQLResolverStep extends UnbatchedStep { variableValues: any, rootValue: any, ): any { + const executionRootValue = extra._requestContext.currentRootValue; if (!extra.stream) { if (this.isNotRoot && source == null) { return source; } + const resolverSource = this.isNotRoot ? source : executionRootValue; const resolveInfo: GraphQLResolveInfo = Object.assign( Object.create(this.resolveInfoBase), { variableValues, - rootValue, + rootValue: executionRootValue, path: { typename: this.resolveInfoBase.parentType.name, key: this.resolveInfoBase.fieldName, @@ -114,7 +116,12 @@ export class GraphQLResolverStep extends UnbatchedStep { }, }, ); - const data = this.resolver?.(source, args, context, resolveInfo); + const data = this.resolver?.( + resolverSource, + args, + context, + resolveInfo, + ); return flagErrorIfErrorAsync(data); } else { if (this.isNotRoot) { @@ -128,10 +135,15 @@ export class GraphQLResolverStep extends UnbatchedStep { { // ENHANCE: add support for path variableValues, - rootValue, + rootValue: executionRootValue, }, ); - const data = this.subscriber(source, args, context, resolveInfo); + const data = this.subscriber( + executionRootValue, + args, + context, + resolveInfo, + ); return flagErrorIfErrorAsync(data); } } diff --git a/grafast/website/grafast/getting-started/existing-schema.md b/grafast/website/grafast/getting-started/existing-schema.md index 092effe869..cbb950206b 100644 --- a/grafast/website/grafast/getting-started/existing-schema.md +++ b/grafast/website/grafast/getting-started/existing-schema.md @@ -22,7 +22,6 @@ following hold: currently populate that in an equivalent fashion - `context` must be an object (anything suitable to be used as the key to a `WeakMap`); if you do not need a context then `{}` is perfectly acceptable -- `rootValue`, if specified, must be an object or `null`/`undefined` - `resolveType` and `isTypeOf`, if specified, must return the GraphQL type name as a string (rather than returning the object type itself) and their version of `GraphQLResolveInfo` is even more cut down (but you