index.js

'use strict';

var logger        = require('yocto-logger');
var mongoose      = require('mongoose');
var _             = require('lodash');
var path          = require('path');
var fs            = require('fs');
var glob          = require('glob');
var joi           = require('joi');
var async         = require('async');
var Schema        = mongoose.Schema;
var Q             = require('q');
var elastic       = require('mongoosastic');
var utils         = require('yocto-utils');
var elasticClient = require('elasticsearch');

// default require for code quality
var modCrud          = require('./modules/crud');
var modValidator     = require('./modules/validator');
var modMethod        = require('./modules/method');
var modEnums         = require('./modules/enum');
var modElastic       = require('./modules/utils/elastic');
var modRedis         = require('./modules/utils/redis');

// Use q. to handle default promise in mongoose
mongoose.Promise = require('q').Promise;

/**
 *
 * Utility tool to manage mongoose connection and auto loading models.
 *
 * @date : 24/09/2015
 * @author : Mathieu ROBERT <mathieu@yocto.re>
 * @copyright : Yocto SAS, All right reserved
 *
 * @class YMongoose
 */
function YMongoose (l) {
  /**
   * Logger instance
   *
   * @private
   * @memberof YMongoose
   * @member {Instance} logger
   */
  this.logger   = l;

  /**
   * Mongoose instance
   *
   * @private
   * @memberof YMongoose
   * @member {Instance} mongoose
   */
  this.mongoose = mongoose;
  /**
   * Model Path definition to use on autoLoading
   *
   * @private
   * @memberof YMongoose
   * @member {Object} paths
   */
  this.paths    = {
    model       : '',
    validator   : '',
    method      : '',
    enums       : ''
  };

  /**
   * Define is we create automatique crud function
   *
   * @private
   * @memberof YMongoose
   * @member {Boolean} crud
   * @default false
   */
  this.crud     = false;

  /**
   * Define if models are loaded
   *
   * @private
   * @memberof YMongoose
   * @member {Boolean} loaded
   * @default false
   */
  this.loaded   = false;

  /**
   * Internal modules
   *
   * @private
   * @memberof YMongoose
   * @member {Boolean} loaded
   */
  this.modules = {
    crud          : modCrud(l),
    validator     : modValidator(l),
    method        : modMethod(l),
    enums         : modEnums(l, mongoose.Types),
    elastic       : modElastic(l),
    redis         : modRedis(l)
  };
}

/**
 * Check is currenct connection is connected
 *
 * @return {Boolean} true if is connected false otherwise
 */
YMongoose.prototype.isConnected = function () {
  // default statement
  return this.mongoose.connection.readyState === this.mongoose.Connection.STATES.connected;
};

/**
 * Check is current connection is disconnected
 *
 * @return {Boolean} true if is connected false otherwise
 */
YMongoose.prototype.isDisconnected = function () {
  // default statement
  return this.mongoose.connection.readyState === this.mongoose.Connection.STATES.disconnected;
};

/**
 * Create a connection on mongo server
 *
 * @param {String} url bdd url connection
 * @param {Object} options options for connection
 * @return {Object} promise status to use for connection testing
 */
YMongoose.prototype.connect = function (url, options) {
  // Create our deferred object, which we will use in our promise chain
  var deferred = Q.defer();

  // try connect
  this.logger.info([ '[ YMongoose.connect ] -',
                     'Try to connect on [', url, ']' ].join(' '));

  // catch open connection
  this.mongoose.connection.on('open', function () {
    // message
    this.logger.info([ '[ YMongoose.connect ] - Connection succeed on', url ].join(' '));
    // success reponse
    deferred.resolve();
  }.bind(this));

  // listen error event
  this.mongoose.connection.on('error', function (error) {
    // message
    this.logger.error([ '[ YMongoose.connect ] - Connection failed.',
                           'Error is :', error.message ].join(' '));
    // error reponse
    deferred.reject(error);
  }.bind(this));

  // catch defined event for debug
  _.each([ 'connecting', 'connected', 'disconnecting', 'disconnected' ], function (e) {
    // catch each event
    this.mongoose.connection.on(e, function () {
      // process log
      this.logger.debug([ '[ YMongoose.connect ] - Mongoose is :',
        _.capitalize(e) ].join(' '));
    }.bind(this));
  }.bind(this));

  // valid url ?
  if (_.isString(url) && !_.isEmpty(url)) {
    // normalized options
    options = _.isObject(options) && !_.isEmpty(options) ? options : {};

    // check if property sslCA exist
    if (_.has(options, 'server.sslCA') && _.isString(options.server.sslCA)) {
      // create buffer of this file
      options.server.sslCA = [
        fs.readFileSync(path.normalize(process.cwd() + '/' + options.server.sslCA))
      ];
    }

    // check if property sslKey exist
    if (_.has(options, 'server.sslKey') && _.isString(options.server.sslKey)) {
      // create buffer of this file
      options.server.sslKey = [
        fs.readFileSync(path.normalize(process.cwd()  + '/' + options.server.sslKey))
      ];
    }

    // check if property sslCert exist
    if (_.has(options, 'server.sslCert') && _.isString(options.server.sslCert)) {
      // create buffer of this file
      options.server.sslCert = [
        fs.readFileSync(path.normalize(process.cwd()  + '/' + options.server.sslCert))
      ];
    }

    // connect only if mongoosed is not connected
    if (this.isDisconnected()) {
      // start connection
      this.mongoose.connect(url, options);
    }
  } else {
    // invalid url cannot connect
    this.logger.error('[ YMongoose.connect ] - Invalid url, cannot connect.');
    // reject connection failed
    deferred.reject();
  }

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

/**
 * Disconnection current connection
 *
 * @return {Object} promise status to use for connection testing
 */
YMongoose.prototype.disconnect = function () {
  // Create our deferred object, which we will use in our promise chain
  var deferred = Q.defer();

  // try to disconnect
  this.logger.info('[ YMongoose.disconnect ] - Try to disconnect all connections');

  // is connected ?
  if (this.isConnected()) {
    // disconnect
    this.mongoose.disconnect(function (error) {

      // has any error ?
      if (error) {
        // message
        this.logger.error([ '[ YMongoose.disconnect ] - Disconnect failed.',
                               'Error is :', error.message ].join(' '));
        // reject disconnect failed
        deferred.reject(error);
      } else {
        // process redis disconnect here
        this.modules.redis.disconnect();
        // clean the other thing
        this.mongoose.connection.close(function (cerror) {
          // has error ?
          if (cerror) {
            // message
            this.logger.error([ '[ YMongoose.disconnect ] - Connection close failed.',
                                   'Error is :', cerror.message ].join(' '));
            // reject disconnect failed
            deferred.reject(cerror);
          } else {
            // successful message
            this.logger.info('[ YMongoose.disconnect ] - Closing connection succeed.');
            // success reponse
            deferred.resolve();
          }
        }.bind(this));
      }
    }.bind(this));
  } else {
    // cant disconnect we are not connected
    this.logger.warning('[ YMongoose.disconnect ] - Cannot disconnect orm is not connected.');
    // reject
    deferred.reject();
  }

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

/**
 * An utility method to save host config for elastic search instances
 *
 * @param {Array} hosts list of hosts to use on elastic configuration
 * @param {Object} options property to set on options
 * @return {Boolean} true if all is ok false otherwise
 */
YMongoose.prototype.enableElasticsearch = function (hosts, options) {
  // normalize hosts
  hosts = _.isArray(hosts) ? hosts : [ hosts || {
    host      : '127.0.0.1',
    port      : 9200,
    protocol  : 'http'
  } ];

  // validation schema
  var schema = joi.array().required().items(
    joi.object().keys({
      host      : joi.string().required().empty().default('127.0.0.1'),
      port      : joi.number().required().default(9200),
      protocol  : joi.string().optional().valid([ 'http', 'https']).default('http'),
      auth      : joi.string().optional().empty()
    }).default({ host : '127.0.0.1', port : 9200, protocol : 'http' })
  ).default([ { host : '127.0.0.1', port : 9200, protocol : 'http' } ]);

  // validate given config
  var validate = joi.validate(hosts, schema);

  // has error ?
  if (validate.error) {
    // log error message
    this.logger.warning([ '[ YMongoose.elasticHosts ] - Invalid host config given :',
                          validate.error ] .join(' '));
    // default invalid statement
    return false;
  }

  // save data
  return this.modules.elastic.enableHosts(validate.value, options);
};

/**
 * enable redis on current module
 *
 * @param {Array} hosts lists of use hosts
 * @param {Object} options options to use on redis instance
 * @param {Boolean} cluster set to true if a cluster connection is needed
 * @param {Number} defaultExpireTime to force the default expire time for all of insertion
 * @return {Object} current redis instance
 */
YMongoose.prototype.enableRedis = function (hosts, options, cluster, defaultExpireTime) {
  // connect redis
  return this.modules.redis.connect(hosts, options, defaultExpireTime, cluster);
};

/**
 * Default accessor to retreive redis instance
 *
 * @return {Object} default redis instance
 */
YMongoose.prototype.getRedis = function () {
  // default statement
  return this.modules.redis;
};

/**
 * Define current models path to use for mapping
 *
 * @param {String} directory directory path to use
 * @return {Boolean} true if add was ok false otherwise
 */
YMongoose.prototype.models = function (directory) {
  // message
  this.logger.debug('[ YMongoose.models ] - Try to set model defintion path.');
  // default statement
  return this.setPath(directory, 'model');
};

/**
 * Define current controller path to use for mapping
 *
 * @param {String} directory directory path to use
 * @return {Boolean} true if add was ok false otherwise
 */
YMongoose.prototype.validators = function (directory) {
  // message
  this.logger.debug('[ YMongoose.validators ] - Try to set validator defintion path.');
  // default statement
  return this.setPath(directory, 'validator');
};

/**
 * Define current methods path to use for mapping
 *
 * @param {String} directory directory path to use
 * @return {Boolean} true if add was ok false otherwise
 */
YMongoose.prototype.methods = function (directory) {
  // message
  this.logger.debug('[ YMongoose.methods ] - Try to set methods defintion path.');
  // default statement
  return this.setPath(directory, 'method');
};

/**
 * Define current enums definition path to use for mapping
 *
 * @param {String} directory directory path to use
 * @return {Boolean} true if add was ok false otherwise
 */
YMongoose.prototype.enums = function (directory) {
  // message
  this.logger.debug('[ YMongoose.enums ] - Try to set enums defintion path.');
  // default statement
  return this.setPath(directory, 'enums');
};

/**
 * Set path of model and directory for loading
 *
 * @param {String} directory directory path to set
 * @param {String} stype defined witch element we prepare to defined
 * @return {Boolean} true if all is ok false otherwise
 */
YMongoose.prototype.setPath = function (directory, stype) {
  // default type value
  var types = {
    model       : { ext : 'json', name : 'model' },
    validator   : { ext : 'js', name  : 'validator' },
    method      : { ext : 'js', name  : 'method' },
    enums       : { ext : 'json', name : 'enums' }
  };

  // set default if we need to set controller directory
  var type = _.find(types, [ 'name', stype ]);

  // is valid format ?
  if (!_.isUndefined(type) && _.isObject(type) && _.isString(directory) && !_.isEmpty(directory)) {
    // normalize directory path
    directory = path.isAbsolute(directory) ?
                directory : path.normalize([ process.cwd(), directory ].join('/'));

    // main process
    try {
      // check access sync
      fs.accessSync(directory, fs.R_OK);
      // retreiving stats of directory
      var stats   = fs.statSync(directory);

      // is directory ?
      if (!stats.isDirectory()) {
        // exception
        throw [ directory, 'is not a valid directory.' ].join(' ');
      }

      // check if has data on directory for warning prevent
      var hasFile = glob.sync([ '**/*.', type.ext ].join(''), {
        cwd : directory
      });
      // so isEmpty ?
      if (_.size(hasFile) === 0) {
        this.logger.warning([ '[ YMongoose.setPath ] - Given directory path for',
                              [ type.name, (type.name !== 'enums' ? 's' : '') ].join(''),
                              'seems to be empty.',
                              'Don\'t forget to ad your', type.ext,
                              'file before load call' ].join(' '));
      }

      // set data
      this.paths[type.name] = directory;
      // log message
      this.logger.debug([ '[ YMongoose.setPath ] -',
                          _.capitalize([ type.name, (type.name !== 'enums' ? 's' : '') ].join('')),
                         'path was set to :',
                         this.paths[type.name] ].join(' '));
    } catch (e) {
      // log message
      this.logger.error([ '[ YMongoose.setPath ] - Set path for',
                          [ type.name, (type.name !== 'enums' ? 's' : '') ].join(''),
                          'failed.', e ].join(' '));
      // error on set path disconnect mongoose
      this.disconnect();
      // default statement
      return false;
    }
    // success statement
    return true;
  }

  // error if we exec here
  this.logger.error([ '[ YMongoose.setPath ] - Cannot set directory for [', stype, ']',
                      'Invalid directory given or cannot retreive types rules' ].join(' '));
  // default statement
  return false;
};

/**
 * Get current curred loaded status
 *
 * @return {Boolean} true if all is ok false otherwise
 */
YMongoose.prototype.isLoaded = function () {
  // default statement
  return this.loaded;
};

/**
 * Check is current connector is ready
 *
 * @param {Boolean} showErrors true is we want
 * @return {Boolean} true if all is ok false otherwise
 */
YMongoose.prototype.isReady = function (showErrors) {
  // show errors ?
  if (showErrors) {
    // check connection
    if (!this.isConnected()) {
      this.logger.error('[ YMongoose.isReady ] - Connection is not ready.');
    }
    // model defintion path is properly set ?
    if (_.isEmpty(this.paths.model)) {
      this.logger.error('[ YMongoose.isReady ] - Model definition path is not set.');
    }
    // controller defintion path is properly set ?
    if (_.isEmpty(this.paths.validator)) {
      this.logger.error('[ YMongoose.isReady ] - Validator definition path is not set.');
    }

    // methods defintion path is properly set ?
    if (_.isEmpty(this.paths.method)) {
      this.logger.error('[ YMongoose.isReady ] - Methods definition path is not set.');
    }
    // enums defintion path is properly set ?
    if (_.isEmpty(this.paths.enums)) {
      this.logger.warning('[ YMongoose.isReady ] - Enum definition path is not set.');
    }

  }

  // default statement
  return this.isConnected() && !_.isEmpty(this.paths.model) &&
         !_.isEmpty(this.paths.validator) && !_.isEmpty(this.paths.method);
};

/**
 * Create a model from given data
 *
 * @param {Object} value data to used for creation
 * @return {Boolean} created model or false if an error occured
 */
YMongoose.prototype.addModel = function (value) {
  // create async
  var deferred = Q.defer();

  // is Ready ??
  if (this.isReady(true)) {
    // has properties property from current model
    if (!_.isObject(value) || _.isEmpty(value) || !_.has(value, 'model') ||
        !_.has(value.model, 'properties') || !_.has(value.model, 'name')) {
      // error message
      this.logger.error('[ YMongoose.addModel ] - Cannot create model. Invalid data given');
      // invalid statement
      deferred.reject();
    }

    // message
    this.logger.debug([ '[ YMongoose.addModel ] - Creating model [',
                        value.model.name, ']' ].join(' '));

    // default statement for next process
    var hasElastic = false;
    // elastic is enabled ?
    if (_.has(value.model, 'elastic') && _.isObject(value.model.elastic)) {
      // is enabled ?
      if (this.modules.elastic.configIsReady() &&
          _.isBoolean(value.model.elastic.enable) && value.model.elastic.enable) {
        // debug message
        this.logger.debug([ '[ YMongoose.addModel ] - Elastic mode is enabled for this model.',
                           'Adding default index on all properties to false' ].join(' '));

        // get defautl indexes
        var indexes = this.modules.elastic.addDefaultIndexes(_.cloneDeep(value.model.properties));

        // merge data to get correct value
        _.merge(indexes, value.model.properties);

        // change elastic status
        hasElastic = true;
      }
    }

    // schema value
    var schema = new Schema(value.model.properties);

    // has elastic enable ?
    if (hasElastic) {
      // debug message
      this.logger.debug([ '[ YMongoose.addModel ] - Elastic mode is enabled for this model.',
                         'Adding mongoosastic plugin to current schema' ].join(' '));

      // define here correct client
      // with correct merge values
      var client = new elasticClient.Client(_.merge(_.merge({
        hosts : this.modules.elastic.getHosts()
      }, _.omit(value.model.elastic.options, [ 'hosts', 'host', 'port', 'protocol', 'auth' ])),
        this.modules.elastic.getOptions()));

      // add plugin with client connection
      schema.plugin(elastic, { esClient : client });
    }

    // add flag on schema
    schema.elastic = hasElastic;

    // add crud ?
    if (_.has(value.model, 'crud') && _.has(value.model.crud, 'enable') &&
        _.isObject(value.model.crud) && value.model.crud.enable) {
      // message
      this.logger.debug([ '[ YMongoose.addModel ] - Crud mode is enabled.',
                         'Try to add default methods' ].join(' '));

      // normalize redis include
      var redisIncludes = false;

      // has redis include define ?
      if (value.model.crud.redis && value.model.crud.redis.include) {
        // redis include
        redisIncludes = {
          value   : value.model.crud.redis.include,
          expire  : value.model.crud.redis.expire
        };
      }

      // process
      var cschema = this.createCrud(schema, value.model.crud.exclude, redisIncludes);

      // is valid ?
      if (cschema) {
        // message
        this.logger.debug([ '[ YMongoose.addModel ] - Adding Crud method success' ].join(' '));
        // assign
        schema = cschema;
      }
    }

    // add validator ?
    if (!_.isUndefined(value.model.validator) && !_.isNull(value.model.validator) &&
       _.isString(value.model.validator) && !_.isEmpty(value.model.validator)) {
      // messsage
      this.logger.debug([ '[ YMongoose.addModel ] - A validator is defined.',
                         'Try to add a validate method.' ].join(' '));
      // process
      var vschema = this.createValidator(schema, value.model.validator,
                                         value.model.name.toLowerCase());

      // is valid ?
      if (vschema) {
        // message
        this.logger.debug([ '[ YMongoose.addModel ] - Adding validate method success' ].join(' '));
        // assign
        schema = vschema;
      }
    }

    // add methods ??
    if (!_.isUndefined(value.model.fn) && !_.isNull(value.model.fn) &&
       _.isArray(value.model.fn) && !_.isEmpty(value.model.fn)) {
      // messsage
      this.logger.debug([ '[ YMongoose.addModel ] - External methods are defined.',
                         'Try to add them.' ].join(' '));

      // process
      var mschema = this.createMethod(schema, value.model.fn, value.model.name.toLowerCase());

      // is valid ?
      if (mschema) {
        // message
        this.logger.debug([ '[ YMongoose.addModel ] - Adding external methods success' ].join(' '));
        // assign
        schema = mschema;
      }
    }

    // path is not empty so load
    if (!_.isEmpty(this.paths.enums)) {
      // load enums works ?
      if (this.modules.enums.load(this.paths.enums)) {
        // load ok
        this.logger.debug('[ YMongoose.addModel ] - loading enums value success');
      } else {
        // load nok
        this.logger.warning('[ YMongoose.addModel ] - loading enums value failed');
      }
    }

    // add default enums instance value
    schema.static('enums', function () {
      // return enums instance
      return this.modules.enums;
    }.bind(this));

    // valid statement & set value to default schema
    var model = this.mongoose.model(value.model.name, schema);

    // has elatic ?
    if (hasElastic) {
      // debug message
      this.logger.debug([ '[ YMongoose.addModel ] - Elastic mode is enabled for this model.',
                         'Create mapping to current model' ].join(' '));

      // create for given model mapping
      model.createMapping(function (err, mapping) {
        // so have error
        if (err) {
          // log error message
          this.logger.error([ '[ YMongoose.addModel ] - Elastic create mapping error :',
                              err ].join(' '));
        } else {
          // log success message
          this.logger.debug([ '[ YMongoose.addModel ] - Elastic create mapping success :',
                              utils.obj.inspect(mapping) ].join(' '));
        }
        // reject or resolve
        if (!err) {
          // resolve if all is ok
          deferred.resolve();
        } else {
          // reject result if mapping failed
          deferred.reject();
        }
      }.bind(this));
    } else {
      // resolve
      deferred.resolve();
    }
  } else {
    // reject
    deferred.reject();
  }

  // default statement
  return deferred.promise;
};

/**
 * Adding crud method to current object
 *
 * @param {Object} value a valid schema instance to use
 * @param {Array} exclude method to exclude on add crud request
 * @param {Object} redisIncludes default redis include config retreive form model definition
 * @return {Object|Boolean} if all is ok return new schema false otherwise
 */
YMongoose.prototype.createCrud = function (value, exclude, redisIncludes) {
  // is Ready ??
  if (this.isReady(true)) {
    if (!(value instanceof Schema)) {
      // invalid instance
      this.logger.warning([ ' [ YMongoose.createCrud ] - Cannot process.',
                            ' given schema is not an instanceof Schema' ].join(' '));
      // invalid statement
      return false;
    }

    // valid statement
    return this.modules.crud.add(value, exclude, redisIncludes, this.modules.redis);
  }
  // default statement
  return false;
};

/**
 * Create & add a validate function on current schema for create usage
 *
 * @param {Object} value default schema to use
 * @param {String} validatorName validator name to retreive on validators files
 * @param {String} modelName model name to use for validator filter files
 * @return {Object|Boolean} if all is ok return new schema false otherwise
 */
YMongoose.prototype.createValidator = function (value, validatorName, modelName) {
  // is Ready ??
  if (this.isReady(true)) {
    if (!(value instanceof Schema)) {
      // invalid instance
      this.logger.warning([ ' [ YMongoose.createValidator ] - Cannot process.',
                            ' given schema is not an instanceof Schema' ].join(' '));
      // invalid statement
      return false;
    }

    // valid statement
    return this.modules.validator.add(value, this.paths.validator, validatorName, modelName);
  }
  // default statement
  return false;
};

/**
 * Adding custom method to current object
 *
 * @param {Object} value default schema to use
 * @param {String} items method name to retreive on validators files
 * @param {String} modelName model name to use for validator filter files
 * @return {Object|Boolean} if all is ok return new schema false otherwise
 */
YMongoose.prototype.createMethod = function (value, items, modelName) {
  // is Ready ??
  if (this.isReady(true)) {
    if (!(value instanceof Schema)) {
      // invalid instance
      this.logger.warning([ ' [ YMongoose.createMethod ] - Cannot process.',
                            ' given schema is not an instanceof Schema' ].join(' '));
      // invalid statement
      return false;
    }

    // valid statement
    return this.modules.method.add(value, this.paths.method, items, modelName, this.modules.redis);
  }
  // default statement
  return false;
};

/**
 * Load models, from given path
 *
 * @return {Object} a valid promise
 */
YMongoose.prototype.load = function () {
  // Create our deferred object, which we will use in our promise chain
  var deferred = Q.defer();

  var errors  = [];   // list of errors

  // nbItem error on load
  var nbItems = {
    total     : 0,
    processed : 0
  };

  // check model definition & controller first
  var model = glob.sync('**/*.json', {
    cwd       : this.paths.model,
    realpath  : true
  });

  // define validator Schema
  var vschema = joi.object().keys({
    model : joi.object().keys({
      name        : joi.string().required(),
      properties  : joi.object().required(),
      crud        : joi.object().required().keys({
        enable  : joi.boolean().required(),
        exclude : joi.array().required().empty(),
        redis   : joi.object().optional().keys({
          enable  : joi.boolean().required().default(false),
          expire  : joi.number().optional().min(0).default(0),
          include : joi.array().items(joi.string().empty().valid([ 'get', 'getOne' ]))
        })
      }).allow('enable', 'exclude'),
      validator   : joi.string().optional()
    }).unknown()
  }).unknown();

  // create execution queue with 100 concurrency
  var queue = async.queue(function (task, callback) {
    // validate
    var status = joi.validate(task.data, vschema);

    // display big message for more readable log
    this.logger.debug([ '----------------------------',
                          'Processing : [', task.file, ']',
                          '----------------------------' ].join(' '));
    // has error ?
    if (!_.isNull(status.error)) {
      // default error message
      var message = [ 'Invalid schema for [', task.file, '] Error is :',
                      status.error ].join(' ');
      // warning message
      this.logger.error([ '[ YMongoose.load.queue ] -', message ].join(' '));
      // callback with error
      return callback(message);
    } else {
      // add new model
      this.addModel(task.data).then(function () {
        // change nb added items value
        nbItems.processed++;
        // normal process all is ok
        callback();
      }).catch(function () {
        // callback with error
        callback([ 'Cannot create model for  [', task.file, ']' ].join(' '));
      });
    }
  }.bind(this), 100);

  // Callback at the end of queue processing
  queue.drain = function () {
    // message drain ending
    this.logger.debug([ _.repeat('-', 28),
      '[ Process Queue Complete ]', _.repeat('-', 28) ].join(' '));
    this.logger.info('[ YMongoose.load ] - All Model was processed & loaded.');

    // changed loaded status
    this.loaded = (nbItems.processed === nbItems.total);
    // all is processed ?
    if (this.loaded) {
      // success message
      // all is ok so resolve
      deferred.resolve();
    } else {
      // all was not processed
      this.logger.error([ '[ YMongoose.load ] -',
                             'All item was NOT correctly processed.',
                             'Check your logs.' ].join(' ')
                          );
      // reject
      deferred.reject();
      // disconnect error occured
      this.disconnect();
    }
  }.bind(this);

  // all tasks storage
  var tasks = [];
  // run each model
  async.each(model, function (m, next) {
    // build file name
    var name = m.replace(path.dirname(m), '');
    // try & catch error
    try {
      // parsed file
      var parsed = JSON.parse(fs.readFileSync(m, 'utf-8'));
      // parsed not failed push to queue
      tasks.push({ file : name, data : parsed });
      // increment counter
      nbItems.total++;
      // got to next item
      next();
    } catch (e) {
      // set correct message
      var message = [ 'Cannot add item to queue. Error is : [', e, '] for [', name, ']' ].join(' ');
      // error occured
      this.logger.error([ '[ YMongoose.load ] -', message ].join(' '));

      // reject with correct message
      deferred.reject(message);
      // disconnect
      this.disconnect();
    }
  }.bind(this), function () {
    // push in queue
    queue.push(tasks, function (error) {
      // has error ?
      if (error) {
        // log error message
        this.logger.error([ '[ YMongoose.load ] - Cannot add an item to queue [',
                                  error , ']' ].join(' '));
        // push error on list for drain reject
        errors.push(error);
      }
    }.bind(this));
  }.bind(this));

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

/**
 * Retreive a valid model for usage
 *
 * @param {String} name model name wanted
 * @param {Boolean} isInstance true is we want an instance false otherwise
 * @return {Boolean|Instance} false if an error occured, a model object if all is ok
 */
YMongoose.prototype.getModel = function (name, isInstance) {
  // is ready ??
  if (this.isReady(true) && this.isLoaded() &&
      _.isString(name) && !_.isEmpty(name)) {
    // do it in try catch
    try {
      // try to get model
      var Model = this.mongoose.model(name);
      // add mongoose Types on model
      Model.Types = mongoose.Types;
      // valid statement
      return (_.isBoolean(isInstance) && isInstance) ? new Model() : Model;
    } catch (e) {
      // show error
      this.logger.error('[ YMongoose.getModel ] - Model not found. Invalid schema name given.');
      // debug message
      this.logger.debug([ '[ YMongoose.getModel ] -', e ].join(' '));
      // invalid statement
      return false;
    }
  }

  // error here
  this.logger.error('[ YMongoose.getModel ] - Cannot get model. Invalid schema name given.');
  // invalid statement
  return false;
};

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