UNPKG

grafast

Version:

Cutting edge GraphQL planning and execution engine

999 lines 199 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OperationPlan = exports.POLYMORPHIC_ROOT_PATHS = exports.POLYMORPHIC_ROOT_PATH = void 0; const tslib_1 = require("tslib"); const lru_1 = tslib_1.__importDefault(require("@graphile/lru")); const graphql = tslib_1.__importStar(require("graphql")); const tamedevil_1 = tslib_1.__importDefault(require("tamedevil")); const assert = tslib_1.__importStar(require("../assert.js")); const constants_js_1 = require("../constants.js"); const graphqlCollectFields_js_1 = require("../graphqlCollectFields.js"); const graphqlMergeSelectionSets_js_1 = require("../graphqlMergeSelectionSets.js"); const index_js_1 = require("../index.js"); const input_js_1 = require("../input.js"); const inspect_js_1 = require("../inspect.js"); const operationPlan_input_js_1 = require("../operationPlan-input.js"); const step_js_1 = require("../step.js"); const __cloneStream_js_1 = require("../steps/__cloneStream.js"); const connection_js_1 = require("../steps/connection.js"); const constant_js_1 = require("../steps/constant.js"); const each_js_1 = require("../steps/each.js"); const graphqlResolver_js_1 = require("../steps/graphqlResolver.js"); const timeSource_js_1 = require("../timeSource.js"); const utils_js_1 = require("../utils.js"); const LayerPlan_js_1 = require("./LayerPlan.js"); const defaultPlanResolver_js_1 = require("./lib/defaultPlanResolver.js"); const withGlobalLayerPlan_js_1 = require("./lib/withGlobalLayerPlan.js"); const lock_js_1 = require("./lock.js"); const OutputPlan_js_1 = require("./OutputPlan.js"); const StepTracker_js_1 = require("./StepTracker.js"); const atpe = typeof process !== "undefined" && process.env.ALWAYS_THROW_PLANNING_ERRORS; const ALWAYS_THROW_PLANNING_ERRORS = atpe === "1"; const THROW_PLANNING_ERRORS_ON_SIDE_EFFECTS = atpe === "2"; const DEFAULT_MAX_DEPTH = 100; /** * Returns true for steps that the system populates automatically without executing. */ function isPrepopulatedStep(step) { return step instanceof index_js_1.__ItemStep || step instanceof index_js_1.__ValueStep; } if (atpe && atpe !== "0" && !(ALWAYS_THROW_PLANNING_ERRORS || THROW_PLANNING_ERRORS_ON_SIDE_EFFECTS)) { throw new Error(`Invalid value for envvar 'ALWAYS_THROW_PLANNING_ERRORS' - expected '1' or '2', but received '${atpe}'`); } // Work around TypeScript CommonJS `graphql_1.isListType` unoptimal access. const { assertObjectType, defaultFieldResolver, getNamedType, getNullableType, isEnumType, isInterfaceType, isListType, isNonNullType, isObjectType, isScalarType, isUnionType, } = graphql; /** * This value allows the first few plans to take more time whilst the JIT warms * up - planning can easily go from 400ms down to 80ms after just a few * executions thanks to V8's JIT. */ let planningTimeoutWarmupMultiplier = 5; const EMPTY_ARRAY = Object.freeze([]); const EMPTY_SET = new Set(); exports.POLYMORPHIC_ROOT_PATH = null; exports.POLYMORPHIC_ROOT_PATHS = null; Object.freeze(exports.POLYMORPHIC_ROOT_PATHS); /** How many times will we try re-optimizing before giving up */ const MAX_OPTIMIZATION_LOOPS = 10; const REASON_ROOT = Object.freeze({ type: "root" }); const OUTPUT_PLAN_TYPE_NULL = Object.freeze({ mode: "null" }); const OUTPUT_PLAN_TYPE_ARRAY = Object.freeze({ mode: "array" }); const newValueStepCallback = (isImmutable) => new index_js_1.__ValueStep(isImmutable); const NO_ARGS = Object.create(null); class OperationPlan { constructor(schema, operation, fragments, variableValuesConstraints, variableValues, contextConstraints, context, rootValueConstraints, rootValue, errorBehavior, options) { this.schema = schema; this.operation = operation; this.fragments = fragments; this.variableValues = variableValues; this.context = context; this.rootValue = rootValue; this.errorBehavior = errorBehavior; /** * For use by `inputStep()` only. * @internal */ this._inputStepCache = new Map(); /** * What state is the OpPlan in? * * 1. init * 2. plan * 3. validate * 5. optimize * 6. finalize * 7. ready * * Once in 'ready' state we can execute the plan. */ this.phase = "init"; /** * Gets updated as we work our way through the plan, useful for making errors more helpful. */ this.loc = index_js_1.isDev ? [] : null /* forbid loc in production */; /** @internal */ this.stepTracker = new StepTracker_js_1.StepTracker(this); /** * @internal */ this.itemStepIdByListStepIdByParentLayerPlanId = Object.create(null); /** * If true, then this operation doesn't use (custom) resolvers. */ this.pure = true; this.startTime = timeSource_js_1.timeSource.now(); this.previousLap = this.startTime; this.laps = []; this.optimizeMeta = new Map(); /** @internal */ this.valueNodeToStaticValueCache = new Map(); this.frozenPlanningPaths = index_js_1.isDev ? null : new Set(); /** @internal Use plan.getStep(id) instead. */ this.getStep = index_js_1.isDev ? (id, requestingStep) => { if (!["plan", "validate", "optimize"].includes(this.phase)) { throw new Error(`Getting a step during the '${this.phase}' phase is forbidden - please do so before or during the optimize phase.`); } // Check that requestingStep is allowed to get steps if (requestingStep.isOptimized && (this.phase !== "optimize" || !requestingStep.allowMultipleOptimizations)) { throw new Error(`Optimized step ${requestingStep} is not permitted to request other steps (requested '${id}')`); } const step = this.stepTracker.getStepById(id); if (step == null) { throw new Error(`Programming error: step with id '${id}' no longer exists (attempted access from ${requestingStep}). Most likely this means that ${requestingStep} has an illegal reference to this step, you should only maintain references to steps via dependencies.`); } return step[constants_js_1.$$proxy] ?? step; } : (id, _requestingStep) => { const step = this.stepTracker.getStepById(id); return step[constants_js_1.$$proxy] ?? step; }; // This is the queue of things to be done _IN ORDER_ this.planningQueue = []; // This is the queue of things to be done grouped by their planning path. // The result of this is not necessarily in order, but it does save us // time and effort in `planPending()` this.planningQueueByPlanningPath = new Map(); this.planFieldBatch = null; this._cacheStepStoreByActionKeyByLayerPlan = new Map(); this._immutableCacheStepStoreAndActionKey = Object.create(null); this.planningTimeout = options?.timeouts?.planning ?? null; this.maxPlanningDepth = options?.maxPlanningDepth ?? DEFAULT_MAX_DEPTH; this.resolveInfoOperationBase = { schema, operation, fragments, }; this.variableValuesConstraints = variableValuesConstraints; this.contextConstraints = contextConstraints; this.rootValueConstraints = rootValueConstraints; this.scalarPlanInfo = { schema: this.schema }; const queryType = schema.getQueryType(); assert.ok(queryType, "Schema must have a query type"); this.queryType = queryType; this.mutationType = schema.getMutationType() ?? null; this.subscriptionType = schema.getSubscriptionType() ?? null; const allTypes = Object.values(schema.getTypeMap()); const allUnions = []; const allObjectTypes = []; this.unionsContainingObjectType = Object.create(null); for (const type of allTypes) { if (isUnionType(type)) { allUnions.push(type); const members = type.getTypes(); for (const memberType of members) { if (!this.unionsContainingObjectType[memberType.name]) { this.unionsContainingObjectType[memberType.name] = [type]; } else { this.unionsContainingObjectType[memberType.name].push(type); } } } else if (isObjectType(type)) { allObjectTypes.push(type); if (!this.unionsContainingObjectType[type.name]) { this.unionsContainingObjectType[type.name] = []; } } } this.operationType = operation.operation; this.phase = "plan"; this.rootLayerPlan = new LayerPlan_js_1.LayerPlan(this, REASON_ROOT); // Set up the shared steps for variables, context and rootValue [this.variableValuesStep, this.trackedVariableValuesStep] = this.track(variableValues, this.variableValuesConstraints, this.operation.variableDefinitions); [this.contextStep, this.trackedContextStep] = this.track(context, this.contextConstraints); [this.rootValueStep, this.trackedRootValueStep] = this.track(rootValue, this.rootValueConstraints); // this.rootLayerPlan.parentStep = this.trackedRootValueStep; this.deduplicateSteps(); this.lap("init"); // Plan the operation this.planOperation(); this.checkTimeout(); this.lap("planOperation"); // Now perform hoisting (and repeat deduplication) this.hoistSteps(); this.checkTimeout(); this.lap("hoistSteps", "planOperation"); if (index_js_1.isDev) { this.phase = "validate"; this.resetCache(); // Helpfully check steps don't do forbidden things. this.validateSteps(); this.lap("validateSteps"); } this.phase = "optimize"; this.resetCache(); // Get rid of temporary steps before `optimize` triggers side-effects. // (Critical due to steps that may have been discarded due to field errors // or similar.) this.stepTracker.treeShakeSteps(); this.checkTimeout(); this.lap("treeShakeSteps", "optimize"); // Replace/inline/optimise steps tamedevil_1.default.batch(() => { this.optimizeSteps(); }); this.checkTimeout(); this.lap("optimizeSteps"); this.inlineSteps(); this.checkTimeout(); this.lap("inlineSteps"); this.phase = "finalize"; this.resetCache(); this.stepTracker.finalizeSteps(); // Get rid of steps that are no longer needed after optimising outputPlans // (we shouldn't see any new steps or dependencies after here) this.stepTracker.treeShakeSteps(); this.checkTimeout(); this.lap("treeShakeSteps", "finalize"); // Now shove steps as deep down as they can go (opposite of hoist) this.pushDownSteps(); this.checkTimeout(); this.lap("pushDownSteps"); // Plans are expected to execute later; they may take steps here to prepare // themselves (e.g. compiling SQL queries ahead of time). tamedevil_1.default.batch(() => { this.finalizeSteps(); }); this.lap("finalizeSteps"); // Replace access plans with direct access, etc (must come after finalizeSteps) tamedevil_1.default.batch(() => { this.optimizeOutputPlans(); }); this.checkTimeout(); this.lap("optimizeOutputPlans"); // AccessSteps may have been removed this.stepTracker.treeShakeSteps(); this.checkTimeout(); this.lap("treeShakeSteps", "optimizeOutputPlans"); this.stepTracker.finalizeOutputPlans(); tamedevil_1.default.batch(() => { this.finalizeLayerPlans(); }); this.lap("finalizeLayerPlans"); tamedevil_1.default.batch(() => { this.finalizeOutputPlans(); }); this.lap("finalizeOutputPlans"); this.finalize(); this.lap("finalizeOperationPlan"); this.phase = "ready"; this.resetCache(); // this.walkFinalizedPlans(); // this.preparePrefetches(); const allMetaKeys = new Set(); for (const step of this.stepTracker.activeSteps) { if (step.metaKey !== undefined) { allMetaKeys.add(step.metaKey); } } const allMetaKeysList = [...allMetaKeys]; this.makeMetaByMetaKey = makeMetaByMetaKeysFactory(allMetaKeysList); this.lap("ready"); const elapsed = timeSource_js_1.timeSource.now() - this.startTime; /* console.log(`Planning took ${elapsed.toFixed(1)}ms`); const entries: Array<{ process: string; duration: string }> = []; for (const lap of this.laps) { const elapsed = lap.elapsed; entries.push({ process: `${lap.category}${ lap.subcategory ? `[${lap.subcategory}]` : `` }`, duration: `${elapsed.toFixed(1)}ms`, }); } console.table(entries); */ // Allow this to be garbage collected this.optimizeMeta = null; context?.grafastMetricsEmitter?.emit("plan", { elapsed, laps: this.laps, }); if (planningTimeoutWarmupMultiplier > 1) { planningTimeoutWarmupMultiplier = Math.max(1, planningTimeoutWarmupMultiplier - 0.5); } } lap(category, subcategory) { const now = timeSource_js_1.timeSource.now(); const elapsed = now - this.previousLap; this.previousLap = now; this.laps.push({ category, subcategory, elapsed }); } checkTimeout() { if (this.planningTimeout === null) return; const now = timeSource_js_1.timeSource.now(); const elapsed = now - this.startTime; if (elapsed > this.planningTimeout * planningTimeoutWarmupMultiplier) { throw new index_js_1.SafeError("Operation took too long to plan and was aborted. Please simplify the request and try again.", { [constants_js_1.$$timeout]: this.planningTimeout, [constants_js_1.$$ts]: now, }); } } /** * Called by the LayerPlan's constructor when it wants to get a new id to use. * * @internal */ addLayerPlan(layerPlan) { return this.stepTracker.addLayerPlan(layerPlan); } /** * Adds a plan to the known steps and returns the number to use as the plan * id. ONLY to be used from Step, user code should never call this directly. * * @internal */ _addStep(plan) { if (!["plan", "validate", "optimize"].includes(this.phase)) { throw new Error(`Creating a plan during the '${this.phase}' phase is forbidden.`); } return this.stepTracker.addStep(plan); } /** * Get a plan without specifying who requested it; this disables all the * caller checks. Only intended to be called from internal code. * * @internal */ dangerouslyGetStep(id) { return this.stepTracker.getStepById(id); } planOperation() { try { switch (this.operationType) { case "query": { this.planQuery(); break; } case "mutation": { this.planMutation(); break; } case "subscription": { this.planSubscription(); break; } default: { const never = this.operationType; throw new index_js_1.SafeError(`Unsupported operation type '${never}'.`); } } } catch (e) { // LOGGING: raise this somewhere critical if (this.loc != null) { console.error(`Error occurred during query planning (at ${this.loc.join(" > ")}): \n${e.stack || e}`); } else { console.error(`Error occurred during query planning: \n${e.stack || e}`); } throw new Error(`Query planning error: ${e.message}`, { cause: e }); } } /** * Plans a GraphQL query operation. */ planQuery() { if (this.loc !== null) this.loc.push("planQuery()"); const rootType = this.queryType; if (!rootType) { throw new index_js_1.SafeError("No query type found in schema"); } const locationDetails = { node: this.operation.selectionSet.selections, parentTypeName: null, // WHAT SHOULD fieldName be here?! fieldName: null, }; const outputPlan = new OutputPlan_js_1.OutputPlan(this.rootLayerPlan, this.rootValueStep, { mode: "root", typeName: this.queryType.name, }, locationDetails); this.rootOutputPlan = outputPlan; this.queueNextLayer(this.planSelectionSet, { outputPlan, path: [], planningPath: rootType.name + ".", polymorphicPaths: exports.POLYMORPHIC_ROOT_PATHS, parentStep: this.trackedRootValueStep, positionType: rootType, layerPlan: this.rootLayerPlan, selections: this.operation.selectionSet.selections, resolverEmulation: true, }); this.planPending(); if (this.loc !== null) this.loc.pop(); } /** * Implements the `PlanOpPlanMutation` algorithm. */ planMutation() { if (this.loc !== null) this.loc.push("planMutation()"); const rootType = this.mutationType; if (!rootType) { throw new index_js_1.SafeError("No mutation type found in schema"); } const locationDetails = { node: this.operation.selectionSet.selections, parentTypeName: null, // WHAT SHOULD fieldName be here?! fieldName: null, }; const outputPlan = new OutputPlan_js_1.OutputPlan(this.rootLayerPlan, this.rootValueStep, { mode: "root", typeName: rootType.name, }, locationDetails); this.rootOutputPlan = outputPlan; this.queueNextLayer(this.planSelectionSet, { outputPlan, path: [], planningPath: rootType.name + ".", polymorphicPaths: exports.POLYMORPHIC_ROOT_PATHS, parentStep: this.trackedRootValueStep, positionType: rootType, layerPlan: this.rootLayerPlan, selections: this.operation.selectionSet.selections, resolverEmulation: true, isMutation: true, }); this.planPending(); if (this.loc !== null) this.loc.pop(); } /** * Implements the `PlanOpPlanSubscription` algorithm. */ planSubscription() { if (this.loc !== null) this.loc.push("planSubscription"); const rootType = this.subscriptionType; if (!rootType) { throw new index_js_1.SafeError("No subscription type found in schema"); } const planningPath = rootType.name + "."; const selectionSet = this.operation.selectionSet; const stepStreamOptions = {}; const groupedFieldSet = (0, withGlobalLayerPlan_js_1.withGlobalLayerPlan)(this.rootLayerPlan, exports.POLYMORPHIC_ROOT_PATHS, planningPath, stepStreamOptions, graphqlCollectFields_js_1.graphqlCollectFields, null, this, this.trackedRootValueStep.id, rootType, selectionSet.selections, (0, graphqlCollectFields_js_1.newSelectionSetDigest)(false)); if (groupedFieldSet.deferred !== undefined) { throw new index_js_1.SafeError("@defer forbidden on subscription root selection set"); } let firstKey = undefined; for (const key of groupedFieldSet.fields.keys()) { if (firstKey !== undefined) { throw new index_js_1.SafeError("subscriptions may only have one top-level field"); } firstKey = key; } assert.ok(firstKey != null, "selection set cannot be empty"); const fields = groupedFieldSet.fields.get(firstKey); if (!fields) { throw new index_js_1.SafeError("Consistency error."); } // All grouped fields are equivalent, as mandated by GraphQL validation rules. Thus we can take the first one. const field = fields[0]; const fieldName = field.name.value; // Unaffected by alias. const rootTypeFields = rootType.getFields(); const fieldSpec = rootTypeFields[fieldName]; const rawSubscriptionPlanResolver = fieldSpec.extensions?.grafast?.subscribePlan; const path = [field.alias?.value ?? fieldName]; const locationDetails = { parentTypeName: rootType.name, fieldName, node: this.operation.selectionSet.selections, }; const subscriptionPlanResolver = rawSubscriptionPlanResolver; const fieldArgsSpec = fieldSpec.args; const trackedArguments = fieldArgsSpec.length > 0 ? (0, withGlobalLayerPlan_js_1.withGlobalLayerPlan)(this.rootLayerPlan, exports.POLYMORPHIC_ROOT_PATHS, planningPath, stepStreamOptions, this.getTrackedArguments, this, fieldArgsSpec, field) : NO_ARGS; if (subscriptionPlanResolver !== undefined) { // PERF: optimize this const { haltTree, step: subscribeStep, latestSideEffectStep, } = this.batchPlanField({ typeName: rootType.name, fieldName, layerPlan: this.rootLayerPlan, path, polymorphicPaths: exports.POLYMORPHIC_ROOT_PATHS, planningPath, planResolver: subscriptionPlanResolver, applyAfterMode: "subscribePlan", rawParentStep: this.trackedRootValueStep, field: fieldSpec, trackedArguments, streamDetails: true, })(); if (haltTree) { throw new index_js_1.SafeError("Failed to setup subscription"); } this.rootLayerPlan.latestSideEffectStep = latestSideEffectStep; this.rootLayerPlan.setRootStep(subscribeStep); const subscriptionEventLayerPlan = new LayerPlan_js_1.LayerPlan(this, { type: "subscription", parentLayerPlan: this.rootLayerPlan, }); const $__item = (0, withGlobalLayerPlan_js_1.withGlobalLayerPlan)(subscriptionEventLayerPlan, exports.POLYMORPHIC_ROOT_PATHS, planningPath, null, () => new index_js_1.__ItemStep(subscribeStep)); subscriptionEventLayerPlan.setRootStep($__item); const streamItemPlan = (0, utils_js_1.hasItemPlan)(subscribeStep) ? (0, withGlobalLayerPlan_js_1.withGlobalLayerPlan)(subscriptionEventLayerPlan, exports.POLYMORPHIC_ROOT_PATHS, planningPath, null, subscribeStep.itemPlan, subscribeStep, $__item) : $__item; const outputPlan = new OutputPlan_js_1.OutputPlan(subscriptionEventLayerPlan, this.rootValueStep, { mode: "root", typeName: rootType.name }, locationDetails); this.rootOutputPlan = outputPlan; this.queueNextLayer(this.planSelectionSet, { outputPlan, path: [], planningPath, polymorphicPaths: exports.POLYMORPHIC_ROOT_PATHS, parentStep: streamItemPlan, positionType: rootType, layerPlan: subscriptionEventLayerPlan, selections: selectionSet.selections, resolverEmulation: false, }); this.planPending(); } else { const subscribeStep = (0, withGlobalLayerPlan_js_1.withGlobalLayerPlan)(this.rootLayerPlan, exports.POLYMORPHIC_ROOT_PATHS, planningPath, null, () => { const $args = (0, index_js_1.object)(trackedArguments); const rawResolver = fieldSpec.resolve; const rawSubscriber = fieldSpec.subscribe; return (0, graphqlResolver_js_1.graphqlResolver)(rawResolver, rawSubscriber, this.trackedRootValueStep, $args, { ...this.resolveInfoOperationBase, fieldName, fieldNodes: fields, parentType: this.subscriptionType, returnType: fieldSpec.type, // @ts-ignore path: { typename: this.subscriptionType.name, key: fieldName, prev: undefined, }, }); }); subscribeStep._stepOptions.stream = stepStreamOptions; // Note this should only have one dependent, so it should not be a // distributor subscribeStep._stepOptions.walkIterable = true; this.rootLayerPlan.setRootStep(subscribeStep); const subscriptionEventLayerPlan = new LayerPlan_js_1.LayerPlan(this, { type: "subscription", parentLayerPlan: this.rootLayerPlan, }); const $__item = (0, withGlobalLayerPlan_js_1.withGlobalLayerPlan)(subscriptionEventLayerPlan, exports.POLYMORPHIC_ROOT_PATHS, planningPath, null, () => new index_js_1.__ItemStep(subscribeStep)); subscriptionEventLayerPlan.setRootStep($__item); const streamItemPlan = (0, utils_js_1.hasItemPlan)(subscribeStep) ? (0, withGlobalLayerPlan_js_1.withGlobalLayerPlan)(subscriptionEventLayerPlan, exports.POLYMORPHIC_ROOT_PATHS, planningPath, null, subscribeStep.itemPlan, subscribeStep, $__item) : $__item; const outputPlan = new OutputPlan_js_1.OutputPlan(subscriptionEventLayerPlan, this.rootValueStep, { mode: "root", typeName: rootType.name }, locationDetails); this.rootOutputPlan = outputPlan; this.queueNextLayer(this.planSelectionSet, { outputPlan, path: [], planningPath, polymorphicPaths: exports.POLYMORPHIC_ROOT_PATHS, parentStep: streamItemPlan, positionType: rootType, layerPlan: subscriptionEventLayerPlan, selections: selectionSet.selections, resolverEmulation: true, }); this.planPending(); } if (this.loc !== null) this.loc.pop(); } getCombinedLayerPlanForLayerPlans(setOfParentLayerPlans) { const parentLayerPlans = [...setOfParentLayerPlans]; // See if one already exists with the same layer plans in the same order for (const lp of this.stepTracker.layerPlans) { if (lp?.reason.type === "combined" && (0, utils_js_1.arraysMatch)(parentLayerPlans, lp.reason.parentLayerPlans)) { return lp; } } return new LayerPlan_js_1.LayerPlan(this, { type: "combined", parentLayerPlans, }); } /** * Gets the item plan for a given parent list plan - this ensures we only * create one item plan per parent plan. */ itemStepForListStep(parentLayerPlan, planningPath, listStep, depth, stream) { const itemStepIdByListStepId = (this.itemStepIdByListStepIdByParentLayerPlanId[parentLayerPlan.id] ??= Object.create(null)); const itemStepId = itemStepIdByListStepId[listStep.id]; if (itemStepId !== undefined) { const itemStep = this.stepTracker.getStepById(itemStepId); if (listStep.polymorphicPaths !== null && itemStep.polymorphicPaths !== null) { for (const p of intersectPolyPaths(listStep.polymorphicPaths, polymorphicPathsForLayer(parentLayerPlan))) { itemStep.polymorphicPaths.add(p); } } return itemStep; } // Create a new LayerPlan for this list item const layerPlan = new LayerPlan_js_1.LayerPlan(this, { type: "listItem", parentLayerPlan, parentStep: listStep, stream, }); const itemStep = (0, withGlobalLayerPlan_js_1.withGlobalLayerPlan)(layerPlan, intersectPolyPaths(listStep.polymorphicPaths, polymorphicPathsForLayer(layerPlan)), planningPath, null, () => new index_js_1.__ItemStep(listStep, depth)); layerPlan.setRootStep(itemStep); itemStepIdByListStepId[listStep.id] = itemStep.id; return itemStep; } *processGroupedFieldSet(details) { const { outputPlan, path, planningPath, polymorphicPaths, parentStep, positionType: objectType, // layerPlan, objectTypeFields, isMutation, groupedFieldSet, } = details; // `__typename` shouldn't bump the mutation index since it has no side effects. let mutationIndex = -1; const $sideEffect = outputPlan.layerPlan.latestSideEffectStep; for (const [responseKey, fieldNodes] of groupedFieldSet.fields.entries()) { let resolverEmulation = groupedFieldSet.resolverEmulation; try { // All grouped fields are equivalent, as mandated by GraphQL validation rules. Thus we can take the first one. const field = fieldNodes[0]; const fieldName = field.name.value; const locationDetails = { parentTypeName: objectType.name, fieldName, node: fieldNodes, }; // explicit matches are the fastest: https://jsben.ch/ajZNf if (fieldName === "__typename") { outputPlan.addChild(objectType, responseKey, { type: "__typename", locationDetails, }); continue; } else if (fieldName === "__schema" || fieldName === "__type") { const variableNames = (0, utils_js_1.findVariableNamesUsed)(this, field); outputPlan.addChild(objectType, responseKey, { type: "outputPlan", isNonNull: fieldName === "__schema", outputPlan: new OutputPlan_js_1.OutputPlan(outputPlan.layerPlan, this.rootValueStep, { mode: "introspection", field, variableNames, // PERF: if variableNames.length === 0 we should be able to optimize this! introspectionCacheByVariableValues: new lru_1.default({ maxLength: 3, }), }, locationDetails), locationDetails, }); continue; } const objectField = objectTypeFields[fieldName]; if (!objectField) { // Field does not exist; this should have been caught by validation // but the spec says to just skip it. continue; } const fieldType = objectField.type; const rawPlanResolver = objectField.extensions?.grafast?.plan; if (rawPlanResolver) { resolverEmulation = false; (0, utils_js_1.assertNotAsync)(rawPlanResolver, `${objectType.name}.${fieldName}.plan`); } const namedReturnType = getNamedType(fieldType); const resolvedResolver = objectField.resolve; const subscriber = objectField.subscribe; const usesDefaultResolver = resolvedResolver == null || resolvedResolver === defaultFieldResolver; // We should use a resolver if: // 1. they give us a non-default resolver // 2. we're emulating resolvers const resolver = resolvedResolver && !usesDefaultResolver ? resolvedResolver : resolverEmulation ? defaultFieldResolver : null; // Apply a default plan to fields that do not have a plan nor a resolver. const planResolver = rawPlanResolver ?? (resolver ? undefined : defaultPlanResolver_js_1.defaultPlanResolver); /* * When considering resolvers on fields, there's three booleans to * consider: * * - typeIsPlanned: Does the type the field is defined on expect a plan? * - NOTE: the root types (Query, Mutation, Subscription) implicitly * expect the "root plan" * - fieldHasPlan: Does the field define a `plan()` method? * - resultIsPlanned: Does the named type that the field returns (the * "named field type") expect a plan? * - NOTE: only object types, unions and interfaces may expect plans; * but not all of them do. * - NOTE: a union/interface expects a plan iff ANY of its object * types expect plans * - NOTE: if ANY object type in an interface/union expects a plan * then ALL object types within the interface/union must expect * plans. * - NOTE: scalars and enums never expect a plan. * * These booleans impact: * * - Whether there must be a `plan()` declaration and what the "parent" * argument is to the same * - If typeIsPlanned: * - Assert: `fieldHasPlan` must be true * - Pass through the parent plan * - Else, if resultIsPlanned: * - Assert: `fieldHasPlan` must be true * - Pass through a `__ValueStep` representing the parent value. * - Else, if fieldHasPlan: * - Pass through a `__ValueStep` representing the parent value. * - Else * - No action necessary. * - If the field may define `resolve()` and what the "parent" argument * is to the same * - If resultIsPlanned * - Assert: there must not be a `resolve()` * - Grafast provides pure resolver. * - Else if fieldHasPlan (which may be implied by typeIsPlanned * above) * - If `resolve()` is not set: * - grafast will return the value from the plan directly * - Otherwise: * - Grafast will wrap this resolver and will call `resolve()` (or * default resolver) with the plan result. IMPORTANT: you may * want to use an `ObjectStep` so that the parent object is of * the expected shape; e.g. your plan might return * `object({username: $username})` for a `User.username` field. * - Else * - Leave `resolve()` untouched - do not even wrap it. * - (Failing that, use a __ValueStep and return the result * directly.) */ if (resolver !== null) { this.pure = false; if (!rawPlanResolver) { resolverEmulation = true; } } const resultIsPlanned = (0, utils_js_1.isTypePlanned)(this.schema, namedReturnType); const fieldHasPlan = !!planResolver; if (resultIsPlanned && !fieldHasPlan && !objectType.extensions?.grafast?.assertStep) { throw new Error(`Field ${objectType.name}.${fieldName} returns a ${namedReturnType.name} which expects a plan to be available; however this field has no plan() method to produce such a plan; please add 'extensions.grafast.plan' to this field.`); } if (resultIsPlanned && resolver) { throw new Error(`Field ${objectType.name}.${fieldName} returns a ${namedReturnType.name} which expects a plan to be available; this means that ${objectType.name}.${fieldName} is forbidden from defining a GraphQL resolver.`); } let step; let haltTree = false; const fieldLayerPlan = isMutation ? new LayerPlan_js_1.LayerPlan(this, { type: "mutationField", parentLayerPlan: outputPlan.layerPlan, mutationIndex: ++mutationIndex, }) : outputPlan.layerPlan; const objectFieldArgs = objectField.args; const fieldPath = [...path, responseKey]; const fieldPlanningPath = planningPath + responseKey; const trackedArguments = objectFieldArgs.length > 0 ? (0, withGlobalLayerPlan_js_1.withGlobalLayerPlan)(this.rootLayerPlan, exports.POLYMORPHIC_ROOT_PATHS, fieldPlanningPath, null, this.getTrackedArguments, this, objectFieldArgs, field) : NO_ARGS; let streamDetails = null; const isList = isListType(getNullableType(fieldType)); if (isList) { // read the @stream directive, if present // TODO: Check SameStreamDirective still exists in @stream spec at release. /* * `SameStreamDirective` * (https://github.com/graphql/graphql-spec/blob/26fd78c4a89a79552dcc0c7e0140a975ce654400/spec/Section%205%20--%20Validation.md#L450-L458) * ensures that every field that has `@stream` must have the same * `@stream` arguments; so we can just check the first node in the * merged set to see our stream options. NOTE: if this changes before * release then we may need to find the stream with the largest * `initialCount` to figure what to do; something like: * * const streamDirective = firstField.directives?.filter( * (d) => d.name.value === "stream", * ).sort( * (a, z) => getArg(z, 'initialCount', 0) - getArg(a, 'initialCount', 0) * )[0] */ for (const n of fieldNodes) { const streamDirective = n.directives?.find((d) => d.name.value === "stream"); if (streamDirective === undefined) { // Undo any previous stream details; the non-@stream wins. streamDetails = null; break; } else if (streamDetails !== null) { // Validation promises the values are the same continue; } else { // Create streamDetails streamDetails = this.withRootLayerPlan(() => ({ initialCount: this.internalDependency((0, utils_js_1.directiveArgument)(this, streamDirective, "initialCount", graphql.Kind.INT) ?? (0, constant_js_1.constant)(0)), if: this.internalDependency((0, utils_js_1.directiveArgument)(this, streamDirective, "if", graphql.Kind.BOOLEAN) ?? (0, constant_js_1.constant)(true)), label: this.internalDependency((0, utils_js_1.directiveArgument)(this, streamDirective, "label", graphql.Kind.STRING) ?? (0, constant_js_1.constant)(undefined)), })); } } } if (typeof planResolver === "function") { let latestSideEffectStep; ({ step, haltTree, latestSideEffectStep } = yield this.batchPlanField({ typeName: objectType.name, fieldName, layerPlan: fieldLayerPlan, path: fieldPath, polymorphicPaths, planningPath: fieldPlanningPath, planResolver, applyAfterMode: "plan", rawParentStep: parentStep, field: objectField, trackedArguments, streamDetails: isList ? (streamDetails ?? false) : null, })); fieldLayerPlan.latestSideEffectStep = latestSideEffectStep; } else { // No plan resolver (or plan resolver fallback) so there must be a // `resolve` method, so we'll feed the full parent step into the // resolver. assert.ok(resolver !== null, "GraphileInternalError<81652257-617a-4d1a-8306-903d0e3d2ddf>: The field has no resolver, so planResolver should exist (either as the field.plan or as the default plan resolver)."); // ENHANCEMENT: should we do `step = parentStep.object()` (i.e. // `$pgSelectSingle.record()`) or similar for "opaque" steps to become // suitable for consumption by resolvers? // Maybe `parentStep.forResolver()` or `parentStep.hydrate()` or `parentStep.toFullObject()`? step = parentStep; } if (resolver !== null) { step = (0, withGlobalLayerPlan_js_1.withGlobalLayerPlan)(fieldLayerPlan, polymorphicPaths, fieldPlanningPath, null, // TODO: fix me? () => { const $args = (0, index_js_1.object)(trackedArguments); return (0, graphqlResolver_js_1.graphqlResolver)(resolver, subscriber, step, $args, { ...this.resolveInfoOperationBase, fieldName, fieldNodes, parentType: objectType, returnType: fieldType, }); }); } // May have changed due to deduplicate step = this.stepTracker.getStepById(step.id); if (haltTree) { // Very similar to this.handlePlanningError. Should probably use it in future. const isNonNull = isNonNullType(fieldType); outputPlan.addChild(objectType, responseKey, { type: "outputPlan", outputPlan: new OutputPlan_js_1.OutputPlan(fieldLayerPlan, step, OUTPUT_PLAN_TYPE_NULL, locationDetails), isNonNull, locationDetails, }); } else { if (index_js_1.isDev && polymorphicPaths) { const invalid = [...polymorphicPaths].filter((p) => !stepIsValidInPolyPath(step, p)); if (invalid.length > 0) { throw new Error(`${objectType}.${fieldName} (as ${responseKey}) returned ${step}, but that's not valid in ${invalid.length} out of ${polymorphicPaths.size} of the expected polymorphic paths. Invalid paths: ${invalid}`); } } outputPlan.expectChild(objectType, responseKey); this.queueNextLayer(this.planFieldReturnType, { outputPlan, path: fieldPath, planningPath: fieldPlanningPath, polymorphicPaths, parentStep: step, positionType: fieldType, layerPlan: fieldLayerPlan, // If one field has a selection set, they all have a selection set (guaranteed by validation). selections: field.selectionSet != null ? fieldNodes.flatMap((n) => n.selectionSet.selections) : undefined, parentObjectType: objectType, responseKey, locationDetails, resolverEmulation, streamDetails, }); } } finally { outputPlan.layerPlan.latestSideEffectStep = $sideEffect; } } if (groupedFieldSet.deferred !== undefined) { for (const deferred of groupedFieldSet.deferred) { const deferredLayerPlan = new LayerPlan_js_1.LayerPlan(this, { type: "defer", parentLayerPlan: outputPlan.layerPlan, label: deferred.label, }); const deferredOutputPlan = new OutputPlan_js_1.OutputPlan(deferredLayerPlan, outputPlan.rootStep, { mode: "object", deferLabel: deferred.label, typeName: objectType.name, }, // LOGGING: the location details should be tweaked to reference this // fragment outputPlan.locationDetails); const $sideEffect = deferredOutputPlan.layerPlan.latestSideEffectStep; try { outputPlan.deferredOutputPlans.push(deferredOutputPlan); this.queueNextLayer(this.processGroupedFieldSet, { outputPlan: deferredOutputPlan, path, planningPath: planningPath + "#", polymorphicPaths, parentStep, positionType: objectType, layerPlan: deferredLayerPlan, objectTypeFields, isMutation, groupedFieldSet: deferred, }); } finally { deferredOutputPlan.layerPlan.latestSideEffectStep = $sideEffect; } } } } /** * * @param outputPlan - The output plan that this selection set is being added to * @param path - The path within the outputPlan that we're adding stuff (only for root/object OutputPlans) * @param parentStep - The step that represents the selection set root * @param objectType - The object type that this selection set is being evaluated for (note polymorphic selection should already have been handled by this point) * @param selections - The GraphQL selections (fields, fragment spreads, inline fragments) to evaluate * @param isMutation - If true this selection set should be executed serially rather than in parallel (each field gets its own LayerPlan) */ planSelectionSet(details) { const { outputPlan, path, planningPath, polymorphicPaths, parentStep, positionType: objectType, layerPlan, selections, resolverEmulation, isMutation = false, } = details; if (this.loc !== null) { this.loc.push(`planSelectionSet(${objectType.name} @ ${outputPlan.layerPlan.id} @ ${path.join(".")} @ ${polymorphicPaths ? [...polymorphicPaths] : ""})`); } if (index_js_1.isDev) { assertObjectType(objectType); if (outputPlan.layerPlan !== layerPlan) { throw new Error(`GrafastInternalError<3ea1a3dd-1e11-4eb7-a31b-e125996d7eb4>: expected ${outputPlan} to have layer plan ${layerPlan}`); } } const groupedFieldSet = (0, withGlobalLayerPlan_js_1.withGlobalLayerPlan)(layerPlan, polymorphicPaths, planningPath, null, graphqlCollectFields_js_1.graphqlCollectFields, null, this, parentStep.id, objectType, selections, (0, graphqlCollectFields_js_1.newSelectionSetDigest)(resolverEmulation), isMutation); const objectTypeFields = objectType.getFields(); this.queueNextLayer(this.processGroupedFieldSet, { outputPlan, path, planningPath: planningPath + ">", polymorphicPaths, parentStep, positionType: objectType, layerPlan, objectTypeFields, isMutation, groupedFieldSet, }); if (this.loc !== null) this.loc.pop(); } queueNextLayer(method, details) { const { pla