UNPKG

@apollo/query-planner

Version:
1,064 lines 144 kB
"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