Source: mongodb/models/lib/treeDocument.js

/**
 * @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);
    });
};