Source: collections/ModelCollection.js


/**
 * An extension of the {@link Rekord.Collection} class for {@link Rekord.Model}
 * instances.
 *
 * @constructor
 * @memberof Rekord
 * @extends Rekord.Collection
 * @param {Rekord.Database} database -
 *    The database for the models in this collection.
 * @param {modelInput[]} [models] -
 *    The initial array of models in this collection.
 * @param {Boolean} [remoteData=false] -
 *    If the models array is from a remote source. Remote sources place the
 *    model directly into the database while local sources aren't stored in the
 *    database until they're saved.
 * @see Rekord.Models.boot
 * @see Rekord.Models.collect
 */
function ModelCollection(database, models, remoteData)
{
  this.init( database, models, remoteData );
}

/**
 * The map of models which keeps an index (by model key) of the models.
 *
 * @memberof Rekord.ModelCollection#
 * @member {Rekord.Map} map
 */

/**
 * The database for the models in this collection.
 *
 * @memberof Rekord.ModelCollection#
 * @member {Rekord.Database} database
 */

extendArray( Collection, ModelCollection,
{

  /**
   * Initializes the model collection by setting the database, the initial set
   * of models, and whether the initial set of models is from a remote source.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {Rekord.Database} database -
   *    The database for the models in this collection.
   * @param {modelInput[]} [models] -
   *    The initial array of models in this collection.
   * @param {Boolean} [remoteData=false] -
   *    If the models array is from a remote source. Remote sources place the
   *    model directly into the database while local sources aren't stored in the
   *    database until they're saved.
   * @return {Rekord.ModelCollection} -
   *    The reference to this collection.
   * @emits Rekord.ModelCollection#reset
   */
  init: function(database, models, remoteData)
  {
    this.map = new Map();
    this.map.values = this;
    this.database = database;
    this.reset( models, remoteData );

    return this;
  },

  /**
   * Documented in Collection.js
   */
  sort: function(comparator, comparatorNullsFirst)
  {
    var cmp = comparator ? createComparator( comparator, comparatorNullsFirst ) : this.comparator;

    if ( !isSorted( cmp, this ) )
    {
      this.map.sort( cmp );

      this.trigger( Collection.Events.Sort, [this] );
    }

    return this;
  },

  /**
   * Takes input provided to the collection for adding, removing, or querying
   * and generates the key which uniquely identifies a model.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {modelInput} input -
   *    The input to convert to a key.
   * @return {modelKey} -
   *    The key built from the input.
   */
  buildKeyFromInput: function(input)
  {
    return this.database.buildKeyFromInput( input );
  },

  /**
   * Takes input provided to this collection for adding, removing, or querying
   * and returns a model instance. An existing model can be referenced or a new
   * model can be created on the spot.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {modelInput} input -
   *    The input to convert to a model instance.
   * @param {Boolean} [remoteData=false] -
   *    If the model is from a remote source. Remote sources place the model
   *    directly into the database while local sources aren't stored in the
   *    database until they're saved.
   * @return {Rekord.Model} -
   *    A model instance parsed from the input.
   */
  parseModel: function(input, remoteData)
  {
    return this.database.parseModel( input, remoteData );
  },

  /**
   * Creates a sub view of this collection known as a filtered collection. The
   * resulting collection changes when this collection changes. Any time an
   * element is added or removed to this collection it may be added or removed
   * from the filtered collection if it fits the filter function. The filter
   * function is created by passing the arguments of this function to
   * {@link Rekord.createWhere}.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {whereInput} [whereProperties] -
   *    See {@link Rekord.createWhere}
   * @param {Any} [whereValue] -
   *    See {@link Rekord.createWhere}
   * @param {equalityCallback} [whereEquals] -
   *    See {@link Rekord.createWhere}
   * @return {Rekord.FilteredModelCollection} -
   *    The newly created live filtered view of this collection.
   * @see Rekord.createWhere
   */
  filtered: function(whereProperties, whereValue, whereEquals)
  {
    var filter = createWhere( whereProperties, whereValue, whereEquals );

    return new FilteredModelCollection( this, filter );
  },

  /**
   * Documented in Collection.js
   *
   * @see Rekord.ModelCollection#buildKeyFromInput
   */
  subtract: function(models, out)
  {
    var target = out || this.cloneEmpty();

    for (var i = 0; i < this.length; i++)
    {
      var a = this[ i ];
      var key = a.$key();
      var exists = false;

      if ( models instanceof ModelCollection )
      {
        exists = models.has( key );
      }
      else
      {
        for (var k = 0; k < models.length && !exists; k++)
        {
          var modelKey = this.buildKeyFromInput( models[ k ] );

          exists = (key === modelKey);
        }
      }

      if (!exists)
      {
        target.push( a );
      }
    }

    return target;
  },

  /**
   * Documented in Collection.js
   */
  intersect: function(models, out)
  {
    var target = out || this.cloneEmpty();

    for (var i = 0; i < models.length; i++)
    {
      var a = models[ i ];
      var key = this.buildKeyFromInput( a );

      if ( this.has( key ) )
      {
        target.push( a );
      }
    }

    return target;
  },

  /**
   * Documented in Collection.js
   */
  complement: function(models, out)
  {
    var target = out || this.cloneEmpty();

    for (var i = 0; i < models.length; i++)
    {
      var a = models[ i ];
      var key = this.buildKeyFromInput( a );

      if ( !this.has( key ) )
      {
        target.push( a );
      }
    }

    return target;
  },

  /**
   * Documented in Collection.js
   */
  clear: function()
  {
    return this.map.reset();
  },

  /**
   * Resets the models in this collection with a new collection of models.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {modelInput[]} [models] -
   *    The initial array of models in this collection.
   * @param {Boolean} [remoteData=false] -
   *    If the models array is from a remote source. Remote sources place the
   *    model directly into the database while local sources aren't stored in the
   *    database until they're saved.
   * @return {Rekord.ModelCollection} -
   *    The reference to this collection.
   * @see Rekord.ModelCollection#parseModel
   * @emits Rekord.ModelCollection#reset
   */
  reset: function(models, remoteData)
  {
    var map = this.map;

    map.reset();

    if ( isArray( models ) )
    {
      for (var i = 0; i < models.length; i++)
      {
        var model = models[ i ];
        var parsed = this.parseModel( model, remoteData );

        if ( parsed )
        {
          map.put( parsed.$key(), parsed );
        }
      }
    }
    else if ( isObject( models ) )
    {
      var parsed = this.parseModel( models, remoteData );

      if ( parsed )
      {
        map.put( parsed.$key(), parsed );
      }
    }

    this.trigger( Collection.Events.Reset, [this] );
    this.sort();

    return this;
  },

  /**
   * Returns whether this collection contains a model with the given key.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {modelKey} key -
   *    The key of the model to check for existence.
   * @return {Boolean} -
   *    True if a model with the given key exists in this collection, otherwise
   *    false.
   */
  has: function(key)
  {
    return this.map.has( key );
  },

  /**
   * Returns the model in this collection with the given key.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {modelKey} key -
   *    The key of the model to return.
   * @return {Rekord.Model} -
   *    The model instance for the given key, or undefined if a model wasn't
   *    found.
   */
  get: function(key)
  {
    return this.map.get( key );
  },

  /**
   * Places a model in this collection providing a key to use.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {modelKey} key -
   *    The key of the model.
   * @param {Rekord.Model} model -
   *    The model instance to place in the collection.
   * @param {Boolean} [delaySort=false] -
   *    Whether automatic sorting should be delayed until the user manually
   *    calls {@link Rekord.ModelCollection#sort sort}.
   * @return {Rekord.ModelCollection} -
   *    The reference to this collection.
   * @emits Rekord.ModelCollection#add
   * @emits Rekord.ModelCollection#sort
   */
  put: function(key, model, delaySort)
  {
    this.map.put( key, model );
    this.trigger( Collection.Events.Add, [this, model] );

    if ( !delaySort )
    {
      this.sort();
    }
  },

  /**
   * Adds a model to this collection - sorting the collection if a comparator
   * is set on this collection and `delaySort` is not a specified or a true
   * value.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {modelInput} input -
   *    The model to add to this collection.
   * @param {Boolean} [delaySort=false] -
   *    Whether automatic sorting should be delayed until the user manually
   *    calls {@link Rekord.ModelCollection#sort sort}.
   * @param {Boolean} [remoteData=false] -
   *    If the model is from a remote source. Remote sources place the model
   *    directly into the database while local sources aren't stored in the
   *    database until they're saved.
   * @return {Rekord.ModelCollection} -
   *    The reference to this collection.
   * @emits Rekord.ModelCollection#add
   * @emits Rekord.ModelCollection#sort
   */
  add: function(input, delaySort, remoteData)
  {
    var model = this.parseModel( input, remoteData );

    this.map.put( model.$key(), model );
    this.trigger( Collection.Events.Add, [this, model] );

    if ( !delaySort )
    {
      this.sort();
    }

    return this;
  },

  /**
   * Adds one or more models to the end of this collection - sorting the
   * collection if a comparator is set on this collection.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {...modelInput} value -
   *    The models to add to this collection.
   * @return {Number} -
   *    The new length of this collection.
   * @emits Rekord.ModelCollection#add
   * @emits Rekord.ModelCollection#sort
   */
  push: function()
  {
    var values = arguments;

    for (var i = 0; i < values.length; i++)
    {
      var model = this.parseModel( values[ i ] );

      this.map.put( model.$key(), model );
    }

    this.trigger( Collection.Events.Adds, [this, AP.slice.apply(values)] );
    this.sort();

    return this.length;
  },

  /**
   * @method
   * @memberof Rekord.ModelCollection#
   * @see Rekord.ModelCollection#push
   * @param {...modelInput} value -
   *    The values to add to this collection.
   * @return {Number} -
   *    The new length of this collection.
   * @emits Rekord.ModelCollection#adds
   * @emits Rekord.ModelCollection#sort
   */
  unshift: function()
  {
    return this.push.apply( this, arguments );
  },

  /**
   * Adds all models in the given array to this collection - sorting the
   * collection if a comparator is set on this collection and `delaySort` is
   * not specified or a true value.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {modelInput[]} models -
   *    The models to add to this collection.
   * @param {Boolean} [delaySort=false] -
   *    Whether automatic sorting should be delayed until the user manually
   *    calls {@link Rekord.ModelCollection#sort sort}.
   * @param {Boolean} [remoteData=false] -
   *    If the model is from a remote source. Remote sources place the model
   *    directly into the database while local sources aren't stored in the
   *    database until they're saved.
   * @return {Rekord.ModelCollection} -
   *    The reference to this collection.
   * @emits Rekord.ModelCollection#adds
   * @emits Rekord.ModelCollection#sort
   */
  addAll: function(models, delaySort, remoteData)
  {
    if ( isArray( models ) )
    {
      for (var i = 0; i < models.length; i++)
      {
        var model = this.parseModel( models[ i ], remoteData );

        this.map.put( model.$key(), model );
      }

      this.trigger( Collection.Events.Adds, [this, models] );

      if ( !delaySort )
      {
        this.sort();
      }
    }
  },

  /**
   * @method
   * @memberof Rekord.ModelCollection#
   * @see Rekord.ModelCollection#add
   * @return {Rekord.ModelCollection} -
   *    The reference to this collection.
   * @emits Rekord.ModelCollection#add
   * @emits Rekord.ModelCollection#sort
   */
  insertAt: function(i, value, delaySort)
  {
    return this.add( value, delaySort );
  },

  /**
   * Removes the last model in this collection and returns it - sorting the
   * collection if a comparator is set on this collection and `delaySort` is
   * no specified or a true value.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {Boolean} [delaySort=false] -
   *    Whether automatic sorting should be delayed until the user manually
   *    calls {@link Rekord.ModelCollection#sort sort}.
   * @return {Rekord.Model} -
   *    The model removed from the end of the collection.
   * @emits Rekord.ModelCollection#remove
   * @emits Rekord.ModelCollection#sort
   */
  pop: function(delaySort)
  {
    var i = this.length - 1;
    var removed = this[ i ];

    this.map.removeAt( i );
    this.trigger( Collection.Events.Remove, [this, removed, i] );

    if ( !delaySort )
    {
      this.sort();
    }

    return removed;
  },

  /**
   * Removes the first model in this collection and returns it - sorting the
   * collection if a comparator is set on this collection and `delaySort` is
   * no specified or a true value.
   *
   * ```javascript
   * var c = Rekord.collect(1, 2, 3, 4);
   * c.shift(); // 1
   * ```
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {Boolean} [delaySort=false] -
   *    Whether automatic sorting should be delayed until the user manually
   *    calls {@link Rekord.ModelCollection#sort sort}.
   * @return {Rekord.Model} -
   *    The model removed from the beginning of the collection.
   * @emits Rekord.ModelCollection#remove
   * @emits Rekord.ModelCollection#sort
   */
  shift: function(delaySort)
  {
    var removed = this[ 0 ];

    this.map.removeAt( 0 );
    this.trigger( Collection.Events.Remove, [this, removed, 0] );

    if ( !delaySort )
    {
      this.sort();
    }

    return removed;
  },

  /**
   * Removes the model in this collection at the given index `i` - sorting
   * the collection if a comparator is set on this collection and `delaySort` is
   * not specified or a true value.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {Number} i -
   *    The index of the model to remove.
   * @param {Boolean} [delaySort=false] -
   *    Whether automatic sorting should be delayed until the user manually
   *    calls {@link Rekord.ModelCollection#sort sort}.
   * @return {Rekord.Model} -
   *    The model removed, or undefined if the index was invalid.
   * @emits Rekord.ModelCollection#remove
   * @emits Rekord.ModelCollection#sort
   */
  removeAt: function(i, delaySort)
  {
    var removing;

    if (i >= 0 && i < this.length)
    {
      removing = this[ i ];

      this.map.removeAt( i );
      this.trigger( Collection.Events.Remove, [this, removing, i] );

      if ( !delaySort )
      {
        this.sort();
      }
    }

    return removing;
  },

  /**
   * Removes the given model from this collection if it exists - sorting the
   * collection if a comparator is set on this collection and `delaySort` is not
   * specified or a true value.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {modelInput} input -
   *    The model to remove from this collection if it exists.
   * @param {Boolean} [delaySort=false] -
   *    Whether automatic sorting should be delayed until the user manually
   *    calls {@link Rekord.ModelCollection#sort sort}.
   * @param {equalityCallback} [equals=Rekord.equalsStrict] -
   *    The function which determines whether one of the elements that exist in
   *    this collection are equivalent to the given value.
   * @return {Rekord.Model} -
   *    The element removed from this collection.
   * @emits Rekord.ModelCollection#remove
   * @emits Rekord.ModelCollection#sort
   */
  remove: function(input, delaySort)
  {
    var key = this.buildKeyFromInput( input );
    var removing = this.map.get( key );

    if ( removing )
    {
      this.map.remove( key );
      this.trigger( Collection.Events.Remove, [this, removing, input] );

      if ( !delaySort )
      {
        this.sort();
      }
    }

    return removing;
  },

  /**
   * Removes the given models from this collection - sorting the collection if
   * a comparator is set on this collection and `delaySort` is not specified or
   * a true value.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {modelInput[]} inputs -
   *    The models to remove from this collection if they exist.
   * @param {Boolean} [delaySort=false] -
   *    Whether automatic sorting should be delayed until the user manually
   *    calls {@link Rekord.ModelCollection#sort sort}.
   * @return {Rekord.Model[]} -
   *    The models removed from this collection.
   * @emits Rekord.ModelCollection#removes
   * @emits Rekord.ModelCollection#sort
   */
  removeAll: function(inputs, delaySort)
  {
    var map = this.map;
    var removed = [];

    for (var i = 0; i < inputs.length; i++)
    {
      var key = this.buildKeyFromInput( inputs[ i ] );
      var removing = map.get( key );

      if ( removing )
      {
        map.remove( key );
        removed.push( removing );
      }
    }

    this.trigger( Collection.Events.Removes, [this, removed] );

    if ( !delaySort )
    {
      this.sort();
    }

    return removed;
  },

  /**
   * Returns the index of the given model in this collection or returns -1
   * if the model doesn't exist in this collection.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {modelInput} input -
   *    The model to search for.
   * @return {Number} -
   *    The index of the model in this collection or -1 if it was not found.
   */
  indexOf: function(input)
  {
    var key = this.buildKeyFromInput( input );
    var index = this.map.indices[ key ];

    return index === undefined ? -1 : index;
  },

  /**
   * Rebuilds the internal index which maps keys to the index of the model in
   * this collection.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @return {Rekord.ModelCollection} -
   *    The reference to this collection.
   */
  rebuild: function()
  {
    this.map.rebuildIndex();
  },

  /**
   * Returns the array of keys that correspond to the models in this collection.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @return {modelKey[]} -
   *    The array of model keys.
   */
  keys: function()
  {
    return this.map.keys;
  },

  /**
   * Reverses the order of models in this collection.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @return {Rekord.ModelCollection} -
   *    The reference to this collection.
   * @emits Rekord.ModelCollection#updates
   */
  reverse: function()
  {
    this.map.reverse();

    this.trigger( Collection.Events.Updates, [this] );

    return this;
  },

  /**
   * Splices elements out of and into this collection - sorting the collection
   * if a comparator is set on this collection.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {Number} start -
   *    Index at which to start changing the array (with origin 0). If greater
   *    than the length of the array, actual starting index will be set to the
   *    length of the array. If negative, will begin that many elements from the end.
   * @param {Number} deleteCount -
   *    An integer indicating the number of old array elements to remove. If
   *    deleteCount is 0, no elements are removed. In this case, you should
   *    specify at least one new element. If deleteCount is greater than the
   *    number of elements left in the array starting at start, then all of the
   *    elements through the end of the array will be deleted.
   *    If deleteCount is omitted, deleteCount will be equal to (arr.length - start).
   * @param {...Any} values -
   *    The elements to add to the array, beginning at the start index. If you
   *    don't specify any elements, splice() will only remove elements from the array.
   * @return {Any[]} -
   *    The array of deleted elements.
   * @emits Rekord.ModelCollection#removes
   * @emits Rekord.ModelCollection#adds
   * @emits Rekord.ModelCollection#sort
   */
  splice: function(start, deleteCount)
  {
    var adding = AP.slice.call( arguments, 2 );
    var addingKeys = [start, deleteCount];
    for (var i = 0; i < adding.length; i++)
    {
      addingKeys.push( this.buildKeyFromInput( adding[ i ] ) );
    }

    var removed = AP.splice.apply( this, arguments );

    AP.splice.apply( this.map.keys, addingKeys );

    if ( deleteCount )
    {
      this.trigger( Collection.Events.Removes, [this, removed] );
    }

    if ( adding.length )
    {
      this.trigger( Collection.Events.Adds, [this, adding] );
    }

    this.sort();

    return removed;
  },

  /**
   * Removes the models from this collection where the given expression is true.
   * The first argument, if `true`, can call {@link Rekord.Model#$remove} on each
   * model removed from this colleciton.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {Boolean} [callRemove=false] -
   *    Whether {@link Rekord.Model#$remove} should be called on each removed model.
   * @param {whereInput} [whereProperties] -
   *    See {@link Rekord.createWhere}
   * @param {Any} [whereValue] -
   *    See {@link Rekord.createWhere}
   * @param {equalityCallback} [whereEquals] -
   *    See {@link Rekord.createWhere}
   * @param {Array} [out=this.cloneEmpty()] -
   *    The array to place the elements that match.
   * @param {Boolean} [delaySort=false] -
   *    Whether automatic sorting should be delayed until the user manually
   *    calls {@link Rekord.Collection#sort sort}.
   * @return {Rekord.Model[]} -
   *    An array of models removed from this collection.
   * @emits Rekord.ModelCollection#removes
   * @emits Rekord.ModelCollection#sort
   */
  removeWhere: function(callRemove, whereProperties, whereValue, whereEquals, out, delaySort)
  {
    var where = createWhere( whereProperties, whereValue, whereEquals );
    var removed = out || this.cloneEmpty();

    batchExecute(function()
    {
      for (var i = 0; i < this.length; i++)
      {
        var model = this[ i ];
        var key = model.$key();

        if ( where( model ) )
        {
          this.map.remove( key );
          removed.push( model );
          i--;

          if ( callRemove )
          {
            model.$remove();
          }
        }
      }

    }, this );

    this.trigger( Collection.Events.Removes, [this, removed] );

    if ( !delaySort )
    {
      this.sort();
    }

    return removed;
  },

  /**
   * Updates the given property(s) in all models in this collection with the
   * given value. If `avoidSave` is not a truthy value then
   * {@link Rekord.Model#$save} is called on every model in this collection.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {String|Object} props -
   *    The property or properties to update.
   * @param {Any} [value] -
   *    The value to set if a String `props` is given.
   * @param {Boolean} [remoteData=false] -
   *    If the properties are from a remote source. Remote sources place the
   *    model directly into the database while local sources aren't stored in the
   *    database until they're saved.
   * @param {Boolean} [avoidSave=false] -
   *    True for NOT calling {@link Rekord.Model#$save}, otherwise false.
   * @param {Number} [cascade] -
   *    Which operations should be performed out of: store, rest, & live.
   * @return {Rekord.ModelCollection} -
   *    The reference to this collection.
   * @emits Rekord.ModelCollection#updates
   * @emits Rekord.ModelCollection#sort
   */
  update: function(props, value, remoteData, avoidSave, cascade)
  {
    batchExecute(function()
    {
      for (var i = 0; i < this.length; i++)
      {
        var model = this[ i ];

        model.$set( props, value, remoteData );

        if ( !avoidSave )
        {
          model.$save( cascade );
        }
      }

    }, this );

    this.trigger( Collection.Events.Updates, [this, this] );
    this.sort();

    return this;
  },

  /**
   * Updates the given property(s) in models in this collection which pass the
   * `where` function with the given value. If `avoidSave` is not a truthy value
   * then {@link Rekord.Model#$save} is called on every model in this collection.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {whereCallback} where -
   *    The function which determines whether a model should be updated.
   * @param {String|Object} props -
   *    The property or properties to update.
   * @param {*} [value] -
   *    The value to set if a String `props` is given.
   * @param {Boolean} [remoteData=false] -
   *    If the properties are from a remote source. Remote sources place the
   *    model directly into the database while local sources aren't stored in the
   *    database until they're saved.
   * @param {Boolean} [avoidSave=false] -
   *    True for NOT calling {@link Rekord.Model#$save}, otherwise false.
   * @param {Number} [cascade] -
   *    Which operations should be performed out of: store, rest, & live.
   * @return {Rekord.Model[]} -
   *    An array of models updated.
   * @emits Rekord.ModelCollection#updates
   * @emits Rekord.ModelCollection#sort
   */
  updateWhere: function(where, props, value, remoteData, avoidSave, cascade)
  {
    var updated = [];

    batchExecute(function()
    {
      for (var i = 0; i < this.length; i++)
      {
        var model = this[ i ];

        if ( where( model ) )
        {
          model.$set( props, value, remoteData );

          if ( !avoidSave )
          {
            model.$save( cascade );
          }

          updated.push( model );
        }
      }

    }, this );

    this.trigger( Collection.Events.Updates, [this, updated] );
    this.sort();

    return updated;
  },

  /**
   * Calls {@link Rekord.Model#$push} on models in this collection that meet
   * the given where expression.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {String[]} [fields] -
   *    The set of fields to save for later popping or discarding. If not
   *    specified, all model fields will be saved.
   * @param {whereInput} [properties] -
   *    See {@link Rekord.createWhere}
   * @param {Any} [value] -
   *    See {@link Rekord.createWhere}
   * @param {equalityCallback} [equals=Rekord.equalsStrict] -
   *    See {@link Rekord.createWhere}
   * @return {Rekord.ModelCollection} -
   *    The reference to this collection.
   * @see Rekord.createWhere
   * @see Rekord.Model#$push
   */
  pushWhere: function(fields, properties, value, equals)
  {
    function pushIt(model)
    {
      model.$push( fields );
    }

    return this.eachWhere( pushIt, properties, value, equals );
  },

  /**
   * Calls {@link Rekord.Model#$pop} on models in this collection that meet
   * the given where expression.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {Boolean} [dontDiscard=false] -
   *    Whether to remove the saved state after the saved state has been applied
   *    back to the model. A falsy value will result in
   *    {@link Rekord.Model#$discard} being called.
   * @param {whereInput} [properties] -
   *    See {@link Rekord.createWhere}
   * @param {Any} [value] -
   *    See {@link Rekord.createWhere}
   * @param {equalityCallback} [equals=Rekord.equalsStrict] -
   *    See {@link Rekord.createWhere}
   * @return {Rekord.ModelCollection} -
   *    The reference to this collection.
   * @see Rekord.createWhere
   * @see Rekord.Model#$pop
   */
  popWhere: function(dontDiscard, properties, value, equals)
  {
    function popIt(model)
    {
      model.$pop( dontDiscard );
    }

    return this.eachWhere( popIt, properties, value, equals );
  },

  /**
   * Calls {@link Rekord.Model#$discard} on models in this collection that meet
   * the given where expression.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {whereInput} [properties] -
   *    See {@link Rekord.createWhere}
   * @param {Any} [value] -
   *    See {@link Rekord.createWhere}
   * @param {equalityCallback} [equals=Rekord.equalsStrict] -
   *    See {@link Rekord.createWhere}
   * @return {Rekord.ModelCollection} -
   *    The reference to this collection.
   * @see Rekord.createWhere
   * @see Rekord.Model#$discard
   */
  discardWhere: function(properties, value, equals)
  {
    function discardIt(model)
    {
      model.$discard();
    }

    return this.eachWhere( discardIt, properties, value, equals );
  },

  /**
   * Calls {@link Rekord.Model#$cancel} on models in this collection that meet
   * the given where expression.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {Boolean} [reset=false] -
   *    If reset is true and the model doesn't have a saved state -
   *    {@link Rekord.Model#$reset} will be called.
   * @param {whereInput} [properties] -
   *    See {@link Rekord.createWhere}
   * @param {Any} [value] -
   *    See {@link Rekord.createWhere}
   * @param {equalityCallback} [equals=Rekord.equalsStrict] -
   *    See {@link Rekord.createWhere}
   * @return {Rekord.ModelCollection} -
   *    The reference to this collection.
   * @see Rekord.createWhere
   * @see Rekord.Model#$cancel
   */
  cancelWhere: function(reset, properties, value, equals)
  {
    function cancelIt(model)
    {
      model.$cancel( reset );
    }

    batchExecute(function()
    {
      this.eachWhere( cancelIt, properties, value, equals );

    }, this );

    return this;
  },

  /**
   * Calls {@link Rekord.Model#$refresh} on models in this collection that meet
   * the given where expression.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {whereInput} [properties] -
   *    See {@link Rekord.createWhere}
   * @param {Any} [value] -
   *    See {@link Rekord.createWhere}
   * @param {equalityCallback} [equals=Rekord.equalsStrict] -
   *    See {@link Rekord.createWhere}
   * @return {Rekord.ModelCollection} -
   *    The reference to this collection.
   * @see Rekord.createWhere
   * @see Rekord.Model#$refresh
   */
  refreshWhere: function(properties, value, equals)
  {
    function refreshIt(model)
    {
      model.$refresh();
    }

    batchExecute(function()
    {
      this.eachWhere( refreshIt, properties, value, equals );

    }, this );

    return this;
  },

  /**
   * Calls {@link Rekord.Model#$save} on models in this collection that meet
   * the given where expression.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {whereInput} [properties] -
   *    See {@link Rekord.createWhere}
   * @param {Any} [value] -
   *    See {@link Rekord.createWhere}
   * @param {equalityCallback} [equals=Rekord.equalsStrict] -
   *    See {@link Rekord.createWhere}
   * @param {Object} [props={}] -
   *    Properties to apply to each model in the collection that pass the where
   *    expression.
   * @param {Number} [cascade] -
   *    Which operations should be performed out of: store, rest, & live.
   * @return {Rekord.ModelCollection} -
   *    The reference to this collection.
   * @see Rekord.createWhere
   * @see Rekord.Model#$refresh
   */
  saveWhere: function(properties, value, equals, props, cascade)
  {
    function saveIt(model)
    {
      model.$save( props, cascade );
    }

    batchExecute(function()
    {
      this.eachWhere( saveIt, properties, value, equals );

    }, this );

    return this;
  },

  /**
   * Returns a clone of this collection. Optionally the models in this
   * collection can also be cloned.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @param {Boolean} [cloneModels=false] -
   *    Whether or not the models should be cloned as well.
   * @param {Boolean} [cloneProperties] -
   *    The properties object which defines what fields should be given a
   *    different (non-cloned) value and which relations need to be cloned.
   * @return {Rekord.ModelCollection} -
   *    The reference to a clone collection.
   * @see Rekord.Model#$clone
   */
  clone: function(cloneModels, cloneProperties)
  {
    var source = this;

    if ( cloneModels )
    {
      source = [];

      for (var i = 0; i < this.length; i++)
      {
        source[ i ] = this[ i ].$clone( cloneProperties );
      }
    }

    return new ModelCollection( this.database, source, true );
  },

  /**
   * Returns an empty clone of this collection.
   *
   * @method
   * @memberof Rekord.ModelCollection#
   * @return {Rekord.ModelCollection} -
   *    The reference to a clone collection.
   */
  cloneEmpty: function()
  {
    return new ModelCollection( this.database );
  }

});