Source: Mapper.js

import utils from './utils'
import Component from './Component'
import Record from './Record'
import Schema from './Schema'
import {
  belongsTo,
  belongsToType,
  hasMany,
  hasManyType,
  hasOne,
  hasOneType
} from './decorators'

const DOMAIN = 'Mapper'

const makeNotify = function (num) {
  return function (...args) {
    const self = this
    const opts = args[args.length - num]
    self.dbg(opts.op, ...args)
    if (opts.notify || (opts.notify === undefined && self.notify)) {
      setTimeout(() => {
        self.emit(opts.op, ...args)
      })
    }
  }
}

// These are the default implementations of all of the lifecycle hooks
const notify = makeNotify(1)
const notify2 = makeNotify(2)

// This object provides meta information used by Mapper#crud to actually
// execute each lifecycle method
const LIFECYCLE_METHODS = {
  count: {
    defaults: [{}, {}],
    skip: true,
    types: []
  },
  destroy: {
    defaults: [{}, {}],
    skip: true,
    types: []
  },
  destroyAll: {
    defaults: [{}, {}],
    skip: true,
    types: []
  },
  find: {
    defaults: [undefined, {}],
    types: []
  },
  findAll: {
    defaults: [{}, {}],
    types: []
  },
  sum: {
    defaults: [undefined, {}, {}],
    skip: true,
    types: []
  },
  update: {
    adapterArgs (mapper, id, props, opts) {
      return [id, mapper.toJSON(props, opts), opts]
    },
    beforeAssign: 1,
    defaults: [undefined, {}, {}],
    types: []
  },
  updateAll: {
    adapterArgs (mapper, props, query, opts) {
      return [mapper.toJSON(props, opts), query, opts]
    },
    beforeAssign: 0,
    defaults: [{}, {}, {}],
    types: []
  },
  updateMany: {
    adapterArgs (mapper, records, opts) {
      return [records.map(function (record) {
        return mapper.toJSON(record, opts)
      }), opts]
    },
    beforeAssign: 0,
    defaults: [[], {}],
    types: []
  }
}

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

  /**
   * Whether to augment {@link Mapper#recordClass} with getter/setter property
   * accessors according to the properties defined in {@link Mapper#schema}.
   * This makes possible validation and change tracking on individual properties
   * when using the dot (e.g. `user.name = "Bob"`) operator to modify a
   * property.
   *
   * @name Mapper#applySchema
   * @type {boolean}
   * @default true
   */
  applySchema: true,

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

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

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

  /**
   * Whether this Mapper should emit operational events.
   *
   * Defaults to `true` in the browser and `false` in Node.js
   *
   * @name Mapper#notify
   * @type {boolean}
   */
  notify: utils.isBrowser,

  /**
   * 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
}

/**
 * ```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
 * @extends Component
 * @param {Object} opts Configuration options.
 * @param {boolean} [opts.applySchema=true] Whether to apply this Mapper's
 * {@link Schema} to the prototype of this Mapper's Record class. The enables
 * features like active change detection, validation during use of the
 * assignment operator, etc.
 * @param {boolean} [opts.debug=false] Wether to log debugging information
 * during operation.
 * @param {string} [opts.defaultAdapter=http] The name of the adapter to use by
 * default.
 * @param {string} [opts.idAttribute=id] The field that uniquely identifies
 * Records that this Mapper will be dealing with. Typically called a primary
 * key.
 * @param {string} opts.name The name for this Mapper. This is the minimum
 * amount of meta information required for a Mapper to be able to execute CRUD
 * operations for a "Resource".
 * @param {boolean} [opts.notify] Whether to emit lifecycle events.
 * @param {boolean} [opts.raw=false] Whether lifecycle methods should return a
 * more detailed reponse object instead of just a Record instance or Record
 * instances.
 */
export default Component.extend({
  constructor: function Mapper (opts) {
    const self = this
    utils.classCallCheck(self, Mapper)
    Mapper.__super__.call(self)
    opts || (opts = {})

    // Prepare certain properties to be non-enumerable
    Object.defineProperties(self, {
      _adapters: {
        value: undefined,
        writable: true
      },

      /**
       * 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: {
        value: undefined,
        writable: true
      },

      lifecycleMethods: {
        value: LIFECYCLE_METHODS
      },

      schema: {
        value: undefined,
        writable: true
      }
    })

    // Apply user-provided configuration
    utils.fillIn(self, opts)
    // Fill in any missing options with the defaults
    utils.fillIn(self, utils.copy(MAPPER_DEFAULTS))

    /**
     * Minimum amount of meta information required for a Mapper to be able to
     * execute CRUD operations for a "Resource".
     *
     * @name Mapper#name
     * @type {string}
     */
    if (!self.name) {
      throw utils.err(`new ${DOMAIN}`, 'opts.name')(400, 'string', self.name)
    }

    // Setup schema, with an empty default schema if necessary
    if (!(self.schema instanceof Schema)) {
      self.schema = new Schema(self.schema || {})
    }

    // Create a subclass of Record that's tied to this Mapper
    if (utils.isUndefined(self.recordClass)) {
      const superClass = Record
      self.recordClass = superClass.extend({
        constructor: (function () {
          var subClass = function Record (props, opts) {
            utils.classCallCheck(this, subClass)
            superClass.call(this, props, opts)
          }
          return subClass
        })()
      })
    }

    if (self.recordClass) {
      self.recordClass.mapper = self

      // We can only apply the schema to the prototype of self.recordClass if the
      // class extends Record
      if (utils.getSuper(self.recordClass, true) === Record && self.schema && self.schema.apply && self.applySchema) {
        self.schema.apply(self.recordClass.prototype)
      }
    }
  },

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

  /**
   * 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} props The `props` argument passed to {@link Mapper#create}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#create}.
   * @param {*} result The result, if any.
   */
  afterCreate: notify2,

  /**
   * 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}.
   * @param {*} result The result, if any.
   */
  afterCreateMany: notify2,

  /**
   * 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}.
   * @param {*} result The result, if any.
   */
  afterDestroy: notify2,

  /**
   * 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}.
   * @param {*} result The result, if any.
   */
  afterDestroyAll: notify2,

  /**
   * 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}.
   * @param {*} result The result, if any.
   */
  afterFind: notify2,

  /**
   * 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} query The `query` argument passed to {@link Mapper#findAll}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#findAll}.
   * @param {*} result The result, if any.
   */
  afterFindAll: notify2,

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

  /**
   * 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}.
   * @param {*} result The result, if any.
   */
  afterUpdate: notify2,

  /**
   * 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} props The `props` argument passed to {@link Mapper#updateAll}.
   * @param {Object} query The `query` argument passed to {@link Mapper#updateAll}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#updateAll}.
   * @param {*} result The result, if any.
   */
  afterUpdateAll: notify2,

  /**
   * 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}.
   * @param {*} result The result, if any.
   */
  afterUpdateMany: notify2,

  /**
   * 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,

  /**
   * 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,

  /**
   * Mapper lifecycle hook called by {@link Mapper#count}. If this method
   * returns a promise then {@link Mapper#count} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#beforeCount
   * @method
   * @param {Object} query The `query` argument passed to {@link Mapper#count}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#count}.
   */
  beforeCount: 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,

  /**
   * 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,

  /**
   * 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,

  /**
   * 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,

  /**
   * Mapper lifecycle hook called by {@link Mapper#sum}. If this method
   * returns a promise then {@link Mapper#sum} will wait for the promise
   * to resolve before continuing.
   *
   * @name Mapper#beforeSum
   * @method
   * @param {string} field The `field` argument passed to {@link Mapper#sum}.
   * @param {Object} query The `query` argument passed to {@link Mapper#sum}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#sum}.
   */
  beforeSum: 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,

  /**
   * 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} props The `props` argument passed to {@link Mapper#updateAll}.
   * @param {Object} query The `query` argument passed to {@link Mapper#updateAll}.
   * @param {Object} opts The `opts` argument passed to {@link Mapper#updateAll}.
   */
  beforeUpdateAll: 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,

  /**
   * This method is called at the end of most lifecycle methods. It does the
   * following:
   *
   * 1. If `opts.raw` is `true`, add this Mapper's configuration to the `opts`
   * argument as metadata for the operation.
   * 2. Wrap the result data appropriately using {@link Mapper#wrap}, which
   * calls {@link Mapper#createRecord}.
   *
   * @name Mapper#_end
   * @method
   * @private
   */
  _end (result, opts, skip) {
    const self = this
    if (opts.raw) {
      utils._(result, opts)
    }
    if (skip) {
      return result
    }
    let _data = opts.raw ? result.data : result
    if (_data && utils.isFunction(self.wrap)) {
      _data = self.wrap(_data, opts)
      if (opts.raw) {
        result.data = _data
      } else {
        result = _data
      }
    }
    return result
  },

  /**
   * Define a belongsTo relationship. Only useful if you're managing your
   * Mappers manually and not using a Container or DataStore component.
   *
   * ```
   * 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)
  },

  /**
   * Using the `query` argument, select records to pull from an adapter.
   * Expects back from the adapter the array of selected records.
   *
   * {@link Mapper#beforeCount} will be called before calling the adapter.
   * {@link Mapper#afterCount} will be called after calling the adapter.
   *
   * @name Mapper#count
   * @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.
   * @return {Promise}
   */
  count (query, opts) {
    return this.crud('count', query, opts)
  },

  /**
   * 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. Each nested create will result in another {@link Mapper#create}
   * or {@link Mapper#createMany} call.
   * @param {string[]} [opts.pass=[]] Relations to send to the adapter as part
   * of the payload. Normally relations are not sent.
   * @return {Promise}
   */
  create (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._(opts, self)
    adapter = opts.adapter = self.getAdapterName(opts)

    // beforeCreate lifecycle hook
    op = opts.op = 'beforeCreate'
    return utils.resolve(self[op](props, opts)).then(function (_props) {
      // Allow for re-assignment from lifecycle hook
      props = utils.isUndefined(_props) ? props : _props

      // Deep pre-create belongsTo relations
      const belongsToRelationData = {}
      opts.with || (opts.with = [])
      let tasks = []
      utils.forEachRelation(self, opts, function (def, optsCopy) {
        const relationData = def.getLocalField(props)
        const relatedMapper = def.getRelation()
        const relatedIdAttribute = relatedMapper.idAttribute
        optsCopy.raw = false
        if (!relationData) {
          return
        }
        if (def.type === belongsToType) {
          // Create belongsTo relation first because we need a generated id to
          // attach to the child
          tasks.push(relatedMapper.create(relationData, optsCopy).then(function (data) {
            def.setLocalField(belongsToRelationData, data)
            def.setForeignKey(props, data)
          }))
        } else if (def.type === hasManyType && def.localKeys) {
          // Create his hasMany relation first because it uses localKeys
          tasks.push(relatedMapper.createMany(relationData, optsCopy).then(function (data) {
            def.setLocalField(belongsToRelationData, data)
            utils.set(props, def.localKeys, data.map(function (record) {
              return utils.get(record, relatedIdAttribute)
            }))
          }))
        }
      })
      return utils.Promise.all(tasks).then(function () {
        // Now delegate to the adapter for the main create
        op = opts.op = 'create'
        self.dbg(op, props, opts)
        return utils.resolve(self.getAdapter(adapter)[op](self, self.toJSON(props, { with: opts.pass || [] }), opts))
      }).then(function (data) {
        const createdRecord = opts.raw ? data.data : data
        // Deep post-create hasMany and hasOne relations
        tasks = []
        utils.forEachRelation(self, opts, function (def, optsCopy) {
          const relationData = def.getLocalField(props)
          if (!relationData) {
            return
          }
          optsCopy.raw = false
          let task
          // Create hasMany and hasOne after the main create because we needed
          // a generated id to attach to these items
          if (def.type === hasManyType && def.foreignKey) {
            def.setForeignKey(createdRecord, relationData)
            task = def.getRelation().createMany(relationData, optsCopy).then(function (data) {
              def.setLocalField(createdRecord, data)
            })
          } else if (def.type === hasOneType) {
            def.setForeignKey(createdRecord, relationData)
            task = def.getRelation().create(relationData, optsCopy).then(function (data) {
              def.setLocalField(createdRecord, data)
            })
          } else if (def.type === belongsToType && def.getLocalField(belongsToRelationData)) {
            def.setLocalField(createdRecord, def.getLocalField(belongsToRelationData))
          } else if (def.type === hasManyType && def.localKeys && def.getLocalField(belongsToRelationData)) {
            def.setLocalField(createdRecord, def.getLocalField(belongsToRelationData))
          }
          if (task) {
            tasks.push(task)
          }
        })
        return utils.Promise.all(tasks).then(function () {
          return data
        })
      })
    }).then(function (result) {
      result = self._end(result, opts)
      // afterCreate lifecycle hook
      op = opts.op = 'afterCreate'
      return utils.resolve(self[op](props, opts, result)).then(function (_result) {
        // Allow for re-assignment from lifecycle hook
        return utils.isUndefined(_result) ? result : _result
      })
    })
  },

  createInstance (props, opts) {
    return this.createRecord(props, opts)
  },

  /**
   * 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 `records` contains nested relations. NOT performed in a
   * transaction. Each nested create will result in another {@link Mapper#createMany}
   * call.
   * @param {string[]} [opts.pass=[]] Relations to send to the adapter as part
   * of the payload. Normally relations are not sent.
   * @return {Promise}
   */
  createMany (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._(opts, self)
    adapter = opts.adapter = self.getAdapterName(opts)

    // beforeCreateMany lifecycle hook
    op = opts.op = 'beforeCreateMany'
    return utils.resolve(self[op](records, opts)).then(function (_records) {
      // Allow for re-assignment from lifecycle hook
      records = utils.isUndefined(_records) ? records : _records

      // Deep pre-create belongsTo relations
      const belongsToRelationData = {}
      opts.with || (opts.with = [])
      let tasks = []
      utils.forEachRelation(self, opts, function (def, optsCopy) {
        const relationData = records.map(function (record) {
          return def.getLocalField(record)
        }).filter(function (relatedRecord) {
          return relatedRecord
        })
        if (def.type === belongsToType && relationData.length === records.length) {
          // Create belongsTo relation first because we need a generated id to
          // attach to the child
          tasks.push(def.getRelation().createMany(relationData, optsCopy).then(function (data) {
            const relatedRecords = optsCopy.raw ? data.data : data
            def.setLocalField(belongsToRelationData, relatedRecords)
            records.forEach(function (record, i) {
              def.setForeignKey(record, relatedRecords[i])
            })
          }))
        }
      })
      return utils.Promise.all(tasks).then(function () {
        // Now delegate to the adapter
        op = opts.op = 'createMany'
        const json = records.map(function (record) {
          return self.toJSON(record, { with: opts.pass || [] })
        })
        self.dbg(op, records, opts)
        return utils.resolve(self.getAdapter(adapter)[op](self, json, opts))
      }).then(function (data) {
        const createdRecords = opts.raw ? data.data : data

        // Deep post-create hasOne relations
        tasks = []
        utils.forEachRelation(self, opts, function (def, optsCopy) {
          const relationData = records.map(function (record) {
            return def.getLocalField(record)
          }).filter(function (relatedRecord) {
            return relatedRecord
          })
          if (relationData.length !== records.length) {
            return
          }
          const belongsToData = def.getLocalField(belongsToRelationData)
          let task
          // Create hasMany and hasOne after the main create because we needed
          // a generated id to attach to these items
          if (def.type === hasManyType) {
            // Not supported
            self.log('warn', 'deep createMany of hasMany type not supported!')
          } else if (def.type === hasOneType) {
            createdRecords.forEach(function (createdRecord, i) {
              def.setForeignKey(createdRecord, relationData[i])
            })
            task = def.getRelation().createMany(relationData, optsCopy).then(function (data) {
              const relatedData = opts.raw ? data.data : data
              createdRecords.forEach(function (createdRecord, i) {
                def.setLocalField(createdRecord, relatedData[i])
              })
            })
          } else if (def.type === belongsToType && belongsToData && belongsToData.length === createdRecords.length) {
            createdRecords.forEach(function (createdRecord, i) {
              def.setLocalField(createdRecord, belongsToData[i])
            })
          }
          if (task) {
            tasks.push(task)
          }
        })
        return utils.Promise.all(tasks).then(function () {
          return data
        })
      })
    }).then(function (result) {
      result = self._end(result, opts)
      // afterCreateMany lifecycle hook
      op = opts.op = 'afterCreateMany'
      return utils.resolve(self[op](records, opts, result)).then(function (_result) {
        // Allow for re-assignment from lifecycle hook
        return utils.isUndefined(_result) ? result : _result
      })
    })
  },

  /**
   * 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|Array} props The properties for the Record instance or an
   * array of property objects for the Record instances.
   * @param {Object} [opts] Configuration options.
   * @param {boolean} [opts.noValidate=false] Whether to skip validation when
   * the Record instances are created.
   * @return {Object|Array} The Record instance or Record instances.
   */
  createRecord (props, opts) {
    props || (props = {})
    const self = this
    if (utils.isArray(props)) {
      return props.map(function (_props) {
        return self.createRecord(_props, opts)
      })
    }
    if (!utils.isObject(props)) {
      throw utils.err(`${DOMAIN}#createRecord`, 'props')(400, 'array or object', props)
    }
    const recordClass = self.recordClass
    const relationList = self.relationList || []
    relationList.forEach(function (def) {
      const relatedMapper = def.getRelation()
      const relationData = def.getLocalField(props)
      if (relationData && !relatedMapper.is(relationData)) {
        if (utils.isArray(relationData) && (!relationData.length || relatedMapper.is(relationData[0]))) {
          return
        }
        utils.set(props, def.localField, relatedMapper.createRecord(relationData, opts))
      }
    })
    // Check to make sure "props" is not already an instance of this Mapper.
    return recordClass ? (props instanceof recordClass ? props : new recordClass(props, opts)) : props // eslint-disable-line
  },

  /**
   * Lifecycle invocation method.
   *
   * @name Mapper#crud
   * @method
   * @param {string} method Name of the lifecycle method to invoke.
   * @param {...*} args Arguments to pass to the lifecycle method.
   * @return {Promise}
   */
  crud (method, ...args) {
    const self = this
    const config = self.lifecycleMethods[method]
    if (!config) {
      throw utils.err(`${DOMAIN}#crud`, method)(404, 'method')
    }

    const upper = `${method.charAt(0).toUpperCase()}${method.substr(1)}`
    const before = `before${upper}`
    const after = `after${upper}`

    let op, adapter

    // Default values for arguments
    config.defaults.forEach(function (value, i) {
      if (utils.isUndefined(args[i])) {
        args[i] = utils.copy(value)
      }
    })

    const opts = args[args.length - 1]

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

    // before lifecycle hook
    op = opts.op = before
    return utils.resolve(self[op](...args)).then(function (_value) {
      if (!utils.isUndefined(config.beforeAssign)) {
        // Allow for re-assignment from lifecycle hook
        args[config.beforeAssign] = utils.isUndefined(_value) ? args[config.beforeAssign] : _value
      }
      // Now delegate to the adapter
      op = opts.op = method
      args = config.adapterArgs ? config.adapterArgs(self, ...args) : args
      self.dbg(op, ...args)
      return utils.resolve(self.getAdapter(adapter)[op](self, ...args))
    }).then(function (result) {
      result = self._end(result, opts, !!config.skip)
      args.push(result)
      // after lifecycle hook
      op = opts.op = after
      return utils.resolve(self[op](...args)).then(function (_result) {
        // Allow for re-assignment from lifecycle hook
        return utils.isUndefined(_result) ? result : _result
      })
    })
  },

  /**
   * 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) {
    return this.crud('destroy', id, opts)
  },

  /**
   * 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) {
    return this.crud('destroyAll', query, opts)
  },

  /**
   * 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) {
    return this.crud('find', id, opts)
  },

  /**
   * 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) {
    return this.crud('findAll', query, opts)
  },

  /**
   * 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 utils.err(`${DOMAIN}#getAdapter`, 'name')(400, 'string', name)
    }
    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
   * @return {Object} This Mapper's adapters
   */
  getAdapters () {
    return this._adapters
  },

  /**
   * Returns this Mapper's schema.
   *
   * @return {Schema} This Mapper's schema.
   */
  getSchema () {
    return this.schema
  },

  /**
   * Defines a hasMany relationship. Only useful if you're managing your
   * Mappers manually and not using a Container or DataStore component.
   *
   * ```
   * User.hasMany(Post, {
   *   localField: 'my_posts'
   * })
   * ```
   *
   * @name Mapper#hasMany
   * @method
   */
  hasMany (relatedMapper, opts) {
    return hasMany(relatedMapper, opts)(this)
  },

  /**
   * Defines a hasOne relationship. Only useful if you're managing your
   * Mappers manually and not using a Container or DataStore component.
   *
   * ```
   * User.hasOne(Profile, {
   *   localField: '_profile'
   * })
   * ```
   *
   * @name Mapper#hasOne
   * @method
   */
  hasOne (relatedMapper, opts) {
    return hasOne(relatedMapper, opts)(this)
  },

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

  /**
   * 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
    }
  },

  /**
   * Using the `query` argument, select records to pull from an adapter.
   * Expects back from the adapter the array of selected records.
   *
   * {@link Mapper#beforeSum} will be called before calling the adapter.
   * {@link Mapper#afterSum} will be called after calling the adapter.
   *
   * @name Mapper#sum
   * @method
   * @param {string} field The field to sum.
   * @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.
   * @return {Promise}
   */
  sum (field, query, opts) {
    return this.crud('sum', field, query, opts)
  },

  /**
   * 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 = {})
    const relationFields = (self ? self.relationFields : []) || []
    let json = {}
    let properties
    if (self && self.schema) {
      properties = self.schema.properties || {}
      // TODO: Make this work recursively
      utils.forOwn(properties, function (opts, prop) {
        json[prop] = utils.plainCopy(record[prop])
      })
    }
    properties || (properties = {})
    if (!opts.strict) {
      for (var key in record) {
        if (!properties[key] && relationFields.indexOf(key) === -1) {
          json[key] = utils.plainCopy(record[key])
        }
      }
    }
    // The user wants to include relations in the resulting plain object
    // representation
    if (self && opts.withAll) {
      opts.with = relationFields.slice()
    }
    if (self && opts.with) {
      if (utils.isString(opts.with)) {
        opts.with = [opts.with]
      }
      utils.forEachRelation(self, opts, function (def, optsCopy) {
        const relationData = def.getLocalField(record)
        if (relationData) {
          // The actual recursion
          if (utils.isArray(relationData)) {
            def.setLocalField(json, relationData.map(function (item) {
              return def.getRelation().toJSON(item, optsCopy)
            }))
          } else {
            def.setLocalField(json, def.getRelation().toJSON(relationData, optsCopy))
          }
        }
      })
    }
    return json
  },

  /**
   * 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) {
    return this.crud('update', id, props, opts)
  },

  /**
   * 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} props Update to apply to selected records.
   * @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
   * 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 (props, query, opts) {
    return this.crud('updateAll', props, query, opts)
  },

  /**
   * 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) {
    return this.crud('updateMany', records, opts)
  },

  /**
   * Validate the given record or records according to this Mapper's
   * {@link Schema}. No return value means no errors.
   *
   * @name Mapper#validate
   * @method
   * @param {Object|Array} record The record or records to validate.
   * @param {Object} [opts] Configuration options. Passed to
   * {@link Schema#validate}.
   * @return {Array} Array of errors or undefined if no errors.
   */
  validate (record, opts) {
    const self = this
    const schema = self.getSchema()
    if (utils.isArray(record)) {
      const errors = record.map(function (_record) {
        return schema.validate(_record, opts)
      })
      let hasErrors = false
      errors.forEach(function (err) {
        if (err) {
          hasErrors = true
        }
      })
      if (hasErrors) {
        return errors
      }
      return undefined
    }
    return schema.validate(record, opts)
  },

  /**
   * Method used to wrap data returned by an adapter with this Mapper's Record
   * class.
   *
   * @name Mapper#wrap
   * @method
   * @param {Object|Array} data The data to be wrapped.
   * @param {Object} [opts] Configuration options. Passed to {@link Mapper#createRecord}.
   * @return {Object|Array}
   */
  wrap (data, opts) {
    return this.createRecord(data, opts)
  }
})