grafast
Version:
Cutting edge GraphQL planning and execution engine
999 lines • 199 kB
JavaScript
"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