bsiddiqui/bookshelf-modelbase

View on GitHub
lib/index.js

Summary

Maintainability
B
6 hrs
Test Coverage
var Joi = require('joi')
var difference = require('lodash.difference')

module.exports = function modelBase (bookshelf, params) {
  if (!bookshelf) {
    throw new Error('Must pass an initialized bookshelf instance')
  }

  var bookshelfModel = bookshelf.Model

  var model = bookshelf.Model.extend({
    constructor: function () {
      bookshelfModel.apply(this, arguments)

      if (this.validate) {
        var baseValidation = {
          // id might be number or string, for optimization
          id: Joi.any().optional(),
          created_at: Joi.date().optional(),
          updated_at: Joi.date().optional()
        }

        this.validate = this.validate.isJoi
          ? this.validate.keys(baseValidation)
          : Joi.object(this.validate).keys(baseValidation)

        this.on('saving', this.validateSave)
      }
    },

    hasTimestamps: ['created_at', 'updated_at'],

    validateSave: function (model, attrs, options) {
      var validation
      // model is not new or update method explicitly set
      if ((model && !model.isNew()) || (options && (options.method === 'update' || options.patch === true))) {
        var schemaKeys = this.validate._inner.children.map(function (child) {
          return child.key
        })
        var presentKeys = Object.keys(attrs)
        var optionalKeys = difference(schemaKeys, presentKeys)
        // only validate the keys that are being updated
        validation = Joi.validate(
          attrs,
          optionalKeys.length
            // optionalKeys() doesn't like empty arrays
            ? this.validate.optionalKeys(optionalKeys)
            : this.validate
        )
      } else {
        validation = Joi.validate(this.attributes, this.validate)
      }

      if (validation.error) {
        validation.error.tableName = this.tableName

        throw validation.error
      } else {
        this.set(validation.value)
        return validation.value
      }
    }

  }, {

    /**
     * Select a collection based on a query
     * @param {Object} [filter]
     * @param {Object} [options] Options used of model.fetchAll
     * @return {Promise(bookshelf.Collection)} Bookshelf Collection of Models
     */
    findAll: function (filter, options) {
      return this.forge().where(filter || {}).fetchAll(options)
    },

    /**
     * Find a model based on it's ID
     * @param {String} id The model's ID
     * @param {Object} [options] Options used of model.fetch
     * @return {Promise(bookshelf.Model)}
     */
    findById: function (id, options) {
      return this.findOne({ [this.prototype.idAttribute]: id }, options)
    },

    /**
     * Select a model based on a query
     * @param {Object} [query]
     * @param {Object} [options] Options for model.fetch
     * @param {Boolean} [options.require=false]
     * @return {Promise(bookshelf.Model)}
     */
    findOne: function (query, options = {}) {
      options = Object.assign({ require: true }, options)
      return this.forge(query).fetch(options)
    },

    /**
     * Insert a model based on data
     * @param {Object} data
     * @param {Object} [options] Options for model.save
     * @return {Promise(bookshelf.Model)}
     */
    create: function (data, options) {
      return this.forge(data)
        .save(null, options)
    },

    /**
     * Update a model based on data
     * @param {Object} data
     * @param {Object} options Options for model.fetch and model.save
     * @param {String|Integer} options.id The id of the model to update
     * @param {Boolean} [options.patch=true]
     * @param {Boolean} [options.require=true]
     * @return {Promise(bookshelf.Model)}
     */
    update: function (data, options = {}) {
      options = Object.assign({ patch: true, require: true }, options)
      return this.forge({ [this.prototype.idAttribute]: options.id }).fetch(options)
        .then(function (model) {
          return model ? model.save(data, options) : undefined
        })
    },

    /**
     * Destroy a model by id
     * @param {Object} options
     * @param {String|Integer} options.id The id of the model to destroy
     * @param {Boolean} [options.require=true]
     * @return {Promise(bookshelf.Model)} empty model
     */
    destroy: function (options = {}) {
      options = Object.assign({ require: true }, options)
      return this.forge({ [this.prototype.idAttribute]: options.id })
        .destroy(options)
    },

    /**
      * Select a model based on data and insert if not found
      * @param {Object} data
      * @param {Object} [options] Options for model.fetch and model.save
      * @param {Object} [options.defaults] Defaults to apply to a create
      * @return {Promise(bookshelf.Model)} single Model
      */
    findOrCreate: function (data, options = {}) {
      return this.findOne(data, Object.assign({}, options, { require: false }))
        .bind(this)
        .then(function (model) {
          var defaults = (options && options.defaults) || {}
          return model || this.create(Object.assign(defaults, data), options)
        })
    },

    /**
     * Select a model based on data and update if found, insert if not found
     * @param {Object} selectData Data for select
     * @param {Object} updateData Data for update
     * @param {Object} [options] Options for model.save
     */
    upsert: function (selectData, updateData, options = {}) {
      return this.findOne(selectData, Object.assign({}, options, { require: false }))
        .bind(this)
        .then(function (model) {
          return model
            ? model.save(
              updateData,
              Object.assign({ patch: true, method: 'update' }, options)
            )
            : this.create(
              Object.assign({}, selectData, updateData),
              Object.assign({}, options, { method: 'insert' })
            )
        })
    }
  })

  return model
}

module.exports.pluggable = function (bookshelf, params) {
  bookshelf.Model = module.exports.apply(null, arguments)
}