Comparing version 1.2.1 to 2.0.0
@@ -0,271 +1,430 @@ | ||
var util = require('util'); | ||
var _ = require('lodash'); | ||
var inflection = require('inflection'); | ||
var merge = require('object-merge'); | ||
function Treeize(options) { | ||
var globalOptions = { | ||
delimiter: ':', | ||
debug: false, | ||
benchmark: { | ||
speed: true, | ||
size: true | ||
this.baseOptions = { | ||
input: { | ||
delimiter: ':', | ||
detectCollections: true, | ||
uniformRows: false, | ||
}, | ||
fast: false, | ||
prune: false, | ||
collections: { | ||
auto: true, | ||
} | ||
output: { | ||
prune: true, | ||
objectOverwrite: true, | ||
resultsAsObject: false, | ||
}, | ||
log: false, | ||
}; | ||
this.options = this.setOptions = function(options) { | ||
if (options) { | ||
_.extend(globalOptions, options); | ||
this.data = { | ||
signature: { | ||
nodes: [], | ||
type: null, | ||
}, | ||
seed: [], | ||
tree: [], | ||
}; | ||
return this; | ||
} else { | ||
return globalOptions; | ||
} | ||
this.stats = { | ||
time: { | ||
total: 0, | ||
signatures: 0, | ||
}, | ||
rows: 0, | ||
sources: 0, | ||
}; | ||
this.getSignatures = function(row, options) { | ||
var paths = []; | ||
var signatures = []; | ||
var localOptions = globalOptions; | ||
var target = null; | ||
// set default options (below) | ||
this.resetOptions(); | ||
_.each(row, function(value, attributePath) { | ||
var splitPath = attributePath.split(localOptions.delimiter); | ||
if (options) { | ||
this.options(options); | ||
} | ||
paths.push({ | ||
path: attributePath, | ||
splitPath: splitPath, | ||
node: splitPath[splitPath.length - 2], | ||
processed: false | ||
}); | ||
}); | ||
return this; | ||
} | ||
// sort paths to prepare for processing | ||
paths.sort(function(a, b) { | ||
return a.splitPath.length < b.splitPath.length ? -1 : 1; | ||
}); | ||
Treeize.prototype.log = function() { | ||
if (this.options().log) { | ||
console.log.apply(this, arguments); | ||
} | ||
while (target = _.findWhere(paths, { processed: false })) { | ||
// get associated group | ||
var group = _.where(paths, { node: target.node, processed: false }); | ||
return this; | ||
}; | ||
// build blueprint for current group | ||
var blueprint = []; | ||
_.each(group, function(groupItem) { | ||
blueprint.push(groupItem.path); | ||
groupItem.processed = true; | ||
}); | ||
Treeize.prototype.getData = function() { | ||
return this.data.tree; | ||
}; | ||
signatures.push(blueprint); | ||
} | ||
Treeize.prototype.getStats = function() { | ||
return this.stats; | ||
}; | ||
return signatures; | ||
/* | ||
Reads the signature from a given row to determine path mapping. If passed without params, assumes | ||
a forced reading which will last | ||
*/ | ||
Treeize.prototype.signature = function(row, options, auto) { | ||
if (!row) { | ||
return this.data.signature; | ||
} | ||
this.removeFrom = function (object, key) { | ||
if (_.isArray(object)) { | ||
object.splice(key, 1); | ||
} else { | ||
delete object[key]; | ||
// start timer | ||
var t1 = (new Date()).getTime(); | ||
// sets the signature as fixed (or not) when manually set | ||
this.data.signature.isFixed = auto !== true; | ||
var nodes = this.data.signature.nodes = []; | ||
var isArray = _.isArray(row); | ||
var opt = merge(this.options(), options || {}); | ||
this.data.signature.type = isArray ? 'array' : 'object'; | ||
_.each(row, function(value, key) { | ||
// set up attribute | ||
var attr = {} | ||
attr.key = typeof key === 'number' ? key : key.replace(/^[\*\-\+]|[\*\-\+]$/g,''); | ||
attr.fullPath = isArray ? value : key; | ||
attr.split = attr.fullPath.split(opt.input.delimiter); | ||
attr.path = _.initial(attr.split).join(opt.input.delimiter);//.replace(/^[\*\-\+]|[\*\-\+]$/g,''); | ||
attr.parent = _.initial(attr.split, 2).join(opt.input.delimiter).replace(/^[\*\-\+]|[\*\-\+]$/g,''); | ||
attr.node = attr.split[attr.split.length - 2]; | ||
attr.attr = _.last(attr.split); | ||
if (attr.attr.match(/\*/gi)) { | ||
attr.attr = attr.attr.replace(/[\*]/gi,''); | ||
attr.pk = true; | ||
} | ||
} | ||
this.prune = function (treeData) { | ||
var self = this; | ||
_.each(treeData, function (itm, key) { | ||
if (_.isNull(itm) || _.isUndefined(itm)) { | ||
self.removeFrom(treeData, key); | ||
} | ||
if (attr.pk) { | ||
this.log('primary key detected in node "' + attr.attr + '"'); | ||
} | ||
if (_.isObject(itm)) { | ||
treeData[key] = itm = self.prune(itm); | ||
if (_.isNull(itm) || _.isUndefined(itm) || (_.isPlainObject(itm) && _.isEmpty(itm))) { | ||
self.removeFrom(treeData, key); | ||
} | ||
} | ||
}); | ||
// set up node reference | ||
var node = _.findWhere(nodes, { path: attr.path }); | ||
if (!node) { | ||
node = { path: attr.path, attributes: [], blueprint: [] }; | ||
nodes.push(node); | ||
} | ||
return treeData; | ||
} | ||
node.isCollection = !attr.node || (opt.input.detectCollections && inflection.pluralize(attr.node) === attr.node); | ||
this.grow = function(flatData, config) { | ||
var t1; | ||
var localOptions = _.extend(this.options(), config || {}); | ||
var translated = []; | ||
var collectionFlag = attr.node && attr.node.match(/^[\-\+]|[\-\+]$/g); | ||
if (collectionFlag) { | ||
this.log('collection flag "' + collectionFlag + '" detected in node "' + attr.node + '"'); | ||
node.flags = true; | ||
node.isCollection = attr.node.match(/^\+|\+$/g); | ||
attr.node = attr.node.replace(/^[\*\-\+]|[\*\-\+]$/g,''); // clean node | ||
} | ||
// optional benchmark | ||
if (localOptions.benchmark.speed) { | ||
t1 = (new Date()).getTime(); | ||
node.name = attr.node; | ||
node.depth = attr.split.length - 1; | ||
node.parent = _.initial(attr.split, 2).join(opt.input.delimiter); | ||
node.attributes.push({ name: attr.attr, key: attr.key }); | ||
if (attr.pk) { | ||
this.log('adding node to blueprint'); | ||
node.flags = true; | ||
node.blueprint.push({ name: attr.attr, key: attr.key }); | ||
} | ||
}, this); | ||
if (!flatData || !flatData.length) { return flatData; } | ||
// backfill blueprint when not specifically defined | ||
_.each(nodes, function(node) { | ||
if (!node.blueprint.length) { | ||
node.blueprint = node.attributes; | ||
} | ||
}); | ||
if (localOptions.fast) { | ||
var signatures = this.getSignatures(flatData[0]); | ||
nodes.sort(function(a, b) { return a.depth < b.depth ? -1 : 1; }); | ||
_.each(flatData, function(row, index) { | ||
var paths = []; | ||
var trails = {}; | ||
var trail = translated; | ||
var target = null; | ||
// end timer and add time | ||
var t2 = ((new Date()).getTime() - t1); | ||
this.stats.time.signatures += t2; | ||
this.stats.time.total += t2; | ||
// set up paths for processing | ||
_.each(row, function(value, attributePath) { | ||
var splitPath = attributePath.split(localOptions.delimiter); | ||
return this; | ||
}; | ||
paths[attributePath] = { | ||
fullPath: _.initial(splitPath, 1).join(localOptions.delimiter), | ||
parentPath: _.initial(splitPath, 2).join(localOptions.delimiter), | ||
node: splitPath[splitPath.length - 2], | ||
attribute: _.last(splitPath), | ||
value: value | ||
}; | ||
}); | ||
Treeize.prototype.getSignature = function() { | ||
return this.signature(); | ||
}; | ||
// proccess each unprocessed path in the row | ||
_.each(signatures, function(signature) { | ||
// set target to first node of group | ||
var target = paths[signature[0]]; | ||
Treeize.prototype.setSignature = function(row, options) { | ||
return this.signature(row, options); | ||
}; | ||
// build blueprint for current group | ||
var blueprint = {}; | ||
_.each(signature, function(item) { | ||
blueprint[paths[item].attribute] = paths[item].value; | ||
}); | ||
Treeize.prototype.setSignatureAuto = function(row, options) { | ||
return this.signature(row, options, true); | ||
}; | ||
// set up first node, everythign else should have parent path | ||
if (!(trail = trails[target.parentPath])) { | ||
if (!(trail = _.findWhere(translated, blueprint))) { | ||
translated.push(trail = blueprint); | ||
} | ||
trails[target.parentPath] = trail; | ||
} | ||
Treeize.prototype.clearSignature = function() { | ||
this.data.signature = { nodes: [], type: null }; | ||
this.data.signature.isFixed = false; | ||
// trail is now at parent node, standing by for current node injection | ||
if (target.node) { // should skip root | ||
var isCollection; | ||
return this; | ||
}; | ||
// if collection auto detection is on, default to pluralization | ||
isCollection = globalOptions.collections.auto && target.node === inflection.pluralize(target.node); | ||
// manual overrides work both with and without collection auto detection | ||
// [nodename]- indicates non collection | ||
// [nodename]+ indicates collection | ||
if (target.node.match(/[\+\-]$/)) { | ||
isCollection = target.node.match(/\+$/) || isCollection; | ||
isCollection = isCollection && !target.node.match(/\-$/); | ||
Treeize.prototype.grow = function(data, options) { | ||
var opt = merge(this.options(), options || {}); | ||
target.node = target.node.replace(/[\+\-]$/, ''); | ||
} | ||
// chain past if no data to grow | ||
if (!data || !_.isArray(data) || !data.length) { | ||
return this; | ||
} | ||
var node = trail[target.node] = (trail[target.node] || (isCollection ? [blueprint] : blueprint)); | ||
this.log('OPTIONS>', opt); | ||
if (isCollection && !(node = _.findWhere(trail[target.node], blueprint))) { | ||
node = blueprint; | ||
trail[target.node].push(node); | ||
} | ||
// locate existing signature (when sharing signatures between data sources) | ||
var signature = this.getSignature(); | ||
trails[target.fullPath] = node; | ||
} | ||
}); | ||
// set data uniformity (locally) to true to avoid signature fetching on data rows | ||
if (_.isArray(data[0])) { | ||
opt.input.uniformRows = true; | ||
} | ||
if (!signature.nodes.length) { | ||
this.log('setting signature from first row of data (auto)'); | ||
// set signature from first row | ||
signature = this.setSignatureAuto(data[0], options).getSignature(); | ||
// remove header row in flat array data (avoids processing headers as actual values) | ||
if (_.isArray(data[0])) { | ||
var originalData = data; | ||
data = []; | ||
// copy data without original signature row before processing | ||
_.each(originalData, function(row, index) { | ||
if (index > 0) { | ||
data.push(row); | ||
} | ||
}); | ||
} else { | ||
// use original method when structure uncertain (slower) | ||
} | ||
} | ||
_.each(flatData, function(row, index) { | ||
var paths = []; | ||
var trails = {}; | ||
var trail = translated; | ||
var target = null; | ||
if (opt.output.resultsAsObject && _.isArray(this.data.tree)) { | ||
this.data.tree = {}; | ||
} | ||
// set up paths for processing | ||
_.each(row, function(value, attributePath) { | ||
var splitPath = attributePath.split(localOptions.delimiter); | ||
this.log('SIGNATURE>', util.inspect(this.getSignature(), false, null)); | ||
paths.push({ | ||
splitPath: _.initial(splitPath, 1), | ||
fullPath: _.initial(splitPath, 1).join(localOptions.delimiter), | ||
parentPath: _.initial(splitPath, 2).join(localOptions.delimiter), | ||
node: splitPath[splitPath.length - 2], | ||
attribute: _.last(splitPath), | ||
value: value, | ||
processed: false | ||
}); | ||
}); | ||
this.stats.sources++; | ||
var t1 = (new Date()).getTime(); | ||
// sort paths to prepare for processing | ||
paths.sort(function(a, b) { | ||
return a.splitPath.length < b.splitPath.length ? -1 : 1; | ||
}); | ||
_.each(data, function(row) { | ||
var trails = {}; // LUT for trails (find parent of new node in trails path) | ||
var trail = root = this.data.tree; // OPTIMIZATION: do we need to reset this trail for each row? | ||
this.log('CURRENT TRAIL STATUS>', trail); | ||
var t = null; | ||
// proccess each unprocessed path in the row | ||
while (target = _.findWhere(paths, { processed: false })) { | ||
// get associated group | ||
var group = _.where(paths, { parentPath: target.parentPath, node: target.node, processed: false }); | ||
// set initial root object path for non-array datasets | ||
if (opt.output.resultsAsObject) { | ||
trails[''] = trail; | ||
} | ||
// build blueprint for current group | ||
var blueprint = {}; | ||
_.each(group, function(groupItem) { | ||
blueprint[groupItem.attribute] = groupItem.value; | ||
groupItem.processed = true; | ||
}); | ||
if (!this.data.signature.isFixed && !opt.input.uniformRows) { | ||
this.log('setting signature from new row of data (auto)'); | ||
// get signature from each row | ||
this.setSignatureAuto(row, opt); | ||
this.log('SIGNATURE>', util.inspect(this.getSignature(), false, null)); | ||
} | ||
// set up first node, everythign else should have parent path | ||
if (!(trail = trails[target.parentPath])) { | ||
if (!(trail = _.findWhere(translated, blueprint))) { | ||
translated.push(trail = blueprint); | ||
} | ||
trails[target.parentPath] = trail; | ||
this.stats.rows++; | ||
if (_.where(this.signature().nodes, { flags: true }).length) { | ||
// flags detected within signature, clean attributes of row | ||
_.each(row, function(value, key) { | ||
if (typeof key === 'string') { | ||
var clean = key.replace(/^[\*\-\+]|[\*\-\+]$/g,''); | ||
if (clean !== key) { | ||
this.log('cleaning key "' + key + '" and embedding as "' + clean + '"'); | ||
row[key.replace(/^[\*\-\+]|[\*\-\+]$/g,'')] = value; // simply embed value at clean path (if not already) | ||
} | ||
} | ||
}, this); | ||
} | ||
// trail is now at parent node, standing by for current node injection | ||
if (target.node) { // should skip root | ||
var isCollection; | ||
_.each(this.signature().nodes, function(node) { | ||
this.log('PROCESSING NODE>', node); | ||
var blueprint = {}; | ||
var blueprintExtended = {}; | ||
// if collection auto detection is on, default to pluralization | ||
isCollection = globalOptions.collections.auto && target.node === inflection.pluralize(target.node); | ||
// create blueprint for locating existing nodes | ||
_.each(node.blueprint, function(attribute) { | ||
var key = (node.path ? (node.path + ':') : '') + attribute.name; | ||
blueprint[attribute.name] = row[attribute.key]; | ||
this.log('creating attribute "' + attribute.name + '" within blueprint', row[attribute.key]); | ||
}, this); | ||
// manual overrides work both with and without collection auto detection | ||
// [nodename]- indicates non collection | ||
// [nodename]+ indicates collection | ||
if (target.node.match(/[\+\-]$/)) { | ||
isCollection = target.node.match(/\+$/) || isCollection; | ||
isCollection = isCollection && !target.node.match(/\-$/); | ||
// create full node signature for insertion/updating | ||
_.each(node.attributes, function(attribute) { | ||
var key = (node.path ? (node.path + ':') : '') + attribute.name; | ||
var value = row[attribute.key]; | ||
target.node = target.node.replace(/[\+\-]$/, ''); | ||
// insert extended blueprint attributes when not empty (or not pruning) | ||
if (!opt.output.prune || (value !== null && value !== undefined)) { | ||
this.log('creating attribute "' + attribute.name + '" within extended blueprint', row[attribute.key]); | ||
blueprintExtended[attribute.name] = row[attribute.key]; | ||
} | ||
}, this); | ||
this.log('EXTENDED BLUEPRINT>', blueprintExtended); | ||
this.log('BLUEPRINT>', blueprint); | ||
// ONLY INSERT IF NOT PRUNED | ||
if (!opt.output.prune || !_.isEmpty(blueprintExtended)) { | ||
// IF 0 DEPTH AND RESULTSASOBJECT, EXTEND ROOT | ||
if (opt.output.resultsAsObject && node.depth === 0) { | ||
_.extend(trails[node.path] = trail = root, blueprintExtended); | ||
this.log('extending blueprint onto root>', trail); | ||
// IF ROOT TRAIL IS NOT YET MAPPED | ||
} else if (node.isCollection && !(trail = trails[node.parent])) { | ||
this.log('PARENT TRAIL NOT FOUND (ROOT?)'); | ||
// set up target node if doesn't exist | ||
if (!(trail = _.findWhere(root, blueprint))) { | ||
root.push(trail = blueprintExtended); | ||
} else { | ||
_.extend(trail, blueprintExtended); | ||
} | ||
trails[node.path] = trail; | ||
// NORMAL NODE TRAVERSAL | ||
} else { | ||
// NOT ROOT CASE | ||
if (node.isCollection) { | ||
// handle collection nodes | ||
this.log('inserting into collection node', trail); | ||
if (!trail[node.name]) { | ||
// node attribute doesnt exist, create array with fresh blueprint | ||
trail[node.name] = [blueprintExtended]; | ||
trails[node.path] = blueprintExtended; | ||
} else { | ||
// node attribute exists, find or inject blueprint | ||
var t; | ||
if (!(t = _.findWhere(trail[node.name], blueprint))) { | ||
trail[node.name].push(trail = blueprintExtended); | ||
} else { | ||
_.extend(t, blueprintExtended); | ||
} | ||
trails[node.path] = t || trail; | ||
} | ||
} else { | ||
// handle non-collection nodes | ||
if (trail == root && node.parent === '') { | ||
root.push(trails[node.parent] = trail = {}); | ||
this.log('root insertion'); | ||
} | ||
trail = trails[node.parent]; | ||
var node = trail[target.node] = (trail[target.node] || (isCollection ? [blueprint] : blueprint)); | ||
// ON DEEP NODES, THE PARENT WILL BE TOO LONG AND FAIL ON THE NEXT IF STATEMENT BELOW | ||
// ASSUMPTION: in deep nodes, no signatures will be present, so entries will simply be pushed onto collections defined within | ||
if (isCollection && !(node = _.findWhere(trail[target.node], blueprint))) { | ||
node = blueprint; | ||
trail[target.node].push(node); | ||
if (!trail) { // do something to fix a broken trail (usually from too deep?) | ||
// backtrack from parent trail segments until trail found, then create creadcrumbs | ||
var breadcrumbs = []; | ||
var segments = node.parent.split(':'); | ||
var pathAttempt = node.parent; | ||
var segmentsStripped = 0; | ||
this.log('path MISSING for location "' + pathAttempt + '"'); | ||
while (!(trail = trails[pathAttempt])) { | ||
segmentsStripped++; | ||
pathAttempt = _.initial(segments, segmentsStripped).join(':'); | ||
this.log('..attempting path location for "' + pathAttempt + '"'); | ||
//infinite loop kickout | ||
if (segmentsStripped > 5) break; | ||
} | ||
this.log('path FOUND for location for "' + pathAttempt + '" after removing ' + segmentsStripped + ' segments'); | ||
// create stored nodes if they don't exist. | ||
_.each(_.rest(segments, segments.length - segmentsStripped), function(segment) { | ||
var isCollection = ((inflection.pluralize(segment) === segment) || segment.match(/^\+|\+$/)) && (!segment.match(/^\-|\-$/)); | ||
// TODO: add modifier detection | ||
this.log('creating or trailing path segment ' + (isCollection ? '[collection]' : '{object}') + ' "' + segment + '"'); | ||
segment = segment.replace(/^[\*\-\+]|[\*\-\+]$/g,''); | ||
if (isCollection) { | ||
// retrieve or set collection segment and push new trail onto it | ||
(trail[segment] = trail[segment] || []).push(trail = {}); | ||
} else { | ||
trail = trail[segment] = trail[segment] || {}; | ||
} | ||
}, this); | ||
} | ||
trails[target.fullPath] = node; | ||
this.log('inserting into non-collection node'); | ||
//if (!trail[node.name]) { // TODO: CONSIDER: add typeof check to this for possible overwriting | ||
if (!trail[node.name] || (opt.output.objectOverwrite && (typeof trail[node.name] !== typeof blueprintExtended))) { | ||
// node attribute doesnt exist, create object | ||
this.log('create object'); | ||
trail[node.name] = blueprintExtended; | ||
trails[node.path] = blueprintExtended; | ||
} else { | ||
// node attribute exists, set path for next pass | ||
// TODO: extend trail?? | ||
this.log('object at node "' + node.name + '" exists as "' + trail[node.name] + '", skipping insertion and adding trail'); | ||
if (_.isObject(trail[node.name])) { | ||
trail[node.name] = merge(trail[node.name], blueprintExtended); | ||
} | ||
this.log('trail[node.name] updated to "' + trail[node.name]); | ||
trails[node.path] = trail[node.path]; | ||
} | ||
} | ||
} | ||
}); | ||
} | ||
// END PRUNE PASS | ||
} | ||
}, this); | ||
}, this); | ||
if (localOptions.prune) { | ||
translated = this.prune(translated); | ||
} | ||
var t2 = ((new Date()).getTime() - t1); | ||
this.stats.time.total += t2; | ||
// output benchmark if enabled | ||
if (localOptions.benchmark.speed) { | ||
console.log('[treeize]: translated ' + flatData.length + ' rows in ' + ((new Date()).getTime() - t1) + 'ms'); | ||
} | ||
// clear signature between growth sets - TODO: consider leaving this wipe pass off if processing multiple identical sources (add) | ||
if (!signature.isFixed) { | ||
this.signature([]); | ||
} | ||
if (localOptions.benchmark.size) { | ||
console.log('[treeize]: stringify size ' + Math.floor(JSON.stringify(flatData).length / 1024) + 'KB => ' + Math.floor(JSON.stringify(translated).length / 1024) + 'KB'); | ||
} | ||
return this; | ||
}; | ||
return translated; | ||
}; | ||
} | ||
/* | ||
.[get|set]options (options) | ||
var treeize = module.exports = new Treeize(); | ||
Get and sets global options. | ||
*/ | ||
Treeize.prototype.options = function(options) { | ||
if (!options) { | ||
return merge({}, this._options); | ||
} | ||
this._options = merge(this._options, options); | ||
return this; | ||
}; | ||
Treeize.prototype.getOptions = function() { | ||
return this.options(); | ||
}; | ||
Treeize.prototype.setOptions = function(options) { | ||
return this.options(options); | ||
}; | ||
Treeize.prototype.resetOptions = function() { | ||
this._options = merge({}, this.baseOptions); | ||
return this; | ||
}; | ||
Treeize.prototype.toString = function treeToString() { | ||
return util.inspect(this.data.tree, false, null); | ||
}; | ||
module.exports = Treeize; |
{ | ||
"name": "treeize", | ||
"version": "1.2.1", | ||
"version": "2.0.0", | ||
"description": "Converts tabular row data (as from SQL joins, flat JSON, etc) to deep object graphs based on simple column naming conventions - without the use of an ORM or models.", | ||
@@ -17,2 +17,4 @@ "main": "./lib/treeize.js", | ||
"hydration", | ||
"incongrous", | ||
"multi-source", | ||
"model", | ||
@@ -23,5 +25,6 @@ "deep", | ||
"flat", | ||
"array", | ||
"ORM" | ||
], | ||
"author": "K. R. Whitley <contact@krwhitley.com> (http://krwhitley.com/)", | ||
"author": "K. R. Whitley <kevin@krwhitley.com> (http://krwhitley.com/)", | ||
"licenses": [ | ||
@@ -38,14 +41,18 @@ { | ||
"lodash": "~1.3.1", | ||
"inflection": "~1.2.6" | ||
"inflection": "~1.2.6", | ||
"object-merge": "~2.5.1" | ||
}, | ||
"devDependencies": { | ||
"grunt": "latest", | ||
"nodeunit": "latest", | ||
"grunt-cli": "latest", | ||
"mocha": "latest", | ||
"should": "latest", | ||
"grunt-contrib-jshint": "latest", | ||
"grunt-contrib-nodeunit": "latest", | ||
"grunt-contrib-watch": "latest" | ||
"grunt-contrib-watch": "latest", | ||
"grunt-mocha-test": "~0.12.0" | ||
}, | ||
"scripts": { | ||
"test": "grunt" | ||
"test": "node_modules/.bin/grunt test" | ||
} | ||
} |
642
README.md
@@ -1,26 +0,7 @@ | ||
treeize | ||
======= | ||
# Treeize.js | ||
Converts row data (in JSON/associative array format) to object/tree structure based on column naming conventions. | ||
[![Build Status via Travis CI](https://travis-ci.org/kwhitley/treeize.svg?branch=feature%2Fmulti-format)](https://travis-ci.org/kwhitley/treeize) | ||
##Why? | ||
Converts row data (in JSON/associative array format or flat array format) to object/tree structure based on simple column naming conventions. | ||
Most of us still have our hands in traditional relational databases (e.g. MySQL). | ||
While the normalized tables do a fine job of representing the parent/child | ||
relationships, the joined SQL results do not. In fact, they look more like an Excel | ||
spreadsheet than anything. This presents us with a | ||
problem when trying to supply a nice deep object graph for applications. | ||
Using a traditional ORM is slow (either many fragmented SQL | ||
calls, slow object hydration of models, or both). Beyond that, for a lightweight API, | ||
you don't want to have to first pick an ORM and then model out all your relationships. For complex queries, especially where results are | ||
filtered by multiple columns across multiple tables, it becomes even more troublesome, | ||
or borderline impossible to use these model helpers. | ||
The problem is, you can write the | ||
complex deeply joined SQL call that has all the results you wanted - but you can't get it back into | ||
an object graph so it looks/behaves like something other than data vomit. | ||
Now you can. | ||
## Installation | ||
@@ -32,64 +13,337 @@ | ||
## API | ||
## Why? | ||
- `treeize.grow(flatData, [options])` - takes your results/rows of flat associative data and returns a full object graph. | ||
Because APIs usually require data in a deep object graph/collection form, but SQL results (especially heavily joined data), excel, csv, and other flat data sources that we're often forced to drive our applications from represent data in a very "flat" way. Treeize takes this flattened data and based on simple column/attribute naming conventions, remaps it into a deep object graph - all without the overhead/hassle of hydrating a traditional ORM. | ||
#### Configuration (first value is default) | ||
#### What it does... | ||
```js | ||
// Treeize turns flat associative data (as from SQL queries) like this: | ||
var peopleData = [ | ||
{ | ||
'name': 'John Doe', | ||
'age': 34, | ||
'pets:name': 'Rex', | ||
'pets:type': 'dog', | ||
'pets:toys:type': 'bone' | ||
}, | ||
{ | ||
'name': 'John Doe', | ||
'age': 34, | ||
'pets:name': 'Rex', | ||
'pets:type': 'dog', | ||
'pets:toys:type': 'ball' | ||
}, | ||
{ | ||
'name': 'Mary Jane', | ||
'age': 19, | ||
'pets:name': 'Mittens', | ||
'pets:type': 'kitten', | ||
'pets:toys:type': 'yarn' | ||
}, | ||
{ | ||
'name': 'Mary Jane', | ||
'age': 19, | ||
'pets:name': 'Fluffy', | ||
'pets:type': 'cat' | ||
} | ||
]; | ||
treeize.options([options]); // universal getter/setter for options. Returns self. | ||
// default options are as follows: | ||
// ...or flat array-of-values data (as from CSV/excel) like this: | ||
var peopleData = [ | ||
['name', 'age', 'pets:name', 'pets:type', 'pets:toys:type'], // header row | ||
['John Doe', 34, 'Rex', 'dog', 'bone'], | ||
['John Doe', 34, 'Rex', 'dog', 'ball'], | ||
['Mary Jane', 19, 'Mittens', 'kitten', 'yarn'], | ||
['Mary Jane', 19, 'Fluffy', 'cat', null] | ||
]; | ||
// ...via a dead-simple implementation: | ||
var Treeize = require('treeize'); | ||
var people = new Treeize(); | ||
people.grow(peopleData); | ||
// ...into deep API-ready object graphs like this: | ||
people.getData() == [ | ||
{ | ||
name: 'John Doe', | ||
age: 34, | ||
pets: [ | ||
{ | ||
name: 'Rex', | ||
type: 'dog', | ||
toys: [ | ||
{ type: 'bone' }, | ||
{ type: 'ball' } | ||
] | ||
} | ||
] | ||
}, | ||
{ | ||
name: 'Mary Jane', | ||
age: 19, | ||
pets: [ | ||
{ | ||
name: 'Mittens', | ||
type: 'kitten', | ||
toys: [ | ||
{ type: 'yarn' } | ||
] | ||
}, | ||
{ | ||
name: 'Fluffy', | ||
type: 'cat' | ||
} | ||
] | ||
} | ||
]; | ||
``` | ||
# API Index | ||
##### 1. get/set options (optional) | ||
- [`options([options])`](#options) - getter/setter for options | ||
- [`setOptions(options)`](#setOptions) - merges new `[options]` with existing | ||
- [`resetOptions()`](#resetOptions) - resets options to defaults | ||
##### 2a. set data signature manually if needed (optional) | ||
- [`signature([row], [options])`](#signature) - getter/setter for signature definitions | ||
- [`setSignature(row, [options])`](#setSignature) - sets signature using a specific row of data/headers (preserves signature between data sets if uniformity option is enabled) | ||
- [`clearSignature()`](#clearSignature) - clear signature between data sets (only needed when previously defined a uniform signature via `setSignature`) | ||
##### 2b. grow tree from data set(s) | ||
- [`grow(data, [options])`](#grow) - grow flat `data`, with optional local `[options]` | ||
##### 3. retrieve transformed data | ||
- [`getData()`](#getData) - gets current tree data | ||
##### * misc/internal methods | ||
- [`getOptions()`](#getOptions) - returns options | ||
- [`getSignature()`](#getSignature) - returns currently defined signature | ||
- [`getStats()`](#getStats) - returns object with growth statistics | ||
- [`toString()`](#toString) - uses `util` to return data in visually formatted object graph | ||
- [`log(arg1, arg2, arg3)`](#log) - console.log output of `arg1..n` when `log` option is set to `true` | ||
# API | ||
### .options([options])<a name="options" /> | ||
[Getter](#getOptions)/[Setter](#setOptions) for options. If options object is passed, this is identical to [.setOptions(options)](#setOptions) and returns self (chainable). If no options are passed, this is identical to [.getOptions()](#getOptions) and returns current options as object. | ||
### .setOptions(options)<a name="setOptions" /> | ||
Sets options globally for the Treeize instance. This is an alias for `.options(options)`. Default options are as follows: | ||
```js | ||
{ | ||
delimiter: ':', // Path delimiter, as in "foo:bar:baz" | ||
benchmark: { | ||
speed: false, // Enable/Disable performance logging | ||
size: false // Enable/Disable compression logging | ||
input: { | ||
delimiter: ':', // delimiter between path segments, defaults to ':' | ||
detectCollections: true, // when true, plural path segments become collections | ||
uniformRows: false, // set to true if each row has identical signatures | ||
}, | ||
fast: false, // Enable/Disable Fast Mode (see below) | ||
prune: false, // Enable/Disable empty/null pruning (see below) | ||
collections: { | ||
auto: true // Defaults to pluralized detection for collections. | ||
// Setting to false requires + operators for | ||
// every collection. | ||
} | ||
output: { | ||
prune: true, // remove blank/null values and empty nodes | ||
objectOverwrite: true, // incoming objects will overwrite placeholder ids | ||
resultsAsObject: false, // root structure defaults to array (instead of object) | ||
}, | ||
log: false, // enable logging | ||
} | ||
``` | ||
### Usage | ||
For example, to change the delimiter and enable output logging, you would use the following: | ||
To use `treeize`, simply pass flat "row data" into `treeize.grow()`. Each | ||
column/attribute of each row will dictate its own destination path using the following format: | ||
```js | ||
.setOptions({ log: true, input: { delimiter: '|' }}); | ||
``` | ||
#### Available Options | ||
`input.delimiter`<a name="optionsInputDelimiter" /> | ||
This sets the delimiter to be used between path segments (e.g. the ":" in "children:mother:name"). | ||
[View test example](https://github.com/kwhitley/treeize/blob/feature/multi-format/test/test.js#L51-58) | ||
`input.detectCollections`<a name="optionsInputDetectCollections" /> | ||
Enables/disables the default behavior of turning plural path segments (e.g. "subjects" vs. "subject") into collections instead of object paths. **Note:** In order to properly merge multiple rows into the same collection item, the collection must have a base-level attribute(s) acting as a signature. | ||
[View test example (enabled)](https://github.com/kwhitley/treeize/blob/feature/multi-format/test/test.js#L79-86) | [or (disabled)](https://github.com/kwhitley/treeize/blob/feature/multi-format/test/test.js#L92-99) | ||
`input.uniformRows`<a name="optionsInputUniformRows" /> | ||
By default row uniformity is disabled to allow the most flexible data merging. This means each and every row of data that is processed (unless flat array-of-array data) will be analyzed and mapped individually into the final structure. If your data rows have uniform attributes/columns, disable this for a performance increase. | ||
`output.prune`<a name="optionsOutputPrune" /> | ||
Removes blank/empty nodes in the structure. This is enabled by default to prevent sparse data sets from injecting blanks and nulls everywhere in your final output. If nulls are important to preserve, disable this. | ||
[View test example](https://github.com/kwhitley/treeize/blob/feature/multi-format/test/test.js#L207-240) | ||
`output.objectOverwrite`<a name="optionsOutputObjectOverwrite" /> | ||
To allow for merging objects directly onto existing placeholder values (e.g. foreign key ids), this is enabled by default. | ||
[View test example](https://github.com/kwhitley/treeize/blob/feature/multi-format/test/test.js#L159-203) | ||
`output.resultsAsObject`<a name="optionsOutputResultsAsObject" /> | ||
This creates a single root object (instead of the default array of objects). | ||
[View test example](https://github.com/kwhitley/treeize/blob/feature/multi-format/test/test.js#L245-278) | ||
`log`<a name="optionsLog" /> | ||
Setting to true enables traversal information to be logged to console during growth process. | ||
### .getOptions()<a name="getOptions" /> | ||
Returns the current global options (as object). | ||
[View example format](#setOptions) | ||
### .resetOptions(options)<a name="resetOptions" /> | ||
Resets all global options to [original defaults](#setOptions) and returns self (chainable). | ||
### .signature([row], [options])<a name="signature" /> | ||
[Getter](#getSignature)/[Setter](#setSignature) for row signatures. If options object is passed, this is identical to [.setSignature(options)](#setSignature) and returns self (chainable). If no options are passed, this is identical to [.getSignature()](#getSignature) and returns currently defined signature as object. | ||
### .setSignature(row, [options])<a name="setSignature" /> | ||
Manually defines the signature for upcoming data sets from argument `row`, with optional `options`. The row may be either in object (key/value) form or flat array form (array of paths). This method is only required if sharing a single signature across multiple data sources (when merging homogeneous data sets), or when the data itself has no header information (for instance, with bulk flat array-of-values data). Returns self (chainable). | ||
```js | ||
// May be set from a single row of associative data | ||
.setSignature({ | ||
'id': 1, | ||
'name:first': 'Kevin', | ||
'name:last': 'Whitley', | ||
'hobbies:name': 'photography' | ||
'hobbies:years': 12 | ||
}) | ||
// Or from header row of flat array data | ||
.setSignature(['id', 'name:first', 'name:last', 'hobbies:name', 'hobbies:years']) | ||
``` | ||
### .getSignature()<a name="getSignature" /> | ||
Returns currently defined signature. _For internal use only._ | ||
### .clearSignature()<a name="clearSignature" /> | ||
Clears currently-defined signature if previously set via [`setSignature(row)`](#setSignature), and returns self (chainable). This is only required between data sets if signature auto-detection should be re-enabled. It is unlikely that you will need to use this. | ||
### .getData()<a name="getData" /> | ||
Returns current data tree. | ||
```js | ||
var tree = new Treeize(); | ||
tree.grow([ | ||
{ 'foo': 'bar', 'logs:a': 1 }, | ||
{ 'foo': 'bar', 'logs:a': 2 }, | ||
{ 'foo': 'baz', 'logs:a': 3 }, | ||
]); | ||
console.log(tree.getData()); | ||
/* | ||
[ | ||
{ foo: 'bar', logs: [{ a: 1 }, { a: 2 }] }, | ||
{ foo: 'baz', logs: [{ a: 3 }]} | ||
] | ||
*/ | ||
``` | ||
### .getStats()<a name="getStats" /> | ||
Returns current growth statistics (e.g. number of sources process, number of rows, etc). _Output and format subject to change - use at your own risk._ | ||
### .toString()<a name="toString" /> | ||
Typecasting a treeize instance to String or manually calling `.toString()` on it will output it visually using the `util` library from node.js. | ||
```js | ||
var tree = new Treeize(); | ||
tree.grow(data); | ||
// use automatic typecasting to trigger | ||
console.log(tree + ''); | ||
// or call manually | ||
console.log(tree.toString()); | ||
``` | ||
### .log(arg1, arg2, ...)<a name="log" /> | ||
Equivalent to `console.log(arg1, arg2, ...)` when the `log` option is set to `true`. Useful for debugging messages or visual output that you can toggle from a single source. | ||
```js | ||
var tree = new Treeize(); | ||
tree.log('my message'); | ||
// 'my message' will NOT be written to the console | ||
tree | ||
.setOptions({ log: true }) | ||
.log('my message') | ||
; | ||
// 'my message' WILL be written to the console | ||
``` | ||
--- | ||
### .grow(data, [options])<a name="grow" /> | ||
The `grow(data, [options])` method provides the core functionality of Treeize. This method expands flat data (of one or more sources) into the final deep tree output. Each attribute path is analyzed for injection into the final object graph. | ||
#### Path Naming | ||
Each column/attribute of each row will dictate its own destination path | ||
using the following format: | ||
```js | ||
{ | ||
"[path1]:[path2]:[pathX]:[attributeName]": [value] | ||
'path1:path2:pathX:attributeName': [value] | ||
} | ||
``` | ||
Each "path" (up to n-levels deep) is optional and represents a single object node if the word is singular, | ||
or a collection if the word is plural. For example, a "favoriteMovie:name" path will | ||
add a "favoriteMovie" object to its path - where "favoriteMovies:name" would add a collection | ||
of movies (complete with a first entry) instead. For root nodes, include only | ||
the attribute name without any preceding paths. If you were creating a final output of a | ||
book collection for instance, the title of the book would likely be pathless as you would want the | ||
value on the high-level collection `book` object. | ||
Each "path" (up to n-levels deep) is optional and represents a single object node if the word is singular, or a collection if the word is plural (with optional +/- override modifiers). For example, a "favoriteMovie:name" path will add a "favoriteMovie" object to its path - where "favoriteMovies:name" would add a collection of movies (complete with a first entry) instead. For root nodes, include only the attribute name without any preceding paths. If you were creating a final output of a book collection for instance, the title of the book would likely be pathless as you would want the value on the high-level `books` collection. | ||
It's important to note that each row will create or find its path within the newly | ||
transformed output being created. Your flat feed will have mass-duplication, the results | ||
will not. | ||
It's important to note that each row will create or find its path within the newly transformed output being created. Your flat feed may have mass-duplication, but the results will not. | ||
##### Merging Multiple Data Sources | ||
Treeize was designed to merge from multiple data sources of both attribute-value and array-of-value format (as long as signatures are provided in some manner), including ones with varying signatures. | ||
```js | ||
var Treeize = require('treeize'); | ||
var arrayOfObjects = require('somesource1.js'); | ||
var arrayOfValues = require('somesource2.js'); | ||
var tree = new Treeize(); | ||
tree | ||
.grow(arrayOfObjects) | ||
.grow(arrayOfValues) // assumes header row as first row | ||
; | ||
// tree.getData() == final merged results | ||
``` | ||
##### How to manually override the default pluralization scheme for collection-detection | ||
In the rare (but possible) case that plural/singular node names are not enough to properly | ||
detect collections, you may add specific overrides to the node name, using the `+` and `-` | ||
indicators. | ||
In the rare (but possible) case that plural/singular node names are not enough to properly detect collections, you may add specific overrides to the node name, using the `+` (for collections) and `-` (for singular objects) indicators. | ||
```js | ||
{ | ||
"name": "Bird", | ||
"attributes:legs": 2, | ||
"attributes:hasWings": true | ||
'name': 'Bird', | ||
'attributes:legs': 2, | ||
'attributes:hasWings': true | ||
} | ||
@@ -101,3 +355,3 @@ | ||
{ | ||
name: "Bird", | ||
name: 'Bird', | ||
attributes: [ | ||
@@ -116,5 +370,5 @@ { | ||
{ | ||
"name": "Bird", | ||
"attributes-:legs": 2, | ||
"attributes-:hasWings": true | ||
'name': 'Bird', | ||
'attributes-:legs': 2, | ||
'attributes-:hasWings': true | ||
} | ||
@@ -126,3 +380,3 @@ | ||
{ | ||
name: "Bird", | ||
name: 'Bird', | ||
attributes: { | ||
@@ -139,27 +393,54 @@ legs: 2, | ||
##### Specifying Your Own Key/Blueprint For Collections | ||
##### Pathing example | ||
By default, all known attributes of a collection node level define a "blueprint" by which to match future rows. For example, in a collection of people, if both `name` and `age` attributes are defined within each row, future rows will require both the `name` and `age` values to match for the additional information to be merged into that record. To override this default behavior and specify your own criteria, simply _mark each required attribute with a leading or tailing `*` modifier._ | ||
```js | ||
{ | ||
"title": "Ender's Game", // creates the first object with a title attribute of "Ender's Game" | ||
"author:name": "Orson Scott Card", // adds an author object (with name) to the book "Ender's Game" (created above) | ||
"author:age": 21, // gives the author object an age attribute | ||
"author:otherBooks:title": "Ender's Shadow", // adds a collection named "otherBooks" to the author, with a first object of "title": "Ender's Shadow" | ||
} | ||
[ | ||
{ | ||
'date': '1/1/2014', | ||
'temperatureF': 90, | ||
'temperatureC': 32 | ||
}, | ||
{ | ||
'date': '1/1/2014', | ||
'humidity': .1 | ||
} | ||
] | ||
// creates the following... | ||
// ...would normally grow into: | ||
[ | ||
{ | ||
date: '1/1/2014', | ||
temperatureF: 90, | ||
temperatureC: 32 | ||
}, | ||
{ | ||
date: '1/1/2014', | ||
humidity: 0.1 | ||
} | ||
] | ||
// ...but by specifying only the "date" attribute as the blueprint/key | ||
[ | ||
{ | ||
title: "Ender's Game", | ||
author: { | ||
name: "Orson Scott Card", | ||
age: 21, | ||
otherBooks: [ | ||
{ title: "Ender's Shadow" } | ||
] | ||
} | ||
'date*': '1/1/2014', | ||
'temperatureF': 90, | ||
'temperatureC': 32 | ||
}, | ||
{ | ||
'date*': '1/1/2014', | ||
'humidity': .1 | ||
} | ||
] | ||
// ...the data merges appropriately | ||
[ | ||
{ | ||
date: '1/1/2014', | ||
temperatureF: 90, | ||
temperatureC: 32, | ||
humidity: 0.1 | ||
} | ||
] | ||
``` | ||
@@ -169,25 +450,10 @@ | ||
- The column/attribute order is not important. All attributes are sorted by depth before mapping. This ensures parent nodes exist before children nodes are created within. | ||
- Each attribute name of the flat data must consist of the full path to its node & attribute, seperated by the delimiter. `id` suggests an `id` attribute on a root element, whereas `name:first` implies a `first` attribute on a `name` object within a root element. | ||
- To imply a collection in the path/attribute-name, use a plural name (e.g. "subjects" instead of "subject"). Otherwise, use a singular name for a singular object. | ||
- Use a `:` delimiter (default) to seperate path nodes. To change this, use the `treeize.set([options])` function. | ||
- Use a `:` delimiter (default) to seperate path segments. To change this, modify the [`input.delimiter`](#optionsInputDelimiter) option. | ||
### Fast Mode | ||
--- | ||
Setting the option `{ fast: true }` enables Fast Mode. In this mode, the column/attribute signature is | ||
pulled from the first row and applied to all other rows. This makes the algorithm about 30% faster for a large | ||
data set by not having to fully analyze the pathing of each row. Only use this when you are certain | ||
each row contains identical column/attribute names. | ||
# Examples | ||
_This is set to `false` by default for backwards compatibility, and to embrace more complicated | ||
data sets (where the attributes may be different for each row)._ | ||
### Node Pruning | ||
Setting the option `{ prune: true }` enables Pruning Mode, courtesy of @EvanK. As he points out, complex joins | ||
can often leave a slew of blank or nulled branches. Enabling Prune Mode removes those, leaving only populated fields. | ||
Big thanks for the complete PR (with tests)! | ||
## Examples | ||
In this short series of examples, we'll take a standard "join dump", originally keyed | ||
@@ -204,55 +470,56 @@ (via attribute names) to organize by movie - and demonstrate how other organizations can | ||
```js | ||
var treeize = require('treeize'); | ||
var movieDump = [ | ||
{ | ||
"title": "The Prestige", | ||
"director": "Christopher Nolan", | ||
"actors:name": "Christian Bale", | ||
"actors:as": "Alfred Borden" | ||
'title': 'The Prestige', | ||
'director': 'Christopher Nolan', | ||
'actors:name': 'Christian Bale', | ||
'actors:as': 'Alfred Borden' | ||
}, | ||
{ | ||
"title": "The Prestige", | ||
"director": "Christopher Nolan", | ||
"actors:name": "Hugh Jackman", | ||
"actors:as": "Robert Angier" | ||
'title': 'The Prestige', | ||
'director': 'Christopher Nolan', | ||
'actors:name': 'Hugh Jackman', | ||
'actors:as': 'Robert Angier' | ||
}, | ||
{ | ||
"title": "The Dark Knight Rises", | ||
"director": "Christopher Nolan", | ||
"actors:name": "Christian Bale", | ||
"actors:as": "Bruce Wayne" | ||
'title': 'The Dark Knight Rises', | ||
'director': 'Christopher Nolan', | ||
'actors:name': 'Christian Bale', | ||
'actors:as': 'Bruce Wayne' | ||
}, | ||
{ | ||
"title": "The Departed", | ||
"director": "Martin Scorsese", | ||
"actors:name": "Leonardo DiCaprio", | ||
"actors:as": "Billy" | ||
'title': 'The Departed', | ||
'director': 'Martin Scorsese', | ||
'actors:name': 'Leonardo DiCaprio', | ||
'actors:as': 'Billy' | ||
}, | ||
{ | ||
"title": "The Departed", | ||
"director": "Martin Scorsese", | ||
"actors:name": "Matt Damon", | ||
"actors:as": "Colin Sullivan" | ||
'title': 'The Departed', | ||
'director': 'Martin Scorsese', | ||
'actors:name': 'Matt Damon', | ||
'actors:as': 'Colin Sullivan' | ||
} | ||
]; | ||
var movies = treeize.grow(movieDump); | ||
var Treeize = require('treeize'); | ||
var movies = new Treeize(); | ||
movies.grow(movieDump); | ||
/* | ||
'movies' now contains the following: | ||
'movies.getData()' now results in the following: | ||
[ | ||
{ | ||
"director": "Christopher Nolan", | ||
"title": "The Prestige", | ||
"actors": [ | ||
'director': 'Christopher Nolan', | ||
'title': 'The Prestige', | ||
'actors': [ | ||
{ | ||
"as": "Alfred Borden", | ||
"name": "Christian Bale" | ||
'as': 'Alfred Borden', | ||
'name': 'Christian Bale' | ||
}, | ||
{ | ||
"as": "Robert Angier", | ||
"name": "Hugh Jackman" | ||
'as': 'Robert Angier', | ||
'name': 'Hugh Jackman' | ||
} | ||
@@ -262,8 +529,8 @@ ] | ||
{ | ||
"director": "Christopher Nolan", | ||
"title": "The Dark Knight Rises", | ||
"actors": [ | ||
'director': 'Christopher Nolan', | ||
'title': 'The Dark Knight Rises', | ||
'actors': [ | ||
{ | ||
"as": "Bruce Wayne", | ||
"name": "Christian Bale" | ||
'as': 'Bruce Wayne', | ||
'name': 'Christian Bale' | ||
} | ||
@@ -273,12 +540,12 @@ ] | ||
{ | ||
"director": "Martin Scorsese", | ||
"title": "The Departed", | ||
"actors": [ | ||
'director': 'Martin Scorsese', | ||
'title': 'The Departed', | ||
'actors': [ | ||
{ | ||
"as": "Billy", | ||
"name": "Leonardo DiCaprio" | ||
'as': 'Billy', | ||
'name': 'Leonardo DiCaprio' | ||
}, | ||
{ | ||
"as": "Colin Sullivan", | ||
"name": "Matt Damon" | ||
'as': 'Colin Sullivan', | ||
'name': 'Matt Damon' | ||
} | ||
@@ -304,56 +571,57 @@ ] | ||
```js | ||
var treeize = require('treeize'); | ||
var moviesDump = [ | ||
{ | ||
"movies:title": "The Prestige", | ||
"movies:director": "Christopher Nolan", | ||
"name": "Christian Bale", | ||
"movies:as": "Alfred Borden" | ||
'movies:title': 'The Prestige', | ||
'movies:director': 'Christopher Nolan', | ||
'name': 'Christian Bale', | ||
'movies:as': 'Alfred Borden' | ||
}, | ||
{ | ||
"movies:title": "The Prestige", | ||
"movies:director": "Christopher Nolan", | ||
"name": "Hugh Jackman", | ||
"movies:as": "Robert Angier" | ||
'movies:title': 'The Prestige', | ||
'movies:director': 'Christopher Nolan', | ||
'name': 'Hugh Jackman', | ||
'movies:as': 'Robert Angier' | ||
}, | ||
{ | ||
"movies:title": "The Dark Knight Rises", | ||
"movies:director": "Christopher Nolan", | ||
"name": "Christian Bale", | ||
"movies:as": "Bruce Wayne" | ||
'movies:title': 'The Dark Knight Rises', | ||
'movies:director': 'Christopher Nolan', | ||
'name': 'Christian Bale', | ||
'movies:as': 'Bruce Wayne' | ||
}, | ||
{ | ||
"movies:title": "The Departed", | ||
"movies:director": "Martin Scorsese", | ||
"name": "Leonardo DiCaprio", | ||
"movies:as": "Billy" | ||
'movies:title': 'The Departed', | ||
'movies:director': 'Martin Scorsese', | ||
'name': 'Leonardo DiCaprio', | ||
'movies:as': 'Billy' | ||
}, | ||
{ | ||
"movies:title": "The Departed", | ||
"movies:director": "Martin Scorsese", | ||
"name": "Matt Damon", | ||
"movies:as": "Colin Sullivan" | ||
'movies:title': 'The Departed', | ||
'movies:director': 'Martin Scorsese', | ||
'name': 'Matt Damon', | ||
'movies:as': 'Colin Sullivan' | ||
} | ||
]; | ||
var actors = treeize.grow(movieDump); | ||
var Treeize = require('treeize'); | ||
var actors = new Treeize(); | ||
actors.grow(movieDump); | ||
/* | ||
'actors' now contains the following: | ||
'actors.getData()' now results in the following: | ||
[ | ||
{ | ||
"name": "Christian Bale", | ||
"movies": [ | ||
'name': 'Christian Bale', | ||
'movies': [ | ||
{ | ||
"as": "Alfred Borden", | ||
"director": "Christopher Nolan", | ||
"title": "The Prestige" | ||
'as': 'Alfred Borden', | ||
'director': 'Christopher Nolan', | ||
'title': 'The Prestige' | ||
}, | ||
{ | ||
"as": "Bruce Wayne", | ||
"director": "Christopher Nolan", | ||
"title": "The Dark Knight Rises" | ||
'as': 'Bruce Wayne', | ||
'director': 'Christopher Nolan', | ||
'title': 'The Dark Knight Rises' | ||
} | ||
@@ -363,8 +631,8 @@ ] | ||
{ | ||
"name": "Hugh Jackman", | ||
"movies": [ | ||
'name': 'Hugh Jackman', | ||
'movies': [ | ||
{ | ||
"as": "Robert Angier", | ||
"director": "Christopher Nolan", | ||
"title": "The Prestige" | ||
'as': 'Robert Angier', | ||
'director': 'Christopher Nolan', | ||
'title': 'The Prestige' | ||
} | ||
@@ -374,8 +642,8 @@ ] | ||
{ | ||
"name": "Leonardo DiCaprio", | ||
"movies": [ | ||
'name': 'Leonardo DiCaprio', | ||
'movies': [ | ||
{ | ||
"as": "Billy", | ||
"director": "Martin Scorsese", | ||
"title": "The Departed" | ||
'as': 'Billy', | ||
'director': 'Martin Scorsese', | ||
'title': 'The Departed' | ||
} | ||
@@ -385,8 +653,8 @@ ] | ||
{ | ||
"name": "Matt Damon", | ||
"movies": [ | ||
'name': 'Matt Damon', | ||
'movies': [ | ||
{ | ||
"as": "Colin Sullivan", | ||
"director": "Martin Scorsese", | ||
"title": "The Departed" | ||
'as': 'Colin Sullivan', | ||
'director': 'Martin Scorsese', | ||
'title': 'The Departed' | ||
} | ||
@@ -393,0 +661,0 @@ ] |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
5885869
13
654
0
3
8
470
+ Addedobject-merge@~2.5.1
+ Addedclone-function@1.0.6(transitive)
+ Addedobject-foreach@0.1.2(transitive)
+ Addedobject-merge@2.5.1(transitive)