@apollo/query-planner
Version:
Apollo Query Planner
1,064 lines • 144 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueryPlanner = exports.compareOptionsComplexityOutOfContext = void 0;
const federation_internals_1 = require("@apollo/federation-internals");
const query_graphs_1 = require("@apollo/query-graphs");
const graphql_1 = require("graphql");
const conditions_1 = require("./conditions");
const config_1 = require("./config");
const generateAllPlans_1 = require("./generateAllPlans");
const QueryPlan_1 = require("./QueryPlan");
const recursiveSelectionsLimit_1 = require("./recursiveSelectionsLimit");
const debug = (0, federation_internals_1.newDebugLogger)('plan');
const SIBLING_TYPENAME_KEY = 'sibling_typename';
const fetchCost = 1000;
const pipeliningCost = 100;
const defaultCostFunction = {
onFetchGroup: (group) => (fetchCost + group.cost()),
onConditions: (_, value) => value,
reduceParallel: (values) => parallelCost(values),
reduceSequence: (values) => sequenceCost(values),
reduceDeferred(_, value) {
return value;
},
reduceDefer(nonDeferred, _, deferredValues) {
return sequenceCost([nonDeferred, parallelCost(deferredValues)]);
},
};
function parallelCost(values) {
return sum(values);
}
function sequenceCost(stages) {
return stages.reduceRight((acc, stage, idx) => (acc + (Math.max(1, idx * pipeliningCost) * stage)), 0);
}
function closedPathToString(p) {
const pathStr = (0, query_graphs_1.simultaneousPathsToString)(p.paths);
return p.selection ? `${pathStr} -> ${p.selection}` : pathStr;
}
function flattenClosedPath(p) {
return p.paths.map((path) => ({ path, selection: p.selection }));
}
function allTailVertices(options) {
const vertices = new Set();
for (const option of options) {
for (const path of option.paths) {
vertices.add(path.tail);
}
}
return vertices;
}
function selectionIsFullyLocalFromAllVertices(selection, vertices, inconsistentAbstractTypesRuntimes) {
let _useInconsistentAbstractTypes = undefined;
const useInconsistentAbstractTypes = () => {
if (_useInconsistentAbstractTypes === undefined) {
_useInconsistentAbstractTypes = selection.some((elt) => elt.kind === 'FragmentElement' && !!elt.typeCondition && inconsistentAbstractTypesRuntimes.has(elt.typeCondition.name));
}
return _useInconsistentAbstractTypes;
};
for (const vertex of vertices) {
if (vertex.hasReachableCrossSubgraphEdges
|| !(0, federation_internals_1.isCompositeType)(vertex.type)
|| !selection.canRebaseOn(vertex.type)
|| useInconsistentAbstractTypes()) {
return false;
}
}
return true;
}
function compareOptionsComplexityOutOfContext(opt1, opt2) {
if (opt1.length === 1) {
if (opt2.length === 1) {
return compareSinglePathOptionsComplexityOutOfContext(opt1[0], opt2[0]);
}
else {
return compareSingleVsMultiPathOptionsComplexityOutOfContext(opt1[0], opt2);
}
}
else if (opt2.length === 1) {
return -compareSingleVsMultiPathOptionsComplexityOutOfContext(opt2[0], opt1);
}
return 0;
}
exports.compareOptionsComplexityOutOfContext = compareOptionsComplexityOutOfContext;
function compareSinglePathOptionsComplexityOutOfContext(p1, p2) {
if (p1.tail.source !== p2.tail.source) {
const { thisJumps: p1Jumps, thatJumps: p2Jumps } = p1.countSubgraphJumpsAfterLastCommonVertex(p2);
if (p1Jumps === 0 && p2Jumps > 0) {
return -1;
}
else if (p1Jumps > 0 && p2Jumps === 0) {
return 1;
}
else {
return 0;
}
}
return 0;
}
function compareSingleVsMultiPathOptionsComplexityOutOfContext(p1, p2s) {
for (const p2 of p2s) {
if (compareSinglePathOptionsComplexityOutOfContext(p1, p2) >= 0) {
return 0;
}
}
return -1;
}
class QueryPlanningTraversal {
constructor(parameters, selectionSet, startFetchIdGen, hasDefers, rootKind, costFunction, initialContext, typeConditionedFetching, nonLocalSelectionsState, excludedDestinations = [], excludedConditions = []) {
var _a;
this.parameters = parameters;
this.startFetchIdGen = startFetchIdGen;
this.hasDefers = hasDefers;
this.rootKind = rootKind;
this.costFunction = costFunction;
this.closedBranches = [];
const { root, federatedQueryGraph } = parameters;
this.typeConditionedFetching = typeConditionedFetching || false;
this.isTopLevel = (0, query_graphs_1.isRootVertex)(root);
this.optionsLimit = (_a = parameters.config.debug) === null || _a === void 0 ? void 0 : _a.pathsLimit;
this.conditionResolver = (0, query_graphs_1.cachingConditionResolver)((edge, context, excludedEdges, excludedConditions, extras) => this.resolveConditionPlan(edge, context, excludedEdges, excludedConditions, extras));
const initialPath = query_graphs_1.GraphPath.create(federatedQueryGraph, root);
const initialOptions = (0, query_graphs_1.createInitialOptions)(initialPath, initialContext, this.conditionResolver, excludedDestinations, excludedConditions, parameters.overrideConditions);
this.stack = mapOptionsToSelections(selectionSet, initialOptions);
if (this.parameters.federatedQueryGraph.nonLocalSelectionsMetadata
&& nonLocalSelectionsState) {
if (this.parameters.federatedQueryGraph.nonLocalSelectionsMetadata
.checkNonLocalSelectionsLimitExceededAtRoot(this.stack, nonLocalSelectionsState, this.parameters.supergraphSchema, this.parameters.inconsistentAbstractTypesRuntimes, this.parameters.overrideConditions)) {
throw Error(`Number of non-local selections exceeds limit of ${query_graphs_1.NonLocalSelectionsMetadata.MAX_NON_LOCAL_SELECTIONS}`);
}
}
}
debugStack() {
if (this.isTopLevel && debug.enabled) {
debug.group('Query planning open branches:');
for (const [selection, options] of this.stack) {
debug.groupedValues(options, opt => `${(0, query_graphs_1.simultaneousPathsToString)(opt)}`, `${selection}:`);
}
debug.groupEnd();
}
}
findBestPlan() {
while (this.stack.length > 0) {
this.debugStack();
const [selection, options] = this.stack.pop();
this.handleOpenBranch(selection, options);
}
this.computeBestPlanFromClosedBranches();
return this.bestPlan;
}
recordClosedBranch(closed) {
const maybeTrimmed = this.maybeEliminateStrictlyMoreCostlyPaths(closed);
debug.log(() => `Closed branch has ${maybeTrimmed.length} options (eliminated ${closed.length - maybeTrimmed.length} that could be proved as unecessary)`);
this.closedBranches.push(maybeTrimmed);
}
handleOpenBranch(selection, options) {
const operation = selection.element;
debug.group(() => `Handling open branch: ${operation}`);
let newOptions = [];
for (const option of options) {
const followupForOption = (0, query_graphs_1.advanceSimultaneousPathsWithOperation)(this.parameters.supergraphSchema, option, operation, this.parameters.overrideConditions);
if (!followupForOption) {
continue;
}
if (followupForOption.length === 0) {
if (operation.kind === 'FragmentElement') {
this.recordClosedBranch(options.map((o) => ({
paths: o.paths.map(p => (0, query_graphs_1.terminateWithNonRequestedTypenameField)(p, this.parameters.overrideConditions))
})));
}
debug.groupEnd(() => `Terminating branch with no possible results`);
return;
}
newOptions = newOptions.concat(followupForOption);
if (this.optionsLimit && newOptions.length > this.optionsLimit) {
throw new Error(`Too many options generated for ${selection}, reached the limit of ${this.optionsLimit}`);
}
}
if (newOptions.length === 0) {
if (this.isTopLevel) {
debug.groupEnd(() => `No valid options to advance ${selection} from ${(0, query_graphs_1.advanceOptionsToString)(options)}`);
throw new Error(`Was not able to find any options for ${selection}: This shouldn't have happened.`);
}
else {
this.stack.splice(0, this.stack.length);
this.closedBranches.splice(0, this.closedBranches.length);
debug.groupEnd(() => `No possible plan for ${selection} from ${(0, query_graphs_1.advanceOptionsToString)(options)}; terminating condition`);
return;
}
}
if (selection.selectionSet) {
const allTails = allTailVertices(newOptions);
if (selectionIsFullyLocalFromAllVertices(selection.selectionSet, allTails, this.parameters.inconsistentAbstractTypesRuntimes)
&& !selection.hasDefer()) {
const selectionSet = addTypenameFieldForAbstractTypes(addBackTypenameInAttachments(selection.selectionSet));
this.recordClosedBranch(newOptions.map((opt) => ({ paths: opt.paths, selection: selectionSet })));
}
else {
for (const branch of mapOptionsToSelections(selection.selectionSet, newOptions)) {
this.stack.push(branch);
}
}
debug.groupEnd();
}
else {
this.recordClosedBranch(newOptions.map((opt) => ({ paths: opt.paths })));
debug.groupEnd(() => `Branch finished`);
}
}
maybeEliminateStrictlyMoreCostlyPaths(branch) {
if (branch.length <= 1) {
return branch;
}
const toHandle = branch.concat();
const keptOptions = [];
while (toHandle.length >= 2) {
const first = toHandle[0];
let shouldKeepFirst = true;
for (let i = toHandle.length - 1; i >= 1; i--) {
const other = toHandle[i];
const cmp = compareOptionsComplexityOutOfContext(first.paths, other.paths);
if (cmp < 0) {
toHandle.splice(i, 1);
}
else if (cmp > 0) {
toHandle.splice(0, 1);
shouldKeepFirst = false;
break;
}
}
if (shouldKeepFirst) {
keptOptions.push(first);
toHandle.splice(0, 1);
}
}
if (toHandle.length > 0) {
keptOptions.push(toHandle[0]);
}
return keptOptions;
}
newDependencyGraph() {
const { supergraphSchema, federatedQueryGraph } = this.parameters;
const rootType = this.isTopLevel && this.hasDefers ? supergraphSchema.schemaDefinition.rootType(this.rootKind) : undefined;
return FetchDependencyGraph.create(supergraphSchema, federatedQueryGraph, this.startFetchIdGen, rootType, this.parameters.config.generateQueryFragments);
}
reorderFirstBranch() {
const firstBranch = this.closedBranches[0];
let i = 1;
while (i < this.closedBranches.length && this.closedBranches[i].length > firstBranch.length) {
i++;
}
this.closedBranches[0] = this.closedBranches[i - 1];
this.closedBranches[i - 1] = firstBranch;
}
sortOptionsInClosedBranches() {
this.closedBranches.forEach((branch) => branch.sort((p1, p2) => {
const p1Jumps = Math.max(...p1.paths.map((p) => p.subgraphJumps()));
const p2Jumps = Math.max(...p2.paths.map((p) => p.subgraphJumps()));
return p1Jumps - p2Jumps;
}));
}
computeBestPlanFromClosedBranches() {
if (this.closedBranches.length === 0) {
return;
}
this.sortOptionsInClosedBranches();
this.closedBranches.sort((b1, b2) => b1.length > b2.length ? -1 : (b1.length < b2.length ? 1 : 0));
let planCount = possiblePlans(this.closedBranches);
debug.log(() => `Query has ${planCount} possible plans`);
let firstBranch = this.closedBranches[0];
const maxPlansToCompute = this.parameters.config.debug.maxEvaluatedPlans;
while (planCount > maxPlansToCompute && firstBranch.length > 1) {
const prevSize = BigInt(firstBranch.length);
firstBranch.pop();
planCount -= planCount / prevSize;
this.reorderFirstBranch();
firstBranch = this.closedBranches[0];
debug.log(() => `Reduced plans to consider to ${planCount} plans`);
}
if (this.parameters.statistics && this.isTopLevel) {
this.parameters.statistics.evaluatedPlanCount += Number(planCount);
}
debug.log(() => `All branches:${this.closedBranches.map((opts, i) => `\n${i}:${opts.map((opt => `\n - ${closedPathToString(opt)}`))}`)}`);
let idxFirstOfLengthOne = 0;
while (idxFirstOfLengthOne < this.closedBranches.length && this.closedBranches[idxFirstOfLengthOne].length > 1) {
idxFirstOfLengthOne++;
}
let initialTree;
let initialDependencyGraph;
const { federatedQueryGraph, root } = this.parameters;
if (idxFirstOfLengthOne === this.closedBranches.length) {
initialTree = query_graphs_1.PathTree.createOp(federatedQueryGraph, root);
initialDependencyGraph = this.newDependencyGraph();
}
else {
const singleChoiceBranches = this
.closedBranches
.slice(idxFirstOfLengthOne)
.flat()
.map((cp) => flattenClosedPath(cp))
.flat();
initialTree = query_graphs_1.PathTree.createFromOpPaths(federatedQueryGraph, root, singleChoiceBranches);
initialDependencyGraph = this.updatedDependencyGraph(this.newDependencyGraph(), initialTree);
if (idxFirstOfLengthOne === 0) {
this.bestPlan = [initialDependencyGraph, initialTree, this.cost(initialDependencyGraph)];
return;
}
}
const otherTrees = this
.closedBranches
.slice(0, idxFirstOfLengthOne)
.map(b => b.map(opt => query_graphs_1.PathTree.createFromOpPaths(federatedQueryGraph, root, flattenClosedPath(opt))));
const { best, cost } = (0, generateAllPlans_1.generateAllPlansAndFindBest)({
initial: { graph: initialDependencyGraph, tree: initialTree },
toAdd: otherTrees,
addFct: (p, t) => {
const updatedDependencyGraph = p.graph.clone();
this.updatedDependencyGraph(updatedDependencyGraph, t);
const updatedTree = p.tree.merge(t);
return { graph: updatedDependencyGraph, tree: updatedTree };
},
costFct: (p) => this.cost(p.graph),
onPlan: (p, cost, prevCost) => {
debug.log(() => {
if (!prevCost) {
return `Computed plan with cost ${cost}: ${p.tree}`;
}
else if (cost > prevCost) {
return `Ignoring plan with cost ${cost} (a better plan with cost ${prevCost} exists): ${p.tree}`;
}
else {
return `Found better with cost ${cost} (previous had cost ${prevCost}: ${p.tree}`;
}
});
},
});
this.bestPlan = [best.graph, best.tree, cost];
}
cost(dependencyGraph) {
const { main, deferred } = dependencyGraph.process(this.costFunction, this.rootKind);
return deferred.length === 0
? main
: this.costFunction.reduceDefer(main, dependencyGraph.deferTracking.primarySelection.get(), deferred);
}
updatedDependencyGraph(dependencyGraph, tree) {
return (0, query_graphs_1.isRootPathTree)(tree)
? computeRootFetchGroups(dependencyGraph, tree, this.rootKind, this.typeConditionedFetching)
: computeNonRootFetchGroups(dependencyGraph, tree, this.rootKind, this.typeConditionedFetching);
}
resolveConditionPlan(edge, context, excludedDestinations, excludedConditions, extraConditions) {
const bestPlan = new QueryPlanningTraversal({
...this.parameters,
root: edge.head,
}, extraConditions !== null && extraConditions !== void 0 ? extraConditions : edge.conditions, 0, false, 'query', this.costFunction, context, this.typeConditionedFetching, null, excludedDestinations, (0, query_graphs_1.addConditionExclusion)(excludedConditions, edge.conditions)).findBestPlan();
return bestPlan ? { satisfied: true, cost: bestPlan[2], pathTree: bestPlan[1] } : query_graphs_1.unsatisfiedConditionsResolution;
}
}
const conditionsMemoizer = (selectionSet) => ({ conditions: (0, conditions_1.conditionsOfSelectionSet)(selectionSet) });
class GroupInputs {
constructor(supergraphSchema) {
this.supergraphSchema = supergraphSchema;
this.usedContexts = new Map;
this.perType = new Map();
this.onUpdateCallback = undefined;
}
add(selection) {
var _a;
(0, federation_internals_1.assert)(selection.parentType.schema() === this.supergraphSchema, 'Inputs selections must be based on the supergraph schema');
const typeName = selection.parentType.name;
let typeSelection = this.perType.get(typeName);
if (!typeSelection) {
typeSelection = federation_internals_1.MutableSelectionSet.empty(selection.parentType);
this.perType.set(typeName, typeSelection);
}
typeSelection.updates().add(selection);
(_a = this.onUpdateCallback) === null || _a === void 0 ? void 0 : _a.call(this);
}
addContext(context, type) {
this.usedContexts.set(context, type);
}
addAll(other) {
for (const otherSelection of other.perType.values()) {
this.add(otherSelection.get());
}
for (const [context, type] of other.usedContexts) {
this.addContext(context, type);
}
}
selectionSets() {
return (0, federation_internals_1.mapValues)(this.perType).map((s) => s.get());
}
toSelectionSetNode(variablesDefinitions, handledConditions) {
const selectionSets = (0, federation_internals_1.mapValues)(this.perType).map((s) => (0, conditions_1.removeConditionsFromSelectionSet)(s.get(), handledConditions));
selectionSets.forEach((s) => s.validate(variablesDefinitions));
const selections = selectionSets.flatMap((sSet) => sSet.selections().map((s) => s.toSelectionNode()));
return {
kind: graphql_1.Kind.SELECTION_SET,
selections,
};
}
contains(other) {
for (const [type, otherSelection] of other.perType) {
const thisSelection = this.perType.get(type);
if (!thisSelection || !thisSelection.get().contains(otherSelection.get())) {
return false;
}
}
if (this.usedContexts.size < other.usedContexts.size) {
return false;
}
for (const [c, _] of other.usedContexts) {
if (!this.usedContexts.has(c)) {
return false;
}
}
return true;
}
equals(other) {
if (this.perType.size !== other.perType.size) {
return false;
}
for (const [type, thisSelection] of this.perType) {
const otherSelection = other.perType.get(type);
if (!otherSelection || !thisSelection.get().equals(otherSelection.get())) {
return false;
}
}
if (this.usedContexts.size !== other.usedContexts.size) {
return false;
}
for (const [c, _] of other.usedContexts) {
if (!this.usedContexts.has(c)) {
return false;
}
}
return true;
}
clone() {
const cloned = new GroupInputs(this.supergraphSchema);
for (const [type, selection] of this.perType.entries()) {
cloned.perType.set(type, selection.clone());
}
for (const [c, v] of this.usedContexts) {
cloned.usedContexts.set(c, v);
}
return cloned;
}
toString() {
const inputs = (0, federation_internals_1.mapValues)(this.perType);
if (inputs.length === 0) {
return '{}';
}
if (inputs.length === 1) {
return inputs[0].toString();
}
return '[' + inputs.join(',') + ']';
}
}
class FetchGroup {
constructor(dependencyGraph, index, subgraphName, rootKind, parentType, isEntityFetch, _selection, _inputs, _contextInputs, mergeAt, deferRef, subgraphAndMergeAtKey, cachedCost, generateQueryFragments = false, isKnownUseful = false, inputRewrites = []) {
this.dependencyGraph = dependencyGraph;
this.index = index;
this.subgraphName = subgraphName;
this.rootKind = rootKind;
this.parentType = parentType;
this.isEntityFetch = isEntityFetch;
this._selection = _selection;
this._inputs = _inputs;
this._contextInputs = _contextInputs;
this.mergeAt = mergeAt;
this.deferRef = deferRef;
this.subgraphAndMergeAtKey = subgraphAndMergeAtKey;
this.cachedCost = cachedCost;
this.generateQueryFragments = generateQueryFragments;
this.isKnownUseful = isKnownUseful;
this.inputRewrites = inputRewrites;
this._parents = [];
this._children = [];
this.mustPreserveSelection = false;
if (this._inputs) {
this._inputs.onUpdateCallback = () => {
this.isKnownUseful = false;
};
}
}
static create({ dependencyGraph, index, subgraphName, rootKind, parentType, hasInputs, mergeAt, deferRef, generateQueryFragments, }) {
var _a;
(0, federation_internals_1.assert)(parentType.schema() === dependencyGraph.subgraphSchemas.get(subgraphName), `Expected parent type ${parentType} to belong to ${subgraphName}`);
return new FetchGroup(dependencyGraph, index, subgraphName, rootKind, parentType, hasInputs, federation_internals_1.MutableSelectionSet.emptyWithMemoized(parentType, conditionsMemoizer), hasInputs ? new GroupInputs(dependencyGraph.supergraphSchema) : undefined, undefined, mergeAt, deferRef, hasInputs ? `${toValidGraphQLName(subgraphName)}-${(_a = mergeAt === null || mergeAt === void 0 ? void 0 : mergeAt.join('::')) !== null && _a !== void 0 ? _a : ''}` : undefined, undefined, generateQueryFragments);
}
cloneShallow(newDependencyGraph) {
var _a;
return new FetchGroup(newDependencyGraph, this.index, this.subgraphName, this.rootKind, this.parentType, this.isEntityFetch, this._selection.clone(), (_a = this._inputs) === null || _a === void 0 ? void 0 : _a.clone(), this._contextInputs ? this._contextInputs.map((c) => ({ ...c })) : undefined, this.mergeAt, this.deferRef, this.subgraphAndMergeAtKey, this.cachedCost, this.generateQueryFragments, this.isKnownUseful, [...this.inputRewrites]);
}
cost() {
if (!this.cachedCost) {
this.cachedCost = selectionCost(this.selection);
}
return this.cachedCost;
}
set id(id) {
(0, federation_internals_1.assert)(!this._id, () => `The id for fetch group ${this} is already set`);
this._id = id;
}
get id() {
return this._id;
}
get isTopLevel() {
return !this.mergeAt;
}
get selection() {
return this._selection.get();
}
selectionUpdates() {
this.cachedCost = undefined;
return this._selection.updates();
}
get inputs() {
return this._inputs;
}
addParents(parents) {
for (const parent of parents) {
this.addParent(parent);
}
}
addParent(parent) {
if (this.isChildOf(parent.group)) {
return;
}
(0, federation_internals_1.assert)(!parent.group.isParentOf(this), () => `Group ${parent.group} is a parent of ${this}, but the child relationship is broken`);
(0, federation_internals_1.assert)(!parent.group.isChildOf(this), () => `Group ${parent.group} is a child of ${this}: adding it as parent would create a cycle`);
this.dependencyGraph.onModification();
this._parents.push(parent);
parent.group._children.push(this);
}
removeChild(child) {
if (!this.isParentOf(child)) {
return;
}
this.dependencyGraph.onModification();
findAndRemoveInPlace((g) => g === child, this._children);
findAndRemoveInPlace((p) => p.group === this, child._parents);
}
isParentOf(maybeChild) {
return this._children.includes(maybeChild);
}
isChildOf(maybeParent) {
return !!this.parentRelation(maybeParent);
}
isDescendantOf(maybeAncestor) {
const children = Array.from(maybeAncestor.children());
while (children.length > 0) {
const child = children.pop();
if (child === this) {
return true;
}
child.children().forEach((c) => children.push(c));
}
return false;
}
isChildOfWithArtificialDependency(maybeParent) {
const relation = this.parentRelation(maybeParent);
if (!relation || !relation.path) {
return false;
}
if (!this.inputs) {
return true;
}
if (relation.path.some((elt) => elt.kind === 'Field')) {
return false;
}
return !!maybeParent.inputs && maybeParent.inputs.contains(this.inputs);
}
parentRelation(maybeParent) {
return this._parents.find(({ group }) => maybeParent === group);
}
parents() {
return this._parents;
}
parentGroups() {
return this.parents().map((p) => p.group);
}
children() {
return this._children;
}
addInputs(selection, rewrites) {
(0, federation_internals_1.assert)(this._inputs, "Shouldn't try to add inputs to a root fetch group");
this._inputs.add(selection);
if (rewrites) {
rewrites.forEach((r) => this.inputRewrites.push(r));
}
}
addInputContext(context, type) {
(0, federation_internals_1.assert)(this._inputs, "Shouldn't try to add inputs to a root fetch group");
this._inputs.addContext(context, type);
}
copyInputsOf(other) {
var _a;
if (other.inputs) {
(_a = this.inputs) === null || _a === void 0 ? void 0 : _a.addAll(other.inputs);
if (other.inputRewrites) {
other.inputRewrites.forEach((r) => {
if (!this.inputRewrites.some((r2) => r2 === r)) {
this.inputRewrites.push(r);
}
});
}
if (other._contextInputs) {
if (!this._contextInputs) {
this._contextInputs = [];
}
other._contextInputs.forEach((r) => {
if (!this._contextInputs.some((r2) => sameKeyRenamer(r, r2))) {
this._contextInputs.push(r);
}
});
}
}
}
addAtPath(path, selection) {
this.selectionUpdates().addAtPath(path, selection);
}
addSelections(selection) {
this.selectionUpdates().add(selection);
}
canMergeChildIn(child) {
var _a;
return this.deferRef === child.deferRef && !!((_a = child.parentRelation(this)) === null || _a === void 0 ? void 0 : _a.path);
}
removeInputsFromSelection() {
const inputs = this.inputs;
if (inputs) {
this.cachedCost = undefined;
const updated = inputs.selectionSets().reduce((prev, value) => prev.minus(value), this.selection);
this._selection = federation_internals_1.MutableSelectionSet.ofWithMemoized(updated, conditionsMemoizer);
}
}
isUseless() {
if (this.isKnownUseful || !this.inputs || this.mustPreserveSelection) {
return false;
}
const conditionInSupergraphIfInterfaceObject = (selection) => {
if (selection.kind === 'FragmentSelection') {
const condition = selection.element.typeCondition;
if (condition && (0, federation_internals_1.isObjectType)(condition)) {
const conditionInSupergraph = this.dependencyGraph.supergraphSchema.type(condition.name);
(0, federation_internals_1.assert)(conditionInSupergraph, () => `Type ${condition.name} should exists in the supergraph`);
if ((0, federation_internals_1.isInterfaceType)(conditionInSupergraph)) {
return conditionInSupergraph;
}
}
}
return undefined;
};
const isInterfaceTypeConditionOnInterfaceObject = (selection) => {
if (selection.kind === "FragmentSelection") {
const parentType = selection.element.typeCondition;
if (parentType && (0, federation_internals_1.isInterfaceType)(parentType)) {
return this.parents().some((p) => {
var _a;
const typeInParent = (_a = this.dependencyGraph.subgraphSchemas
.get(p.group.subgraphName)) === null || _a === void 0 ? void 0 : _a.type(parentType.name);
return typeInParent && (0, federation_internals_1.isInterfaceObjectType)(typeInParent);
});
}
}
return false;
};
const inputSelections = this.inputs.selectionSets().flatMap((s) => s.selections());
const isUseless = this.selection.selections().every((selection) => {
if (isInterfaceTypeConditionOnInterfaceObject(selection)) {
return false;
}
const conditionInSupergraph = conditionInSupergraphIfInterfaceObject(selection);
if (!conditionInSupergraph) {
return inputSelections.some((input) => input.contains(selection));
}
const implemTypeNames = conditionInSupergraph.possibleRuntimeTypes().map((t) => t.name);
const interfaceInputSelections = [];
const implementationInputSelections = [];
for (const inputSelection of inputSelections) {
(0, federation_internals_1.assert)(inputSelection.kind === 'FragmentSelection', () => `Unexpecting input selection ${inputSelection} on ${this}`);
const inputCondition = inputSelection.element.typeCondition;
(0, federation_internals_1.assert)(inputCondition, () => `Unexpecting input selection ${inputSelection} on ${this} (missing condition)`);
if (inputCondition.name == conditionInSupergraph.name) {
interfaceInputSelections.push(inputSelection);
}
else if (implemTypeNames.includes(inputCondition.name)) {
implementationInputSelections.push(inputSelection);
}
}
const subSelectionSet = selection.selectionSet;
(0, federation_internals_1.assert)(subSelectionSet, () => `Should not be here for ${selection}`);
if (interfaceInputSelections.length > 0) {
return interfaceInputSelections.some((input) => input.selectionSet.contains(subSelectionSet));
}
return implementationInputSelections.length > 0
&& implementationInputSelections.every((input) => input.selectionSet.contains(subSelectionSet));
});
this.isKnownUseful = !isUseless;
return isUseless;
}
mergeChildIn(child) {
const relationToChild = child.parentRelation(this);
(0, federation_internals_1.assert)(relationToChild, () => `Cannot merge ${child} into ${this}: the former is not a child of the latter`);
const childPathInThis = relationToChild.path;
(0, federation_internals_1.assert)(childPathInThis, () => `Cannot merge ${child} into ${this}: the path of the former into the later is unknown`);
this.mergeInInternal(child, childPathInThis);
}
canMergeSiblingIn(sibling) {
const ownParents = this.parents();
const siblingParents = sibling.parents();
return this.deferRef === sibling.deferRef
&& this.subgraphName === sibling.subgraphName
&& sameMergeAt(this.mergeAt, sibling.mergeAt)
&& ownParents.length === 1
&& siblingParents.length === 1
&& ownParents[0].group === siblingParents[0].group;
}
mergeSiblingIn(sibling) {
this.copyInputsOf(sibling);
this.mergeInInternal(sibling, []);
}
canMergeGrandChildIn(grandChild) {
var _a;
const gcParents = grandChild.parents();
if (gcParents.length !== 1) {
return false;
}
return this.deferRef === grandChild.deferRef && !!gcParents[0].path && !!((_a = gcParents[0].group.parentRelation(this)) === null || _a === void 0 ? void 0 : _a.path);
}
mergeGrandChildIn(grandChild) {
const gcParents = grandChild.parents();
(0, federation_internals_1.assert)(gcParents.length === 1, () => `Cannot merge ${grandChild} as it has multiple parents ([${gcParents}])`);
const gcParent = gcParents[0];
const gcGrandParent = gcParent.group.parentRelation(this);
(0, federation_internals_1.assert)(gcGrandParent, () => `Cannot merge ${grandChild} into ${this}: the former parent (${gcParent.group}) is not a child of the latter`);
(0, federation_internals_1.assert)(gcParent.path && gcGrandParent.path, () => `Cannot merge ${grandChild} into ${this}: some paths in parents are unknown`);
this.mergeInInternal(grandChild, (0, federation_internals_1.concatOperationPaths)(gcGrandParent.path, gcParent.path));
}
mergeInWithAllDependencies(other) {
(0, federation_internals_1.assert)(this.deferRef === other.deferRef, () => `Can only merge unrelated groups within the same @defer block: cannot merge ${this} and ${other}`);
(0, federation_internals_1.assert)(this.subgraphName === other.subgraphName, () => `Can only merge unrelated groups to the same subraphs: cannot merge ${this} and ${other}`);
(0, federation_internals_1.assert)(sameMergeAt(this.mergeAt, other.mergeAt), () => `Can only merge unrelated groups at the same "mergeAt": ${this} has mergeAt=${this.mergeAt}, but ${other} has mergeAt=${other.mergeAt}`);
this.copyInputsOf(other);
this.mergeInInternal(other, [], true);
}
mergeInInternal(merged, path, mergeParentDependencies = false) {
(0, federation_internals_1.assert)(!merged.isTopLevel, "Shouldn't remove top level groups");
if (path.length === 0) {
this.addSelections(merged.selection);
}
else {
const mergePathConditionalDirectives = (0, federation_internals_1.conditionalDirectivesInOperationPath)(path);
this.addAtPath(path, removeUnneededTopLevelFragmentDirectives(merged.selection, mergePathConditionalDirectives));
}
this.dependencyGraph.onModification();
this.relocateChildrenOnMergedIn(merged, path);
if (mergeParentDependencies) {
this.relocateParentsOnMergedIn(merged);
}
if (merged.mustPreserveSelection) {
this.mustPreserveSelection = true;
}
this.dependencyGraph.remove(merged);
}
removeUselessChild(child) {
const relationToChild = child.parentRelation(this);
(0, federation_internals_1.assert)(relationToChild, () => `Cannot remove useless ${child} of ${this}: the former is not a child of the latter`);
const childPathInThis = relationToChild.path;
(0, federation_internals_1.assert)(childPathInThis, () => `Cannot remove useless ${child} of ${this}: the path of the former into the later is unknown`);
this.dependencyGraph.onModification();
this.relocateChildrenOnMergedIn(child, childPathInThis);
this.dependencyGraph.remove(child);
}
relocateChildrenOnMergedIn(merged, pathInThis) {
var _a;
for (const child of merged.children()) {
if (this.isParentOf(child)) {
continue;
}
const pathInMerged = (_a = child.parentRelation(merged)) === null || _a === void 0 ? void 0 : _a.path;
child.addParent({ group: this, path: concatPathsInParents(pathInThis, pathInMerged) });
}
}
relocateParentsOnMergedIn(merged) {
for (const parent of merged.parents()) {
if (parent.group.isParentOf(this)) {
continue;
}
if (parent.group.isDescendantOf(this)) {
continue;
}
this.addParent(parent);
}
}
finalizeSelection(variableDefinitions, handledConditions) {
const selectionWithoutConditions = (0, conditions_1.removeConditionsFromSelectionSet)(this.selection, handledConditions);
const selectionWithTypenames = addTypenameFieldForAbstractTypes(selectionWithoutConditions);
const { updated: selection, outputRewrites } = addAliasesForNonMergingFields(selectionWithTypenames);
selection.validate(variableDefinitions, true);
return { selection, outputRewrites };
}
conditions() {
return this._selection.memoized().conditions;
}
toPlanNode(queryPlannerConfig, handledConditions, variableDefinitions, fragments, operationName, directives) {
var _a, _b;
if (this.selection.isEmpty()) {
return undefined;
}
for (const [context, type] of (_b = (_a = this.inputs) === null || _a === void 0 ? void 0 : _a.usedContexts) !== null && _b !== void 0 ? _b : []) {
(0, federation_internals_1.assert)((0, federation_internals_1.isInputType)(type), () => `Expected ${type} to be a input type`);
variableDefinitions.add(new federation_internals_1.VariableDefinition(type.schema(), new federation_internals_1.Variable(context), type));
}
const { selection, outputRewrites } = this.finalizeSelection(variableDefinitions, handledConditions);
const inputNodes = this._inputs ? this._inputs.toSelectionSetNode(variableDefinitions, handledConditions) : undefined;
const subgraphSchema = this.dependencyGraph.subgraphSchemas.get(this.subgraphName);
let operation = this.isEntityFetch
? operationForEntitiesFetch(subgraphSchema, selection, variableDefinitions, operationName, directives)
: operationForQueryFetch(subgraphSchema, this.rootKind, selection, variableDefinitions, operationName, directives);
if (this.generateQueryFragments) {
operation = operation.generateQueryFragments();
}
else {
operation = operation.optimize(fragments === null || fragments === void 0 ? void 0 : fragments.forSubgraph(this.subgraphName, subgraphSchema), federation_internals_1.DEFAULT_MIN_USAGES_TO_OPTIMIZE, variableDefinitions);
}
const collector = new federation_internals_1.VariableCollector();
selection.collectVariables(collector);
operation.collectVariablesInAppliedDirectives(collector);
if (operation.fragments) {
for (const namedFragment of operation.fragments.definitions()) {
namedFragment.collectVariables(collector);
}
}
const usedVariables = collector.variables();
const operationDocument = (0, federation_internals_1.operationToDocument)(operation);
const fetchNode = {
kind: 'Fetch',
id: this.id,
serviceName: this.subgraphName,
requires: inputNodes ? (0, QueryPlan_1.trimSelectionNodes)(inputNodes.selections) : undefined,
variableUsages: usedVariables.map((v) => v.name),
operation: (0, graphql_1.stripIgnoredCharacters)((0, graphql_1.print)(operationDocument)),
operationKind: schemaRootKindToOperationKind(operation.rootKind),
operationName: operation.name,
operationDocumentNode: queryPlannerConfig.exposeDocumentNodeInFetchNode ? operationDocument : undefined,
inputRewrites: this.inputRewrites.length === 0 ? undefined : this.inputRewrites,
outputRewrites: outputRewrites.length === 0 ? undefined : outputRewrites,
contextRewrites: this._contextInputs,
};
return this.isTopLevel
? fetchNode
: {
kind: 'Flatten',
path: this.mergeAt,
node: fetchNode,
};
}
addContextRenamer(renamer) {
if (!this._contextInputs) {
this._contextInputs = [];
}
if (!this._contextInputs.some((c) => sameKeyRenamer(c, renamer))) {
this._contextInputs.push(renamer);
}
}
toString() {
const base = `[${this.index}]${this.deferRef ? '(deferred)' : ''}${this._id ? `{id: ${this._id}}` : ''} ${this.subgraphName}`;
return this.isTopLevel
? `${base}[${this._selection}]`
: `${base}@(${this.mergeAt})[${this._inputs} => ${this._selection}]`;
}
}
class RebasedFragments {
constructor(queryFragments) {
this.queryFragments = queryFragments;
this.bySubgraph = new Map();
}
forSubgraph(name, schema) {
var _a;
let frags = this.bySubgraph.get(name);
if (frags === undefined) {
frags = (_a = this.queryFragments.rebaseOn(schema)) !== null && _a !== void 0 ? _a : null;
this.bySubgraph.set(name, frags);
}
return frags !== null && frags !== void 0 ? frags : undefined;
}
}
function genAliasName(baseName, unavailableNames) {
let counter = 0;
let candidate = `${baseName}__alias_${counter}`;
while (unavailableNames.has(candidate)) {
candidate = `${baseName}__alias_${++counter}`;
}
return candidate;
}
function selectionSetAsKeyRenamers(selectionSet, relPath, alias) {
if (!selectionSet || selectionSet.isEmpty()) {
return [
{
kind: 'KeyRenamer',
path: relPath,
renameKeyTo: alias,
}
];
}
return selectionSet.selections().map((selection) => {
if (selection.kind === 'FieldSelection') {
if (relPath[relPath.length - 1] === '..' && selectionSet.parentType.name !== 'Query') {
return (0, federation_internals_1.possibleRuntimeTypes)(selectionSet.parentType).map((t) => selectionSetAsKeyRenamers(selectionSet, [...relPath, `... on ${t.name}`], alias)).flat();
}
else {
return selectionSetAsKeyRenamers(selection.selectionSet, [...relPath, selection.element.name], alias);
}
}
else if (selection.kind === 'FragmentSelection') {
const element = selection.element;
if (element.typeCondition) {
return selectionSetAsKeyRenamers(selection.selectionSet, [...relPath, `... on ${element.typeCondition.name}`], alias);
}
}
return undefined;
}).filter(federation_internals_1.isDefined)
.reduce((acc, val) => acc.concat(val), []);
}
function computeAliasesForNonMergingFields(selections, aliasCollector) {
const seenResponseNames = new Map();
const rebasedFieldsInSet = (s) => (s.selections.fieldsInSet().map(({ path, field }) => ({ fieldPath: s.path.concat(path), field })));
for (const { fieldPath, field } of selections.map((s) => rebasedFieldsInSet(s)).flat()) {
const fieldName = field.element.name;
const responseName = field.element.responseName();
const fieldType = field.element.definition.type;
const previous = seenResponseNames.get(responseName);
if (previous) {
if (previous.fieldName === fieldName && (0, federation_internals_1.typesCanBeMerged)(previous.fieldType, fieldType)) {
if ((0, federation_internals_1.isCompositeType)((0, federation_internals_1.baseType)(fieldType))) {
(0, federation_internals_1.assert)(previous.selections, () => `Should have added selections for ${previous.fieldType}`);
const selections = previous.selections.concat({ path: fieldPath.concat(responseName), selections: field.selectionSet });
seenResponseNames.set(responseName, { ...previous, selections });
}
}
else {
const alias = genAliasName(responseName, seenResponseNames);
const selections = field.selectionSet ? [{ path: fieldPath.concat(alias), selections: field.selectionSet }] : undefined;
seenResponseNames.set(alias, { fieldName, fieldType, selections });
aliasCollector.push({
path: fieldPath,
responseName,
alias
});
}
}
else {
const selections = field.selectionSet ? [{ path: fieldPath.concat(responseName), selections: field.selectionSet }] : undefined;
seenResponseNames.set(responseName, { fieldName, fieldType, selections });
}
}
for (const selections of seenResponseNames.values()) {
if (!selections.selections) {
continue;
}
computeAliasesForNonMergingFields(selections.selections, aliasCollector);
}
}
function addAliasesForNonMergingFields(selectionSet) {
const aliases = [];
computeAliasesForNonMergingFields([{ path: [], selections: selectionSet }], aliases);
const updated = withFieldAliased(selectionSet, aliases);
const outputRewrites = aliases.map(({ path, responseName, alias }) => ({
kind: 'KeyRenamer',
path: path.concat(alias),
renameKeyTo: responseName,
}));
return { updated, outputRewrites };
}
function withFieldAliased(selectionSet, aliases) {
if (aliases.length === 0) {
return selectionSet;
}
const atCurrentLevel = new Map();
const remaining = new Array();
for (const alias of aliases) {
if (alias.path.length > 0) {
remaining.push(alias);
}
else {
atCurrentLevel.set(alias.responseName, alias);
}
}
return selectionSet.lazyMap((selection) => {
const pathElement = selection.element.asPathElement();
const subselectionAliases = remaining.map((alias) => {
if (alias.path[0] === pathElement) {
return {
...alias,
path: alias.path.slice(1),
};
}
else {
return undefined;
}
}).filter(federation_internals_1.isDefined);
const updatedSelectionSet = selection.selectionSet
? withFieldAliased(selection.selectionSet, subselectionAliases)
: undefined;
if (selection.kind === 'FieldSelection') {
const field = selection.element;
const alias = pathElement && atCurrentLevel.get(pathElement);
return !alias && selection.selectionSet === updatedSelectionSet
? selection
: selection.withUpdatedComponents(alias ? field.withUpdatedAlias(alias.alias) : field, updatedSelectionSet);
}
else {
return selection.selectionSet === updatedSelectionSet
? selection
: selection.withUpdatedSelectionSet(updatedSelectionSet);
}
});
}
class DeferredInfo {
constructor(label, path, subselection, deferred = new Set(), dependencies = new Set()) {
this.label = label;
this.path = path;
this.subselection = subselection;
this.deferred = deferred;
this.dependencies = dependencies;
}
static empty(label, path, parentType) {
return new DeferredInfo(label, path, federation_internals_1.MutableSelectionSet.empty(parentType));
}
clone() {
return new DeferredInfo(this.label, this.path, this.subselection.clone(), new Set(this.deferred), new Set(this.dependencies));
}
}
const emptyDeferContext = {
currentDeferRef: undefined,
pathToDeferParent: [],
activeDeferRef: undefined,
isPartOfQuery: true,
};
function deferContextForConditions(baseContext) {
return {
...baseContext,
isPartOfQuery: false,
currentDeferRef: baseContext.activeDeferRef,
};
}
function deferContextAfterSubgraphJump(baseContext) {
return baseContext.currentDeferRef === baseContext.activeDeferRef
? baseContext
: {
...baseContext,
activeDeferRef: baseContext.currentDeferRef,
};
}
function filterOperationPath(path, schema) {
return path.map((elt) => {
if (elt.kind === 'FragmentElement' && elt.typeCondition && !schema.type(elt.typeCondition.name)) {
return elt.appliedDirectives.length > 0 ? elt.withUpdatedCondition(undefined) : undefined;
}
return elt;
}).filter(federation_internals_1.isDefined);
}
class GroupPath {
constructor(fullPath, pat