UNPKG

hierarchy-model

Version:

A data model for a hierarchy of nodes, used by roles-hierarchy and permissions-hierarchy

215 lines (191 loc) 7.91 kB
"use strict"; import TreeModel from 'tree-model'; import topiary from 'topiary'; import _ from 'underscore'; if (process) { process.env.SUPPRESS_NO_CONFIG_WARNING = 'y'; } export default class Hierarchy { /** * create a new instance of Hierarchy * @param {Object} paramsObj containing a Hierarchy and a loggingConfig (optional) and a TreeModel config (optional): * { * hierarchy: {"name":"teacher", "children": [ {"name":"student"} ]}, * treeModelConfig: { "childrenPropertyName": "children" }, * loggerCallback: an object that has debug, info, warn and error properties whose values are logging functions. * } */ constructor(paramsObj) { // set up config defaults let treeModelConfig = paramsObj.treeModelConfig || { "childrenPropertyName": "children" }; this.logger = paramsObj.loggerCallback || {debug: (msg)=>{}, info: (msg)=>{}, warn: (msg)=>{}, error: (msg, err)=>{}}; this.childrenPropertyName = treeModelConfig.childrenPropertyName; // actual constructor stuff here. // get treeModelConfig from config // we need a clone of the treeModelConfig (it doesn't work straight from node-config) treeModelConfig = JSON.parse(JSON.stringify(treeModelConfig)); this.treeModel = new TreeModel(treeModelConfig); this.root = this.treeModel.parse(paramsObj.hierarchy); this.logger.debug(this.getTopiaryAsString()); } /** * re-create the hierarchy with a new object structure. * @param {Object} hierarchy */ reparse(hierarchy) { this.root = this.treeModel.parse(hierarchy); } _findNode(nodeName, startNode = this.root) { return startNode.first({ strategy: 'breadth' }, function (node) { return node.model.name === nodeName; }); } _getOrganizationsForUser(myUserObj) { let myOrganizations = []; if (myUserObj) { // figure out which organizations we belong to. if (myUserObj.profile && (myUserObj.profile.organization || myUserObj.profile.organizations)) { // note the plural if (myUserObj.profile.organization) { // there can be only one. myOrganizations = [myUserObj.profile.organization]; } else { // this guy is in multiple organizations. myOrganizations = JSON.parse(JSON.stringify(myUserObj.profile.organizations)); // clone organizations } } else { // default to the global group if there is no org info stored on the profile myOrganizations = [_GLOBAL_GROUP]; } } return myOrganizations; } /** * Find the model for a node in the hierarchy, by name * @param {string} nodeName - the name of the node to find (i.e. 'name' property value) * @param {object} [startNode] - the node in the hierarchy to start from * @returns {object} - the model of the node in the tree that matches */ findNodeInHierarchy(nodeName, startNode) { let result = this._findNode(nodeName, startNode); if (result && result.model) { this.logger.debug(`findNodeInHierarchy(${nodeName}) => returning ${JSON.stringify(result.model, null, 2)}`); return result.model; } this.logger.debug(`findNodeInHierarchy(${nodeName}) => returning undefined`); } /** * Find the node object for a node in the hierarchy, by name * @param {string} nodeName - the name of the node to find (i.e. 'name' property value) * @param {object} [startNode] - the node in the hierarchy to start from */ findNodeObj(nodeName, startNode) { return this._findNode(nodeName, startNode); } /** * Return the descendent node of the given nodeName if found. * @param {string} nodeName - the name of the node underneath which we should search * @param {string} descendantNodeName - the name of the descendant node to find * @param {object} [startNode] - the node in the hierarchy to start from * @returns {object} - the node of the descendant, or undefined or false if not found. */ findDescendantNodeByName(nodeName, descendantNodeName, startNode) { // get the node for the node name let senior = this._findNode(nodeName, startNode); if (!senior) { return false; } let junior = this._findNode(descendantNodeName, senior); if (junior) { this.logger.debug(`findDescendantNodeByName(${nodeName},${descendantNodeName}) => returning ${JSON.stringify(junior.model, null, 2)}`); return junior.model; } else { this.logger.debug(`findDescendantNodeByName(${nodeName}) => returning undefined`); } } /** * Get the names of subordinate nodes as an array * @param {string} nodeName - the name of the senior node i.e. 'name' property value * @param {object} [startNode] - the node in the hierarchy to start from * @returns {Array} - the subordinate node names if any, otherwise undefined. */ getAllDescendantNodesAsArray(nodeName, startNode) { // find the node for the given node name let seniorNode = this._findNode(nodeName, startNode); if (seniorNode) { // get all the nodes under this one let result = seniorNode.all({strategy: 'breadth'}, function (node) { return node.model.name !== nodeName; }).map((item) => { // get the names of each node return item.model.name; }); this.logger.debug(`getAllDescendantNodesAsArray(${nodeName}) => returning ${JSON.stringify(result, null, 2)}`); return result; } } /** * get a string suitable for printing, via the topiary library. * @param {object} hierarchy - a Hierarchy instance * @returns {string} a string representation of the hierarchy */ getTopiaryAsString(hierarchy = this.root) { return topiary(hierarchy.model, this.childrenPropertyName); } /** * Process each node in the tree via a callback, halting when your callback returns false. * @param {function} callback a function that takes a single parameter, 'node', * which is the value of the node currently being processed. Return false from the callback to halt the traversal. */ walkNodes(callback) { this.root.walk(callback); } /** * Add a child to a parent. * @param {Object} parentNode the node in the hierarchy to which the child should be added * @param {Object} childNode a node or tree * @returns {Object} the child node. */ addNodeAsChildOfNode(parentNode, childNode) { let debug = this.logger.debug; debug(`parentNode: ${JSON.stringify(parentNode.model)}`); debug(`childNode: ${JSON.stringify(childNode.model)}`); return parentNode.addChild(childNode); } /** * Get the array of Nodes representing the path from the root to this Node (inclusive). * @param {Object} node * @returns {Object} the array of Nodes representing the path from the root to this Node (inclusive). */ static getPathOfNode(node) { return node.getPath(); } /** * Get the array of Node names representing the path from the root to this Node (inclusive). * @param {Object} node * @returns {Array<String>} the array of Strings representing the path from the root to this Node (inclusive). */ static getNamesOfNodePath(node) { return _.map(node.getPath(), (thisNode) => { return thisNode.model.name; }); } /** * Drop the subtree starting at this node. Returns the node itself, which is now a root node. * @param {Object} node the node in the hierarchy to drop. * @returns {Object} node the node that just got dropped. */ static deleteNodeFromHierarchy(node) { return node.drop(); } /** * get the underlying TreeModel instance * @returns {Object} the underlying TreeModel instance. */ getTreeModel() { return this.treeModel; } /** * Create Node (which is itself just a TreeModel) * @param {Object} paramsObj - an object which has 'name' and 'children' properties */ getNewNode(paramsObj) { return this.treeModel.parse(paramsObj); } }