grafast
Version:
Cutting edge GraphQL planning and execution engine
1,145 lines • 41.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.GraphQLSpecifiedErrorBehaviors = exports.sleep = exports.canonicalJSONStringify = exports.valueNodeToStaticValue = exports.sharedNull = exports.ROOT_VALUE_OBJECT = void 0;
exports.assertNullPrototype = assertNullPrototype;
exports.defaultValueToValueNode = defaultValueToValueNode;
exports.isPromise = isPromise;
exports.isPromiseLike = isPromiseLike;
exports.isDeferred = isDeferred;
exports.arraysMatch = arraysMatch;
exports.maybeArraysMatch = maybeArraysMatch;
exports.mapsMatch = mapsMatch;
exports.recordsMatch = recordsMatch;
exports.setsMatch = setsMatch;
exports.objectSpec = objectSpec;
exports.newObjectTypeBuilder = newObjectTypeBuilder;
exports.objectFieldSpec = objectFieldSpec;
exports.newGrafastFieldConfigBuilder = newGrafastFieldConfigBuilder;
exports.newInputObjectTypeBuilder = newInputObjectTypeBuilder;
exports.inputObjectFieldSpec = inputObjectFieldSpec;
exports.getEnumValueConfigs = getEnumValueConfigs;
exports.getEnumValueConfig = getEnumValueConfig;
exports.stack = stack;
exports.arrayOfLength = arrayOfLength;
exports.arrayOfLengthCb = arrayOfLengthCb;
exports.findVariableNamesUsedInValueNode = findVariableNamesUsedInValueNode;
exports.findVariableNamesUsed = findVariableNamesUsed;
exports.isTypePlanned = isTypePlanned;
exports.sudo = sudo;
exports.writeableArray = writeableArray;
exports.stepADependsOnStepB = stepADependsOnStepB;
exports.stepAMayDependOnStepB = stepAMayDependOnStepB;
exports.stepAShouldTryAndInlineIntoStepB = stepAShouldTryAndInlineIntoStepB;
exports.pathsFromAncestorToTargetLayerPlan = pathsFromAncestorToTargetLayerPlan;
exports.layerPlanHeirarchyContains = layerPlanHeirarchyContains;
exports.stepsAreInSamePhase = stepsAreInSamePhase;
exports.isPhaseTransitionLayerPlan = isPhaseTransitionLayerPlan;
exports.assertNotAsync = assertNotAsync;
exports.assertNotPromise = assertNotPromise;
exports.hasItemPlan = hasItemPlan;
exports.exportNameHint = exportNameHint;
exports.isTuple = isTuple;
exports.digestKeys = digestKeys;
exports.directiveArgument = directiveArgument;
exports.stableStringSort = stableStringSort;
exports.stableStringSortFirstTupleEntry = stableStringSortFirstTupleEntry;
exports.asyncIteratorWithCleanup = asyncIteratorWithCleanup;
exports.terminateIterable = terminateIterable;
const tslib_1 = require("tslib");
const graphql = tslib_1.__importStar(require("graphql"));
const assert = tslib_1.__importStar(require("./assert.js"));
const dev_js_1 = require("./dev.js");
const error_js_1 = require("./error.js");
const inspect_js_1 = require("./inspect.js");
const constant_js_1 = require("./steps/constant.js");
const { GraphQLBoolean, GraphQLEnumType, GraphQLFloat, GraphQLID, GraphQLInputObjectType, GraphQLInt, GraphQLInterfaceType, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLString, GraphQLUnionType, Kind, } = graphql;
/**
* The parent object is used as the key in `GetValueStepId()`; for root level
* fields it's possible that the parent will be null/undefined (in all other
* cases it will be an object), so we need a value that can be the key in a
* WeakMap to represent the root.
*/
exports.ROOT_VALUE_OBJECT = Object.freeze(Object.create(null));
function assertNullPrototype(object, description) {
if (dev_js_1.isDev) {
assert.strictEqual(Object.getPrototypeOf(object), null, `Expected ${description} to have a null prototype`);
}
}
/**
* Converts a JSON value into the equivalent ValueNode _without_ checking that
* it's compatible with the expected type. Typically only used with scalars
* (since they can use any ValueNode) - other parts of the GraphQL schema
* should use explicitly compatible ValueNodes.
*
* WARNING: It's possible for this to generate `LIST(INT, FLOAT, STRING)` which
* is not possible in GraphQL since lists have a single defined type. This should
* only be used with custom scalars.
*/
function dangerousRawValueToValueNode(value) {
if (value == null) {
return { kind: Kind.NULL };
}
if (typeof value === "boolean") {
return { kind: Kind.BOOLEAN, value };
}
if (typeof value === "number") {
if (value === Math.round(value)) {
return { kind: Kind.INT, value: String(value) };
}
else {
return { kind: Kind.FLOAT, value: String(value) };
}
}
if (typeof value === "string") {
return { kind: Kind.STRING, value };
}
if (Array.isArray(value)) {
return {
kind: Kind.LIST,
values: value.map(dangerousRawValueToValueNode),
};
}
if (typeof value === "object" && value !== null) {
return {
kind: Kind.OBJECT,
fields: Object.keys(value).map((key) => ({
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: key },
value: dangerousRawValueToValueNode(value[key]),
})),
};
}
const never = value;
console.error(`Unhandled type when converting custom scalar to ValueNode: ${(0, inspect_js_1.inspect)(never)}`);
throw new Error(`Unhandled type when converting custom scalar to ValueNode`);
}
/**
* Takes a value (typically a JSON-compatible value) and converts it into a
* ValueNode that's compatible with the given GraphQL type.
*/
function rawValueToValueNode(type, value) {
if (type instanceof GraphQLNonNull) {
if (value == null) {
throw new Error("defaultValue contained null/undefined at a position that is marked as non-null");
}
return rawValueToValueNode(type.ofType, value);
}
// Below here null/undefined are allowed.
if (value === undefined) {
return undefined;
}
if (value === null) {
return { kind: Kind.NULL };
}
if (type === GraphQLBoolean) {
if (typeof value !== "boolean") {
throw new Error("defaultValue contained invalid value at a position expecting boolean");
}
return { kind: Kind.BOOLEAN, value };
}
if (type === GraphQLInt) {
if (typeof value !== "number") {
throw new Error("defaultValue contained invalid value at a position expecting int");
}
return { kind: Kind.INT, value: String(parseInt(String(value), 10)) };
}
if (type === GraphQLFloat) {
if (typeof value !== "number") {
throw new Error("defaultValue contained invalid value at a position expecting int");
}
return { kind: Kind.FLOAT, value: String(value) };
}
if (type === GraphQLString || type === GraphQLID) {
if (typeof value !== "string") {
throw new Error("defaultValue contained invalid value at a position expecting string");
}
return { kind: Kind.STRING, value };
}
if (type instanceof GraphQLEnumType) {
const enumValues = type.getValues();
const enumValue = enumValues.find((v) => v.value === value);
if (!enumValue) {
console.error(`Default contained invalid value for enum ${type.name}: ${(0, inspect_js_1.inspect)(value)}`);
throw new Error(`Default contained invalid value for enum ${type.name}`);
}
return { kind: Kind.ENUM, value: enumValue.name };
}
if (type instanceof GraphQLScalarType) {
return dangerousRawValueToValueNode(value);
}
if (type instanceof GraphQLList) {
if (!Array.isArray(value)) {
throw new Error("defaultValue contained invalid value at location expecting a list");
}
return {
kind: Kind.LIST,
values: value.map((entry) => {
const entryValueNode = rawValueToValueNode(type.ofType, entry);
if (entryValueNode === undefined) {
throw new Error("defaultValue contained invalid list (contained `undefined`)");
}
return entryValueNode;
}),
};
}
if (type instanceof GraphQLInputObjectType) {
if (typeof value !== "object" || value === null) {
throw new Error("defaultValue contained invalid value at location expecting an object");
}
const fieldDefs = type.getFields();
const fields = [];
for (const fieldName in fieldDefs) {
const fieldDef = fieldDefs[fieldName];
const fieldType = fieldDef.type;
const rawValue = value[fieldName] !== undefined
? value[fieldName]
: fieldDef.defaultValue;
const fieldValueNode = rawValueToValueNode(fieldType, rawValue);
if (fieldValueNode !== undefined) {
fields.push({
kind: Kind.OBJECT_FIELD,
name: { kind: Kind.NAME, value: fieldName },
value: fieldValueNode,
});
}
}
return {
kind: Kind.OBJECT,
fields,
};
}
const never = type;
console.error(`Encountered unexpected type when processing defaultValue ${(0, inspect_js_1.inspect)(never)}`);
throw new Error(`Encountered unexpected type when processing defaultValue`);
}
/**
* Specifically allows for the `defaultValue` to be undefined, but otherwise
* defers to {@link rawValueToValueNode}
*/
function defaultValueToValueNode(type, defaultValue) {
// NOTE: even if `type` is non-null it's okay for `defaultValue` to be
// undefined. However it is not okay for defaultValue to be null if type is
// non-null.
if (defaultValue === undefined) {
return undefined;
}
return rawValueToValueNode(type, defaultValue);
}
function isPromise(t) {
return (typeof t === "object" &&
t !== null &&
typeof t.then === "function" &&
typeof t.catch === "function");
}
/**
* Is "thenable".
*/
function isPromiseLike(t) {
return t != null && typeof t.then === "function";
}
/**
* Is a promise that can be externally resolved.
*/
function isDeferred(t) {
return (isPromise(t) &&
typeof t.resolve === "function" &&
typeof t.reject === "function");
}
/**
* Returns true if array1 and array2 have the same length, and every pair of
* values within them pass the `comparator` check (which defaults to `===`).
*/
function arraysMatch(array1, array2, comparator) {
if (array1 === array2)
return true;
const l = array1.length;
if (l !== array2.length) {
return false;
}
if (comparator !== undefined) {
for (let i = 0; i < l; i++) {
const a = array1[i];
const b = array2[i];
if (a !== b && !comparator(a, b)) {
return false;
}
}
}
else {
for (let i = 0; i < l; i++) {
if (array1[i] !== array2[i]) {
return false;
}
}
}
return true;
}
function maybeArraysMatch(array1, array2, comparator) {
return (array1 === array2 ||
(array1 != null &&
array2 != null &&
arraysMatch(array1, array2, comparator)));
}
/**
* Returns true if map1 and map2 have the same keys, and every matching entry
* value within them pass the `comparator` check (which defaults to `===`).
*/
function mapsMatch(map1, map2, comparator) {
if (map1 === map2)
return true;
const l = map1.size;
if (l !== map2.size) {
return false;
}
const allKeys = new Set([...map1.keys(), ...map2.keys()]);
if (allKeys.size !== l) {
return false;
}
if (comparator !== undefined) {
for (const k of allKeys) {
const a = map1.get(k);
const b = map2.get(k);
if (a !== b && !comparator(k, a, b)) {
return false;
}
}
}
else {
for (const k of allKeys) {
if (map1.get(k) !== map2.get(k)) {
return false;
}
}
}
return true;
}
/**
* Returns true if record1 and record2 are equivalent, i.e. every value within
* them pass the `comparator` check (which defaults to `===`).
*
* Currently keys are ignored (`record[key] = undefined` is treated the same as
* `record[key]` being unset), but this may not always be the case.
*/
function recordsMatch(record1, record2, comparator) {
if (record1 === record2)
return true;
const k1 = Object.keys(record1);
const k2 = Object.keys(record2);
const allKeys = new Set([...k1, ...k2]);
if (comparator !== undefined) {
for (const k of allKeys) {
const a = record1[k];
const b = record2[k];
if (a !== b && !comparator(k, a, b)) {
return false;
}
}
}
else {
for (const k of allKeys) {
if (record1[k] !== record2[k]) {
return false;
}
}
}
return true;
}
function setsMatch(s1, s2) {
if (s1 === s2)
return true;
if (s1 == null)
return false;
if (s2 == null)
return false;
if (s1.size !== s2.size)
return false;
for (const p of s1) {
if (!s2.has(p))
return false;
}
return true;
}
/**
* Saves us having to write `extensions: {grafast: {...}}` everywhere.
*/
function objectSpec(spec) {
const { assertStep, planType, ...rest } = spec;
const modifiedSpec = {
...rest,
...(assertStep || planType
? {
extensions: {
...spec.extensions,
grafast: {
...(assertStep ? { assertStep } : null),
...(planType ? { planType } : null),
...spec.extensions?.grafast,
},
},
}
: null),
fields: () => {
const fields = typeof spec.fields === "function" ? spec.fields() : spec.fields;
const modifiedFields = Object.keys(fields).reduce((o, key) => {
o[key] = objectFieldSpec(fields[key], `${spec.name}.${key}`);
return o;
}, {});
return modifiedFields;
},
};
return modifiedSpec;
}
/**
* @remarks This is a mess because the first two generics need to be specified manually, but the latter one we want inferred.
*/
function newObjectTypeBuilder(assertStep) {
return (spec) => new GraphQLObjectType(objectSpec({ assertStep, ...spec }));
}
/**
* Saves us having to write `extensions: {grafast: {...}}` everywhere.
*/
function objectFieldSpec(grafastSpec, path) {
const { plan, subscribePlan, args, ...spec } = grafastSpec;
assertNotAsync(plan, `${path ?? "?"}.plan`);
assertNotAsync(subscribePlan, `${path ?? "?"}.subscribePlan`);
const argsWithExtensions = args
? Object.keys(args).reduce((memo, argName) => {
const grafastArgSpec = args[argName];
// TODO: remove this code
if (grafastArgSpec.inputPlan ||
grafastArgSpec.autoApplyAfterParentPlan ||
grafastArgSpec.autoApplyAfterParentSubscribePlan) {
throw new Error(`Argument at ${path} has inputPlan or autoApplyAfterParentPlan or autoApplyAfterParentSubscribePlan set; these properties no longer do anything and should be removed.`);
}
const { applyPlan, applySubscribePlan, ...argSpec } = grafastArgSpec;
assertNotAsync(applyPlan, `${path ?? "?"}(${argName}:).applyPlan`);
assertNotAsync(applySubscribePlan, `${path ?? "?"}(${argName}:).applySubscribePlan`);
memo[argName] = {
...argSpec,
...(applyPlan || applySubscribePlan
? {
extensions: {
...argSpec.extensions,
grafast: {
...argSpec.extensions?.grafast,
...(applyPlan ? { applyPlan } : null),
...(applySubscribePlan ? { applySubscribePlan } : null),
},
},
}
: null),
};
return memo;
}, Object.create(null))
: {};
return {
...spec,
args: argsWithExtensions,
...(plan || subscribePlan
? {
extensions: {
...spec.extensions,
grafast: {
...spec.extensions?.grafast,
...(plan ? { plan } : null),
...(subscribePlan ? { subscribePlan } : null),
},
},
}
: null),
};
}
/**
* "Constrainted identity function" for field configs.
*
* @see {@link https://kentcdodds.com/blog/how-to-write-a-constrained-identity-function-in-typescript}
*/
function newGrafastFieldConfigBuilder() {
return (config) => config;
}
function inputObjectSpec(spec) {
const modifiedSpec = {
...spec,
fields: () => {
const fields = typeof spec.fields === "function" ? spec.fields() : spec.fields;
const modifiedFields = Object.keys(fields).reduce((o, key) => {
o[key] = inputObjectFieldSpec(fields[key], `${spec.name}.${key}`);
return o;
}, {});
return modifiedFields;
},
};
return modifiedSpec;
}
function newInputObjectTypeBuilder() {
return (spec) => new GraphQLInputObjectType(inputObjectSpec(spec));
}
/**
* Saves us having to write `extensions: {grafast: {...}}` everywhere.
*/
function inputObjectFieldSpec(grafastSpec, path) {
// TODO: remove this code
if (grafastSpec.applyPlan ||
grafastSpec.inputPlan ||
grafastSpec.autoApplyAfterParentApplyPlan ||
grafastSpec.autoApplyAfterParentInputPlan) {
throw new Error(`Input field at ${path} has applyPlan or inputPlan or autoApplyAfterParentApplyPlan or autoApplyAfterParentInputPlan set; these properties no longer do anything and should be removed.`);
}
const { apply, ...spec } = grafastSpec;
assertNotAsync(apply, `${path ?? "?"}.apply`);
return apply
? {
...spec,
extensions: {
...spec.extensions,
grafast: {
...spec.extensions?.grafast,
apply,
},
},
}
: spec;
}
const $$valueConfigByValue = Symbol("valueConfigByValue");
function getEnumValueConfigs(enumType) {
// We cache onto the enumType directly so that garbage collection can clear up after us easily.
if (enumType[$$valueConfigByValue] === undefined) {
const config = enumType.toConfig();
enumType[$$valueConfigByValue] = Object.values(config.values).reduce((memo, value) => {
memo[value.value] = value;
return memo;
}, Object.create(null));
}
return enumType[$$valueConfigByValue];
}
/**
* This would be equivalent to `enumType._valueLookup.get(outputValue)` except
* that's not a public API so we have to do a bit of heavy lifting here. Since
* it is heavy lifting, we cache the result, but we don't know when enumType
* will go away so we use a weakmap.
*/
function getEnumValueConfig(enumType, outputValue) {
return getEnumValueConfigs(enumType)[outputValue];
}
/**
* It's a peculiarity of V8 that `{}` is twice as fast as
* `Object.create(null)`, but `Object.create(sharedNull)` is the same speed as
* `{}`. Hat tip to `@purge` for bringing this to my attention.
*
* @internal
*/
exports.sharedNull = Object.freeze(Object.create(null));
/**
* Prints out the stack trace to the current position with a message; useful
* for debugging which code path has hit this line.
*
* @internal
*/
function stack(message, length = 4) {
try {
throw new Error(message);
}
catch (e) {
const lines = e.stack.split("\n");
const start = lines.findIndex((line) => line.startsWith("Error:"));
if (start < 0) {
console.dir(e.stack);
return;
}
const filtered = [
lines[start],
...lines.slice(start + 2, start + 2 + length),
];
const mapped = filtered.map((line) => line.replace(/^(.*?)\(\/home[^)]*\/packages\/([^)]*)\)/, (_, start, rest) => `${start}${" ".repeat(Math.max(0, 45 - start.length))}(${rest})`));
console.log(mapped.join("\n"));
}
}
/**
* Ridiculously, this is faster than `new Array(length).fill(fill)`
*/
function arrayOfLength(length, fill) {
const arr = [];
for (let i = 0; i < length; i++) {
arr[i] = fill;
}
return arr;
}
/**
* Builds an array of length `length` calling `fill` for each entry in the
* list and storing the result.
*
* @internal
*/
function arrayOfLengthCb(length, fill) {
const arr = [];
for (let i = 0; i < length; i++) {
arr[i] = fill();
}
return arr;
}
exports.valueNodeToStaticValue = graphql.valueFromAST;
function findVariableNamesUsedInValueNode(valueNode, variableNames) {
switch (valueNode.kind) {
case Kind.INT:
case Kind.FLOAT:
case Kind.STRING:
case Kind.BOOLEAN:
case Kind.NULL:
case Kind.ENUM: {
// Static -> no variables
return;
}
case Kind.LIST: {
for (const value of valueNode.values) {
findVariableNamesUsedInValueNode(value, variableNames);
}
return;
}
case Kind.OBJECT: {
for (const field of valueNode.fields) {
findVariableNamesUsedInValueNode(field.value, variableNames);
}
return;
}
case Kind.VARIABLE: {
variableNames.add(valueNode.name.value);
return;
}
default: {
const never = valueNode;
throw new Error(`Unsupported valueNode: ${JSON.stringify(never)}`);
}
}
}
function findVariableNamesUsedInDirectives(directives, variableNames) {
if (directives !== undefined) {
for (const dir of directives) {
if (dir.arguments !== undefined) {
for (const arg of dir.arguments) {
findVariableNamesUsedInValueNode(arg.value, variableNames);
}
}
}
}
}
function findVariableNamesUsedInArguments(args, variableNames) {
if (args !== undefined) {
for (const arg of args) {
findVariableNamesUsedInValueNode(arg.value, variableNames);
}
}
}
function findVariableNamesUsedInSelectionNode(operationPlan, selection, variableNames) {
findVariableNamesUsedInDirectives(selection.directives, variableNames);
switch (selection.kind) {
case Kind.FIELD: {
findVariableNamesUsedInFieldNode(operationPlan, selection, variableNames);
return;
}
case Kind.INLINE_FRAGMENT: {
findVariableNamesUsedInDirectives(selection.directives, variableNames);
for (const innerSelection of selection.selectionSet.selections) {
findVariableNamesUsedInSelectionNode(operationPlan, innerSelection, variableNames);
}
return;
}
case Kind.FRAGMENT_SPREAD: {
findVariableNamesUsedInDirectives(selection.directives, variableNames);
const fragmentName = selection.name.value;
const fragment = operationPlan.fragments[fragmentName];
findVariableNamesUsedInDirectives(fragment.directives, variableNames);
if (fragment.variableDefinitions?.length) {
throw new error_js_1.SafeError("Grafast doesn't support variable definitions on fragments yet.");
}
for (const innerSelection of fragment.selectionSet.selections) {
findVariableNamesUsedInSelectionNode(operationPlan, innerSelection, variableNames);
}
return;
}
default: {
const never = selection;
throw new Error(`Unsupported selection ${never.kind}`);
}
}
}
function findVariableNamesUsedInFieldNode(operationPlan, field, variableNames) {
findVariableNamesUsedInArguments(field.arguments, variableNames);
findVariableNamesUsedInDirectives(field.directives, variableNames);
if (field.selectionSet !== undefined) {
for (const selection of field.selectionSet.selections) {
findVariableNamesUsedInSelectionNode(operationPlan, selection, variableNames);
}
}
}
/**
* Given a FieldNode, recursively walks and finds all the variable references,
* returning a list of the (unique) variable names used.
*/
function findVariableNamesUsed(operationPlan, field) {
const variableNames = new Set();
findVariableNamesUsedInFieldNode(operationPlan, field, variableNames);
return [...variableNames].sort();
}
function isTypePlanned(schema, namedType) {
if (namedType instanceof GraphQLObjectType) {
return !!namedType.extensions?.grafast?.assertStep;
}
else if (namedType instanceof GraphQLUnionType ||
namedType instanceof GraphQLInterfaceType) {
const types = namedType instanceof GraphQLUnionType
? namedType.getTypes()
: schema.getImplementations(namedType).objects;
let firstHadPlan = null;
let i = 0;
for (const type of types) {
const hasPlan = !!type.extensions?.grafast?.assertStep;
if (firstHadPlan === null) {
firstHadPlan = hasPlan;
}
else if (hasPlan !== firstHadPlan) {
// ENHANCE: validate this at schema build time
throw new Error(`The '${namedType.name}' interface or union type's first type '${types[0]}' ${firstHadPlan ? "expected a plan" : "did not expect a plan"}, however the type '${type}' (index = ${i}) ${hasPlan ? "expected a plan" : "did not expect a plan"}. All types in an interface or union must be in agreement about whether a plan is expected or not.`);
}
i++;
}
return !!firstHadPlan;
}
else {
return false;
}
}
/**
* Make protected/private methods accessible.
*
* @internal
*/
function sudo(obj) {
return obj;
}
/**
* We want everything else to treat things like `dependencies` as read only,
* however we ourselves want to be able to write to them, so we can use
* writeable for this.
*
* @internal
*/
function writeableArray(a) {
return a;
}
/**
* Returns `true` if the first argument depends on the second argument either
* directly or indirectly (via a chain of dependencies).
*/
function stepADependsOnStepB(stepA, stepB, sansSideEffects = false) {
if (stepA === stepB) {
throw new Error("Invalid call to stepADependsOnStepB");
}
// PERF: bredth-first might be better.
// PERF: we can stop looking once we pass a certain layerPlan boundary.
// PERF: maybe some form of caching here would be sensible?
// Depth-first search for match
for (const dep of sudo(stepA).dependencies) {
if (dep === stepB) {
return true;
}
if (sansSideEffects &&
dep.implicitSideEffectStep &&
dep.implicitSideEffectStep !== stepB.implicitSideEffectStep) {
return false;
}
if (stepADependsOnStepB(dep, stepB)) {
return true;
}
}
if (stepA.implicitSideEffectStep) {
if (stepA.implicitSideEffectStep === stepB)
return true;
return stepADependsOnStepB(stepA.implicitSideEffectStep, stepB);
}
else {
return false;
}
}
function stepAIsOrDependsOnStepB(stepA, stepB) {
return stepA === stepB || stepADependsOnStepB(stepA, stepB);
}
/**
* Returns true if stepA is allowed to depend on stepB, false otherwise. (This
* mostly relates to heirarchy.)
*/
function stepAMayDependOnStepB($a, $b) {
if ($a.isFinalized) {
return false;
}
if ($a._isUnaryLocked && $a._isUnary && !$b._isUnary) {
return false;
}
if (!$a.layerPlan.ancestry.includes($b.layerPlan)) {
return false;
}
return !stepADependsOnStepB($b, $a);
}
function stepAShouldTryAndInlineIntoStepB($a, $b) {
if ($a.implicitSideEffectStep !== $b.implicitSideEffectStep) {
return false;
}
// If there's any side effects in the path, reject
if (dev_js_1.isDev && !stepADependsOnStepB($a, $b)) {
throw new Error(`Shouldn't try and inline into something you're not dependent on!`);
}
if (!stepsAreInSamePhase($b, $a))
return false;
// TODO: review the rules about polymorphism here; e.g. "only if most of the
// polymorphic paths are covered" or something. We don't want the parent to
// do lots of work for lots of polymorphic paths that won't be covered, but
// equally we don't want to necessarily require 100% of the polymorphic
// branches to be matched.
const paths = pathsFromAncestorToTargetLayerPlan($b.layerPlan, $a.layerPlan);
let path;
if (paths.length === 0) {
throw new Error(`No path from ${$a} back to ${$b}?`);
}
else if (paths.length > 1) {
const commonPath = [];
const firstPath = paths[0];
for (const lp of firstPath) {
if (paths.every((p) => p.includes(lp))) {
commonPath.push(lp);
}
}
path = commonPath;
}
else {
path = paths[0];
}
for (const lp of path) {
if (lp.reason.type === "polymorphicPartition") {
return false;
}
}
// Don't go past any side effects
if (!stepADependsOnStepB($a, $b, true)) {
return false;
}
return true;
}
function pathsFromAncestorToTargetLayerPlan(ancestor, lp) {
if (lp === ancestor) {
// One path, and it's the null path - stay where you are.
return [[]];
}
if (lp.reason.type === "root") {
// No paths found - lp doesn't inherit from ancestor.
return [];
}
else if (lp.reason.type === "combined") {
const childPaths = lp.reason.parentLayerPlans.flatMap((plp) => pathsFromAncestorToTargetLayerPlan(ancestor, plp));
for (const path of childPaths) {
path.push(lp);
}
return childPaths;
}
else {
const childPaths = pathsFromAncestorToTargetLayerPlan(ancestor, lp.reason.parentLayerPlan);
for (const path of childPaths) {
path.push(lp);
}
return childPaths;
}
}
function layerPlanHeirarchyContains(lp, targetLp) {
if (lp === targetLp)
return true;
if (lp.reason.type === "root") {
return false;
}
else if (lp.reason.type === "combined") {
return lp.reason.parentLayerPlans.some((plp) => layerPlanHeirarchyContains(plp, targetLp));
}
else {
// PERF: loop would be faster than recursion
return layerPlanHeirarchyContains(lp.reason.parentLayerPlan, targetLp);
}
}
/**
* For a regular GraphQL query with no `@stream`/`@defer`, the entire result is
* calculated and then the output is generated and sent to the client at once.
* Thus you can think of this as every plan is in the same "phase".
*
* However, if you introduce a `@stream`/`@defer` selection, then the steps
* inside that selection should run _later_ than the steps in the parent
* selection - they should run in two different phases. Similar is true for
* subscriptions.
*
* When optimizing your plans, if you are not careful you may end up pushing
* what should be later work into the earlier phase, resulting in the initial
* payload being delayed whilst things that should have been deferred are being
* calculated. Thus, you should generally check that two plans are in the same phase
* before you try and merge them.
*
* This is not a strict rule, though, because sometimes it makes more sense to
* push work into the parent phase because it would be faster overall to do
* that work there, and would not significantly delay the initial payload's
* execution time - for example it's unlikely that it would make sense to defer
* selecting an additional boolean column from a database table even if the
* operation indicates that's what you should do.
*
* As a step class author, it's your responsiblity to figure out the right
* approach. Once you have, you can use this function to help you, should you
* need it.
*/
function stepsAreInSamePhase(ancestor, descendent) {
if (dev_js_1.isDev && !stepADependsOnStepB(descendent, ancestor)) {
throw new Error(`Shouldn't try and inline into something you're not dependent on!`);
}
const ancestorDepth = ancestor.layerPlan.depth;
const descendentDepth = descendent.layerPlan.depth;
if (descendentDepth < ancestorDepth) {
throw new Error(`descendent is deeper than ancestor; did you pass ancestor/descendent the wrong way around?`);
}
const descDeferBoundary = descendent.layerPlan.ancestry[descendent.layerPlan.deferBoundaryDepth];
if (ancestor.layerPlan.ancestry[ancestor.layerPlan.deferBoundaryDepth] !==
descDeferBoundary) {
// Still possible to be okay if ancestor is the source of a streamed list item or deferred step
if (descDeferBoundary.reason.type === "listItem" &&
descDeferBoundary.reason.stream != null &&
descendent.layerPlan.ancestry[descendent.layerPlan.deferBoundaryDepth - 1] === ancestor.layerPlan) {
if (stepAIsOrDependsOnStepB(descDeferBoundary.reason.parentStep, ancestor)) {
return true;
}
}
// Nope, don't allow
return false;
}
for (let i = 0; i < ancestorDepth; i++) {
if (ancestor.layerPlan.ancestry[i] !== descendent.layerPlan.ancestry[i]) {
return false;
}
}
for (let i = ancestorDepth + 1; i < descendentDepth; i++) {
const currentLayerPlan = descendent.layerPlan.ancestry[i];
const t = currentLayerPlan.reason.type;
switch (t) {
case "combined": {
continue;
}
case "subscription":
case "defer": {
// These indicate boundaries over which plans shouldn't be optimized
// together (generally).
return false;
}
case "listItem": {
if (currentLayerPlan.reason.stream) {
// It's streamed, but we can still inline the step into its parent since its the parent that is being streamed (so it should not add to the initial execution overhead).
// OPTIMIZE: maybe we should only allow this if the parent actually has `stream` support, and disable it otherwise?
continue;
}
else {
continue;
}
}
case "root":
case "nullableBoundary":
case "subroutine":
case "polymorphic":
case "polymorphicPartition":
case "mutationField": {
continue;
}
default: {
const never = t;
throw new Error(`Unhandled layer plan type '${never}'`);
}
}
}
return true;
}
function isPhaseTransitionLayerPlan(layerPlan) {
const t = layerPlan.reason.type;
switch (t) {
case "subscription":
case "defer": {
return true;
}
case "listItem": {
if (layerPlan.reason.stream) {
return true;
}
else {
return false;
}
}
case "polymorphic":
case "polymorphicPartition":
case "root":
case "nullableBoundary":
case "subroutine":
case "combined": // TODO: CHECK ME!
case "mutationField": {
return false;
}
default: {
const never = t;
throw new Error(`Unhandled layer plan type '${never}'`);
}
}
}
// ENHANCE: implement this!
const canonicalJSONStringify = (o) => JSON.stringify(o);
exports.canonicalJSONStringify = canonicalJSONStringify;
// PERF: only do this if isDev; otherwise replace with NOOP?
function assertNotAsync(fn, name) {
if (fn?.constructor?.name === "AsyncFunction") {
throw new Error(`Plans must be synchronous, but this schema has an async function at '${name}': ${fn.toString()}`);
}
}
// PERF: only do this if isDev; otherwise replace with NOOP?
function assertNotPromise(value, fn, name) {
if (isPromiseLike(value)) {
throw new Error(`Plans must be synchronous, but this schema has an function at '${name}' that returned a promise-like object: ${fn.toString()}`);
}
return value;
}
function hasItemPlan(step) {
return "itemPlan" in step && typeof step.itemPlan === "function";
}
function exportNameHint(obj, nameHint) {
if ((typeof obj === "object" && obj != null) || typeof obj === "function") {
if (!("$exporter$name" in obj)) {
Object.defineProperty(obj, "$exporter$name", {
writable: true,
value: nameHint,
});
}
else if (!obj.$exporter$name) {
obj.$exporter$name = nameHint;
}
}
}
function isTuple(t) {
return Array.isArray(t);
}
/**
* Turns an array of keys into a digest, avoiding conflicts.
* Symbols are treated as equivalent. (Theoretically faster
* than JSON.stringify().)
*/
function digestKeys(keys) {
let str = "";
for (let i = 0, l = keys.length; i < l; i++) {
const item = keys[i];
if (typeof item === "string") {
// str += `|§${item.replace(/§/g, "§§")}§`;
str += `§${item.length}:${item}`;
}
else if (typeof item === "number") {
str += `N${item}`;
}
else {
str += "!";
}
}
return str;
}
/**
* If the directive has the argument `argName`, return a step representing that
* arguments value, whether that be a step representing the relevant variable
* or a constant step representing the hardcoded value in the document.
*
* @remarks NOT SUITABLE FOR USAGE WITH LISTS OR OBJECTS! Does not evaluate
* internal variable usages e.g. `[1, $b, 3]`
*/
function directiveArgument(operationPlan, directive, argName, expectedKind) {
const arg = directive.arguments?.find((n) => n.name.value === argName);
if (!arg)
return undefined;
const val = arg.value;
return val.kind === graphql.Kind.VARIABLE
? operationPlan.variableValuesStep.get(val.name.value)
: val.kind === expectedKind
? (0, constant_js_1.constant)(val.kind === Kind.INT
? parseInt(val.value, 10)
: val.kind === Kind.FLOAT
? parseFloat(val.value)
: // boolean, string
val.value)
: undefined;
}
function stableStringSort(a, z) {
return a < z ? -1 : a > z ? 1 : 0;
}
/**
* Sorts tuples by a string sort of their first entry - useful for
* `Object.fromEntries(Object.entries(...).sort(stableStringSortFirstTupleEntry))`
*/
function stableStringSortFirstTupleEntry(a, z) {
return a[0] < z[0] ? -1 : a[0] > z[0] ? 1 : 0;
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
exports.sleep = sleep;
// Save on garbage collection by just using this promise for everything
const DONE_PROMISE = Promise.resolve({
done: true,
value: undefined,
});
/**
* Returns a new version of `iterable` that calls `callback()` on termination,
* **even if `next()` is never called**.
*
* @experimental
*/
function asyncIteratorWithCleanup(iterable, callback) {
let iterator = null;
let done = false;
function cleanup(e) {
if (!done) {
done = true;
callback(e);
}
}
function checkDone(result) {
if (done)
return;
if (result.done)
cleanup();
}
return {
[Symbol.asyncIterator]() {
iterator ??= iterable[Symbol.asyncIterator]();
return this;
},
[Symbol.asyncDispose]() {
iterator ??= iterable[Symbol.asyncIterator]();
cleanup();
return iterator[Symbol.asyncDispose]?.() ?? Promise.resolve();
},
return(value) {
iterator ??= iterable[Symbol.asyncIterator]();
cleanup();
return iterator.return?.(value) ?? DONE_PROMISE;
},
throw(e) {
iterator ??= iterable[Symbol.asyncIterator]();
cleanup(e);
return iterator.throw?.(e) ?? DONE_PROMISE;
},
next() {
iterator ??= iterable[Symbol.asyncIterator]();
const result = iterator.next();
result.then(checkDone, cleanup);
return result;
},
};
}
function terminateIterable(iterable) {
if ("return" in iterable && typeof iterable.return === "function") {
iterable.return();
}
}
exports.GraphQLSpecifiedErrorBehaviors = Object.freeze([
"PROPAGATE",
"NULL",
"HALT",
]);
//# sourceMappingURL=utils.js.map