@theguild/federation-composition
Version:
Open Source Composition library for Apollo Federation
299 lines (298 loc) • 12.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PathFinder = void 0;
exports.concatIfNotExistsString = concatIfNotExistsString;
exports.concatIfNotExistsFields = concatIfNotExistsFields;
const edge_js_1 = require("./edge.js");
const errors_js_1 = require("./errors.js");
const helpers_js_1 = require("./helpers.js");
function concatIfNotExistsString(list, item) {
if (list.includes(item)) {
return list;
}
return list.concat(item);
}
function concatIfNotExistsFields(list, item) {
if (list.some((f) => f.equals(item))) {
return list;
}
return list.concat(item);
}
class PathFinder {
logger;
graph;
moveValidator;
constructor(logger, graph, moveValidator) {
this.logger = logger;
this.graph = graph;
this.moveValidator = moveValidator;
}
findDirectPaths(path, typeName, fieldName, visitedEdges, labelValues) {
const nextPaths = [];
const errors = new errors_js_1.LazyErrors();
const tail = path.tail() ?? path.rootNode();
const isFieldTarget = fieldName !== null;
const id = isFieldTarget
? `${typeName}.${fieldName}`
: `... on ${typeName}`;
this.logger.group(() => "Direct paths to " + id + " from: " + tail);
const edges = isFieldTarget
? this.graph.fieldEdgesOfHead(tail, fieldName)
: this.graph.abstractEdgesOfHead(tail);
this.logger.log(() => "Checking " + edges.length + " edges");
let i = 0;
for (const edge of edges) {
this.logger.group(() => "Checking #" + i++ + " " + edge);
if (nextPaths.some((p) => p.tail() === edge.tail)) {
this.logger.groupEnd(() => "Already resolvable tail: " + edge);
continue;
}
if (edge.isCrossGraphEdge()) {
this.logger.groupEnd(() => "Cross graph edge: " + edge);
continue;
}
if (visitedEdges.includes(edge)) {
this.logger.groupEnd(() => "Already visited: " + edge);
continue;
}
if (!isFieldTarget) {
if ((0, edge_js_1.isAbstractEdge)(edge) && edge.tail.typeName === typeName) {
this.logger.groupEnd(() => "Resolvable: " + edge);
const newPath = path.clone().move(edge);
nextPaths.push(newPath);
continue;
}
}
if (isFieldTarget &&
(0, edge_js_1.isFieldEdge)(edge) &&
edge.move.fieldName === fieldName) {
const resolvable = this.moveValidator.isEdgeResolvable(edge, path, [], [], [], labelValues);
if (!resolvable.success) {
errors.add(resolvable.error);
this.logger.groupEnd(() => "Not resolvable: " + edge);
continue;
}
this.logger.groupEnd(() => "Resolvable: " + edge);
const newPath = path.clone().move(edge);
nextPaths.push(newPath);
continue;
}
this.logger.groupEnd(() => "Not matching");
}
this.logger.groupEnd(() => "Found " + nextPaths.length + " direct paths");
if (nextPaths.length > 0) {
return {
success: true,
paths: nextPaths,
errors: undefined,
};
}
if (!errors.isEmpty()) {
return {
success: false,
errors,
paths: undefined,
};
}
if (!isFieldTarget) {
if (tail.typeState?.kind === "interface" &&
tail.typeState.hasInterfaceObject) {
const typeStateInGraph = tail.typeState.byGraph.get(tail.graphId);
if (typeStateInGraph?.isInterfaceObject) {
return {
success: false,
errors: new errors_js_1.LazyErrors().add((0, helpers_js_1.lazy)(() => errors_js_1.SatisfiabilityError.forNoImplementation(tail.graphName, tail.typeName))),
paths: undefined,
};
}
}
return {
success: true,
errors: undefined,
paths: [],
};
}
errors.add((0, helpers_js_1.lazy)(() => errors_js_1.SatisfiabilityError.forMissingField(tail.graphName, typeName, fieldName)));
errors.add((0, helpers_js_1.lazy)(() => {
const errors = [];
const typeNodes = this.graph.nodesOf(typeName);
for (const typeNode of typeNodes) {
const edges = this.graph.fieldEdgesOfHead(typeNode, fieldName);
for (const edge of edges) {
if ((0, edge_js_1.isFieldEdge)(edge) &&
edge.move.fieldName === fieldName &&
!this.moveValidator.isExternal(edge)) {
const typeStateInGraph = edge.head.typeState &&
edge.head.typeState.kind === "object" &&
edge.head.typeState.byGraph.get(edge.head.graphId);
const keys = typeStateInGraph
? typeStateInGraph.keys.filter((key) => key.resolvable)
: [];
if (keys.length === 0) {
errors.push(errors_js_1.SatisfiabilityError.forNoKey(tail.graphName, edge.tail.graphName, typeName, fieldName));
}
}
}
}
return errors;
}));
return {
success: false,
errors,
paths: undefined,
};
}
findFieldIndirectly(path, typeName, fieldName, visitedEdges, visitedGraphs, visitedFields, labelValues, errors, finalPaths, queue, resolvedGraphs, edge) {
if (!(0, edge_js_1.isEntityEdge)(edge) && !(0, edge_js_1.isAbstractEdge)(edge)) {
this.logger.groupEnd(() => "Ignored");
return;
}
if (resolvedGraphs.includes(edge.tail.graphName)) {
this.logger.groupEnd(() => "Ignore: already resolved this graph");
return;
}
if (!!edge.move.keyFields &&
visitedFields.some((f) => f.equals(edge.move.keyFields))) {
this.logger.groupEnd(() => "Ignore: already visited fields");
return;
}
if ((0, edge_js_1.isAbstractEdge)(edge)) {
const tailEdge = path.edge();
if (tailEdge && (0, edge_js_1.isAbstractEdge)(tailEdge) && !edge.move.keyFields) {
this.logger.groupEnd(() => "Ignore: cannot do two abstract moves in a row");
return;
}
if (!edge.isCrossGraphEdge()) {
const newPath = path.clone().move(edge);
queue.push([visitedGraphs, visitedFields, newPath]);
this.logger.log(() => "Abstract move");
this.logger.groupEnd(() => "Adding to queue: " + newPath);
return;
}
}
const resolvable = this.moveValidator.isEdgeResolvable(edge, path, visitedEdges.concat(edge), visitedGraphs, visitedFields, labelValues);
if (!resolvable.success) {
errors.add(resolvable.error);
this.logger.groupEnd(() => "Not resolvable: " + resolvable.error);
return;
}
const newPath = path.clone().move(edge);
this.logger.log(() => "From indirect path, look for direct paths to " +
typeName +
"." +
fieldName +
" from: " +
edge);
const direct = this.findDirectPaths(newPath, typeName, fieldName, [edge], labelValues);
if (direct.success) {
this.logger.groupEnd(() => "Resolvable: " + edge + " with " + direct.paths.length + " paths");
finalPaths.push(...direct.paths);
resolvedGraphs.push(newPath.edge().tail.graphName);
return;
}
errors.add(direct.errors);
resolvedGraphs.push(newPath.edge().tail.graphName);
queue.push([
concatIfNotExistsString(visitedGraphs, edge.tail.graphName),
"keyFields" in edge.move && edge.move.keyFields
? concatIfNotExistsFields(visitedFields, edge.move.keyFields)
: visitedFields,
newPath,
]);
this.logger.log(() => "Did not find direct paths");
this.logger.groupEnd(() => "Adding to queue: " + newPath);
}
findTypeIndirectly(path, typeName, visitedGraphs, visitedFields, labelValues, finalPaths, queue, resolvedGraphs, edge) {
if (!(0, edge_js_1.isAbstractEdge)(edge)) {
this.logger.groupEnd(() => "Ignored");
return;
}
if (resolvedGraphs.includes(edge.tail.graphName)) {
this.logger.groupEnd(() => "Already resolved the graph");
return;
}
if (edge.move.keyFields &&
visitedFields.some((f) => f.equals(edge.move.keyFields))) {
this.logger.groupEnd(() => "Ignore: already visited fields");
return;
}
const newPath = path.clone().move(edge);
if (edge.tail.typeName === typeName) {
resolvedGraphs.push(edge.tail.graphName);
finalPaths.push(newPath);
}
else {
queue.push([
visitedGraphs,
edge.move.keyFields
? concatIfNotExistsFields(visitedFields, edge.move.keyFields)
: visitedFields,
newPath,
]);
}
this.logger.groupEnd(() => "Resolvable");
}
findIndirectPaths(path, typeName, fieldName, visitedEdges, visitedGraphs, visitedFields, labelValues) {
const errors = new errors_js_1.LazyErrors();
const tail = path.tail() ?? path.rootNode();
const sourceGraphName = tail.graphName;
const isFieldTarget = fieldName !== null;
const id = isFieldTarget
? `${typeName}.${fieldName}`
: `... on ${typeName}`;
this.logger.group(() => "Indirect paths to " + id + " from: " + tail);
const queue = [
[visitedGraphs, visitedFields, path],
];
const finalPaths = [];
const resolvedGraphs = [];
while (queue.length > 0) {
const item = queue.pop();
if (!item) {
throw new Error("Unexpected end of queue");
}
const [visitedGraphs, visitedFields, path] = item;
const tail = path.tail() ?? path.rootNode();
const edges = this.graph.indirectEdgesOfHead(tail);
this.logger.log(() => "At path: " + path);
this.logger.log(() => "Checking " + edges.length + " edges");
let i = 0;
for (const edge of edges) {
this.logger.group(() => "Checking #" + i++ + " " + edge);
this.logger.log(() => "Visited graphs: " + visitedGraphs.join(","));
if (visitedGraphs.includes(edge.tail.graphName)) {
this.logger.groupEnd(() => "Ignore: already visited graph");
continue;
}
if (visitedEdges.includes(edge)) {
this.logger.groupEnd(() => "Ignore: already visited edge");
continue;
}
if (edge.tail.graphName === sourceGraphName && !(0, edge_js_1.isAbstractEdge)(edge)) {
this.logger.groupEnd(() => "Ignore: we are back to the same graph");
continue;
}
if (isFieldTarget) {
this.findFieldIndirectly(path, typeName, fieldName, visitedEdges, visitedGraphs, visitedFields, labelValues, errors, finalPaths, queue, resolvedGraphs, edge);
}
else {
this.findTypeIndirectly(path, typeName, visitedGraphs, visitedFields, labelValues, finalPaths, queue, resolvedGraphs, edge);
}
}
}
this.logger.groupEnd(() => "Found " + finalPaths.length + " indirect paths");
if (finalPaths.length === 0) {
return {
success: false,
errors,
paths: undefined,
};
}
return {
success: true,
paths: finalPaths,
errors: undefined,
};
}
}
exports.PathFinder = PathFinder;