yoctore/yocto-mongoose

View on GitHub
src/modules/crud/index.js

Summary

Maintainability
C
1 day
Test Coverage
'use strict';

var logger    = require('yocto-logger');
var _         = require('lodash');
var Q         = require('q');
var Schema    = require('mongoose').Schema;

/**
 *
 * Manage Crud function for adding model
 *
 * @date : 25/09/2015
 * @author : Mathieu ROBERT <mathieu@yocto.re>
 * @copyright : Yocto SAS, All right reserved
 *
 * @class Crud
 */
function Crud (logger) {
  /**
   * Logger instance
   *
   * @property logger
   */
  this.logger     = logger;

  /**
   * Alias object for exclusion process
   *
   * @property alias
   */
  this.alias      = {
    'create'  : [ 'insert' ],
    'get'     : [ 'read' ],
    'getOne'  : [ 'readOne' ],
    'delete'  : [ 'destroy' ],
    'update'  : [ 'modify' ]
  };
}

/**
 * Alias method for create method
 *
 * @return {Promise} promise object to use for handling
 */
Crud.prototype.insert = function () {
  // default instance
  return this.create.apply(this, arguments);
};

/**
 * Alias method for get method
 *
 * @return {Promise} promise object to use for handling
 */
Crud.prototype.read = function () {
  // default instance
  return this.get.apply(this, arguments);
};

/**
 * Alias method for getOne method
 *
 * @return {Promise} promise object to use for handling
 */
Crud.prototype.readOne = function () {
  // default instance
  return this.getOne.apply(this, arguments);
};

/**
 * Alias method for update method
 *
 * @return {Promise} promise object to use for handling
 */
Crud.prototype.modify = function () {
  // default instance
  return this.update.apply(this, arguments);
};

/**
 * Alias method for delete method
 *
 * @return {Promise} promise object to use for handling
 */
Crud.prototype.destroy = function () {
  // default instance
  return this.delete.apply(this, arguments);
};

/**
 * Get One item from given rules
 *
 * @param {String|Object} conditions conditions to use for search
 * @param {String|Object} filter filter to use to process filter
 * @return {Promise} promise object to use for handling
 */
Crud.prototype.getOne = function (conditions, filter) {
  // call main get function
  return this.get(conditions, filter, 'findOne');
};

/**
 * Get data from a model
 *
 * @param {Object|String} conditions query rules to add in find
 * @param {Object} filter object property to process filter action
 * @param {String} method id a method name is ginve force method name usage
 * @return {Promise} promise object to use for handleling
 */
Crud.prototype.get = function (conditions, filter, method) {
  // process redis usage
  var redis = this[ method === 'findOne' ? 'getOneRedis' : 'getRedis' ];

  // defined default method name to use
  method  = _.isString(method) && !_.isEmpty(method) ? method : 'find';

  // is string ? so if for findById request. change method name
  method = _.isString(conditions) ? 'findById' : method;

  // Create our deferred object, which we will use in our promise chain
  var deferred = Q.defer();

  // normalize filter object
  filter = _.isString(filter) && !_.isEmpty(filter) ? filter : '';

  // save context for possible strict violation
  var context = this;

  /**
   * Default method to retreive data
   *
   * @param {Object|String} conditions query rules to add in find
   * @param {Object} filter object property to process filter action
   */
  function defaultFind (conditions, filter, store) {
    // normal process
    context[method](conditions, filter, function (error, data) {
      // has error ?
      if (error) {
        // reject
        deferred.reject(error);
      } else {
        // in case of no data
        if (_.isObject(store)) {
          // store data on db
          redis.instance.add(store.key, data, store.expire);
          // do not process promise catch here beacause this process must not stop normal process
          // in any case
        }
        // valid
        deferred.resolve(data);
      }
    });
  }

  // has redis ?
  if (redis) {
    // normalize redisKey
    var redisKey = _.merge(_.isString(conditions) ?
      _.set([ this.modelName, conditions ].join('-'), conditions) : conditions || {}, filter || {});

    // get key
    redis.instance.get(redisKey).then(function (success) {
      // success resolve
      deferred.resolve(success);
    }).catch(function (error) {
      // normal stuff
      defaultFind.call(this, conditions, filter, _.isNull(error) ? {
        key     : redisKey,
        expire  : redis.expire
      } : error);
    }.bind(this));
  } else {
    // normal process
    defaultFind.call(this, conditions, filter);
  }

  // return deferred promise
  return deferred.promise;
};

/**
 * Find and Remove a specific model
 *
 * @param {String} id query rules to add in find
 * @return {Promise} promise object to use for handling
 */
Crud.prototype.delete = function (id) {
  // Create our deferred object, which we will use in our promise chain
  var deferred = Q.defer();

  // is valid type ?
  if (_.isString(id) && !_.isEmpty(id)) {
    // try to find
    this.findByIdAndRemove(id, function (error, data) {
      // has error ?
      if (error) {
        // reject
        deferred.reject(error);
      } else {
        // valid
        deferred.resolve(data);
      }
    });
  } else {
    // reject
    deferred.reject([ 'Given id is not a string',
                     _.isString(id) && _.isEmpty(id) ? ' and is empty' : '' ].join(' '));
  }

  // return deferred promise
  return deferred.promise;
};

/**
 * Find a model and update it
 *
 * @param {Object|String} conditions query rules to add in find
 * @param {String} update data to use for update
 * @param {Boolean} multi set to true to process to un multi update action
 * @return {Promise} promise object to use for handling
 */
Crud.prototype.update = function (conditions, update, multi) {
  // is string ? so if for findByIdAndUpdate request. change method name
  var method = _.isString(conditions) ? 'findByIdAndUpdate' : 'findOneAndUpdate';

  // Create our deferred object, which we will use in our promise chain
  var deferred = Q.defer();

  // is multi request ??
  if (_.isBoolean(multi) && multi) {
    // process specific where
    this.where().setOptions({ multi : true }).update(conditions, update, function (error, data) {
      // has error ?
      if (error) {
        // reject
        deferred.reject(error);
      } else {
        // valid
        deferred.resolve(data);
      }
    });
  } else {
    // try to find
    this[method](conditions, update, { new : true }, function (error, data) {
      // has error ?
      if (error) {
        // reject
        deferred.reject(error);
      } else {
        // valid
        deferred.resolve(data);
      }
    });
  }

  // return deferred promise
  return deferred.promise;
};

/**
 * Insert new data in bdd for current model
 *
 * @param {Object} value value to use for create action
 * @return {Promise} promise object to use for handling
 */
Crud.prototype.create = function (value) {
  // Create our deferred object, which we will use in our promise chain
  var deferred = Q.defer();
  // create default instance model
  var model = !_.isFunction(this.save) ? new this() : this;

  // default status
  var status = true;
  var errors = [];

  // has a validate function ?
  if (_.isFunction(this.validate)) {
    // so try to validate
    status = this.validate(value);
    // save error
    errors = status.error;
    // change value here if validate is was call
    value  = _.has(status, 'value') ? status.value : value;
    // get status
    status = _.isNull(status.error);
  }

  // is valid ?
  if (status) {
    // model is a valid instance ?
    if (model instanceof this) {
      // extend data before save
      _.extend(model, value);
      // try to find
      model.save(function (error, data) {
        // has error ?
        if (error) {
          // reject
          deferred.reject(error);
        } else {
          // elastic is enable on schema ?
          if (this.schema.elastic) {
            // add this a listener to log indexes action
            model.on('es-indexed', function (err) {
              // log succes message
              if (err) {
                // reject with error message
                deferred.reject([ '[ Crud.create ] - Indexes creation failed :', err ].join(' '));
              } else {
                // resolve default statement
                deferred.resolve(data);
              }
            });
          } else {
            // valid
            deferred.resolve(data);
          }
        }
      }.bind(this));
    } else {
      // reject invalid instance model
      deferred.reject('[ Crud.create ] - Cannot save. invalid instance model');
    }
  } else {
    // reject schema validation error
    deferred.reject([ '[ Crud.create ] - Cannot save new schema.',
                      errors ].join(' '));
  }

  // return deferred promise
  return deferred.promise;
};

/**
 * An utility method to use for search request to elastic search instances
 *
 * @param {Object} query query to use on elastic search request
 * @param {Object} options Optional options, eg. : hydrate, from, size
 * @return {Promise} promise object to use for handling
 */
Crud.prototype.esearch = function (query, options) {
  // Create our deferred object, which we will use in our promise chain
  var deferred    = Q.defer();

  // elastic is enabled ?
  if (!_.isUndefined(this.search) && _.isFunction(this.search)) {
    // try to find
    this.search(query || {}, options || {}, function (error, data) {
      // has error ?
      if (error) {
        // reject
        deferred.reject(error);
      } else {
        // valid
        deferred.resolve(data);
      }
    });
  } else {
    // reject with error message
    deferred.reject('Elastic search is not enabled. Cannot process a search request');
  }

  // return deferred promise
  return deferred.promise;
};

/**
 * Add a crud method to statics givent schema
 *
 * @param {Object} schema default schema to use
 * @param {Array} exclude array of method to exclude
 * @param {Object} redisIncludes default redis include config retreive form model definition
 * @param {Object} redis current redis instance to use on current crud method
 * @return {Object|Boolean} modified schema with new requested method
 */
Crud.prototype.add = function (schema, exclude, redisIncludes, redis) {
  // valid data ?
  if ((!_.isObject(schema) && !(schema instanceof Schema)) || !_.isArray(exclude)) {
    this.logger.warning('[ Crud.add ] - Schema or exclude item given is invalid');
    // invalid statement
    return false;
  }

  // default difference
  var difference = [ 'add' ];

  // elastic is disable ?
  if (!schema.elastic) {
    // add search method to diff to remove default crud method
    difference.push('elasticsearch');
  }

  // keep only correct method
  var existing  = _.difference(Object.keys(Crud.prototype), difference);
  // normalize data
  exclude       = _.isArray(exclude) ? exclude : [];

  // try to add alias on exclude array
  if (!_.isEmpty(exclude) && _.isArray(exclude)) {
    // build excluded alias
    var excludeAlias = _.intersection(Object.keys(this.alias), exclude);
    // parse alias to add item
    _.each(excludeAlias, function (ex) {
      // push it
      exclude.push(this.alias[ex]);
    }.bind(this));

    // flatten array to have unique level
    exclude = _.flatten(exclude);
  }

  // keep only needed methods
  var saved     = _.difference(existing, exclude);

  // parse all
  _.each(saved, function (s) {
    // is a valid func ?
    if (_.isFunction(this[s])) {
      // has redis config define ?
      if (redisIncludes) {
        // current method is include on redis config ?
        if (_.includes(redisIncludes.value || [], s)) {
          // assign method via static method and bind of redis on it
          schema.static([ s, 'Redis' ].join(''), {
            instance  : redis,
            expire    : redisIncludes.expire || 0
          });
        }
      }
      // assign method via static method
      schema.static(s, this[s]);
    }
  }.bind(this));

  // default statement
  return schema;
};

// Default export
module.exports = function (l) {
  // is a valid logger ?
  if (_.isUndefined(l) || _.isNull(l)) {
    logger.warning('[ Crud.constructor ] - Invalid logger given. Use internal logger');
    // assign
    l = logger;
  }
  // default statement
  return new (Crud)(l);
};