Source: query/ModelTraverse.js

'use strict';

require('insulin').factory('ndm_ModelTraverse', ndm_modelTraverseProducer);

function ndm_modelTraverseProducer() {
  /** A class that has helper methods for traversing a database model. */
  class ModelTraverse {

    /**
     * @typedef ModelTraverse~ModelMeta
     * @type {Object}
     * @property {string} tableMapping - The mapping (mapTo) of the table with
     * which the model is associated.
     * @property {Object} model - The model itself.
     * @property {Object} parent - The model's parent, or null if the model has
     * no parent.
     */

    /**
     * A callback prototype that is called upon each iteration of a model.
     * @callback ModelTraverse~modelMetaCallback
     * @param {ModelTraverse~ModelMeta} meta - A ModelTraverse~ModelMeta
     * instance describing the current model.
     */

    /**
     * Traverse the keys in model.
     * @param {Object|object[]} model - The object or array to traverse.
     * @param {ModelTraverse~modelMetaCallback} callback - A function that
     * shall be invoked with for each sub-model within model.
     * @param {Database} database - An optional Database instance.  If passed,
     * the callback will only be called if a key in the model corresponds to a
     * table mapping.
     * @return {void}
     */
    modelOnly(model, callback, database) {
      for (let tableMapping in model) {
        const modelParts = (model[tableMapping] instanceof Array) ?
          model[tableMapping] : [model[tableMapping]];

        for (let i = 0; i < modelParts.length; ++i) {
          if (!database || database.isTableMapping(tableMapping))
            callback({tableMapping: tableMapping, model: modelParts[i], parent: null});
        }
      }
    }

    /**
     * Traverse model (either an object or an array) using a depth-first traversal.
     * @param {Object|object[]} model - The object or array to traverse.
     * @param {ModelTraverse~modelMetaCallback} callback - A function that
     * shall be invoked with for each sub-model within model.
     * @param {Database} database - An optional Database instance.  If passed,
     * the callback will only be called if a key in the model corresponds to a
     * table mapping.
     * @return {void}
     */
    depthFirst(model, callback, database) {
      // Private helper function to recurse through the model.
      (function _depthFirst(model, callback, database, tableMapping=null, parent=null) {
        if (model instanceof Array) {
          // Each element in the array could be a sub-model.  The key for each
          // array element (the table mapping) is the name of the property in
          // the model.
          for (let i = 0; i < model.length; ++i)
            _depthFirst(model[i], callback, database, tableMapping, parent);
        }
        else if (model instanceof Object) {
          const nextParent =
            tableMapping && (!database || database.isTableMapping(tableMapping)) ?
            model : null;

          // Each property in the object could be a sub model.  Traverse each.
          for (let modelProp in model)
            _depthFirst(model[modelProp], callback, database, modelProp, nextParent);

          // Don't fire the callback unless mapping is defined (if it is
          // undefined it is the top-level object).
          if (tableMapping && (!database || database.isTableMapping(tableMapping)))
            callback({tableMapping, model, parent});
        }
      })(model, callback, database);
    }

    /**
     * Traverse model (either an object or an array) using a breadth-first traversal.
     * @param {Object|object[]} model - The object or array to traverse.
     * @param {ModelTraverse~modelMetaCallback} callback - A function that
     * shall be invoked with for each sub-model within model.
     * @param {Database} database - An optional Database instance.  If passed,
     * the callback will only be called if a key in the model corresponds to a
     * table mapping.
     * @return {void}
     */
    breadthFirst(model, callback, database) {
      const queue = [{tableMapping: null, model: model, parent: null}];

      while (queue.length !== 0) {
        const item = queue.shift();

        if (item.model instanceof Array) {
          // If there is a database instance, parent is only set if it is
          // a valid table alias.
          const parent =
            (!database || item.parent && database.isTableMapping(item.parent.tableMapping)) ?
            item.parent : null;

          // Each element in the array could be a sub model.  The key for each
          // array element is the name of the array property in the object.
          for (let i = 0; i < item.model.length; ++i)
            queue.push({tableMapping: item.tableMapping, model: item.model[i], parent: parent});
        }
        else if (item.model instanceof Object) {
          const parent =
            (!database || database.isTableMapping(item.tableMapping)) ?
            item : null;

          // Each property in the object could be a sub model.  Queue each.
          for (let modelProp in item.model)
            queue.push({tableMapping: modelProp, model: item.model[modelProp], parent: parent});

          // Don't fire the callback unless key is non-null (if it is null it is
          // the top-level object).
          if (item.tableMapping && (!database || database.isTableMapping(item.tableMapping)))
            callback(item);
        }
      }
    }
  }

  return ModelTraverse;
}