|
| 1 | +import { FlowElement } from "./FlowElement"; |
1 | 2 | import { isDivergingGateway } from "./modelUtils"; |
2 | | -import { FlowModelerProps, FlowContent, FlowGatewayDiverging } from "../types/FlowModelerProps"; |
3 | 3 |
|
4 | | -const recursivelyCollectPaths = ( |
| 4 | +import { FlowModelerProps } from "../types/FlowModelerProps"; |
| 5 | + |
| 6 | +/** |
| 7 | + * Check the given input model for circular references, which are specifically disallowed. |
| 8 | + * |
| 9 | + * @param {string} targetElementId - id of the element to check |
| 10 | + * @param {object} elements - collection of all defined elements in the input model |
| 11 | + * @param {Array.<string>} currentPath - ids of elements in front of target element |
| 12 | + * @throws Error in case of a circular reference being present |
| 13 | + */ |
| 14 | +export const checkForCircularReference = ( |
5 | 15 | targetElementId: string, |
6 | | - elements: { [key: string]: FlowContent | FlowGatewayDiverging }, |
7 | | - currentPath: Array<string>, |
8 | | - otherPaths: Array<Array<string>> |
| 16 | + elements: FlowModelerProps["flow"]["elements"], |
| 17 | + currentPath: Array<string> = [] |
9 | 18 | ): void => { |
| 19 | + const targetElement = elements[targetElementId]; |
| 20 | + if (!targetElement) { |
| 21 | + // path ends, i.e. there was no circular reference here |
| 22 | + return; |
| 23 | + } |
10 | 24 | if (currentPath.includes(targetElementId)) { |
11 | 25 | // no reason to go in circles, stop it right there |
12 | 26 | throw new Error(`Circular reference to element: ${targetElementId}`); |
13 | 27 | } |
14 | 28 | currentPath.push(targetElementId); |
15 | | - const targetElement = elements[targetElementId]; |
16 | | - if (!targetElement) { |
17 | | - // current path ends here, i.e. it should be added to the overall list |
18 | | - otherPaths.push(currentPath); |
19 | | - } else if (isDivergingGateway(targetElement)) { |
20 | | - // ensure that there are always at least two sub elements under a gateway to allow for respective End elements to be displayed |
21 | | - let subElements; |
22 | | - if (targetElement.nextElements.length > 1) { |
23 | | - subElements = targetElement.nextElements; |
24 | | - } else if (targetElement.nextElements.length === 1) { |
25 | | - subElements = [...targetElement.nextElements, {}]; |
26 | | - } else { |
27 | | - subElements = [{}, {}]; |
28 | | - } |
29 | | - subElements.forEach((next) => recursivelyCollectPaths(next.id, elements, currentPath.slice(0), otherPaths)); |
| 29 | + if (isDivergingGateway(targetElement)) { |
| 30 | + // check on each sub-path after the targeted diverging gateway |
| 31 | + targetElement.nextElements.forEach((next) => checkForCircularReference(next.id, elements, currentPath.slice(0))); |
30 | 32 | } else { |
31 | | - recursivelyCollectPaths(targetElement.nextElementId, elements, currentPath, otherPaths); |
| 33 | + // continue check after the targeted content element |
| 34 | + checkForCircularReference(targetElement.nextElementId, elements, currentPath); |
32 | 35 | } |
33 | 36 | }; |
34 | | -/* |
35 | | -const filterPathsWithDifferingStart = ([elementId, pathsIncludingElement]: [string, Array<Array<string>>]): boolean => { |
36 | | - if (pathsIncludingElement.length < 2) { |
37 | | - return false; |
38 | | - } |
39 | | - const elementIndex = pathsIncludingElement[0].indexOf(elementId); |
40 | | - if (pathsIncludingElement.some((path) => path.indexOf(elementId) !== elementIndex)) { |
41 | | - return true; |
| 37 | + |
| 38 | +/** |
| 39 | + * Check whether the given two parents of the specified child element are neighbours. |
| 40 | + * |
| 41 | + * @param {FlowElement} child - element being referenced from multiple parents (thereby being preceded by an implicit converging gateway) |
| 42 | + * @param {FlowElement} firstParent - leading specific preceding element from which the designated child is being referenced |
| 43 | + * @param {FlowElement} secondParent - trailing specific preceding element from which the designated child is being referenced |
| 44 | + * @returns {boolean} whether the implicit converging gateway is valid. |
| 45 | + */ |
| 46 | +const areParentsNeighbours = (child: FlowElement, firstParent: FlowElement, secondParent: FlowElement): boolean => { |
| 47 | + // collect path to second element |
| 48 | + const topPathToSecond: Array<FlowElement> = [child, secondParent]; |
| 49 | + let leadingParentOfSecond = secondParent; |
| 50 | + while (leadingParentOfSecond.getPrecedingElements().length) { |
| 51 | + // in case of converging gateway, always take the top element |
| 52 | + leadingParentOfSecond = leadingParentOfSecond.getPrecedingElements()[0]; |
| 53 | + topPathToSecond.push(leadingParentOfSecond); |
42 | 54 | } |
43 | | - const leadingPathPart = pathsIncludingElement[0].slice(0, elementIndex); |
44 | | - return pathsIncludingElement.some((path) => leadingPathPart.some((value, index) => value !== path[index])); |
| 55 | + // iterate backwards over path to first element until finding a common parent (worst case: the root element) |
| 56 | + const bottomPathToFirst: Array<FlowElement> = [child]; |
| 57 | + let firstBranch = firstParent; |
| 58 | + do { |
| 59 | + bottomPathToFirst.push(firstBranch); |
| 60 | + if (topPathToSecond.indexOf(firstBranch) > -1) { |
| 61 | + break; |
| 62 | + } |
| 63 | + const parents = firstBranch.getPrecedingElements(); |
| 64 | + firstBranch = parents[parents.length - 1]; |
| 65 | + // keep going, worst case till the single root element |
| 66 | + } while (true); |
| 67 | + const commonIndexInPathToSecond = topPathToSecond.indexOf(firstBranch); |
| 68 | + const commonSiblings = firstBranch.getFollowingElements(); |
| 69 | + // check whether the two paths are neighbouring when branching off from their right-most common parent |
| 70 | + const firstBranchIndex = commonSiblings.indexOf(bottomPathToFirst[bottomPathToFirst.length - 2]); |
| 71 | + const secondBranchIndex = commonSiblings.indexOf(topPathToSecond[commonIndexInPathToSecond - 1]); |
| 72 | + return firstBranchIndex + 1 === secondBranchIndex; |
45 | 73 | }; |
46 | | -*/ |
47 | 74 |
|
48 | | -export const validatePaths = ({ firstElementId, elements }: FlowModelerProps["flow"]): void => { |
49 | | - const paths: Array<Array<string>> = []; |
50 | | - recursivelyCollectPaths(firstElementId, elements, [], paths); |
51 | | - /* |
52 | | - const elementsOnMultiplePaths = Object.keys(elements) |
53 | | - .map((elementId) => [elementId, paths.filter((path) => path.includes(elementId))] as [string, Array<Array<string>>]) |
54 | | - .filter(filterPathsWithDifferingStart); |
55 | | - const getIndexOfPath = (path: Array<string>): number => paths.indexOf(path); |
56 | | - const invalidElements = elementsOnMultiplePaths.filter(([, pathsWithOverlap]) => { |
57 | | - const indexes = pathsWithOverlap.map(getIndexOfPath); |
58 | | - return indexes.length && indexes[0] + indexes.length != indexes[indexes.length - 1] + 1; |
59 | | - }); |
| 75 | +/** |
| 76 | + * Check whether the implicit converging gateway in front of the given element is valid, i.e. whether all connection pairs are direct neighbours. |
| 77 | + * |
| 78 | + * @param {FlowElement} convergingGateway - element being referenced from multiple parents (thereby being preceded by an implicit converging gateway) |
| 79 | + * @returns {boolean} whether the implicit converging gateway is invalid (beware the negation!) |
| 80 | + */ |
| 81 | +const isInvalidConvergingGateway = (convergingGateway: FlowElement): boolean => { |
| 82 | + const connectedElements = convergingGateway.getPrecedingElements(); |
| 83 | + return connectedElements |
| 84 | + .slice(1) |
| 85 | + .some((nextElement, previousIndex) => !areParentsNeighbours(convergingGateway, connectedElements[previousIndex], nextElement)); |
| 86 | +}; |
| 87 | + |
| 88 | +/** |
| 89 | + * Validate that the parsed data model can be properly displayed. Ensuring that only directly neighbouring paths can link to the same element. |
| 90 | + * |
| 91 | + * @param {FlowElement} treeRootElement - root of the parsed data model to validate |
| 92 | + * @throws Error in case of any (implicit) converging gateways connecting non-neighbouring paths |
| 93 | + */ |
| 94 | +export const validatePaths = (treeRootElement: FlowElement): void => { |
| 95 | + // use Set to automatically filter out duplicates and thereby avoid checking the same gateway repeatedly |
| 96 | + const convergingGateways = new Set<FlowElement>(); |
| 97 | + const collectConvergingGateways = (element: FlowElement): void => { |
| 98 | + if (element.getId()) { |
| 99 | + if (element.getPrecedingElements().length > 1) { |
| 100 | + convergingGateways.add(element); |
| 101 | + } |
| 102 | + element.getFollowingElements().forEach(collectConvergingGateways); |
| 103 | + } |
| 104 | + }; |
| 105 | + collectConvergingGateways(treeRootElement); |
| 106 | + const invalidElements = Array.from(convergingGateways).filter(isInvalidConvergingGateway); |
60 | 107 | if (invalidElements.length) { |
61 | 108 | throw new Error( |
62 | 109 | `Multiple references only valid from neighbouring paths. Invalid references to: '${invalidElements |
63 | | - .map(([elementId]) => elementId) |
| 110 | + .map((gateway) => gateway.getId()) |
64 | 111 | .join("', '")}'` |
65 | 112 | ); |
66 | 113 | } |
67 | | - */ |
68 | 114 | }; |
0 commit comments