UNPKG

nosql-constraints

Version:

Helpers to manage constrants (i.e. cascade delete) in a NoSQL database

378 lines 15.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DiGraph = void 0; const lodash_1 = require("lodash"); class DiGraph { #vertices; constructor() { this.#vertices = new Map(); } get vertices() { return [...this.#vertices.values()]; } static fromRaw(raw) { const digraph = new DiGraph(); for (const vertex of Object.values(raw)) { digraph.addVertex(vertex); } return digraph; } get isAcyclic() { return !this.hasCycles(); } toDict() { return Object.fromEntries(this.#vertices.entries()); } hasVertex(vertexId) { return this.#vertices.has(vertexId); } addVertex(vertex) { const graphVerticesIds = [...this.#vertices.keys()]; vertex.adjacentTo = vertex.adjacentTo ?? []; if (!graphVerticesIds.includes(vertex.id)) { this.#vertices.set(vertex.id, vertex); } } addVertices(...vertices) { const graphVerticesIds = [...this.#vertices.keys()]; for (const uniqueVertex of this.keepUniqueVertices(vertices)) { if (!graphVerticesIds.includes(uniqueVertex.id)) { this.addVertex(uniqueVertex); } } } deleteVertex(vertexId) { this.#vertices.delete(vertexId); for (const vertexDependingOnDeletedVertex of this.getParents(vertexId)) { this.deleteEdge({ from: vertexDependingOnDeletedVertex.id, to: vertexId }); } } addEdge({ from, to }) { if (from === to) { return; } const [fromVertex, toVertex] = [this.#vertices.get(from), this.#vertices.get(to)]; if (fromVertex && toVertex) { const hasNotSameAdjacentVertex = !fromVertex.adjacentTo.find((adjacentVertex) => adjacentVertex === toVertex.id); if (hasNotSameAdjacentVertex) { fromVertex.adjacentTo = fromVertex.adjacentTo.concat(toVertex.id); } } } deleteEdge({ from, to }) { const fromVertex = this.#vertices.get(from); if (fromVertex) { fromVertex.adjacentTo = fromVertex.adjacentTo.filter((adjacentVertexId) => adjacentVertexId !== to); } } /** * This function updates the vertex's body with the provided value without * doing any merging with the previous value. If you want to preserve/update * values, check `mergeVertexBody` instead. * @example * updateVertexBody("Node1", { * // body only contains this property "newProperty" now. * newProperty: [] * }); * */ updateVertexBody(vertexId, body) { const rootVertexToMutate = this.#vertices.get(vertexId); if (rootVertexToMutate) { rootVertexToMutate.body = body; } } /** * This function lets you choose the way of merging the vertex's body * by providing a callback function with the corresponding vertex instance. * @example * mergeVertexBody("Node1", (nodeBody) => { * // either by directly mutating the value * nodeBody.someProperty.list[0] = {}; * // either by providing a new reference * nodeBody.someProperty.list = newCollection.map(operation); * }); */ mergeVertexBody(vertexId, mergeCallback) { const rootVertexToMutate = this.#vertices.get(vertexId); if (rootVertexToMutate) { mergeCallback(rootVertexToMutate.body); } } /** * Base API to traverse walk through a DiGraph instance either in a DFS or BFS * manner. Providing `rootVertexId` will force the traversal to start from it. * If no `rootVertexId` is provided, the traversal will start from the first vertex * found in the graph, which will most likely be the first entry that was added * in it. */ *traverse(options) { const { rootVertexId, traversal } = { traversal: options?.traversal ?? 'bfs', rootVertexId: options?.rootVertexId }; if (rootVertexId) { if (traversal === 'bfs') { return yield* this.breadthFirstTraversalFrom(rootVertexId); } return yield* this.depthFirstTraversalFrom(rootVertexId); } return yield* this.traverseAll(traversal); } traverseEager(options) { return Array.from(this.traverse(options)); } /** * Allows top-to-bottom traversals by finding only the first relationship level * of children dependencies of the provided vertex. * @example * // given A --> B, A depends on B hence B is a children dependency of A * assert.deepEqual(graph.getChildren("A"), [VertexB]) // ok */ getChildren(rootVertexId) { return [...this.#vertices.values()].filter((vertex) => this.#vertices.get(rootVertexId)?.adjacentTo.includes(vertex.id)); } /** * Same as `getChildren()`, but doesn't stop at the first level hence deeply * collects all children dependencies in a Depth-First Search manner. * Allows top-to-bottom traversals i.e: which nodes are dependencies of * the provided rootVertexId. */ *getDeepChildren(rootVertexId, depthLimit) { const rootVertex = this.#vertices.get(rootVertexId); if (!rootVertex) { return; } const visitedVertices = []; for (const adjacentVertexId of rootVertex.adjacentTo) { const adjacentVertex = this.#vertices.get(adjacentVertexId); if (!adjacentVertex) { continue; } yield* this.findDeepDependencies('top-to-bottom', rootVertex, adjacentVertex, depthLimit, visitedVertices); } } /** * Allows bottom-to-top traversals by finding only the first relationship level * of parent dependencies of the provided vertex. * @example * // given A --> B, A depends on B hence A is a parent dependency of B * assert.deepEqual(graph.getParents("B"), [VertexA]) // ok */ getParents(rootVertexId) { return [...this.#vertices.values()].filter((vertex) => vertex.adjacentTo.includes(rootVertexId)); } /** * Same as `getParents()`, but doesn't stop at the first level hence deeply * collects all parent dependencies in a Depth-First Search manner. * Allows bottom-to-top traversals i.e: which nodes are depending on * the provided rootVertexId. */ *getDeepParents(rootVertexId, depthLimit) { const rootVertex = this.#vertices.get(rootVertexId); if (!rootVertex) { return; } const visitedVertices = []; for (const adjacentVertex of this.getParents(rootVertex.id)) { yield* this.findDeepDependencies('bottom-to-top', rootVertex, adjacentVertex, depthLimit, visitedVertices); } } /** * Returns `true` if atleast one circular dependency exists in the graph, * otherwise, returns `false`. * If you want to know precisely what are the circular dependencies and * know what vertices are involved, use `findCycles()` instead. */ hasCycles({ maxDepth } = { maxDepth: Number.POSITIVE_INFINITY }) { let hasCycles = false; if (maxDepth === 0) { return hasCycles; } for (const [rootVertex, rootAdjacentVertex] of this.collectRootAdjacencyLists()) { // early exit as we stop on the first cycle found if (hasCycles) { break; } const adjacencyList = new Set(); for (const deepAdjacentVertexId of this.findDeepDependencies('top-to-bottom', rootVertex, rootAdjacentVertex, maxDepth)) { adjacencyList.add(deepAdjacentVertexId); if (deepAdjacentVertexId === rootVertex.id || adjacencyList.has(rootVertex.id)) { hasCycles = true; break; } } } return hasCycles; } findCycles({ maxDepth } = { maxDepth: Number.POSITIVE_INFINITY }) { const cyclicPathsWithMaybeDuplicates = []; if (maxDepth === 0) { return []; } for (const [rootVertex, rootAdjacentVertex] of this.collectRootAdjacencyLists()) { const adjacencyList = new Set(); for (const deepAdjacentVertexId of this.findDeepDependencies('top-to-bottom', rootVertex, rootAdjacentVertex, maxDepth)) { adjacencyList.add(deepAdjacentVertexId); if (deepAdjacentVertexId === rootVertex.id || adjacencyList.has(rootVertex.id)) { const adjacencyListAsArray = [...adjacencyList]; /** * We found a cycle, the first thing to do is to only keep the segment * from X to X with "X" being the root vertex of the current DFS. * It allows us to build sub cycles at any point in the path. */ const verticesInBetweenCycle = adjacencyListAsArray.slice(0, adjacencyListAsArray.indexOf(rootVertex.id) + 1); cyclicPathsWithMaybeDuplicates.push(this.backtrackVerticesInvolvedInCycle([rootVertex.id, ...verticesInBetweenCycle])); } } } return this.keepUniqueVerticesPaths([...cyclicPathsWithMaybeDuplicates]); } *limitCycleDetectionDepth(dependenciesWalker, maxDepth) { /** * At this point, we already traversed 2 levels of depth dependencies by: * - accessing the root's node adjacency list (depth === 1) * - then we continue by accessing the adjacent's node adjacency list (depth === 2) * Consequently we start recursing using the limit only at depth 2 already */ const TRAVERSAL_STEPS_ALREADY_DONE = 2; for (let depth = 0; depth <= maxDepth - TRAVERSAL_STEPS_ALREADY_DONE; depth++) { const { done, value } = dependenciesWalker.next(); if (done) { return; } yield value; } } *collectRootAdjacencyLists() { for (const rootVertex of this.#vertices.values()) { for (const rootAdjacentVertexId of rootVertex.adjacentTo) { const rootAdjacentVertex = this.#vertices.get(rootAdjacentVertexId); if (!rootAdjacentVertex) { continue; } yield [rootVertex, rootAdjacentVertex]; } } } /** * This method is used to deeply find either all lower dependencies of a given * vertex or all its upper dependencies. */ *findDeepDependencies(dependencyTraversal, rootVertex, traversedVertex, depthLimit = Number.POSITIVE_INFINITY, verticesAlreadyVisited = []) { if (verticesAlreadyVisited.includes(traversedVertex.id)) { return; } yield traversedVertex.id; verticesAlreadyVisited.push(traversedVertex.id); // Cycle reached, we must exit before entering in the infinite loop if (rootVertex.id === traversedVertex.id) { return; } const nextDependencies = dependencyTraversal === 'top-to-bottom' ? traversedVertex.adjacentTo : this.getParents(traversedVertex.id).map(({ id }) => id); for (const adjacentVertexId of nextDependencies) { const adjacentVertex = this.#vertices.get(adjacentVertexId); if (adjacentVertex) { yield* this.limitCycleDetectionDepth(this.findDeepDependencies(dependencyTraversal, rootVertex, adjacentVertex, depthLimit, verticesAlreadyVisited), depthLimit); } } } keepUniqueVerticesPaths(paths) { return (0, lodash_1.uniqWith)(paths, (pathA, pathB) => { // Narrow down the comparison to avoid unnecessary operations if (pathA.length !== pathB.length) { return false; } /** * In order for paths to be compared by values, arrays must be sorted e.g: * [a, b] !== [b, a] when strictly comparing values. */ return (0, lodash_1.isEqual)(pathA.slice().sort(), pathB.slice().sort()); }); } /** * Once the cycle found, many vertices actually not involved in the cycle * might have been visited. To only keep vertices that are effectively involved * in the cyclic path, we must check that for any vertex there is an existing * path from its ancestor leading to the root node. */ backtrackVerticesInvolvedInCycle(verticesInCyclicPath) { for (let i = verticesInCyclicPath.length; i > 1; i--) { const currentNode = verticesInCyclicPath[i - 1]; // The node just before the current one who is eventually its parent const nodeBeforeInPath = this.#vertices.get(verticesInCyclicPath[i - 2]); const isCurrentNodeParent = nodeBeforeInPath?.adjacentTo.includes(currentNode); /** * there is no path existing from the node just before to the current node, * meaning that the cycle path can't be coming from that path. */ if (!isCurrentNodeParent) { // We must remove incrementally vertices that aren't involved in the cycle verticesInCyclicPath.splice(i - 2, 1); } } return [...new Set(verticesInCyclicPath)]; } *keepUniqueVertices(vertices) { const uniqueVerticesIds = new Set(); for (const vertex of vertices) { if (!uniqueVerticesIds.has(vertex.id)) { uniqueVerticesIds.add(vertex.id); yield vertex; } } } *depthFirstTraversalFrom(rootVertexId, traversedVertices = new Set()) { if (traversedVertices.has(rootVertexId)) { return; } const rootVertex = this.#vertices.get(rootVertexId); if (!rootVertex) { return; } yield rootVertex; traversedVertices.add(rootVertexId); for (const vertexId of rootVertex.adjacentTo) { yield* this.depthFirstTraversalFrom(vertexId, traversedVertices); } } *breadthFirstTraversalFrom(rootVertexId, visitedVerticesIds = new Set()) { const vertex = this.#vertices.get(rootVertexId); if (!vertex) return; if (!visitedVerticesIds.has(rootVertexId)) { visitedVerticesIds.add(rootVertexId); yield vertex; } const nextVerticesToVisit = []; for (const vertexId of vertex.adjacentTo) { const adjacentVertex = this.#vertices.get(vertexId); if (!adjacentVertex || visitedVerticesIds.has(adjacentVertex.id)) continue; visitedVerticesIds.add(adjacentVertex.id); nextVerticesToVisit.push(adjacentVertex); yield adjacentVertex; } for (const nextVertex of nextVerticesToVisit) { yield* this.breadthFirstTraversalFrom(nextVertex.id, visitedVerticesIds); } } *traverseAll(traversal) { const visitedVertices = new Set(); for (const vertexId of this.#vertices.keys()) { if (traversal === 'dfs') { yield* this.depthFirstTraversalFrom(vertexId, visitedVertices); } else { yield* this.breadthFirstTraversalFrom(vertexId, visitedVertices); } } } } exports.DiGraph = DiGraph; //# sourceMappingURL=digraph.js.map