all files / lib/offshore/utils/ acyclicTraversal.js

96.55% Statements 28/29
89.47% Branches 17/19
100% Functions 4/4
100% Lines 27/27
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111                                                                    20× 20×   20×       20×         20×                                             32× 16×         16×   16×             16×     16× 12×     16×          
/**
 * Module dependencies
 */
 
var _ = require('lodash');
 
 
/**
 * Traverse the schema to build a populate plan object
 * that will populate every relation, sub-relation, and so on
 * reachable from the initial model and relation at least once
 * (perhaps most notable is that this provides access to most
 * related data without getting caught in loops.)
 *
 * @param  {[type]} schema          [description]
 * @param  {[type]} initialModel    [description]
 * @param  {[type]} initialRelation [description]
 * @return {[type]}                 [description]
 */
module.exports = function acyclicTraversal(schema, initialModel, initialRelation) {
 
  // Track the edges which have already been traversed
  var alreadyTraversed = [
    // {
    //   relation: initialRelation,
    //   model: initialModel
    // }
  ];
 
  return traverseSchemaGraph(initialModel, initialRelation);
 
  /**
   * Recursive function
   * @param  {[type]} modelIdentity  [description]
   * @param  {[type]} nameOfRelation [description]
   * @return {[type]}                [description]
   */
  function traverseSchemaGraph(modelIdentity, nameOfRelation) {
 
    var currentModel = schema[modelIdentity];
    var currentAttributes = currentModel.attributes;
 
    var isRedundant;
 
    // If this relation has already been traversed, return.
    // (i.e. `schema.attributes.modelIdentity.nameOfRelation`)
    isRedundant = _.find(alreadyTraversed, {
      alias: nameOfRelation,
      model: modelIdentity
    });
 
    if (isRedundant) return;
 
    // Push this relation onto the `alreadyTraversed` stack.
    alreadyTraversed.push({
      alias: nameOfRelation,
      model: modelIdentity
    });
 
 
    var relation = currentAttributes[nameOfRelation];
    Iif (!relation) throw new Error('Unknown relation in schema: ' + modelIdentity + '.' + nameOfRelation);
    var identityOfRelatedModel = relation.model || relation.collection;
 
    // Get the related model
    var relatedModel = schema[identityOfRelatedModel];
 
    // If this relation is a collection with a `via` back-reference,
    // push it on to the `alreadyTraversed` stack.
    // (because the information therein is probably redundant)
    // TODO: evaluate this-- it may or may not be a good idea
    // (but I think it's a nice touch)
    Eif (relation.via) {
      alreadyTraversed.push({
        alias: relation.via,
        model: identityOfRelatedModel
      });
    }
 
    // Lookup ALL the relations OF THE RELATED model.
    var relations =
      _(relatedModel.attributes).reduce(function buildSubsetOfAssociations(relations, attrDef, attrName) {
        if (_.isObject(attrDef) && (attrDef.model || attrDef.collection)) {
          relations.push(_.merge({
            alias: attrName,
            identity: attrDef.model || attrDef.collection,
            cardinality: attrDef.model ? 'model' : 'collection'
          }, attrDef));
          return relations;
        }
        return relations;
      }, []);
 
    // Return a piece of the result plan by calling `traverseSchemaGraph`
    // on each of the RELATED model's relations.
    return _.reduce(relations, function(resultPlanPart, relation) {
 
      // Recursive step
      resultPlanPart[relation.alias] = traverseSchemaGraph(identityOfRelatedModel, relation.alias);
 
      // Trim undefined result plan parts
      if (resultPlanPart[relation.alias] === undefined) {
        delete resultPlanPart[relation.alias];
      }
 
      return resultPlanPart;
    }, {});
  }
 
};