Collection.js

import _ from 'lodash';
import Serializable from './Serializable';
import Model from './Model';
import {MissingIndexError} from './errors';
import {toJSON, getType} from './utils';
/**
 *
 * @extends Serializable
 */
class Collection extends Serializable {
  /**
   *
   * Collection is a dictionary helper class. A Collection requires a model to maintain consistency
   * and has methods that allow the addition, removal, and getting access to Models.
   *
   * @summary
   * A Collection is an index of Models. It is a helper object designed around maintaining this index with consistency.
   *
   * @example
   * class PokemonCollection extends Collection {
   *   constructor(data) {
   *     super(data, Person, 'name');
   *   }
   * }
   *
   * const name = 'Ditto';
   * const quote = 'Ditto!';
   * const pokemonCollection = new PokemonCollection({
   *   name,
   *   quote,
   * });
   *
   * assert(pokemonCollection.get(name).quote, quote) // true
   *
   * @param {Object|Array} data - data to create a collection with on construction. data can be passed after instantiation
   * @param {Model} model - the Model to be used when adding data to a collection. A Collection will force all data into this Model.
   * @param {String} (indexBy) [id] - the field that the collection should index on
   * @throws {Error}
   */
  constructor(data, model, indexBy='id') {
    super();
    if (!model || !(model.prototype instanceof Model)) {
      throw new Error('A Collection requires a Model, and the Model must inherit from the Model Base Class.');
    }
    this.index = {};
    this.indexBy = indexBy;
    this.Model = model;
    if (data) {
      this.set(data);
    }
  }
  /**
   *
   * @example
   * assert(pokemonCollection.count === 1); // true
   *
   * @summary
   * returns the number of items in the Collection
   *
   * @returns {Number}
   */
  get count() {
    return Object.keys(this.index).length;
  }
  /**
   *
   * A method that clears the index and sets the collection index again from an array of models or just a model.
   *
   * @example
   * pokemonCollection.set({
   *   name: 'Pikachu'
   * });
   *
   * assert(pokemonCollection.count === 1); // true
   *
   * @param {Object|Array} data
   * @returns {Collection}
   * @throws {TypeError}
   */
  set(data) {
    this.clear();
    switch (getType(data)) {
      case 'array':
        _.each(data, datum => setFromObject.call(this, datum));
        break;
      case 'object':
        setFromObject.call(this, data);
        break;
      default:
        throw new TypeError(`Expected data to be of type {Object|Array} and got ${typeof data}`);
    }
    return this;
  }
  /**
   *
   * A method that adds a model to the index. This method will throw if the same index key already exists
   *
   * @example
   * pokemonCollection.add({
   *    name: 'Ditto'
   * });
   *
   * assert(pokemonCollection.count === 2); // true
   *
   * @param {Object|Model} model
   * @returns {Collection}
   * @throws {Error}
   */
  add(model) {
    let convertedModel = convertToModel.call(this, model);
    const key = convertedModel[this.indexBy];
    if (this.index[key]) {
      throw new Error('Model already exists in Collection index. Call update if you wish to update model');
    }
    this.index[key] = convertedModel;
    return this;
  }
  /**
   *
   * Remove a model by it's index key (the property that has been set to ne indexed in the models constructor)
   *
   * @example
   * pokemonCollection.remove('Mewtwo');
   *
   * assert(pokemonCollection.count === 1); //true
   * assert(pokemonCollection.get('Mewtwo') === null); // true
   *
   * @param {String} modelId
   * @returns {Collection}
   */
  remove(modelId) {
    let model = this.get(modelId);
    if (!model) {
      throw new Error('Model does not exist in Collection index. Call add().');
    }
    let convertedModel = convertToModel.call(this, model);
    const key = convertedModel[this.indexBy];
    delete this.index[key];
    return this;
  }
  /**
   *
   * Updates a model in the index by it's index key (in the example case 'name') and overwrites all field data
   *
   * @example
   * const quote = 'Ditto!';
   * pokemonCollection.update({
   *   name: 'Ditto',
   *   quote
   * });
   *
   * assert(pokemonCollection.quote === quote); // true
   *
   * @param {Object|Model} model
   * @returns {Collection}
   * @throws {Error}
   */
  update(model) {
    let convertedModel = convertToModel.call(this, model);
    const key = convertedModel[this.indexBy];
    if (!this.index[key]) {
      throw new Error('Model to update does not exist in Collection index');
    }
    this.index[key] = convertedModel;
    return this;
  }
  /**
   *
   * Get a model by it's index key or get the entire collection (by passing no params).
   * By not passing an id the call is synonymous with calling the .toArray() on an instance.
   *
   * @example
   * const ditto = pokemonCollection.get('Ditto');
   * assert(ditto === pokemonCollection.index['Ditto']); // true
   *
   * @param {String} (id)
   * @returns {Object|Model|Array<Object|Model>}
   */
  get(id) {
    if (id) {
      return this.index[id];
    }
    return this.toArray();
  }
  /**
   *
   * if id is passed in returns true if the specific model exists otherwise returns true if the collection has > 0 models
   *
   * @param {String} (id)
   * @returns {Boolean}
   */
  contains(id) {
    let result = this.get(id);
    if (id) {
      return !!result;
    }
    return result.length > 0;
  }
  /**
   *
   * clears all items in the index
   *
   * @returns {Collection}
   */
  clear() {
    this.index = {};
    return this;
  }
  /**
   *
   * returns the index as an array
   *
   * @returns {Array<Object>}
   *
   */
  toArray() {
    return _.chain(this.index)
      .values()
      .map(item => convertToModel.call(this, item).toObject())
      .value();
  }
  /**
   *
   * converts index to an array and stringifies
   *
   * @returns {String}
   */
  toJSON() {
    return toJSON(this.toArray());
  }
}
/**
 *
 * @ignore
 * @param {Object} datum
 */
function setFromObject(datum) {
  const model = convertToModel.call(this, datum);
  const key = model[this.indexBy];
  if (!key) {
    throw new MissingIndexError(model, this.indexBy);
  }
  this.index[key] = model;
}
/**
 *
 * @ignore
 * @param {Object|Model} model
 * @returns {Model}
 */
function convertToModel(model) {
  if (!(model instanceof this.Model)) {
    model = new this.Model(model);
  }
  return model;
}
export default Collection;