import partition from 'lodash/partition'; export class TreeNode { public key: string; public value: T; public parent: TreeNode | undefined; public children: TreeNode[]; constructor(key: string, value: T, parent?: TreeNode) { this.key = key; this.value = value; this.parent = parent; this.children = []; } get isLeaf() { return this.children.length === 0; } get hasChildren() { return !this.isLeaf; } } export class Tree { public root: TreeNode; constructor(key: string, value: T) { this.root = new TreeNode(key, value); } *preOrderTraversal(node = this.root): Generator, void, unknown> { yield node; if (node.children.length) { for (const child of node.children) { yield* this.preOrderTraversal(child); } } } *postOrderTraversal(node = this.root): Generator, void, unknown> { if (node.children.length) { for (const child of node.children) { yield* this.postOrderTraversal(child); } } yield node; } *parentTraversal(node: TreeNode): Generator, void, unknown> { let current: TreeNode | undefined = node; while (current) { yield current; current = current.parent; } } getPathToNode(node: TreeNode) { return Array.from(this.parentTraversal(node)).reverse(); } insert(parentNodeKey: string, key: string, value: T) { for (const node of this.preOrderTraversal()) { if (node.key === parentNodeKey) { node.children.push(new TreeNode(key, value, node)); return true; } } return false; } remove(key: string) { for (const node of this.preOrderTraversal()) { const filtered = node.children.filter((c) => c.key !== key); if (filtered.length !== node.children.length) { node.children = filtered; return true; } } return false; } find(key: string) { for (const node of this.preOrderTraversal()) { if (node.key === key) return node; } return undefined; } size() { let size = 0; for (const _ of this.preOrderTraversal()) { size++; } return size; } replaceNode(oldNodeKey: string, newNodeKey: string, newNodeValue: T) { for (const node of this.preOrderTraversal()) { const oldNodeIndex = node.children.findIndex((c) => c.key === oldNodeKey); if (oldNodeIndex >= 0) { const newNode = new TreeNode(newNodeKey, newNodeValue, node); newNode.children = [...(node.children[oldNodeIndex]?.children ?? [])]; node.children.splice(oldNodeIndex, 1, newNode); return; } } } /** * Returns an array constructed from iterating over all the nodes * in the tree and extracting a particular value from each node, * as defined the by extract function. */ getValues(extract: (node: T) => U) { const values: U[] = []; for (const node of this.preOrderTraversal()) { values.push(extract(node.value)); } return values; } /** * Function that determines whether all nodes in the * tree are unique, or whether there are duplicate nodes. * @param isEqual - a function that determines equality * between two objects of type T */ duplicateValues(isEqual: (v1: T, v2: T) => boolean) { const nodes: TreeNode[] = []; const duplicates: T[] = []; for (const node of this.preOrderTraversal()) { if ( nodes.some((previousNode) => isEqual(node.value, previousNode.value)) ) { duplicates.push(node.value); } nodes.push(node); } return duplicates; } filterTree(filterPredicate: (nodeValue: T) => boolean): Tree | null { const filterRecursive = (currentNode: TreeNode): TreeNode | null => { const filteredChildren = currentNode.children .map((child) => filterRecursive(child)) .filter((child): child is TreeNode => child !== null); if (filterPredicate(currentNode.value) || filteredChildren.length > 0) { const newNode = new TreeNode(currentNode.key, currentNode.value); newNode.children = filteredChildren; for (const child of filteredChildren) { child.parent = newNode; } return newNode; } return null; }; const filteredRoot = filterRecursive(this.root); if (!filteredRoot) { return null; } // Manually reconstruct the tree from the filteredRoot const newTree = new Tree(filteredRoot.key, filteredRoot.value); newTree.root.children = filteredRoot.children; // Ensure all child nodes have their parent property correctly set for (const child of newTree.root.children) { child.parent = newTree.root; } return newTree; } } export function sortTreeNodeList(nodeList: readonly any[], agg: any[]): any[] { if (nodeList.length === 0) { return agg; } const [nextLevelNodes, lowerNodes] = partition( nodeList, (node) => node.parentId == null || agg.some((it) => it.id === node.parentId), ); return sortTreeNodeList(lowerNodes, [...agg, ...nextLevelNodes]); } /** * Constructs a tree from a flattened list of nodes, where each node has an id * and an optional parentId. An example input is: * [ * { id: '1', parentId: null }, * { id: '2', parentId: null }, * { id: '3', parentId: '1' }, * { id: '4', parentId: '2' }, * ] * And the output would be a tree with the following structure: * root * / \ * 1 2 * / \ * 3 4 */ export function treeFromList( nodeList: readonly any[], root: T, constructNode: (values: any) => T, ): Tree { const tree = new Tree('root', root); const sortedList = sortTreeNodeList(nodeList, []); for (const policy of sortedList) { const parentKey = sortedList.find((it) => it.id === policy.parentId)?.name ?? 'root'; tree.insert(parentKey, policy.name, constructNode(policy)); } return tree; } /** * Constructs a multilevel list from a flattened list of nodes, where each node * has an id and an optional parentId. An example input is: * [ * { id: '1', parentId: null }, * { id: '2', parentId: null }, * { id: '3', parentId: '1' }, * { id: '4', parentId: '2' }, * { id: '5', parentId: '2' }, * ] * And the output would be a list with the following structure: * [ * { id: '1', parentId: null, children: [{ id: '3', parentId: '1' }] }, * { id: '2', parentId: null, children: [{ id: '4', parentId: '2' }, { id: '5', parentId: '2' }] }, * ] */ export function multilevelListFromFlatList< T extends { id: string; parentId?: string | null | undefined }, >( nodeList: readonly T[], ): (T & { children?: T[]; })[] { const map = new Map(); const roots: (T & { children?: T[] })[] = []; // Initialize map with all nodes nodeList.forEach((node) => { map.set(node.id, { ...node } as T & { children?: T[] }); }); // Build the tree nodeList.forEach((node) => { if (node.parentId == null) { roots.push(map.get(node.id)!); } else { const parent = map.get(node.parentId); if (parent) { if (!parent.children) { parent.children = []; } parent.children.push(map.get(node.id)!); } } }); return roots; }