@theguild/federation-composition
Version:
Open Source Composition library for Apollo Federation
309 lines (308 loc) • 13.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Walker = exports.WalkTracker = void 0;
const graphql_1 = require("graphql");
const edge_js_1 = require("./edge.js");
const finder_js_1 = require("./finder.js");
const operation_path_js_1 = require("./operation-path.js");
class WalkTracker {
superPath;
paths;
errors = [];
constructor(superPath, paths) {
this.superPath = superPath;
this.paths = paths;
}
move(edge) {
if ((0, edge_js_1.isFieldEdge)(edge) || (0, edge_js_1.isAbstractEdge)(edge)) {
return new WalkTracker(this.superPath.clone().move(edge), []);
}
throw new Error('Expected edge to be FieldMove or AbstractMove');
}
addPath(path) {
this.paths.push(path);
this.errors = [];
}
addError(error) {
this.errors.push(error);
}
isPossible() {
return this.paths.length > 0;
}
givesEmptyResult() {
const lastEdge = this.superPath.edge();
return (this.paths.length === 0 && this.errors.length === 0 && !!lastEdge && (0, edge_js_1.isAbstractEdge)(lastEdge));
}
isEdgeVisited(edge) {
return this.superPath.isVisitedEdge(edge);
}
listErrors() {
return this.errors
.map(error => error.get())
.filter((error, i, all) => all.findIndex(e => e.toString() === error.toString()) === i)
.filter(error => {
if (error.kind !== 'KEY') {
return true;
}
const steps = this.superPath.steps();
const lastStep = steps[steps.length - 1];
if (!lastStep || lastStep.typeName !== error.typeName) {
return true;
}
return true;
});
}
}
exports.WalkTracker = WalkTracker;
const defaultIsEdgeIgnored = () => false;
class Walker {
moveChecker;
supergraph;
mergedGraph;
logger;
pathFinder;
constructor(logger, moveChecker, supergraph, mergedGraph) {
this.moveChecker = moveChecker;
this.supergraph = supergraph;
this.mergedGraph = mergedGraph;
this.logger = logger.create('Walker');
this.pathFinder = new finder_js_1.PathFinder(this.logger, this.mergedGraph, this.moveChecker);
}
walkTrail(operationType, steps) {
if (steps.length === 0) {
throw new Error('Expected at least one step');
}
const rootNode = this.supergraph.nodeOf(operationType === graphql_1.OperationTypeNode.QUERY
? 'Query'
: operationType === graphql_1.OperationTypeNode.MUTATION
? 'Mutation'
: 'Subscription', false);
if (!rootNode) {
throw new Error(`Expected root node for operation type ${operationType}`);
}
let state = new WalkTracker(new operation_path_js_1.OperationPath(rootNode), this.mergedGraph.nodesOf(rootNode.typeName, false).map(n => new operation_path_js_1.OperationPath(n)));
for (const step of steps) {
const stepId = 'fieldName' in step && step.fieldName
? `${step.typeName}.${step.fieldName}`
: step.typeName;
const isFieldStep = 'fieldName' in step;
const isEdgeIgnored = (edge) => {
if (isFieldStep) {
return !(0, edge_js_1.isFieldEdge)(edge) || edge.move.fieldName !== step.fieldName;
}
return true;
};
let called = 0;
let unreachable = false;
let emptyObjectResult = false;
this.nextStep(state, state.superPath.tail() ?? state.superPath.rootNode(), (nextState, superEdge) => {
if (called++ > 1) {
throw new Error('Expected nextStep to be called only once');
}
state = nextState;
if (nextState.isPossible()) {
if (this.logger.isEnabled) {
for (const path of nextState.paths) {
this.logger.log(() => path.toString());
}
}
this.logger.groupEnd(() => 'Advanced to ' + superEdge + ' with ' + nextState.paths.length + ' paths');
}
else if (nextState.givesEmptyResult()) {
emptyObjectResult = true;
this.logger.groupEnd(() => 'Federation will resolve an empty object for ' + superEdge);
}
else {
unreachable = true;
this.logger.log(() => 'Dead end', '🚨 ');
if (this.logger.isEnabled) {
for (const path of state.paths) {
this.logger.log(() => path.toString());
}
}
this.logger.groupEnd(() => 'Unreachable path ' + nextState.superPath.toString());
}
}, isEdgeIgnored);
if (unreachable) {
break;
}
if (emptyObjectResult) {
break;
}
}
return state;
}
walk(method = 'bfs') {
if (method === 'dfs') {
return this.dfs();
}
return this.bfs();
}
nextStep(state, superTail, next, isEdgeIgnored = defaultIsEdgeIgnored) {
const graphsLeadingToNode = Array.from(new Set(state.paths.map(p => {
const edge = p.edge();
const tail = p.tail() ?? p.rootNode();
const tailGraphName = tail.graphName;
if (edge && (0, edge_js_1.isFieldEdge)(edge) && edge.move.provides) {
return `${tailGraphName}#provides`;
}
return tailGraphName;
})));
if (superTail.isGraphComboVisited(graphsLeadingToNode)) {
this.logger.log(() => 'Node already visited: ' + superTail);
return;
}
superTail.setGraphComboAsVisited(graphsLeadingToNode);
const superEdges = this.supergraph.edgesOfHead(superTail);
for (const superEdge of superEdges) {
if (isEdgeIgnored(superEdge)) {
continue;
}
this.logger.group(() => 'Attempt to advance to ' + superEdge + ' (' + state.paths.length + ' paths)');
if (state.isEdgeVisited(superEdge)) {
this.logger.groupEnd(() => 'Edge already visited: ' + superEdge);
continue;
}
if (!((0, edge_js_1.isFieldEdge)(superEdge) || (0, edge_js_1.isAbstractEdge)(superEdge))) {
throw new Error('Expected edge to have a FieldMove or AbstractMove');
}
const nextState = state.move(superEdge);
const shortestPathPerTail = new Map();
const isFieldMove = (0, edge_js_1.isFieldEdge)(superEdge);
const id = isFieldMove
? `${superEdge.move.typeName}.${superEdge.move.fieldName}`
: `... on ${superEdge.tail.typeName}`;
for (const path of state.paths) {
this.logger.group(() => 'Advance path: ' + path.toString());
const directPathsResult = this.pathFinder.findDirectPaths(path, isFieldMove ? superEdge.move.typeName : superEdge.tail.typeName, isFieldMove ? superEdge.move.fieldName : null, []);
if (directPathsResult.success && directPathsResult.paths.length === 0) {
this.logger.groupEnd(() => 'Abstract type');
continue;
}
if (directPathsResult.success) {
setShortestPath(shortestPathPerTail, directPathsResult.paths);
}
else {
for (const error of directPathsResult.errors) {
nextState.addError(error);
}
}
if (directPathsResult.success && superEdge.tail.isLeaf) {
this.logger.groupEnd(() => 'Reached leaf node, no need to find indirect paths');
continue;
}
const indirectPathsResult = this.pathFinder.findIndirectPaths(path, isFieldMove ? superEdge.move.typeName : superEdge.tail.typeName, isFieldMove ? superEdge.move.fieldName : null, [], [], []);
if (indirectPathsResult.success) {
setShortestPath(shortestPathPerTail, indirectPathsResult.paths);
}
else {
for (const error of indirectPathsResult.errors) {
nextState.addError(error);
}
}
this.logger.groupEnd(() => directPathsResult.success || indirectPathsResult.success
? 'Can advance to ' + id
: 'Cannot advance to ' + id);
}
for (const shortestPathByTail of shortestPathPerTail.values()) {
nextState.addPath(shortestPathByTail);
}
next(nextState, superEdge);
}
}
dfs() {
const unreachable = [];
const rootNodes = ['Query', 'Mutation', 'Subscription']
.map(name => this.supergraph.nodeOf(name, false))
.filter((node) => !!node);
for (const rootNode of rootNodes) {
this._dfs(rootNode, new WalkTracker(new operation_path_js_1.OperationPath(rootNode), this.mergedGraph.nodesOf(rootNode.typeName, false).map(n => new operation_path_js_1.OperationPath(n))), unreachable);
}
return unreachable;
}
_dfs(superTail, state, unreachable) {
if (superTail.isLeaf) {
return;
}
this.nextStep(state, superTail, (nextState, superEdge) => {
if (nextState.isPossible()) {
if (this.logger.isEnabled) {
for (const path of nextState.paths) {
this.logger.log(() => path.toString());
}
}
this.logger.groupEnd(() => 'Advanced to ' + superEdge + ' with ' + nextState.paths.length + ' paths');
this._dfs(superEdge.tail, nextState, unreachable);
}
else if (nextState.givesEmptyResult()) {
this.logger.groupEnd(() => 'Federation will resolve an empty object for ' + superEdge);
}
else {
unreachable.push(nextState);
this.logger.log(() => 'Dead end', '🚨 ');
if (this.logger.isEnabled) {
for (const path of state.paths) {
this.logger.log(() => path.toString());
}
}
this.logger.groupEnd(() => 'Unreachable path ' + nextState.superPath.toString());
}
});
}
bfs() {
const unreachable = [];
const queue = [];
for (const name of ['Query', 'Mutation', 'Subscription']) {
const rootNode = this.supergraph.nodeOf(name, false);
if (!rootNode) {
continue;
}
queue.push(new WalkTracker(new operation_path_js_1.OperationPath(rootNode), this.mergedGraph.nodesOf(rootNode.typeName, false).map(n => new operation_path_js_1.OperationPath(n))));
}
while (queue.length > 0) {
const state = queue.pop();
if (!state) {
throw new Error('Unexpected end of queue');
}
const superTail = state.superPath.tail() ?? state.superPath.rootNode();
if (superTail.isLeaf) {
continue;
}
this.nextStep(state, superTail, (nextState, superEdge) => {
if (nextState.isPossible()) {
if (this.logger.isEnabled) {
for (const path of nextState.paths) {
this.logger.log(() => path.toString());
}
}
this.logger.groupEnd(() => 'Advanced to ' + superEdge + ' with ' + nextState.paths.length + ' paths');
queue.push(nextState);
}
else if (nextState.givesEmptyResult()) {
this.logger.groupEnd(() => 'Federation will resolve an empty object for ' + superEdge);
}
else {
unreachable.push(nextState);
this.logger.log(() => 'Dead end', '🚨 ');
if (this.logger.isEnabled) {
for (const path of state.paths) {
this.logger.log(() => path.toString());
}
}
this.logger.groupEnd(() => 'Unreachable path ' + nextState.superPath.toString());
}
});
}
return unreachable;
}
}
exports.Walker = Walker;
function setShortestPath(shortestPathPerTail, paths) {
for (const path of paths) {
const tail = path.tail() ?? path.rootNode();
const shortestByTail = shortestPathPerTail.get(tail);
if (!shortestByTail || shortestByTail.depth() > path.depth()) {
shortestPathPerTail.set(tail, path);
}
}
}