/**
* @module mongodb/models/lib/treeDocument
*/
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var util = require('util');
var _ = require('underscore');
/**
* Abstract class to generalize commonly-used tree-traversal
* queries & other tree-oriented utilities for Mongo collections
* containing arbitrary levels of nested documents.
*
* Since Mongo encourages storage of "flattened" or "normalized"
* documents, accessing highly nested documents via Mongoose's built-in
* query tools can be a bit tricky, so this is an attempt to generalize
* what can be generalized so you don't have to rewrite the same
* stupid query functions for each document tree you write.
*
* Couple of key assumptions about how your data is structured:
* 1. One document in top-level node collection per tree
* 2. Child models are stored in document arrays under each parent
* 3. Child model document arrays are keyed by PLURALIZED, LOWERCASED MODELNAME, i.e.
* model = Placement ==> { placements: [array of placements }
*
* @param {String} top_level_node top-level model name
* @param {Array} branches list of all branches in tree, not including top node.
* Each branch is a list of mongoose models ordered from highest to lowest order
* @param {Object} [options] options object
* @param {String} [options.read='secondary'] mongoose read variable. Default is
* 'secondary' which allows queries against slave DB.
* @class
* @abstract
*/
var TreeDocument = exports.TreeDocument = function(top_level_node, branches, options){
this.options = options || {};
this.read = this.options.read || 'secondary';
this.top_level_node = top_level_node;
this.branches = branches;
this.child_keys = [];
var self = this;
this.branches.forEach(function(branch){
var branch_keys = branch.map(function(val){
return TreeDocument.getChildrenKey(val)
});
self.child_keys.push(branch_keys);
});
};
TreeDocument.getChildrenKey = function(modelName){
return modelName.toLowerCase() + 's';
};
/**
* Forms Mongoose query arg based on branches specified on init
* @param modelName
* @returns {*|Array.<T>|string|Blob|Query}
* @private
*/
TreeDocument.prototype._getChildDocumentQueryArg = function(modelName){
//var child_keys = this.child_keys.slice(1);
var level = TreeDocument.getChildrenKey(modelName);
// loop over branches in child_keys list of lists and find one that contains
// this modelName. Shouldn't matter which one we find, since if multiple
// branches contain the node, then by definition the ancestor list will
// be the same in all branches in which it's contained
var level_index = null;
var this_branch = null;
this.child_keys.forEach(function(branch){
if (branch.indexOf(level) > -1){
level_index = branch.indexOf(level);
this_branch = branch;
}
});
if (level_index === null) {
throw new Error('modelName ' + modelName + ' not found in model tree hierarchy');
}
return this_branch.slice(0, level_index + 1);
};
/**
* This function handles the looping and parsing logic to be performed once parent
* document is found in Mongo, and walks down the tree contained in the document to find the child node.
*
* @param document
* @param child_object_id
* @param modelName
* @param callback
* @returns {*}
* @private
*/
TreeDocument.prototype._walkDownAndFindNestedDoc = function(document, child_object_id, modelName, callback){
var self = this;
var child_keys = this._getChildDocumentQueryArg(modelName);
var args;
function _inner(document, parents, current_level){
parents = parents || {};
current_level = current_level || TreeDocument.getChildrenKey(self.top_level_node);
var child_key = child_keys[child_keys.indexOf(current_level) + 1];
if (child_key == TreeDocument.getChildrenKey(modelName)) {
var child = _.findWhere(document.get(child_key), {id: child_object_id});
if (child) {
parents[current_level] = document;
args = {child: child, parents: parents};
}
return;
} else {
document.get(child_key).forEach(function (child) {
var new_parents = {};
new_parents[current_level] = document;
_.extend(new_parents, parents);
_inner(child, new_parents, child_key);
});
}
}
// wrapper to catch when no child documents exist and return
// callback with error
_inner(document);
if (args){
return callback(null, args.child, args.parents);
} else {
var e = new ReferenceError('Nested document ID ' + child_object_id + ' not found');
return callback(e);
}
};
/**
* Gets nested document by ObjectId any number of levels deep in a document tree.
*
* Really just queries on nested ID to find parent document, and then loops
* recursively over all children of that parent document to only return the
* desired child.
*
* NOTE: Returned object will also contain keys of the form 'parent_[model]',
* which contain the child's parent object at a specific level of the tree.
*
* Seems kind of silly that this functionality doesn't exist in Mongoose,
* but I looked for a while and got frustrated so I wrote it myself.
*
* @param objectId
* @param modelName
* @param [populate] fields to populate in document. Space delimited string of paths, or array of paths.
* Passed to model.deepPopulate
* @param callback
*/
TreeDocument.prototype.getNestedObjectById = function(objectId, modelName, populate, callback){
if (arguments.length === 3){
callback = populate;
populate = null;
}
var self = this;
var child_keys = this._getChildDocumentQueryArg(modelName);
var q = {};
q[child_keys.join('.') + '._id'] = objectId;
var query = this[this.top_level_node].findOne(q);
if (populate){
query = query.deepPopulate(populate);
}
query.read(this.read).exec(
function(err, doc){
if (err) return callback(err);
if (doc){
// TODO: got to be a better way to do this, or is this just
// TODO: the accepted way of querying deeply nested documents?
self._walkDownAndFindNestedDoc(doc, objectId, modelName,function(err, child, parents){
for (var k in parents){
if (parents.hasOwnProperty(k)){
var parentkey = util.format('parent_%s', k.substring(0, k.length -1));
child[parentkey] = parents[k];
}
}
return callback(null, child);
});
} else {
var e = util.format('No %ss with ObjectId %s found', modelName, objectId);
callback(e);
}
}
);
};
/**
* Gets nested document by ObjectId any number of levels deep in a document tree,
* given an existing parent-level document.
*
* NOTE: Returned object will also contain keys of the form 'parent_[model]',
* which contain the child's parent object at a specific level of the tree.
*
* Seems kind of silly that this functionality doesn't exist in Mongoose,
* but I looked for a while and got frustrated so I wrote it myself.
*
* @param objectId
* @param modelName
* @param parentDoc
* @param callback
*/
TreeDocument.prototype.getChildDocument = function(objectId, modelName, parentDoc, callback){
this._walkDownAndFindNestedDoc(parentDoc, objectId, modelName, function(err, child, parents){
if (err) return callback(err);
for (var k in parents){
if (parents.hasOwnProperty(k)){
var parentkey = util.format('parent_%s', k.substring(0, k.length -1));
child[parentkey] = parents[k];
}
}
return callback(null, child);
});
};