Source: Mapper.js

import * as utils from './utils'
import {
  belongsTo,
  hasMany,
  hasOne
} from './decorators'
import Record from './Record'
import Schema from './Schema'

const {
  resolve
} = utils

const notify = function (...args) {
  const self = this
  const opts = args.pop()
  self.dbg(opts.op, ...args)
  if (opts.notify || (opts.notify === undefined && self.notify)) {
    setTimeout(() => {
      self.emit(opts.op, ...args)
    })
  }
}

const MAPPER_DEFAULTS = {
  /**
   * Hash of registered adapters. Don't modify. Use {@link Mapper#registerAdapter}.
   *
   * @name Mapper#_adapters
   * @private
   */
  _adapters: null,

  /**
   * Hash of registered listeners. Don't modify. Use {@link Mapper#on} and
   * {@link Mapper#off}.
   *
   * @name Mapper#_listeners
   * @private
   */
  _listeners: null,

  /**
   * The name of the registered adapter that this Mapper should used by default.
   *
   * @name Mapper#defaultAdapter
   * @type {string}
   * @default "http"
   */
  defaultAdapter: 'http',

  /**
   * Whether to enable debug-level logs.
   *
   * @name Mapper#debug
   * @type {boolean}
   * @default false
   */
  debug: false,

  /**
   * The field used as the unique identifier on records handled by this Mapper.
   *
   * @name Mapper#idAttribute
   * @type {string}
   * @default id
   */
  idAttribute: 'id',

  /**
   * Minimum amount of meta information required to start operating against a
   * resource.
   *
   * @name Mapper#name
   * @type {string}
   */
  name: null,

  /**
   * Whether this Mapper should emit operational events.
   *
   * @name Mapper#notify
   * @type {boolean}
   * @default true
   */
  notify: true,

  /**
   * Whether {@link Mapper#create}, {@link Mapper#createMany}, {@link Mapper#save},
   * {@link Mapper#update}, {@link Mapper#updateAll}, {@link Mapper#updateMany},
   * {@link Mapper#find}, {@link Mapper#findAll}, {@link Mapper#destroy}, and
   * {@link Mapper#destroyAll} should return a raw result object that contains
   * both the instance data returned by the adapter _and_ metadata about the
   * operation.
   *
   * The default is to NOT return the result object, and instead return just the
   * instance data.
   *
   * @name Mapper#raw
   * @type {boolean}
   * @default false
   */
  raw: false,

  /**
   * Set the `false` to force the Mapper to work with POJO objects only.
   *
   * ```javascript
   * import {Mapper, Record} from 'js-data'
   * const UserMapper = new Mapper({ RecordClass: false })
   * UserMapper.RecordClass // false
   * const user = UserMapper#createRecord()
   * user instanceof Record // false
   * ```
   *
   * Set to a custom class to have records wrapped in your custom class.
   *
   * ```javascript
   * import {Mapper, Record} from 'js-data'
   *  // Custom class
   * class User {
   *   constructor (props = {}) {
   *     for (var key in props) {
   *       if (props.hasOwnProperty(key)) {
   *         this[key] = props[key]
   *       }
   *     }
   *   }
   * }
   * const UserMapper = new Mapper({ RecordClass: User })
   * UserMapper.RecordClass // function User() {}
   * const user = UserMapper#createRecord()
   * user instanceof Record // false
   * user instanceof User // true
   * ```
   *
   * Extend the {@link Record} class.
   *
   * ```javascript
   * import {Mapper, Record} from 'js-data'
   *  // Custom class
   * class User extends Record {
   *   constructor () {
   *     super(props)
   *   }
   * }
   * const UserMapper = new Mapper({ RecordClass: User })
   * UserMapper.RecordClass // function User() {}
   * const user = UserMapper#createRecord()
   * user instanceof Record // true
   * user instanceof User // true
   * ```
   *
   * @name Mapper#RecordClass
   * @default {@link Record}
   */
  RecordClass: undefined,

  schema: null,

  /**
   * Whether {@link Mapper#create} and {@link Mapper#createMany} should instead
   * call {@link Mapper#update} and {@link Mapper#updateMany} if the provided
   * record(s) already contain a primary key.
   *
   * @name Mapper#upsert
   * @type {boolean}
   * @default true
   */
  upsert: true
}

/**
 * ```javascript
 * import {Mapper} from 'js-data'
 * ```
 *
 * The core of JSData's [ORM/ODM][orm] implementation. Given a minimum amout of
 * meta information about a resource, a Mapper can perform generic CRUD
 * operations against that resource. Apart from its configuration, a Mapper is
 * stateless. The particulars of various persistence layers has been abstracted
 * into adapters, which a Mapper uses to perform its operations.
 *
 * The term "Mapper" comes from the [Data Mapper Pattern][pattern] described in
 * Martin Fowler's [Patterns of Enterprise Application Architecture][book]. A
 * Data Mapper moves data between [in-memory object instances][record] and a
 * relational or document-based database. JSData's Mapper can work with any
 * persistence layer you can write an adapter for.
 *
 * _("Model" is a heavily overloaded term and is avoided in this documentation
 * to prevent confusion.)_
 *
 * [orm]: https://en.wikipedia.org/wiki/Object-relational_mapping
 * [pattern]: https://en.wikipedia.org/wiki/Data_mapper_pattern
 * [book]: http://martinfowler.com/books/eaa.html
 * [record]: Record.html
 *
 * @class Mapper
 * @param {Object} [opts] Configuration options.
 */
export default function Mapper (opts) {
  const self = this
  utils.classCallCheck(self, Mapper)

  opts || (opts = {})
  utils.fillIn(self, opts)
  utils.fillIn(self, utils.copy(MAPPER_DEFAULTS))

  if (!self.name) {
    throw new Error('mapper cannot function without a name!')
  }

  self._adapters || (self._adapters = {})
  self._listeners || (self._listeners = {})

  if (!(self.schema instanceof Schema)) {
    self.schema = new Schema(self.schema || {})
  }

  if (utils.isUndefined(self.RecordClass)) {
    self.RecordClass = Record.extend()
  }
  if (self.RecordClass) {
    self.RecordClass.Mapper = self
  }
}

/**
 * Instance members
 */
utils.addHiddenPropsToTarget(Mapper.prototype, {
  /**
   * TODO
   *
   * @name Mapper#end
   * @method
   */
  end (data, opts) {
    const self = this
    if (opts.raw) {
      utils._(opts, data)
    }
    let _data = opts.raw ? data.data : data
    if (utils.isArray(_data)) {
      _data = _data.map(function (item) {
        return self.createRecord(item)
      })
    } else {
      _data = self.createRecord(_data)
    }
    if (opts.raw) {
      data.data = _data
    } else {
      data = _data
    }
    if (opts.notify) {
      setTimeout(function () {
        self.emit(opts.op, data, opts)
      })
    }
    return data
  },

  /**
   * Create an unsaved, uncached instance of this Mapper's
   * {@link Mapper#RecordClass}.
   *
   * Returns `props` if `props` is already an instance of
   * {@link Mapper#RecordClass}.
   *
   * @name Mapper#createRecord
   * @method
   * @param {Object} props The initial properties of the new unsaved record.
   * @param {Object} [opts] Configuration options.
   * @param {boolean} [opts.noValidate=false] Whether to skip validation on the
   * initial properties.
   * @return {Object} The unsaved record.
   */
  createRecord (props, opts) {
    const RecordClass = this.RecordClass
    // Check to make sure "props" is not already an instance of this Mapper.
    return RecordClass ? (props instanceof RecordClass ? props : new RecordClass(props, opts)) : props
  },

  /**
   * Return whether `record` is an instance of this Mappers's RecordClass.
   *
   * @name Mapper#is
   * @method
   * @param {Object} record The record to check.
   * @return {boolean} Whether `record` is an instance of this Mappers's
   * {@ link Mapper#RecordClass}.
   */
  is (record) {
    const RecordClass = this.RecordClass
    return RecordClass ? record instanceof RecordClass : false
  },

  /**
   * Return a plain object representation of the given record.
   *
   * @name Mapper#toJSON
   * @method
   * @param {Object} record Record from which to create a plain object
   * representation.
   * @param {Object} [opts] Configuration options.
   * @param {string[]} [opts.with] Array of relation names or relation fields
   * to include in the representation.
   * @return {Object} Plain object representation of the record.
   */
  toJSON (record, opts) {
    const self = this
    opts || (opts = {})
    let json = record
    if (self.is(record)) {
      json = utils.copy(record)
      // The user wants to include relations in the resulting plain object
      // representation
      if (self && self.relationList && opts.with) {
        if (utils.isString(opts.with)) {
          opts.with = [opts.with]
        }
        self.relationList.forEach(def => {
          let containedName
          if (opts.with.indexOf(def.relation) !== -1) {
            containedName = def.relation
          } else if (opts.with.indexOf(def.localField) !== -1) {
            containedName = def.localField
          }
          if (containedName) {
            const optsCopy = { with: opts.with.slice() }

            // Prepare to recurse into deeply nested relations
            optsCopy.with.splice(optsCopy.with.indexOf(containedName), 1)
            optsCopy.with.forEach((relation, i) => {
              if (relation && relation.indexOf(containedName) === 0 && relation.length >= containedName.length && relation[containedName.length] === '.') {
                optsCopy.with[i] = relation.substr(containedName.length + 1)
              } else {
                optsCopy.with[i] = ''
              }
            })
            const relationData = utils.get(record, def.localField)

            if (relationData) {
              // The actual recursion
              if (utils.isArray(relationData)) {
                utils.set(json, def.localField, relationData.map(item => def.getRelation().toJSON(item, optsCopy)))
              } else {
                utils.set(json, def.localField, def.getRelation().toJSON(relationData, optsCopy))
              }
            }
          }
        })
      }
    }
    return json
  },

  /**
   * Return the registered adapter with the given name or the default adapter if
   * no name is provided.
   *
   * @name Mapper#getAdapter
   * @method
   * @param {string} [name] The name of the adapter to retrieve.
   * @return {Adapter} The adapter.
   */
  getAdapter (name) {
    const self = this
    self.dbg('getAdapter', 'name:', name)
    const adapter = self.getAdapterName(name)
    if (!adapter) {
      throw new ReferenceError(`${adapter} not found!`)
    }
    return self.getAdapters()[adapter]
  },

  /**
   * Return the name of a registered adapter based on the given name or options,
   * or the name of the default adapter if no name provided.
   *
   * @name Mapper#getAdapterName
   * @method
   * @param {(Object|string)} [opts] The name of an adapter or options, if any.
   * @return {string} The name of the adapter.
   */
  getAdapterName (opts) {
    opts || (opts = {})
    if (utils.isString(opts)) {
      opts = { adapter: opts }
    }
    return opts.adapter || opts.defaultAdapter
  },

  /**
   * @name Mapper#getAdapters
   * @method
   */
  getAdapters () {
    return this._adapters
  },

  getSchema () {
    return this.schema
  },

  /**
   * Mapper lifecycle hook called by {@link Mapper#create}. If this method
   * returns a promise then {@link Mapper#create} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#beforeCreate
   * @method
   * @param {Object} props The `props` argument passed to {@link Mapper#create}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#create}.
   */
  beforeCreate: notify,

  checkUpsertCreate (props, opts) {
    const self = this
    return (opts.upsert || (opts.upsert === undefined && self.upsert)) &&
          utils.get(props, self.idAttribute)
  },

  /**
   * Create and save a new the record using the provided `props`.
   *
   * {@link Mapper#beforeCreate} will be called before calling the adapter.
   * {@link Mapper#afterCreate} will be called after calling the adapter.
   *
   * @name Mapper#create
   * @method
   * @param {Object} props The properties for the new record.
   * @param {Object} [opts] Configuration options.
   * @param {boolean} [opts.adapter={@link Mapper#defaultAdapter}] Name of the
   * adapter to use.
   * @param {boolean} [opts.notify={@link Mapper#notify}] Whether to emit
   * lifecycle events.
   * @param {boolean} [opts.raw={@link Mapper#raw}] If `false`, return the
   * created data. If `true` return a response object that includes the created
   * data and metadata about the operation.
   * @param {string[]} [opts.with=[]] Relations to create in a cascading
   * create if `props` contains nested relations. NOT performed in a transaction.
   * @return {Promise}
   */
  create (props, opts) {
    let op, adapter
    const self = this

    // Default values for arguments
    props || (props = {})
    opts || (opts = {})

    // Check whether we should do an upsert instead
    if (self.checkUpsertCreate(props, opts)) {
      return self.update(utils.get(props, self.idAttribute), props, opts)
    }

    // Fill in "opts" with the Mapper's configuration
    utils._(self, opts)
    adapter = opts.adapter = self.getAdapterName(opts)

    // beforeCreate lifecycle hook
    op = opts.op = 'beforeCreate'
    return resolve(self[op](props, opts)).then(function (_props) {
      // Allow for re-assignment from lifecycle hook
      props = _props || props
      // Now delegate to the adapter
      op = opts.op = 'create'
      const json = self.toJSON(props, opts)
      self.dbg(op, json, opts)
      return resolve(self.getAdapter(adapter)[op](self, json, opts))
    }).then(function (data) {
      // afterCreate lifecycle hook
      op = opts.op = 'afterCreate'
      return resolve(self[op](data, opts)).then(function (_data) {
        // Allow for re-assignment from lifecycle hook
        data = _data || data
        // Possibly formulate result object
        return self.end(data, opts)
      })
    })
  },

  /**
   * Mapper lifecycle hook called by {@link Mapper#create}. If this method
   * returns a promise then {@link Mapper#create} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#afterCreate
   * @method
   * @param {Object} data The `data` return by the adapter.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#create}.
   */
  afterCreate: notify,

  /**
   * Mapper lifecycle hook called by {@link Mapper#createMany}. If this method
   * returns a promise then {@link Mapper#createMany} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#beforeCreateMany
   * @method
   * @param {Array} records The `records` argument passed to {@link Mapper#createMany}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#createMany}.
   */
  beforeCreateMany: notify,

  checkUpsertCreateMany (records, opts) {
    const self = this
    if (opts.upsert || (opts.upsert === undefined && self.upsert)) {
      return records.reduce(function (hasId, item) {
        return hasId && utils.get(item, self.idAttribute)
      }, true)
    }
  },

  /**
   * Given an array of records, batch create them via an adapter.
   *
   * {@link Mapper#beforeCreateMany} will be called before calling the adapter.
   * {@link Mapper#afterCreateMany} will be called after calling the adapter.
   *
   * @name Mapper#createMany
   * @method
   * @param {Array} records Array of records to be created in one batch.
   * @param {Object} [opts] Configuration options.
   * @param {boolean} [opts.adapter={@link Mapper#defaultAdapter}] Name of the
   * adapter to use.
   * @param {boolean} [opts.notify={@link Mapper#notify}] Whether to emit
   * lifecycle events.
   * @param {boolean} [opts.raw={@link Mapper#raw}] If `false`, return the
   * updated data. If `true` return a response object that includes the updated
   * data and metadata about the operation.
   * @param {string[]} [opts.with=[]] Relations to create in a cascading create
   * if the records to be created have linked/nested relations. NOT performed
   * in a transaction.
   * @return {Promise}
   */
  createMany (records, opts) {
    let op, adapter
    const self = this

    // Default values for arguments
    records || (records = [])
    opts || (opts = {})

    // Check whether we should do an upsert instead
    if (self.checkUpsertCreateMany(records, opts)) {
      return self.updateMany(records, opts)
    }

    // Fill in "opts" with the Mapper's configuration
    utils._(self, opts)
    adapter = opts.adapter = self.getAdapterName(opts)

    // beforeCreateMany lifecycle hook
    op = opts.op = 'beforeCreateMany'
    return resolve(self[op](records, opts))
      .then(function (_records) {
        // Allow for re-assignment from lifecycle hook
        records = _records || records
        // Now delegate to the adapter
        op = opts.op = 'createMany'
        const json = records.map(function (item) {
          return self.toJSON(item, opts)
        })
        self.dbg(op, json, opts)
        return resolve(self.getAdapter(adapter)[op](self, json, opts))
      }).then(function (data) {
        // afterCreateMany lifecycle hook
        op = opts.op = 'afterCreateMany'
        return resolve(self[op](data, opts)).then(function (_data) {
          // Allow for re-assignment from lifecycle hook
          data = _data || data
          // Possibly inject result and/or formulate result object
          return self.end(data, opts)
        })
      })
  },

  /**
   * Mapper lifecycle hook called by {@link Mapper#createMany}. If this method
   * returns a promise then {@link Mapper#createMany} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#afterCreateMany
   * @method
   * @param {Array} records The `records` argument passed to {@link Mapper#createMany}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#createMany}.
   */
  afterCreateMany: notify,

  /**
   * Mappers lifecycle hook called by {@link Mapper#find}. If this method
   * returns a promise then {@link Mapper#find} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#beforeFind
   * @method
   * @param {(string|number)} id The `id` argument passed to {@link Mapper#find}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#find}.
   */
  beforeFind: notify,

  /**
   * Retrieve via an adapter the record with the given primary key.
   *
   * {@link Mapper#beforeFind} will be called before calling the adapter.
   * {@link Mapper#afterFind} will be called after calling the adapter.
   *
   * @name Mapper#find
   * @method
   * @param {(string|number)} id The primary key of the record to retrieve.
   * @param {Object} [opts] Configuration options.
   * @param {boolean} [opts.adapter={@link Mapper#defaultAdapter}] Name of the
   * adapter to use.
   * @param {boolean} [opts.notify={@link Mapper#notify}] Whether to emit
   * lifecycle events.
   * @param {boolean} [opts.raw={@link Mapper#raw}] If `false`, return the
   * updated data. If `true` return a response object that includes the updated
   * data and metadata about the operation.
   * @param {string[]} [opts.with=[]] Relations to eager load in the request.
   * @return {Promise}
   */
  find (id, opts) {
    let op, adapter
    const self = this

    // Default values for arguments
    opts || (opts = {})

    // Fill in "opts" with the Mappers's configuration
    utils._(self, opts)
    adapter = opts.adapter = self.getAdapterName(opts)

    // beforeFind lifecycle hook
    op = opts.op = 'beforeFind'
    return resolve(self[op](id, opts)).then(function (_id) {
      // Allow for re-assignment from lifecycle hook
      id = _id === undefined ? id : _id
      // Now delegate to the adapter
      op = opts.op = 'find'
      self.dbg(op, id, opts)
      return resolve(self.getAdapter(adapter)[op](self, id, opts))
    }).then(function (data) {
      // afterFind lifecycle hook
      op = opts.op = 'afterFind'
      return resolve(self[op](data, opts)).then(function (_data) {
        // Allow for re-assignment from lifecycle hook
        data = _data || data
        // Possibly inject result and/or formulate result object
        return self.end(data, opts)
      })
    })
  },

  /**
   * Mapper lifecycle hook called by {@link Mapper#find}. If this method
   * returns a promise then {@link Mapper#find} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#afterFind
   * @method
   * @param {(string|number)} id The `id` argument passed to {@link Mapper#find}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#find}.
   */
  afterFind: notify,

  /**
   * Mapper lifecycle hook called by {@link Mapper#findAll}. If this method
   * returns a promise then {@link Mapper#findAll} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#beforeFindAll
   * @method
   * @param {Object} query The `query` argument passed to {@link Mapper#findAll}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#findAll}.
   */
  beforeFindAll: notify,

  /**
   * Using the `query` argument, select records to pull from an adapter.
   * Expects back from the adapter the array of selected records.
   *
   * {@link Mapper#beforeFindAll} will be called before calling the adapter.
   * {@link Mapper#afterFindAll} will be called after calling the adapter.
   *
   * @name Mapper#findAll
   * @method
   * @param {Object} [query={}] Selection query.
   * @param {Object} [query.where] Filtering criteria.
   * @param {number} [query.skip] Number to skip.
   * @param {number} [query.limit] Number to limit to.
   * @param {Array} [query.orderBy] Sorting criteria.
   * @param {Object} [opts] Configuration options.
   * @param {boolean} [opts.adapter={@link Mapper#defaultAdapter}] Name of the
   * adapter to use.
   * @param {boolean} [opts.notify={@link Mapper#notify}] Whether to emit
   * lifecycle events.
   * @param {boolean} [opts.raw={@link Mapper#raw}] If `false`, return the
   * resulting data. If `true` return a response object that includes the
   * resulting data and metadata about the operation.
   * @param {string[]} [opts.with=[]] Relations to eager load in the request.
   * @return {Promise}
   */
  findAll (query, opts) {
    let op, adapter
    const self = this

    // Default values for arguments
    query || (query = {})
    opts || (opts = {})

    // Fill in "opts" with the Mapper's configuration
    utils._(self, opts)
    adapter = opts.adapter = self.getAdapterName(opts)

    // beforeFindAll lifecycle hook
    op = opts.op = 'beforeFindAll'
    return resolve(self[op](query, opts)).then(function (_query) {
      // Allow for re-assignment from lifecycle hook
      query = _query || query
      // Now delegate to the adapter
      op = opts.op = 'findAll'
      self.dbg(op, query, opts)
      return resolve(self.getAdapter(adapter)[op](self, query, opts))
    }).then(function (data) {
      // afterFindAll lifecycle hook
      op = opts.op = 'afterFindAll'
      return resolve(self[op](data, query, opts)).then(function (_data) {
        // Allow for re-assignment from lifecycle hook
        data = _data || data
        // Possibly inject result and/or formulate result object
        return self.end(data, opts)
      })
    })
  },

  /**
   * Mapper lifecycle hook called by {@link Mapper#findAll}. If this method
   * returns a promise then {@link Mapper#findAll} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#afterFindAll
   * @method
   * @param {Object} data The `data` returned by the adapter.
   * @param {Object} query The `query` argument passed to {@link Mapper#findAll}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#findAll}.
   */
  afterFindAll: notify,

  /**
   * Mapper lifecycle hook called by {@link Mapper#update}. If this method
   * returns a promise then {@link Mapper#update} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#beforeUpdate
   * @method
   * @param {(string|number)} id The `id` argument passed to {@link Mapper#update}.
   * @param {props} props The `props` argument passed to {@link Mapper#update}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#update}.
   */
  beforeUpdate: notify,

  /**
   * Using an adapter, update the record with the primary key specified by the
   * `id` argument.
   *
   * {@link Mapper#beforeUpdate} will be called before updating the record.
   * {@link Mapper#afterUpdate} will be called after updating the record.
   *
   * @name Mapper#update
   * @method
   * @param {(string|number)} id The primary key of the record to update.
   * @param {Object} props The update to apply to the record.
   * @param {Object} [opts] Configuration options.
   * @param {boolean} [opts.adapter={@link Mapper#defaultAdapter}] Name of the
   * adapter to use.
   * @param {boolean} [opts.notify={@link Mapper#notify}] Whether to emit
   * lifecycle events.
   * @param {boolean} [opts.raw={@link Mapper#raw}] If `false`, return the
   * updated data. If `true` return a response object that includes the updated
   * data and metadata about the operation.
   * @param {string[]} [opts.with=[]] Relations to update in a cascading
   * update if `props` contains nested updates to relations. NOT performed in a
   * transaction.
   * @return {Promise}
   */
  update (id, props, opts) {
    let op, adapter
    const self = this

    // Default values for arguments
    props || (props = {})
    opts || (opts = {})

    // Fill in "opts" with the Mapper's configuration
    utils._(self, opts)
    adapter = opts.adapter = self.getAdapterName(opts)

    // beforeUpdate lifecycle hook
    op = opts.op = 'beforeUpdate'
    return resolve(self[op](id, props, opts)).then(function (_props) {
      // Allow for re-assignment from lifecycle hook
      props = _props || props
      // Now delegate to the adapter
      op = opts.op = 'update'
      const json = self.toJSON(props, opts)
      self.dbg(op, id, json, opts)
      return resolve(self.getAdapter(adapter)[op](self, id, json, opts))
    }).then(function (data) {
      // afterUpdate lifecycle hook
      op = opts.op = 'afterUpdate'
      return resolve(self[op](id, data, opts)).then(function (_data) {
        // Allow for re-assignment from lifecycle hook
        data = _data || data
        // Possibly inject result and/or formulate result object
        return self.end(data, opts)
      })
    })
  },

  /**
   * Mapper lifecycle hook called by {@link Mapper#update}. If this method
   * returns a promise then {@link Mapper#update} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#afterUpdate
   * @method
   * @param {(string|number)} id The `id` argument passed to {@link Mapper#update}.
   * @param {props} props The `props` argument passed to {@link Mapper#update}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#update}.
   */
  afterUpdate: notify,

  /**
   * Mapper lifecycle hook called by {@link Mapper#updateMany}. If this method
   * returns a promise then {@link Mapper#updateMany} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#beforeUpdateMany
   * @method
   * @param {Array} records The `records` argument passed to {@link Mapper#updateMany}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#updateMany}.
   */
  beforeUpdateMany: notify,

  /**
   * Given an array of updates, perform each of the updates via an adapter. Each
   * "update" is a hash of properties with which to update an record. Each
   * update must contain the primary key to be updated.
   *
   * {@link Mapper#beforeUpdateMany} will be called before making the update.
   * {@link Mapper#afterUpdateMany} will be called after making the update.
   *
   * @name Mapper#updateMany
   * @method
   * @param {Array} records Array up record updates.
   * @param {Object} [opts] Configuration options.
   * @param {boolean} [opts.adapter={@link Mapper#defaultAdapter}] Name of the
   * adapter to use.
   * @param {boolean} [opts.notify={@link Mapper#notify}] Whether to emit
   * lifecycle events.
   * @param {boolean} [opts.raw={@link Mapper#raw}] If `false`, return the
   * updated data. If `true` return a response object that includes the updated
   * data and metadata about the operation.
   * @param {string[]} [opts.with=[]] Relations to update in a cascading
   * update if each record update contains nested updates for relations. NOT
   * performed in a transaction.
   * @return {Promise}
   */
  updateMany (records, opts) {
    let op, adapter
    const self = this

    // Default values for arguments
    records || (records = [])
    opts || (opts = {})

    // Fill in "opts" with the Mapper's configuration
    utils._(self, opts)
    adapter = opts.adapter = self.getAdapterName(opts)

    // beforeUpdateMany lifecycle hook
    op = opts.op = 'beforeUpdateMany'
    return resolve(self[op](records, opts)).then(function (_records) {
      // Allow for re-assignment from lifecycle hook
      records = _records || records
      // Now delegate to the adapter
      op = opts.op = 'updateMany'
      const json = records.map(function (item) {
        return self.toJSON(item, opts)
      })
      self.dbg(op, json, opts)
      return resolve(self.getAdapter(adapter)[op](self, json, opts))
    }).then(function (data) {
      // afterUpdateMany lifecycle hook
      op = opts.op = 'afterUpdateMany'
      return resolve(self[op](data, opts)).then(function (_data) {
        // Allow for re-assignment from lifecycle hook
        data = _data || data
        // Possibly inject result and/or formulate result object
        return self.end(data, opts)
      })
    })
  },

  /**
   * Mapper lifecycle hook called by {@link Mapper#updateMany}. If this method
   * returns a promise then {@link Mapper#updateMany} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#afterUpdateMany
   * @method
   * @param {Array} records The `records` argument passed to {@link Mapper#updateMany}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#updateMany}.
   */
  afterUpdateMany: notify,

  /**
   * Mapper lifecycle hook called by {@link Mapper#updateAll}. If this method
   * returns a promise then {@link Mapper#updateAll} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#beforeUpdateAll
   * @method
   * @param {Object} query The `query` argument passed to {@link Mapper#updateAll}.
   * @param {Object} props The `props` argument passed to {@link Mapper#updateAll}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#updateAll}.
   */
  beforeUpdateAll: notify,

  /**
   * Using the `query` argument, perform the a single updated to the selected
   * records. Expects back from the adapter an array of the updated records.
   *
   * {@link Mapper#beforeUpdateAll} will be called before making the update.
   * {@link Mapper#afterUpdateAll} will be called after making the update.
   *
   * @name Mapper#updateAll
   * @method
   * @param {Object} [query={}] Selection query.
   * @param {Object} [query.where] Filtering criteria.
   * @param {number} [query.skip] Number to skip.
   * @param {number} [query.limit] Number to limit to.
   * @param {Array} [query.orderBy] Sorting criteria.
   * @param {Object} props Update to apply to selected records.
   * @param {Object} [opts] Configuration options.
   * @param {boolean} [opts.adapter={@link Mapper#defaultAdapter}] Name of the
   * adapter to use.
   * @param {boolean} [opts.notify={@link Mapper#notify}] Whether to emit
   * lifecycle events.
   * @param {boolean} [opts.raw={@link Mapper#raw}] If `false`, return the
   * updated data. If `true` return a response object that includes the updated
   * data and metadata about the operation.
   * @param {string[]} [opts.with=[]] Relations to update in a cascading
   * update if `props` contains nested updates to relations. NOT performed in a
   * transaction.
   * @return {Promise}
   */
  updateAll (query, props, opts) {
    let op, adapter
    const self = this

    // Default values for arguments
    query || (query = {})
    props || (props = {})
    opts || (opts = {})

    // Fill in "opts" with the Mapper's configuration
    utils._(self, opts)
    adapter = opts.adapter = self.getAdapterName(opts)

    // beforeUpdateAll lifecycle hook
    op = opts.op = 'beforeUpdateAll'
    return resolve(self[op](query, props, opts)).then(function (_props) {
      // Allow for re-assignment from lifecycle hook
      props = _props || props
      // Now delegate to the adapter
      op = opts.op = 'updateAll'
      const json = self.toJSON(props, opts)
      self.dbg(op, query, json, opts)
      return resolve(self.getAdapter(adapter)[op](self, query, json, opts))
    }).then(function (data) {
      // afterUpdateAll lifecycle hook
      op = opts.op = 'afterUpdateAll'
      return resolve(self[op](query, data, opts)).then(function (_data) {
        // Allow for re-assignment from lifecycle hook
        data = _data || data
        // Possibly inject result and/or formulate result object
        return self.end(data, opts)
      })
    })
  },

  /**
   * Mapper lifecycle hook called by {@link Mapper#updateAll}. If this method
   * returns a promise then {@link Mapper#updateAll} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#afterUpdateAll
   * @method
   * @param {Object} query The `query` argument passed to {@link Mapper#updateAll}.
   * @param {Object} props The `props` argument passed to {@link Mapper#updateAll}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#updateAll}.
   */
  afterUpdateAll: notify,

  /**
   * Mapper lifecycle hook called by {@link Mapper#destroy}. If this method
   * returns a promise then {@link Mapper#destroy} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#beforeDestroy
   * @method
   * @param {(string|number)} id The `id` argument passed to {@link Mapper#destroy}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#destroy}.
   */
  beforeDestroy: notify,

  /**
   * Using an adapter, destroy the record with the primary key specified by the
   * `id` argument.
   *
   * {@link Mapper#beforeDestroy} will be called before destroying the record.
   * {@link Mapper#afterDestroy} will be called after destroying the record.
   *
   * @name Mapper#destroy
   * @method
   * @param {(string|number)} id The primary key of the record to destroy.
   * @param {Object} [opts] Configuration options.
   * @param {boolean} [opts.adapter={@link Mapper#defaultAdapter}] Name of the
   * adapter to use.
   * @param {boolean} [opts.notify={@link Mapper#notify}] Whether to emit
   * lifecycle events.
   * @param {boolean} [opts.raw={@link Mapper#raw}] If `false`, return the
   * ejected data (if any). If `true` return a response object that includes the
   * ejected data (if any) and metadata about the operation.
   * @param {string[]} [opts.with=[]] Relations to destroy in a cascading
   * delete. NOT performed in a transaction.
   * @return {Promise}
   */
  destroy (id, opts) {
    let op, adapter
    const self = this

    // Default values for arguments
    opts || (opts = {})

    // Fill in "opts" with the Mapper's configuration
    utils._(self, opts)
    adapter = opts.adapter = self.getAdapterName(opts)

    // beforeDestroy lifecycle hook
    op = opts.op = 'beforeDestroy'
    return resolve(self[op](id, opts)).then(function (_id) {
      // Allow for re-assignment from lifecycle hook
      id = _id === undefined ? id : _id
      // Now delegate to the adapter
      op = opts.op = 'destroy'
      self.dbg(op, id, opts)
      return resolve(self.getAdapter(adapter)[op](self, id, opts))
    }).then(function (data) {
      // afterDestroy lifecycle hook
      op = opts.op = 'afterDestroy'
      return resolve(self[op](data, opts)).then(function (_data) {
        // Allow for re-assignment from lifecycle hook
        data = _data || data
        if (opts.raw) {
          utils._(opts, data)
          return data
        }
        return data
      })
    })
  },

  /**
   * Mapper lifecycle hook called by {@link Mapper#destroy}. If this method
   * returns a promise then {@link Mapper#destroy} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#afterDestroy
   * @method
   * @param {(string|number)} id The `id` argument passed to {@link Mapper#destroy}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#destroy}.
   */
  afterDestroy: notify,

  /**
   * Mapper lifecycle hook called by {@link Mapper#destroyAll}. If this method
   * returns a promise then {@link Mapper#destroyAll} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#beforeDestroyAll
   * @method
   * @param {query} query The `query` argument passed to {@link Mapper#destroyAll}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#destroyAll}.
   */
  beforeDestroyAll: notify,

  /**
   * Using the `query` argument, destroy the selected records via an adapter.
   * If no `query` is provided then all records will be destroyed.
   *
   * {@link Mapper#beforeDestroyAll} will be called before destroying the records.
   * {@link Mapper#afterDestroyAll} will be called after destroying the records.
   *
   * @name Mapper#destroyAll
   * @method
   * @param {Object} [query={}] Selection query.
   * @param {Object} [query.where] Filtering criteria.
   * @param {number} [query.skip] Number to skip.
   * @param {number} [query.limit] Number to limit to.
   * @param {Array} [query.orderBy] Sorting criteria.
   * @param {Object} [opts] Configuration options.
   * @param {boolean} [opts.adapter={@link Mapper#defaultAdapter}] Name of the
   * adapter to use.
   * @param {boolean} [opts.notify={@link Mapper#notify}] Whether to emit
   * lifecycle events.
   * @param {boolean} [opts.raw={@link Mapper#raw}] If `false`, return the
   * ejected data (if any). If `true` return a response object that includes the
   * ejected data (if any) and metadata about the operation.
   * @param {string[]} [opts.with=[]] Relations to destroy in a cascading
   * delete. NOT performed in a transaction.
   * @return {Promise}
   */
  destroyAll (query, opts) {
    let op, adapter
    const self = this

    // Default values for arguments
    query || (query = {})
    opts || (opts = {})

    // Fill in "opts" with the Mapper's configuration
    utils._(self, opts)
    adapter = opts.adapter = self.getAdapterName(opts)

    // beforeDestroyAll lifecycle hook
    op = opts.op = 'beforeDestroyAll'
    return resolve(self[op](query, opts)).then(function (_query) {
      // Allow for re-assignment from lifecycle hook
      query = _query || query
      // Now delegate to the adapter
      op = opts.op = 'destroyAll'
      self.dbg(op, query, opts)
      return resolve(self.getAdapter(adapter)[op](self, query, opts))
    }).then(function (data) {
      // afterDestroyAll lifecycle hook
      op = opts.op = 'afterDestroyAll'
      return resolve(self[op](data, query, opts)).then(function (_data) {
        // Allow for re-assignment from lifecycle hook
        data = _data || data
        if (opts.raw) {
          utils._(opts, data)
          return data
        }
        return data
      })
    })
  },

  /**
   * Mapper lifecycle hook called by {@link Mapper#destroyAll}. If this method
   * returns a promise then {@link Mapper#destroyAll} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#afterDestroyAll
   * @method
   * @param {*} data The `data` returned by the adapter.
   * @param {query} query The `query` argument passed to {@link Mapper#destroyAll}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#destroyAll}.
   */
  afterDestroyAll: notify,

  /**
   * @name Mapper#log
   * @method
   */
  log (level, ...args) {
    if (level && !args.length) {
      args.push(level)
      level = 'debug'
    }
    if (level === 'debug' && !this.debug) {
      return
    }
    const prefix = `${level.toUpperCase()}: (${this.name || 'mapper'})`
    if (console[level]) {
      console[level](prefix, ...args)
    } else {
      console.log(prefix, ...args)
    }
  },

  /**
   * @name Mapper#dbg
   * @method
   */
  dbg (...args) {
    this.log('debug', ...args)
  },

  /**
   * Usage:
   *
   * Post.belongsTo(User, {
   *   localKey: 'myUserId'
   * })
   *
   * Comment.belongsTo(User)
   * Comment.belongsTo(Post, {
   *   localField: '_post'
   * })
   *
   * @name Mapper#belongsTo
   * @method
   */
  belongsTo (RelatedMapper, opts) {
    return belongsTo(RelatedMapper, opts)(this)
  },

  /**
   * Usage:
   *
   * User.hasMany(Post, {
   *   localField: 'my_posts'
   * })
   *
   * @name Mapper#hasMany
   * @method
   */
  hasMany (RelatedMapper, opts) {
    return hasMany(RelatedMapper, opts)(this)
  },

  /**
   * Usage:
   *
   * User.hasOne(Profile, {
   *   localField: '_profile'
   * })
   *
   * @name Mapper#hasOne
   * @method
   */
  hasOne (RelatedMapper, opts) {
    return hasOne(RelatedMapper, opts)(this)
  },

  /**
   * Register an adapter on this mapper under the given name.
   *
   * @name Mapper#registerAdapter
   * @method
   * @param {string} name The name of the adapter to register.
   * @param {Adapter} adapter The adapter to register.
   * @param {Object} [opts] Configuration options.
   * @param {boolean} [opts.default=false] Whether to make the adapter the
   * default adapter for this Mapper.
   */
  registerAdapter (name, adapter, opts) {
    const self = this
    opts || (opts = {})
    self.getAdapters()[name] = adapter
    // Optionally make it the default adapter for the target.
    if (opts === true || opts.default) {
      self.defaultAdapter = name
    }
  }
})

/**
 * Create a Mapper subclass.
 *
 * ```javascript
 * var MyMapper = Mapper.extend({
 *   foo: function () { return 'bar' }
 * })
 * var mapper = new MyMapper()
 * mapper.foo() // "bar"
 * ```
 *
 * @name Mapper.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 Mapper.
 */
Mapper.extend = utils.extend

/**
 * Register a new event listener on this Mapper.
 *
 * @name Mapper#on
 * @method
 */

/**
 * Remove an event listener from this Mapper.
 *
 * @name Mapper#off
 * @method
 */

/**
 * Trigger an event on this Mapper.
 *
 * @name Mapper#emit
 * @method
 * @param {string} event Name of event to emit.
 */

/**
 * A Mapper's registered listeners are stored at {@link Mapper#_listeners}.
 */
utils.eventify(
  Mapper.prototype,
  function () {
    return this._listeners
  },
  function (value) {
    this._listeners = value
  }
)