Skip to content

Commit

Permalink
feat: validate circulair deps (detached branches
Browse files Browse the repository at this point in the history
  • Loading branch information
BrightGrafana committed Nov 16, 2023
1 parent 7f6ed11 commit e4691c2
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 153 deletions.
158 changes: 79 additions & 79 deletions src/tree.ts
Expand Up @@ -2,101 +2,101 @@ import { RawNode, TreeLevelOrderMode, TreeNode } from 'models';
import { Validator } from 'validator';

export class Tree {
private tree: TreeNode[] = [];

constructor(private rawNodes: RawNode[], private orderLevels: TreeLevelOrderMode) {
Validator.validateTreeInput(this.rawNodes);
this.buildTree();
// TODO: find circulair deps
private tree: TreeNode[] = [];

constructor(private rawNodes: RawNode[], private orderLevels: TreeLevelOrderMode) {
Validator.validateTreeInput(this.rawNodes);
this.buildTree();
Validator.validateTreeBranches(this.rawNodes, this.tree);
}

private buildTree() {
if (this.orderLevels === TreeLevelOrderMode.Asc) {
this.rawNodes.sort((a, b) => a.name.localeCompare(b.name));
} else if (this.orderLevels === TreeLevelOrderMode.Desc) {
this.rawNodes.sort((a, b) => b.name.localeCompare(a.name));
}

private buildTree() {
if (this.orderLevels === TreeLevelOrderMode.Asc) {
this.rawNodes.sort((a, b) => a.name.localeCompare(b.name));
} else if (this.orderLevels === TreeLevelOrderMode.Desc) {
this.rawNodes.sort((a, b) => b.name.localeCompare(a.name));
}

const nodeMap: { [id: string]: TreeNode } = {};

// Create a mapping from id to nodes
for (const rawNode of this.rawNodes) {
nodeMap[rawNode.id] = {
id: rawNode.id,
name: rawNode.name,
children: [],
};
}
const nodeMap: { [id: string]: TreeNode } = {};

const rootNodes: TreeNode[] = [];
// Create a mapping from id to nodes
for (const rawNode of this.rawNodes) {
nodeMap[rawNode.id] = {
id: rawNode.id,
name: rawNode.name,
children: [],
};
}

// Connect children to their parent nodes
for (const rawNode of this.rawNodes) {
const node = nodeMap[rawNode.id];
if (rawNode.parent && nodeMap[rawNode.parent]) {
nodeMap[rawNode.parent].children.push(node);
} else {
rootNodes.push(node);
}
}
const rootNodes: TreeNode[] = [];

this.tree = rootNodes;
// Connect children to their parent nodes
for (const rawNode of this.rawNodes) {
const node = nodeMap[rawNode.id];
if (rawNode.parent && nodeMap[rawNode.parent]) {
nodeMap[rawNode.parent].children.push(node);
} else {
rootNodes.push(node);
}
}

getTree(): TreeNode[] {
return this.tree;
this.tree = rootNodes;
}

getTree(): TreeNode[] {
return this.tree;
}

/**
* Get the TreeNode IDs up to a specified depth.
*
* @param {number} maxDepth - The maximum depth to consider for expanded nodes.
* @returns {string[]} An array of node IDs that are expanded.
*/
getNodeIdsForDepth(maxDepth: number): string[] {
if (!maxDepth || maxDepth < 0) {
throw new ReferenceError('maxDepth should be positive number');
}

/**
* Get the TreeNode IDs up to a specified depth.
*
* @param {number} maxDepth - The maximum depth to consider for expanded nodes.
* @returns {string[]} An array of node IDs that are expanded.
*/
getNodeIdsForDepth(maxDepth: number): string[] {
if (!maxDepth || maxDepth < 0) {
throw new ReferenceError('maxDepth should be positive number');
}

const traverse = (nodes: TreeNode[], currentDepth: number): string[] => {
const result: string[] = [];
const traverse = (nodes: TreeNode[], currentDepth: number): string[] => {
const result: string[] = [];

for (const node of nodes) {
if (node.children.length === 0) {
continue;
}
for (const node of nodes) {
if (node.children.length === 0) {
continue;
}

if (currentDepth < maxDepth) {
result.push(node.id);
}
if (currentDepth < maxDepth) {
result.push(node.id);
}

if (currentDepth + 1 < maxDepth) {
result.push(...traverse(node.children || [], currentDepth + 1));
}
}
if (currentDepth + 1 < maxDepth) {
result.push(...traverse(node.children || [], currentDepth + 1));
}
}

return result;
};
return result;
};

return traverse(this.tree, 0);
}
return traverse(this.tree, 0);
}

getPath(id: TreeNode['id']): Array<TreeNode['id']> {
const traverse = (nodes: TreeNode[], search: TreeNode['id']): Array<TreeNode['id']> => {
for (const node of nodes) {
if (node.id === search) {
return [node.id];
}
getPath(id: TreeNode['id']): Array<TreeNode['id']> {
const traverse = (nodes: TreeNode[], search: TreeNode['id']): Array<TreeNode['id']> => {
for (const node of nodes) {
if (node.id === search) {
return [node.id];
}

const childResult = traverse(node.children, search);
if (childResult.length > 0) {
return [node.id, ...childResult];
}
}
const childResult = traverse(node.children, search);
if (childResult.length > 0) {
return [node.id, ...childResult];
}
}

return [];
};
return [];
};

return traverse(this.tree, id);
}
return traverse(this.tree, id);
}
}
148 changes: 74 additions & 74 deletions src/validator.ts
Expand Up @@ -6,92 +6,92 @@ import { Utils } from './utils';
* A utility class for validating a tree structure represented by an array of nodes.
*/
export class Validator {
/**
* Validates the input tree for duplicate node IDs and child relationships.
* @param {RawNode[]} rawNodes - The array of nodes representing the tree structure.
* @throws {Error} If a duplicate node ID is found.
*/
public static validateTreeInput(rawNodes: RawNode[]): void {
/**
* Validates the input tree for duplicate node IDs and child relationships.
* @param {RawNode[]} rawNodes - The array of nodes representing the tree structure.
* @throws {Error} If a duplicate node ID is found.
* Set to store encountered node IDs.
* @type {Set<RawNode['id']>}
*/
public static validateTreeInput(rawNodes: RawNode[]): void {
/**
* Set to store encountered node IDs.
* @type {Set<RawNode['id']>}
*/
const nodeIds: Set<RawNode['id']> = new Set();
const nodeIds: Set<RawNode['id']> = new Set();

rawNodes.forEach((node: RawNode) => {
if (nodeIds.has(node.id)) {
throw new ReferenceError(`Duplicated ID found for id: ${node.id}`);
}
if (node.id === node.parent) {
throw new ReferenceError(`Parent can not be mapped to itself. For id: ${node.id}`);
}
rawNodes.forEach((node: RawNode) => {
if (nodeIds.has(node.id)) {
throw new ReferenceError(`Duplicated ID found for id: ${node.id}`);
}
if (node.id === node.parent) {
throw new ReferenceError(`Parent can not be mapped to itself. For id: ${node.id}`);
}

nodeIds.add(node.id);
});
nodeIds.add(node.id);
});

// list of all ID's is build
// list of all ID's is build

rawNodes.forEach((node: RawNode) => {
if (node.parent && !nodeIds.has(node.parent)) {
throw new ReferenceError(`Parent not found for id ${node.id} parent ${node.parent}`);
}
});
}
rawNodes.forEach((node: RawNode) => {
if (node.parent && !nodeIds.has(node.parent)) {
throw new ReferenceError(`Parent not found for id ${node.id} parent ${node.parent}`);
}
});
}

public static validateTreeBranches(rawNodes: RawNode[], tree: TreeNode[]): void {
// fetch all nodes in tree.
const treeNodes: string[] = [];
const findNodeIds = (nodes: TreeNode[]) => {
nodes.forEach((node) => {
treeNodes.push(node.id);
if (node.children) {
findNodeIds(node.children);
}
});
};
findNodeIds(tree);
public static validateTreeBranches(rawNodes: RawNode[], tree: TreeNode[]): void {
// fetch all nodes used in the tree.
const uniqueTreeNodes: string[] = [];
const findNodeIds = (nodes: TreeNode[]) => {
nodes.forEach((node) => {
uniqueTreeNodes.push(node.id);
if (node.children) {
findNodeIds(node.children);
}
});
};
findNodeIds(tree);

// find unused (detached) rawNodes
rawNodes.forEach((node) => {
if (!treeNodes.includes(node.id)) {
throw new ReferenceError(`Detached branch detected for id: ${node.id}`);
}
});
}
// find unused (detached) rawNodes
rawNodes.forEach((node) => {
if (!uniqueTreeNodes.includes(node.id)) {
throw new ReferenceError(`Detached branch detected for id: ${node.id}`);
}
});
}

public static validateOptionsInput(options: PanelOptions, data: PanelData): void {
// Check for required panel options
if (options.displayedTreeDepth === undefined || options.displayedTreeDepth < 0) {
throw new ReferenceError("'Expanded levels' must be defined and >= 0 in panel options.");
}
public static validateOptionsInput(options: PanelOptions, data: PanelData): void {
// Check for required panel options
if (options.displayedTreeDepth === undefined || options.displayedTreeDepth < 0) {
throw new ReferenceError("'Expanded levels' must be defined and >= 0 in panel options.");
}

if (
!options.idColumn ||
!options.labelColumn ||
!options.parentIdColumn ||
options.idColumn.trim() === '' ||
options.labelColumn.trim() === '' ||
options.parentIdColumn.trim() === ''
) {
throw new ReferenceError(
"'Node id field name', 'Node label field name', and 'Node parent id field name' must be defined in panel options."
);
}
if (
!options.idColumn ||
!options.labelColumn ||
!options.parentIdColumn ||
options.idColumn.trim() === '' ||
options.labelColumn.trim() === '' ||
options.parentIdColumn.trim() === ''
) {
throw new ReferenceError(
"'Node id field name', 'Node label field name', and 'Node parent id field name' must be defined in panel options."
);
}

if (!options.dashboardVariableName || options.dashboardVariableName.trim() === '') {
throw new ReferenceError(
"'Dashboard variable name' must be defined in panel options, when using dashboard variable on click mode."
);
}
if (!options.dashboardVariableName || options.dashboardVariableName.trim() === '') {
throw new ReferenceError(
"'Dashboard variable name' must be defined in panel options, when using dashboard variable on click mode."
);
}

// Validate column names
const colNames = Utils.getDataFrameColumnNames(data);
const requiredColumns = [options.idColumn, options.labelColumn, options.parentIdColumn];
// Validate column names
const colNames = Utils.getDataFrameColumnNames(data);
const requiredColumns = [options.idColumn, options.labelColumn, options.parentIdColumn];

for (const colName of requiredColumns) {
if (!colNames.includes(colName)) {
throw new ReferenceError(`'${colName}' is not a table column.`);
}
}
for (const colName of requiredColumns) {
if (!colNames.includes(colName)) {
throw new ReferenceError(`'${colName}' is not a table column.`);
}
}
}
}

0 comments on commit e4691c2

Please sign in to comment.