lib/base/model.js

// Base Model
// ---------------
'use strict';

const _ = require('lodash');
const inherits = require('util').inherits;
const Events = require('./events');
const constants = require('../constants');

/**
 * @class
 * @classdesc
 * @extends Events
 * @inheritdoc
 * @description
 *
 * The "ModelBase" is similar to the 'Active Model' in Rails, it defines a
 * standard interface from which other objects may inherit.
 */
function ModelBase(attributes, options) {
  let attrs = attributes || {};
  options = options || {};
  this.attributes = Object.create(null);
  this._previousAttributes = {};
  this._reset();
  this.relations = {};
  this.cid = _.uniqueId('c');

  if (options.parse) attrs = this.parse(attrs, options) || {};
  if (options.visible) this.visible = _.clone(options.visible);
  if (options.hidden) this.hidden = _.clone(options.hidden);
  if (typeof options.requireFetch === 'boolean') this.requireFetch = options.requireFetch;
  if (options.tableName) this.tableName = options.tableName;
  if (typeof options.hasTimestamps === 'boolean' || Array.isArray(options.hasTimestamps)) {
    this.hasTimestamps = options.hasTimestamps;
  }

  this.set(attrs, options);
  this.initialize.apply(this, arguments);
}

/**
 * Registers an event listener.
 *
 * @method ModelBase#on
 * @example
 * customer.on('fetching', function(model) {
 *   // Do something before the data is fetched from the database
 * })
 * @see Events#on
 */

/**
 * @method ModelBase#off
 * @example
 *
 * customer.off('fetched fetching');
 * ship.off(); // This will remove all event listeners
 *
 * @see Events#off
 */

/**
 * @method ModelBase#trigger
 * @example
 *
 * ship.trigger('fetched');
 *
 * @see Events#trigger
 */
inherits(ModelBase, Events);

/**
 * @method ModelBase#initialize
 * @description
 *
 * Called by the {@link Model Model constructor} when creating a new instance.
 * Override this function to add custom initialization, such as event listeners.
 * Because plugins may override this method in subclasses, make sure to call
 * your super (extended) class.  e.g.
 *
 *     initialize: function() {
 *         this.constructor.__super__.initialize.apply(this, arguments);
 *         // Your initialization code ...
 *     }
 *
 * @see Model
 *
 * @param {Object} attributes
 *   Initial values for this model's attributes.
 * @param {Object=}  options
 *   The hash of options passed to {@link Model constructor}.
 */
ModelBase.prototype.initialize = function() {};

/**
 * @name ModelBase#tableName
 * @member {string}
 * @description
 *
 * A required property for any database usage, The
 * {@linkcode Model#tableName tableName} property refers to the database
 * table name the model will query against.
 *
 * @example
 *
 * var Television = bookshelf.model('Television', {
 *   tableName: 'televisions'
 * });
 */

/**
 * A special property of models which represents their unique identifier, named by the
 * {@link Model#idAttribute idAttribute}. If you set the `id` in the attributes hash,
 * it will be copied onto the model as a direct property.
 *
 * Models can be retrieved by their id from collections, and the id is used when fetching
 * models and building model relations.
 *
 * Note that a model's `id` property can always be accessed even when the value of its
 * {@link Model#idAttribute idAttribute} is not `'id'`.
 *
 * @member {(number|string)}
 * @example
 * const Television = bookshelf.model('Television', {
 *   tableName: 'televisions',
 *   idAttribute: 'coolId'
 * })
 *
 * new Television({coolId: 1}).fetch(tv => {
 *   tv.get('coolId') // 1
 *   tv.id // 1
 * })
 */
ModelBase.prototype.id;

/**
 * @member {string}
 * @default "id"
 * @description
 *
 * This tells the model which attribute to expect as the unique identifier
 * for each database row (typically an auto-incrementing primary key named
 * `'id'`). Note that if you are using {@link Model#parse parse} and {@link
 * Model#format format} (to have your model's attributes in `camelCase`,
 * but your database's columns in `snake_case`, for example) this refers to
 * the name returned by parse (`myId`), not the actual database column
 * (`my_id`).
 *
 * You can also get the parsed id attribute value by using the model's
 * {@link Model#parsedIdAttribute parsedIdAttribute} method.
 *
 * If the table you're working with does not have a Primary-Key in the form
 * of a single column you'll have to override it with a getter that returns
 * `null`. Overriding with `undefined` does not cascade the default behavior of
 * the value `'id'`. Such a getter in ES6 would look like
 * `get idAttribute() { return null }`.
 */
ModelBase.prototype.idAttribute = 'id';

/**
 * @member {Object|Null}
 * @default null
 * @description
 *
 * This can be used to define any default values for attributes that are not
 * present when creating or updating a model in a {@link Model#save save} call.
 * The default behavior is to *not* use these default values on updates unless
 * the `defaults: true` option is passed to the {@link Model#save save} call.
 * For inserts the default values will always be used if present.
 *
 * @example
 *
 * var MyModel = bookshelf.model('MyModel', {
 *   defaults: {property1: 'foo', property2: 'bar'},
 *   tableName: 'my_models'
 * })
 *
 * MyModel.forge({property1: 'blah'}).save().then(function(model) {
 *   // {property1: 'blah', property2: 'bar'}
 * })
 */
ModelBase.prototype.defaults = null;

/**
 * Allows defining the default behavior when there are no results when fetching a model from the
 * database. This applies only when fetching a single model using {@link Model#fetch fetch} or
 * {@link Collection#fetchOne}.
 *
 * You can override this model option when fetching by passing the `{require: false}` or
 * `{require: true}` option to any of the fetch methods mentioned above.
 *
 * @type {boolean}
 * @default true
 * @since 1.0.0
 * @example
 *
 * // Default behavior
 * const MyModel = bookshelf.model('MyModel', {
 *   tableName: 'my_models'
 * })
 *
 * new MyModel({id: 1}).fetch().catch(error => {
 *   // Will throw NotFoundError if there are no results
 * })
 *
 * // Overriding the default behavior
 * const MyModel = bookshelf.model('MyModel', {
 *   requireFetch: false,
 *   tableName: 'my_models'
 * })
 *
 * new MyModel({id: 1}).fetch(model => {
 *   // model will be null if there are no results
 })
 */
ModelBase.prototype.requireFetch = true;

/**
 * @member {Boolean|Array}
 * @default false
 * @description
 *
 * Automatically sets the current date and time on the timestamp attributes
 * `created_at` and `updated_at` based on the type of save method. The *update*
 * method will only update `updated_at`, while the *insert* method will set
 * both values.
 *
 * To override the default attribute names, assign an array to this property.
 * The first element will be the *created* column name and the second will be
 * the *updated* one. If any of these elements is set to `null` that particular
 * timestamp attribute will not be used in the model. For example, to
 * automatically update only the `created_at` attribute set this property to
 * `['created_at', null]`.
 *
 * You can override the timestamp attribute values of a model and those values
 * will be used instead of the automatic ones when saving.
 *
 * @example
 *
 * var MyModel = bookshelf.model('MyModel', {
 *   hasTimestamps: true,
 *   tableName: 'my_models'
 * })
 *
 * var myModel = MyModel.forge({name: 'blah'}).save().then(function(savedModel) {
 *   // {
 *   //   name: 'blah',
 *   //   created_at: 'Sun Mar 25 2018 15:07:11 GMT+0100 (WEST)',
 *   //   updated_at: 'Sun Mar 25 2018 15:07:11 GMT+0100 (WEST)'
 *   // }
 * })
 *
 * myModel.save({created_at: new Date(2015, 5, 2)}).then(function(updatedModel) {
 *   // {
 *   //   name: 'blah',
 *   //   created_at: 'Tue Jun 02 2015 00:00:00 GMT+0100 (WEST)',
 *   //   updated_at: 'Sun Mar 25 2018 15:07:11 GMT+0100 (WEST)'
 *   // }
 * })
 */
ModelBase.prototype.hasTimestamps = false;

/**
 * @member {null|Array}
 * @default null
 * @description
 *
 * List of model attributes to exclude from the output when serializing it. This works as a
 * blacklist, and all attributes not present in this list will be shown whan calling
 * {@link Model#toJSON toJSON}.
 *
 * By default this is `null` which means that no attributes will be excluded from the output.
 *
 * You can override this list by passing the `{hidden: ['list']}` option directly to the
 * {@link Model#toJSON toJSON} or {@link Model#serialize serialize} call.
 *
 * If both the `hidden` and the {@link Model#visible visible} model properties are set, the
 * `hidden` list will take precedence.
 *
 * @example
 * const MyModel = bookshelf.model('MyModel', {
 *   tableName: 'my_models',
 *   hidden: ['password']
 * })
 *
 * const myModel = MyModel.forge({
 *   name: 'blah',
 *   password: 'secure'
 * }).save().then(function(savedModel) {
 *   console.log(savedModel.toJSON())
 *   // {
 *   //   name: 'blah',
 *   //   created_at: 'Sun Mar 25 2018 15:07:11 GMT+0100 (WEST)',
 *   //   updated_at: 'Sun Mar 25 2018 15:07:11 GMT+0100 (WEST)'
 *   // }
 * })
 */
ModelBase.prototype.hidden = null;

/**
 * @member {null|Array}
 * @default null
 * @description
 *
 * List of model attributes to include in the output when serializing it. This works as a
 * whitelist, and all attributes not present in this list will be hidden whan calling
 * {@link Model#toJSON toJSON}.
 *
 * By default this is `null` which means that all attributes will be included in the output.
 *
 * You can override this list by passing the `{visible: ['list']}` option directly to the
 * {@link Model#toJSON toJSON} or {@link Model#serialize serialize} call.
 *
 * If both the {@link Model#hidden hidden} and the `visible` model properties are set, the
 * `hidden` list will take precedence.
 *
 * @example
 * const MyModel = bookshelf.model('MyModel', {
 *   tableName: 'my_models',
 *   visible: ['name', 'created_at']
 * })
 *
 * const myModel = MyModel.forge({
 *   name: 'blah',
 *   password: 'secure'
 * }).save().then(function(savedModel) {
 *   console.log(savedModel.toJSON())
 *   // {
 *   //   name: 'blah',
 *   //   created_at: 'Sun Mar 25 2018 15:07:11 GMT+0100 (WEST)',
 *   // }
 * })
 */
ModelBase.prototype.visible = null;

/**
 * @method
 * @private
 * @description
 *
 * Converts the timestamp keys to actual Date objects. This will not run if the
 * model doesn't have {@link Model#hasTimestamps hasTimestamps} set to either
 * `true` or an array of key names.
 * This method is run internally when reading data from the database to ensure
 * data consistency between the several database implementations.
 * It returns the model instance that called it, so it allows chaining of other
 * model methods.
 *
 * @returns {Model} The model that called this.
 */
ModelBase.prototype.formatTimestamps = function formatTimestamps() {
  if (!this.hasTimestamps) return this;

  this.getTimestampKeys().forEach((key) => {
    if (this.get(key)) this.set(key, new Date(this.get(key)));
  });

  return this;
};

/**
 * @method
 * @description  Get the current value of an attribute from the model.
 * @example      note.get("title");
 *
 * @param {string} attribute - The name of the attribute to retrieve.
 * @returns {mixed} Attribute value.
 */
ModelBase.prototype.get = function(attr) {
  return this.attributes[attr];
};

/**
 * @method
 * @private
 * @description
 *
 * Returns the model's {@link Model#idAttribute idAttribute} after applying the
 * model's {@link Model#parse parse} method to it. Doesn't mutate the original
 * value of {@link Model#idAttribute idAttribute} in any way.
 *
 * @example
 *
 * var Customer = bookshelf.model('Customer', {
 *   idAttribute: 'id',
 *   parse: function(attrs) {
 *     return _.mapKeys(attrs, function(value, key) {
 *       return 'parsed_' + key;
 *     });
 *   }
 * });
 *
 * customer.parsedIdAttribute() // 'parsed_id'
 *
 * @returns {mixed} Whatever value the parse method returns.
 */
ModelBase.prototype.parsedIdAttribute = function() {
  var parsedAttributes = this.parse({[this.idAttribute]: null});
  return parsedAttributes && Object.keys(parsedAttributes)[0];
};

/**
 * @method
 * @description  Set a hash of attributes (one or many) on the model.
 * @example
 *
 * customer.set({first_name: "Joe", last_name: "Customer"});
 * customer.set("telephone", "555-555-1212");
 *
 * @param {string|Object} attribute Attribute name, or hash of attribute names and values.
 * @param {mixed=} value If a string was provided for `attribute`, the value to be set.
 * @param {Object=} options
 * @param {Object} [options.unset=false] Remove attributes from the model instead of setting them.
 * @returns {Model} This model.
 */
ModelBase.prototype.set = function(key, val, options) {
  if (key == null) return this;
  let attrs;

  // Handle both `"key", value` and `{key: value}` -style arguments.
  if (typeof key === 'object') {
    attrs = key;
    options = val;
  } else {
    (attrs = {})[key] = val;
  }
  options = _.clone(options) || {};

  // Extract attributes and options.
  const unset = options.unset;
  const current = this.attributes;
  const prev = this.previousAttributes();

  // Check for changes of `id`.
  if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
  else if (this.parsedIdAttribute() in attrs) this.id = attrs[this.parsedIdAttribute()];

  // For each `set` attribute, update or delete the current value.
  for (const attr in attrs) {
    val = attrs[attr];
    if (!_.isEqual(prev[attr], val)) {
      this.changed[attr] = val;
    } else {
      delete this.changed[attr];
    }
    if (unset) {
      delete current[attr];
    } else {
      current[attr] = val;
    }
  }
  return this;
};

/**
 * @method
 * @description
 *
 * Checks for the existence of an id to determine whether the model is
 * considered "new".
 *
 * @example
 *
 * var modelA = new bookshelf.Model();
 * modelA.isNew(); // true
 *
 * var modelB = new bookshelf.Model({id: 1});
 * modelB.isNew(); // false
 */
ModelBase.prototype.isNew = function() {
  return this.id == null;
};

/**
 * Return a copy of the model's {@link Model#attributes attributes} for JSON
 * stringification. If the {@link Model model} has any relations defined, this
 * will also call {@link Model#toJSON toJSON} on each of the related
 * objects, and include them on the object unless `{shallow: true}` is
 * passed as an option.
 *
 * You can define a whitelist of model attributes to include on the ouput with
 * the `{visible: ['list', 'of', 'attributes']}` option. The `{hidden: []}`
 * option produces the opposite effect, hiding attributes from the output.
 *
 * This method is called internally by {@link Model#toJSON toJSON}. Override
 * this function if you want to customize its output.
 *
 * @example
 * var artist = new bookshelf.Model({
 *   firstName: "Wassily",
 *   lastName: "Kandinsky"
 * });
 *
 * artist.set({birthday: "December 16, 1866"});
 *
 * console.log(JSON.stringify(artist));
 * // {firstName: "Wassily", lastName: "Kandinsky", birthday: "December 16, 1866"}
 *
 * @param {Object} [options]
 * @param {Boolean} [options.shallow=false] Whether to exclude relations from the output or not.
 * @param {Boolean} [options.omitPivot=false]
 *   Whether to exclude pivot values from the output or not.
 * @param {Array} [options.hidden] List of model attributes to exclude from the output.
 * @param {Array} [options.visible]
     List of model attributes to include on the output. All other attributes will be hidden.
 * @param {Boolean} [options.visibility=true]
 *   Whether to use visibility options or not. If set to `false` the `hidden` and `visible` options
 *   will be ignored.
 * @returns {Object} Serialized model as a plain object.
 */
ModelBase.prototype.serialize = function(options) {
  if (typeof options !== 'object' || options === null) options = {};
  if (options.visibility === null || options.visibility === undefined) options.visibility = true;

  if (options.omitNew && this.isNew()) return null;

  let attributes = Object.assign({}, this.attributes);

  if (options.shallow !== true) {
    let relations = _.mapValues(this.relations, (relation) => (relation.toJSON ? relation.toJSON(options) : relation));
    relations = _.omitBy(relations, _.isNull);

    const pivot = this.pivot && !options.omitPivot && this.pivot.attributes;
    const pivotAttributes = _.mapKeys(pivot, (value, key) => `${constants.PIVOT_PREFIX}${key}`);

    attributes = Object.assign(attributes, relations, pivotAttributes);
  }

  if (options.visibility) {
    const visible = options.visible || this.visible;
    const hidden = options.hidden || this.hidden;

    if (visible) attributes = _.pick(attributes, visible);
    if (hidden) attributes = _.omit(attributes, hidden);
  }

  return attributes;
};

/**
 * @method
 * @description
 *
 * Called automatically by {@link
 * https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON()_behavior
 * `JSON.stringify`}. To customize serialization, override {@link
 * Model#serialize serialize}.
 *
 * @param {Object=} options Options passed to {@link Model#serialize}.
 */
ModelBase.prototype.toJSON = function(options) {
  return this.serialize(options);
};

/**
 * @method
 * @private
 * @returns String representation of the object.
 */
ModelBase.prototype.toString = function() {
  return '[Object Model]';
};

/**
 * @method
 * @description Get the HTML-escaped value of an attribute.
 * @param {string} attribute The attribute to escape.
 * @returns {string} HTML-escaped value of an attribute.
 */
ModelBase.prototype.escape = function(key) {
  return _.escape(this.get(key));
};

/**
 * @method
 * @description
 * Returns `true` if the attribute contains a value that is not null or undefined.
 * @param {string} attribute The attribute to check.
 * @returns {Boolean} True if `attribute` is set, otherwise `false`.
 */
ModelBase.prototype.has = function(attr) {
  return this.get(attr) != null;
};

/**
 * @method
 * @description
 *
 * The `parse` method is called whenever a {@link Model model}'s data is
 * returned in a {@link Model#fetch fetch} call. The function is passed the raw
 * database response object, and should return the {@link Model#attributes
 * attributes} hash to be {@link Model#set set} on the model. The default
 * implementation is a no-op, simply passing through the JSON response.
 * Override this if you need to format the database responses - for example
 * calling {@link
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
 * JSON.parse} on a text field containing JSON, or explicitly typecasting a
 * boolean in a sqlite3 database response.
 *
 * If you need to format your data before it is saved to the database, override
 * the {@link Model#format format} method in your models. That method does the
 * opposite operation of `parse`.
 *
 * @example
 * // Example of a parser to convert snake_case to camelCase, using lodash
 * // This is just an example. You can use the official case converter plugin
 * // to achieve the same functionality.
 * model.parse = function(attrs) {
 *   return _.mapKeys(attrs, function(value, key) {
 *     return _.camelCase(key);
 *   });
 * };
 *
 * @param {Object} attributes Hash of attributes to parse.
 * @returns {Object} Parsed attributes.
 */
ModelBase.prototype.parse = _.identity;

/**
 * @method
 * @description
 *
 * Remove an attribute from the model. `unset` is a noop if the attribute
 * doesn't exist.
 *
 * Note that unsetting an attribute from the model will not affect the related
 * record's column value when saving the model. In order to clear the value of a
 * column in the database record, set the attribute value to `null` instead:
 * `model.set("column_name", null)`.
 *
 * @param attribute Attribute to unset.
 * @returns {Model} This model.
 */
ModelBase.prototype.unset = function(attr, options) {
  return this.set(attr, void 0, _.extend({}, options, {unset: true}));
};

/**
 * @method
 * @description Clear all attributes on the model.
 * @returns {Model} This model.
 */
ModelBase.prototype.clear = function(options) {
  const undefinedKeys = _.mapValues(this.attributes, () => undefined);
  return this.set(undefinedKeys, Object.assign({}, options, {unset: true}));
};

/**
 * @method
 * @description
 *
 * The `format` method is used to modify the current state of the model before
 * it is persisted to the database. The `attributes` passed are a shallow clone
 * of the {@link Model model}, and are only used for inserting/updating - the
 * current values of the model are left intact.
 *
 * Do note that `format` is used to modify the state of the model when
 * accessing the database, so if you remove an attribute in your `format`
 * method, that attribute will never be persisted to the database, but it will
 * also never be used when doing a `fetch()`, which may cause unexpected
 * results. You should be very cautious with implementations of this method
 * that may remove the primary key from the list of attributes.
 *
 * If you need to modify the database data before it is given to the model,
 * override the {@link Model#parse parse} method instead. That method does the
 * opposite operation of `format`.
 *
 * @param {Object} attributes The attributes to be converted.
 * @returns {Object} Formatted attributes.
 */
ModelBase.prototype.format = _.identity;

/**
 * This method returns a specified relation loaded on the relations hash on the model, or calls the associated relation
 * method and adds it to the relations hash if one exists and has not yet been loaded.
 *
 * @example
 * new Photo({id: 1}).fetch({
 *   withRelated: ['account']
 * }).then(function(photo) {
 *   var account = photo.related('account') // Get the eagerly loaded account
 *
 *   if (account.id) {
 *     // Fetch a relation that has not been eager loaded yet
 *     return account.related('trips').fetch()
 *   }
 * })
 *
 * @param {string} name The name of the relation to retrieve.
 * @returns {Model|Collection|undefined}
 *   The specified relation as defined by a method on the model, or `undefined` if it does not exist.
 */
ModelBase.prototype.related = function(name) {
  return this.relations[name] || (this[name] ? (this.relations[name] = this[name]()) : void 0);
};

/**
 * @method
 * @description
 * Returns a new instance of the model with identical {@link
 * Model#attributes attributes}, including any relations from the cloned
 * model.
 *
 * @returns {Model} Cloned instance of this model.
 */
ModelBase.prototype.clone = function() {
  const model = new this.constructor(this.attributes);
  Object.assign(
    model.relations,
    _.mapValues(this.relations, (r) => r.clone())
  );
  model._previousAttributes = _.clone(this._previousAttributes);
  model.changed = _.clone(this.changed);
  return model;
};

/**
 * @method
 * @private
 * @description
 *
 * Returns the method that will be used on save, either 'update' or 'insert'.
 * This is an internal helper that uses `isNew` and `options.method` to
 * determine the correct method. If `option.method` is provided, it will be
 * returned, but lowercased for later comparison.
 *
 * @returns {string} Either `'insert'` or `'update'`.
 */
ModelBase.prototype.saveMethod = function(options) {
  if (!options) options = {};

  if (options.patch) {
    if (options.method === 'insert')
      throw new TypeError(`Cannot accept incompatible options: method=insert, patch=${options.patch}`);

    options.method = 'update';
  }
  return ((options.patch && 'update') || options.method) == null
    ? this.isNew()
      ? 'insert'
      : 'update'
    : options.method.toLowerCase();
};

/**
 * @method
 * @private
 * @description
 *
 * Returns the automatic timestamp key names set on this model. Note that this
 * will always return a value even if the model has {@link Model#hasTimestamps
 * hasTimestamps} set to `false`. In this case and when set to `true` the
 * return value will be the default names of `created_at` and `updated_at`.
 *
 * @returns {Array<string>} The two timestamp key names.
 */
ModelBase.prototype.getTimestampKeys = function() {
  return Array.isArray(this.hasTimestamps) ? this.hasTimestamps : constants.DEFAULT_TIMESTAMP_KEYS;
};

/**
 * @method
 * @description
 * Automatically sets the timestamp attributes on the model, if
 * {@link Model#hasTimestamps hasTimestamps} is set to `true` or an array. It
 * checks if the model is new and sets the `created_at` and `updated_at`
 * attributes (or any other custom attribute names you have set) to the current
 * date. If the model is not new and is just being updated then only the
 * `updated_at` attribute gets automatically updated.
 *
 * If the model contains any user defined `created_at` or `updated_at` values,
 * there won't be any automatic updated of these attributes and the user
 * supplied values will be used instead.
 *
 * @param {Object=} options
 * @param {string} [options.method]
 *   Either `'insert'` or `'update'` to specify what kind of save the attribute
 *   update is for.
 * @param {string} [options.date]
 *   Either a Date object or ms since the epoch. Specify what date is used for
 *   updateing the timestamps, i.e. if something other than `new Date()` should be used.
 * @returns {Object} A hash of timestamp attributes that were set.
 */
ModelBase.prototype.timestamp = function(options) {
  if (!this.hasTimestamps) return {};

  const now = (options || {}).date ? new Date(options.date) : new Date();
  const attributes = {};
  const method = this.saveMethod(options);
  const timestampKeys = this.getTimestampKeys();
  const createdAtKey = timestampKeys[0];
  const updatedAtKey = timestampKeys[1];
  const isNewModel = method === 'insert';

  if (updatedAtKey && (isNewModel || this.hasChanged()) && !this.hasChanged(updatedAtKey)) {
    attributes[updatedAtKey] = now;
  }

  if (createdAtKey && isNewModel && !this.hasChanged(createdAtKey)) {
    attributes[createdAtKey] = now;
  }

  this.set(attributes, _.extend(options, {silent: true}));

  return attributes;
};

/**
 * @method
 * @description
 *
 * Returns `true` if any {@link Model#attributes attribute} has changed since
 * the last {@link Model#fetch fetch} or {@link Model#save save}. If an
 * attribute name is passed as argument, returns `true` only if that specific
 * attribute has changed.
 *
 * Note that even if an attribute is changed by using the {@link Model#set set}
 * method, but the new value is exactly the same as the existing one, the
 * attribute is not considered *changed*.
 *
 * @example
 * Author.forge({id: 1}).fetch().then(function(author) {
 *   author.hasChanged() // false
 *   author.set('name', 'Bob')
 *   author.hasChanged('name') // true
 * })
 *
 * @param {string=} attribute A specific attribute to check for changes.
 * @returns {Boolean}
 *   `true` if any attribute has changed, `false` otherwise. Alternatively, if
 *   the `attribute` argument was specified, checks if that particular
 *   attribute has changed.
 */
ModelBase.prototype.hasChanged = function(attr) {
  if (attr == null) return !_.isEmpty(this.changed);
  return _.has(this.changed, attr);
};

/**
 * @method
 * @description
 *
 * Returns the value of an attribute like it was before the last change. A
 * change is usually done with the {@link Model#set set} method, but it can
 * also be done with the {@link Model#save save} method. This is useful for
 * getting back the original attribute value after it's been changed. It can
 * also be used to get the original value after a model has been saved to the
 * database or destroyed.
 *
 * In case you want to get the previous value of all attributes at once you
 * should use the {@link Model#previousAttributes previousAttributes} method.
 *
 * Note that this will return `undefined` if the model hasn't been fetched,
 * saved, destroyed or eager loaded. However, in case one of these operations
 * did take place, it will return the current value if an attribute hasn't
 * changed. If you want to check if an attribute has changed see the
 * {@link Model#hasChanged hasChanged} method.
 *
 * @example
 * Author.forge({id: 1}).fetch().then(function(author) {
 *   author.get('name') // Alice
 *   author.set('name', 'Bob')
 *   author.previous('name') // 'Alice'
 * })
 *
 * @param {string} attribute The attribute to check.
 * @returns {mixed} The previous value.
 */
ModelBase.prototype.previous = function(attribute) {
  return this._previousAttributes[attribute];
};

/**
 * @method
 * @description
 *
 * Returns a copy of the {@link Model model}'s attributes like they were before
 * the last change. A change is usually done with the {@link Model#set set}
 * method, but it can also be done with the {@link Model#save save} method.
 * This is mostly useful for getting a diff of the model's attributes after
 * changing some of them. It can also be used to get the previous state of a
 * model after it has been saved to the database or destroyed.
 *
 * In case you want to get the previous value of a single attribute you should
 * use the {@link Model#previous previous} method.
 *
 * Note that this will return an empty object if no changes have been made to
 * the model and it hasn't been fetched, saved or eager loaded.
 *
 * @example
 * Author.forge({id: 1}).fetch().then(function(author) {
 *   author.get('name') // Alice
 *   author.set('name', 'Bob')
 *   author.previousAttributes() // {id: 1, name: 'Alice'}
 * })
 *
 * Author.forge({id: 1}).fetch().then(function(author) {
 *   author.get('name') // Alice
 *   return author.save({name: 'Bob'})
 * }).then(function(author) {
 *   author.get('name') // Bob
 *   author.previousAttributes() // {id: 1, name: 'Alice'}
 * })
 *
 * @returns {Object}
 *   The attributes as they were before the last change, or an empty object in
 *   case the model data hasn't been fetched yet.
 */
ModelBase.prototype.previousAttributes = function() {
  return _.clone(this._previousAttributes) || {};
};

/**
 * @method
 * @private
 * @description
 *
 * Resets the `changed` hash for the model. Typically called after a `sync`
 * action (save, fetch, destroy).
 *
 * @returns {Model} This model.
 */
ModelBase.prototype._reset = function() {
  this.changed = Object.create(null);
  return this;
};

/**
 * @method ModelBase#pick
 * @see http://lodash.com/docs/#pick
 */
/**
 * @method ModelBase#omit
 * @see http://lodash.com/docs/#omit
 */
// "_" methods that we want to implement on the Model.
const modelMethods = ['pick', 'omit'];

// Mix in each "_" method as a proxy to `Model#attributes`.
_.each(modelMethods, function(method) {
  ModelBase.prototype[method] = function() {
    return _[method].apply(_, [this.attributes].concat(Array.from(arguments)));
  };
});

/**
 * This static method allows you to create your own Model classes by extending {@link Model bookshelf.Model}.
 *
 * It correctly sets up the prototype chain, which means that subclasses created this way can be further extended and
 * subclassed as far as you need.
 *
 * @example
 * const Promise = require('bluebird')
 * const compare = require('some-crypt-library')
 *
 * const Customer = bookshelf.model('Customer', {
 *   initialize() {
 *     this.constructor.__super__.initialize.apply(this, arguments)
 *
 *     // Setting up a listener for the 'saving' event
 *     this.on('saving', this.validateSave)
 *   },
 *
 *   validateSave() {
 *     return doValidation(this.attributes)
 *   },
 *
 *   account() {
 *     // Defining a relation with the Account model
 *     return this.belongsTo(Account)
 *   }
 * }, {
 *   login: Promise.method(function(email, password) {
 *     if (!email || !password)
 *       throw new Error('Email and password are both required')
 *
 *     return new this({email: email.toLowerCase()})
 *       .fetch()
 *       .tap(function(customer) {
 *         if (!compare(password, customer.get('password'))
 *           throw new Error('Invalid password')
 *       })
 *   })
 * })
 *
 * @method Model.extend
 * @param {Object} [prototypeProperties] Instance methods and properties to be attached to instances of the new class.
 * @param {Object} [classProperties]
 *   Class (i.e. static) functions and properties to be attached to the constructor of the new class.
 * @returns {Function} Constructor for new Model subclass.
 */
ModelBase.extend = require('../extend');

module.exports = ModelBase;