UNPKG

@theguild/federation-composition

Version:
278 lines (277 loc) 11.8 kB
import { isAbstractEdge, isEntityEdge, isFieldEdge } from './edge.js'; import { SatisfiabilityError } from './errors.js'; import { lazy } from './helpers.js'; export function concatIfNotExistsString(list, item) { if (list.includes(item)) { return list; } return list.concat(item); } export function concatIfNotExistsFields(list, item) { if (list.some(f => f.equals(item))) { return list; } return list.concat(item); } export class PathFinder { logger; graph; moveValidator; constructor(logger, graph, moveValidator) { this.logger = logger; this.graph = graph; this.moveValidator = moveValidator; } findDirectPaths(path, typeName, fieldName, visitedEdges) { const nextPaths = []; const errors = []; 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 (isAbstractEdge(edge) && edge.tail.typeName === typeName) { this.logger.groupEnd(() => 'Resolvable: ' + edge); const newPath = path.clone().move(edge); nextPaths.push(newPath); continue; } } if (isFieldTarget && isFieldEdge(edge) && edge.move.fieldName === fieldName) { const resolvable = this.moveValidator.isEdgeResolvable(edge, path, [], [], []); if (!resolvable.success) { errors.push(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.length > 0) { 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: [ lazy(() => SatisfiabilityError.forNoImplementation(tail.graphName, tail.typeName)), ], paths: undefined, }; } } return { success: true, errors: undefined, paths: [], }; } errors.push(lazy(() => SatisfiabilityError.forMissingField(tail.graphName, typeName, fieldName))); const typeNodes = this.graph.nodesOf(typeName); for (const typeNode of typeNodes) { const edges = this.graph.fieldEdgesOfHead(typeNode, fieldName); for (const edge of edges) { if (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(lazy(() => SatisfiabilityError.forNoKey(tail.graphName, edge.tail.graphName, typeName, fieldName))); } } } } return { success: false, errors, paths: undefined, }; } findFieldIndirectly(path, typeName, fieldName, visitedEdges, visitedGraphs, visitedFields, errors, finalPaths, queue, resolvedGraphs, edge) { if (!isEntityEdge(edge) && !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 (isAbstractEdge(edge)) { const tailEdge = path.edge(); if (tailEdge && 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); if (!resolvable.success) { errors.push(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]); 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.push(...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, finalPaths, queue, resolvedGraphs, edge) { if (!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) { const errors = []; 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 && !isAbstractEdge(edge)) { this.logger.groupEnd(() => 'Ignore: we are back to the same graph'); continue; } if (isFieldTarget) { this.findFieldIndirectly(path, typeName, fieldName, visitedEdges, visitedGraphs, visitedFields, errors, finalPaths, queue, resolvedGraphs, edge); } else { this.findTypeIndirectly(path, typeName, visitedGraphs, visitedFields, 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, }; } }