Skip to content

Commit

Permalink
feat(Tree): implement tree structure w/ tests
Browse files Browse the repository at this point in the history
This commit implements a Tree class on the server for constructing trees
out of accounts, units, or other tree structures.  The only requirement
is that the objects have references pointing them to their parents.
  • Loading branch information
jniles committed Jan 26, 2018
1 parent 1ef175c commit 85892b6
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 3 deletions.
4 changes: 1 addition & 3 deletions server/controllers/finance/accounts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* GET /accounts/:id/balance/
* POST /accounts
* PUT /accounts/:id
* DELETE /accounts/:id
* DELETE /accounts/:id
*
* @todo - move away from calling lookup() before action. This is an
* unnecessary database request.
Expand Down Expand Up @@ -131,8 +131,6 @@ function list(req, res, next) {
let sql =
'SELECT a.id, a.number, a.label, a.locked, a.type_id, a.parent FROM account AS a';

let locked;

if (req.query.detailed === '1') {
sql = `
SELECT a.id, a.enterprise_id, a.locked, a.cc_id, a.pc_id, a.created,
Expand Down
78 changes: 78 additions & 0 deletions server/lib/Tree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* @class Tree
*
* @description
* This file contains the generic class definition of a tree. A tree is defined
* as an array of JSON objects having a parent key referring to another member
* of the array. The only exception is the root node, which does not need to be
* in the tree.
*/
const _ = require('lodash');

/**
* @function buildTreeFromArray
*
* @description
* This function makes a tree data structure from a properly formatted array.
*/
function buildTreeFromArray(nodes, parentId, parentKey) {
// recursion base-case: return nothing if empty array
if (nodes.length === 0) { return null; }

// find nodes which are the children of parentId
const children = nodes.filter(node => node[parentKey] === parentId);

// recurse - for each child node, compute their child-trees using the same
// buildTreeFromArray() command
children.forEach(node => {
node.children = buildTreeFromArray(nodes, node.id, parentKey);
});

// return the list of children
return children;
}

/**
* @function flatten
*
* @description
* Operates on constructed trees which have "children" attributes holding all
* child nodes. It computes the depth of the node and affixes it to the child
* node. This function is recursive.
*
* @param {Array} tree - tree structure created by the tree constructor
* @param {Number} depth - depth attribute
* @param {Boolen} pruneChildren - instructs the function to remove children
*/
function flatten(tree, depth, pruneChildren = true) {
let currentDepth = (Number.isNaN(depth) || _.isUndefined(depth)) ? -1 : depth;
currentDepth += 1;

return tree.reduce((array, node) => {
node.depth = currentDepth;
const items = [node].concat(node.children ?
flatten(node.children, currentDepth, pruneChildren) : []);

if (pruneChildren) { delete node.children; }

return array.concat(items);
}, []);
}


class Tree {
constructor(data = [], parentKey = 'parent', rootId = 0) {
this._data = data;
this._parentKey = parentKey;
this._rootId = rootId;

// build the tree with the provided root id and parentKey
this._tree = buildTreeFromArray(_.cloneDeep(data), rootId, parentKey);
}

toArray() {
return flatten(this._tree);
}
}

module.exports = Tree;
87 changes: 87 additions & 0 deletions test/server-unit/Tree.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const Tree = require('../../server/lib/Tree');
const { expect } = require('chai');

function TreeUnitTests() {
/*
* This tree looks like this:
* ROOT
* / | \
* id:1 id:4 id:6
* / \
* id:2 id:3
* /
* id:5
*/
const nodes = [{
id : 1,
parent : 0,
}, {
id : 2,
parent : 1,
}, {
id : 3,
parent : 6,
}, {
id : 4,
parent : 0,
}, {
id : 5,
parent : 2,
}, {
id : 6,
parent : 0,
}];

it('#constructor() should populate private variables', () => {
const tree = new Tree(nodes);
expect(tree._data).to.deep.equal(nodes);
expect(tree._tree).to.not.be.undefined;
});

it('#constructor() should not have side-effects', () => {
const cloned = JSON.parse(JSON.stringify(nodes));
const tree = new Tree(nodes);
expect(tree._data).to.deep.equal(cloned);
});

it('#constructor() node id:0 should have three childen', () => {
const tree = new Tree(nodes)._tree;
expect(tree).to.have.length(3);
});

it('#constructor() nodes should all have "children" arrays', () => {
const tree = new Tree(nodes)._tree;
tree.forEach(node => {
expect(node).to.have.property('children');
expect(node.children).to.be.an('array');
});
});

it('#constructor() node id:4 should not have any children', () => {
const tree = new Tree(nodes)._tree;
const node4 = tree[1];
expect(node4.id).to.equal(4);
expect(node4.children).to.have.length(0);
});

it('#constructor() node id:6 should have one child', () => {
const tree = new Tree(nodes)._tree;
const node6 = tree[2];
expect(node6.id).to.equal(6);
expect(node6.children).to.have.length(1);
});

it('#toArray() should return an array', () => {
const array = new Tree(nodes).toArray();
expect(array).to.be.an('array');
});

it('#toArray() should populate the "depth" key on all nodes', () => {
const array = new Tree(nodes).toArray();
array.forEach(node => {
expect(node).to.have.property('depth');
});
});
}

describe('Tree.js', TreeUnitTests);

0 comments on commit 85892b6

Please sign in to comment.