wbyoung/azul

View on GitHub
lib/relations/has_many.js

Summary

Maintainability
B
5 hrs
Test Coverage
'use strict';

var _ = require('lodash');
var Promise = require('bluebird');
var BaseRelation = require('./base');
var Actionable = require('maguey').Actionable;
var Hooks = require('./has_many_hooks');
var Config = require('./has_many_config');
var Query = require('./has_many_query');
var Associations = require('./has_many_associations');
var Overrides = require('./has_many_overrides');
var Through = require('./has_many_through');
var Prefetch = require('./has_many_prefetch');
var Collection = require('./has_many_collection');
var InFlight = require('./has_many_in_flight');

/**
 * The has many relation for models.
 *
 * For example:
 *
 *     var User = Model.extend({
 *       articles: hasMany('articles')
 *     });
 *
 * @public
 * @param {Class|String} [relatedModel] The model to which this relates.
 * @param {Object} [options]
 * @param {String} [options.inverse] The name of the inverse relationship.
 * @param {String} [options.primaryKey] The name of the primary key in the
 * relationship.
 * @param {String} [options.foreignKey] The name of the foreign key in the
 * relationship.
 * @param {String} [options.through] Specify the name of a relationship through
 * which this collection is accessed.
 * @param {String} [options.source] When using `through` this is the name of
 * the relationship on the destination model. The default value is the name of
 * the attribute for the relationship.
 * @param {Boolean} [options.implicit] Whether this relation is being added
 * implicitly by the system.
 * @function Database#hasMany
 */

/**
 * The has many relation for models.
 *
 * @protected
 * @constructor HasMany
 * @extends BaseRelation
 * @see {@link Database#hasMany}
 */
var HasMany = BaseRelation.extend();

HasMany.reopen(Hooks); // must be first, contains overridable methods
HasMany.reopen(Config);
HasMany.reopen(Query);
HasMany.reopen(Associations);
HasMany.reopen(Overrides);
HasMany.reopen(Through);
HasMany.reopen(Prefetch);
HasMany.reopen(Collection);
HasMany.reopen(InFlight);

HasMany.reopenClass({
  joinKey: 'primaryKey',
  inverseKey: 'foreignKey',
});

/**
 * Notify of changes around a call to super.
 *
 * @function HasMany~notify
 * @private
 * @param {String} name The suffix of the before/after methods to call.
 * @return {Function} A method that will call before & after methods.
 */
var notify = function(name) {
  var before = 'before' + _.capitalize(name);
  var after = 'after' + _.capitalize(name);
  return function() {
    this[before].apply(this, arguments);
    this._super.apply(this, arguments);
    this[after].apply(this, arguments);
  };
};

HasMany.reopen(/** @lends HasMany# */ {

  /**
   * The create object method for this relation.
   *
   * This simply creates a new object that has the appropriate relation data
   * already set.
   *
   * This method invalidates the {@link HasMany#objectsQuery} cache and adds
   * the created object to the {@link HasMany#collection} (if loaded).
   *
   * It is accessible on an individual model via `create<Singular>`. For
   * instance, a user that has many articles would cause this method to get
   * triggered via `user.createArticle`.
   *
   * The naming conventions are set forth in {@link HasMany#overrides}.
   *
   * Mixins can override {@link HasMany#createObject} to change the way related
   * objects are created. This is the default implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @param {...Object} [args] The arguments to the method.
   * @see {@link BaseRelation#methods}
   */
  _createObject: function(instance) {
    var args = _.rest(arguments);
    var relatedModel = this._relatedModel;
    var result = relatedModel.create.apply(relatedModel, args);

    this.beforeCreatingObject(instance, result); // note: no after hook
    this.associate(instance, result);

    return result;
  },

  /**
   * The add objects method for this relation.
   *
   * This method invalidates the {@link HasMany#objectsQuery} cache and adds
   * the related objects to the {@link HasMany#collection} (if loaded).
   *
   * It is accessible on an individual model via `add<Singular>` and
   * `add<Plural>`. For instance a user that has many articles would cause this
   * method to get triggered via `user.addArticle` or `user.addArticles`.
   *
   * The naming conventions are set forth in {@link HasMany#overrides}.
   *
   * Mixins can override {@link HasMany#addObjects} to change the way related
   * objects are added. This is the default implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @param {...Object} [args] The arguments to the method.
   * @return {Actionable} A thenable object, use of which will trigger the
   * instance to be saved.
   * @see {@link BaseRelation#methods}
   */
  _addObjects: function(instance) {
    var args = _.rest(arguments);
    var objects = _.flatten(args);
    this.beforeAddingObjects(instance, objects);
    this.associateObjects(instance, objects);
    return Actionable.create(instance.save.bind(instance));
  },

  /**
   * Perform the necessary updates to add objects for this relation.
   *
   * This method invalidates the {@link HasMany#objectsQuery}.
   *
   * Mixins can override {@link HasMany#executeAdd} to change the way updates
   * are performed when related objects are added. This is the default
   * implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @param {Array} objects The objects to add to the relationship.
   */
  _executeAdd: function(instance, objects) {
    var self = this;
    var after = this.afterAddingObjects.bind(this, instance, objects);
    return self._updateForeignKeys(instance, objects, instance.id).tap(after);
  },

  /**
   * The remove objects method for this relation.
   *
   * This method invalidates the {@link HasMany#objectsQuery} cache and removes
   * the related objects from the {@link HasMany#collection} (if loaded).
   *
   * It is accessible on an individual model via `remove<Singular>` and
   * `remove<Plural>`. For instance a user that has many articles would cause
   * this method to get triggered via `user.removeArticle` or
   * `user.removeArticles`.
   *
   * The naming conventions are set forth in {@link HasMany#overrides}.
   *
   * Mixins can override {@link HasMany#removeObjects} to change the way
   * related objects are removed. This is the default implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @param {...Object} [args] The arguments to the method.
   * @return {Actionable} A thenable object, use of which will trigger the
   * instance to be saved.
   * @see {@link BaseRelation#methods}
   */
  _removeObjects: function(instance) {
    var args = _.rest(arguments);
    var objects = _.flatten(args);
    this.beforeRemovingObjects(instance, objects);
    this.disassociateObjects(instance, objects);
    return Actionable.create(instance.save.bind(instance));
  },

  /**
   * Perform the necessary updates to remove objects for this relation.
   *
   * This method invalidates the {@link HasMany#objectsQuery}.
   *
   * Mixins can override {@link HasMany#executeRemove} to change the way
   * updates are performed when related objects are removed. This is the
   * default implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @param {Array} objects The objects to remove from the relationship.
   */
  _executeRemove: function(instance, objects) {
    var self = this;
    var after = this.afterRemovingObjects.bind(this, instance, objects);
    var removable = _.filter(objects, 'persisted');
    return self._updateForeignKeys(instance, removable, undefined).tap(after);
  },

  /**
   * The clear objects method for this relation.
   *
   * This method invalidates the {@link HasMany#objectsQuery} cache and removes
   * all objects from the {@link HasMany#collection} (if loaded).
   *
   * It is accessible on an individual model via `clear<Plural>`. For
   * instance, a user that has many articles would cause this method to get
   * triggered via `user.clearArticles`.
   *
   * The naming conventions are set forth in {@link HasMany#overrides}.
   *
   * Mixins can override {@link HasMany#clearObjects} to change the way related
   * objects are cleared. This is the default implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @return {Actionable} A thenable object, use of which will trigger the
   * instance to be saved.
   * @see {@link BaseRelation#methods}
   */
  _clearObjects: function(instance) {
    this.beforeClearingObjects(instance);
    return Actionable.create(instance.save.bind(instance));
  },

  /**
   * Perform the necessary updates to clear objects for this relation.
   *
   * This method invalidates the {@link HasMany#objectsQuery}.
   *
   * Mixins can override {@link HasMany#executeClear} to change the way clears
   * are performed when related objects are cleared. This is the default
   * implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   */
  _executeClear: function(instance) {
    var updates = _.object([[this.foreignKey, undefined]]);
    var query = this.objectsQuery(instance).update(updates);
    var after = this.afterClearingObjects.bind(this, instance);
    return query.execute().tap(after);
  },

  /**
   * Update foreign keys for a set of related objects.
   *
   * This will update the foreign keys of several objects in a single query.
   * This allows much faster creation of associations at the database level.
   *
   * After a successful update, it will clean the foreign key attribute of
   * each updated object, likely leaving the object completely clean.
   *
   * @method
   * @private
   * @param {Model} instance The model instance on which to operate.
   * @param {Array.<Model>} relatedObjects The related models on which to
   * update keys.
   * @param {?} id The id to use as the value of the foreign key.
   * @return {Promise} A promise that resolves when the update has finished.
   */
  _updateForeignKeys: Promise.method(function(instance, relatedObjects, id) {
    if (!_.any(relatedObjects, 'dirty')) { return; }

    var foreignKey = this.foreignKey;
    var foreignKeyAttr = this.foreignKeyAttr;
    var relatedPks = _.map(relatedObjects, 'pk');

    var updates = _.object([[foreignKey, id]]);
    var conditions = {};
    if (relatedPks.length === 1) { conditions.pk = relatedPks[0]; }
    else { conditions.pk$in = relatedPks; }

    var query = this._relatedModel.objects;
    return query.where(conditions).update(updates).execute().then(function() {
      relatedObjects.forEach(function(obj) {
        obj.cleanAttribute(foreignKeyAttr);
      });
    });
  }),

  /**
   * Override of {@link HasMany#associateFetchedObjects} to call before & after
   * hooks so that mixins can get involved in the association process.
   *
   * @method
   * @protected
   * @see {@link HasMany#associateFetchedObjects}
   */
  associateFetchedObjects: notify('associatingFetchedObjects'),

  /**
   * Override of {@link HasMany#associateObjects} to call before & after hooks
   * so that mixins can get involved in the association process.
   *
   * @method
   * @protected
   * @see {@link HasMany#associateObjects}
   */
  associateObjects: notify('associatingObjects'),

  /**
   * Override of {@link HasMany#disassociateObjects} to call before & after
   * hooks so that mixins can get involved in the disassociation process.
   *
   * @method
   * @protected
   * @see {@link HasMany#disassociateObjects}
   */
  disassociateObjects: notify('disassociatingObjects'),

  /**
   * Override of {@link BaseRelation#overrides}.
   *
   * @method
   * @protected
   * @see {@link BaseRelation#overrides}
   */
  overrides: function() {
    this._super();

    if (!this._options.implicit) {
      this.overrideProperty('<plural>', 'collection');
      this.overrideProperty('<singular>Objects', 'objectsQuery');
      this.addHelper('create<Singular>', 'createObject');
      this.addHelper('add<Singular>', 'addObjects');
      this.addHelper('add<Plural>', 'addObjects');
      this.addHelper('remove<Singular>', 'removeObjects');
      this.addHelper('remove<Plural>', 'removeObjects');
      this.addHelper('clear<Plural>', 'clearObjects');
      this.overrideMethod('init', 'initialize');
      this.overrideMethod('_dependents', 'dependents');
      this.overrideMethod('_reliants', 'reliants');
      this.overrideMethod('_postsave', 'postsave');
    }
  },

});

module.exports = HasMany.reopenClass({ __name__: 'HasMany' });