Source: LinkedCollection.js

import {
  classCallCheck,
  extend,
  get,
  getSuper,
  isArray,
  isFunction,
  isObject,
  isUndefined,
  set
} from './utils'
import {
  belongsToType,
  hasManyType,
  hasOneType
} from './decorators'
import Collection from './Collection'

/**
 * TODO
 *
 * ```javascript
 * import {LinkedCollection} from 'js-data'
 * ```
 *
 * @class LinkedCollection
 * @extends Collection
 * @param {Array} [records] Initial set of records to insert into the
 * collection. See {@link Collection}.
 * @param {Object} [opts] Configuration options. See {@link Collection}.
 * @return {Mapper}
 */
const LinkedCollection = Collection.extend({
  constructor (records, opts) {
    const self = this
    classCallCheck(self, LinkedCollection)

    getSuper(self).call(self, records, opts)

    // Make sure this collection has somewhere to store "added" timestamps
    self._added = {}

    // Make sure this collection a reference to a datastore
    if (!self.datastore) {
      throw new Error('This collection must have a datastore!')
    }
    return self
  },

  add (records, opts) {
    const self = this
    const datastore = self.datastore
    const mapper = self.mapper
    const relationList = mapper.relationList || []
    const timestamp = new Date().getTime()
    const usesRecordClass = !!mapper.RecordClass
    let singular

    if (isObject(records) && !isArray(records)) {
      singular = true
      records = [records]
    }

    if (relationList.length && records.length) {
      // Check the currently visited record for relations that need to be
      // inserted into their respective collections.
      mapper.relationList.forEach(function (def) {
        const relationName = def.relation
        // A reference to the Mapper that this Mapper is related to
        const Relation = datastore.getMapper(relationName)
        // The field used by the related Mapper as the primary key
        const relationIdAttribute = Relation.idAttribute
        // Grab the foreign key in this relationship, if there is one
        const foreignKey = def.foreignKey
        const localField = def.localField
        // A lot of this is an optimization for being able to insert a lot of
        // data as quickly as possible
        const relatedCollection = datastore.getCollection(relationName)
        const type = def.type
        const isBelongsTo = type === belongsToType
        const isHasMany = type === hasManyType
        const isHasOne = type === hasOneType
        const idAttribute = mapper.idAttribute
        const shouldAdd = isUndefined(def.add) ? true : !!def.add
        let relatedData

        records.forEach(function (record) {
          // Grab a reference to the related data attached or linked to the
          // currently visited record
          relatedData = get(record, localField)

          if (isFunction(def.add)) {
            def.add(datastore, def, record)
          } else if (relatedData) {
            const id = get(record, idAttribute)
            // Otherwise, if there is something to be added, add it
            if (isHasMany) {
              // Handle inserting hasMany relations
              relatedData = relatedData.map(function (toInsertItem) {
                // Check that this item isn't the same item that is already in the
                // store
                if (toInsertItem !== relatedCollection.get(relatedCollection.recordId(toInsertItem))) {
                  // Make sure this item has its foreignKey
                  if (foreignKey) {
                    set(toInsertItem, foreignKey, id)
                  }
                  // Finally add this related item
                  if (shouldAdd) {
                    toInsertItem = relatedCollection.add(toInsertItem)
                  }
                }
                return toInsertItem
              })
              // If it's the parent that has the localKeys
              if (def.localKeys) {
                set(record, def.localKeys, relatedData.map(function (inserted) {
                  return get(inserted, relationIdAttribute)
                }))
              }
            } else {
              const relatedDataId = get(relatedData, relationIdAttribute)
              // Handle inserting belongsTo and hasOne relations
              if (relatedData !== relatedCollection.get(relatedDataId)) {
                // Make sure foreignKey field is set
                if (isBelongsTo) {
                  set(record, foreignKey, relatedDataId)
                } else if (isHasOne) {
                  set(relatedData, foreignKey, id)
                }
                // Finally insert this related item
                if (shouldAdd) {
                  relatedData = relatedCollection.add(relatedData)
                }
              }
            }
            set(record, localField, relatedData)
          }
        })
      })
    }

    records = getSuper(self).prototype.add.call(self, records, opts)

    records.forEach(function (record) {
      // Track when this record was added
      self._added[self.recordId(record)] = timestamp

      if (usesRecordClass) {
        record._set('$', timestamp)
      }
    })

    return singular ? records[0] : records
  },

  remove (id, opts) {
    const self = this
    delete self._added[id]
    const record = getSuper(self).prototype.remove.call(self, id, opts)
    if (record) {
      const mapper = self.mapper
      if (mapper.RecordClass) {
        record._set('$') // unset
      }
    }
    return record
  },

  removeAll (query, opts) {
    const self = this
    const records = getSuper(self).prototype.removeAll.call(self, query, opts)
    records.forEach(function (record) {
      delete self._added[self.recordId(record)]
    })
    return records
  }
})

/**
 * Create a LinkedCollection subclass.
 *
 * ```javascript
 * var MyLinkedCollection = LinkedCollection.extend({
 *   foo: function () { return 'bar' }
 * })
 * var collection = new MyLinkedCollection()
 * collection.foo() // "bar"
 * ```
 *
 * @name LinkedCollection.extend
 * @method
 * @param {Object} [props={}] Properties to add to the prototype of the
 * subclass.
 * @param {Object} [classProps={}] Static properties to add to the subclass.
 * @return {Function} Subclass of LinkedCollection.
 */
LinkedCollection.extend = extend

export {
  LinkedCollection as default
}