CleverStack/node-seed

View on GitHub
lib/classes/Service.js

Summary

Maintainability
F
3 days
Test Coverage
var injector    = require('injector')
  , exceptions  = require('exceptions')
  , Classes     = require('classes')
  , underscore  = require('underscore')
  , Class       = Classes.Class
  , Model       = Classes.Model
  , Promise     = injector.getInstance('Promise')
  , util        = require('util')
  , utils       = require('utils')
  , debug       = require('debug')('cleverstack:services')
  , services    = {}
  , Service;

/**
 * @classdesc CleverStack Service Class
 * @class     Service
 * @extends   Class
 */
Service = Class.extend(
/** @lends Service **/
{
  model: null,

  db: null
},
/** @lends Service# */
{
  db: false,

  model: false,

  // Currently only supports Sequelize
  // @TODO this needs a serious refactor
  query: function(sql, raw) {
    raw = raw || false;
    this.db.query(sql, null, { raw: raw });
  },

  transaction: function(queryOptions) {
    var service = this;

    queryOptions = queryOptions || {};

    return new Promise(function transaction(resolve, reject) {
      if (queryOptions.transaction) {
        resolve(queryOptions);
      } else {
        service.db.transaction({ autocommit: false }).then(function(transaction) {
          if (transaction) {
            queryOptions.transaction = transaction;
            resolve(queryOptions);
          } else {
            reject(transaction);
          }
        });
      }
    });
  },

  // Create a new model
  create: function(values, queryOptions) {
    var service = this;

    values         = values || {};
    queryOptions = queryOptions || {};

    return new Promise(function create(resolve, reject) {
      if (!service.model) {
        reject(new Error('Model not found, either set ' + service._name + '.model or implement ' + service._name + '.create()'));
        return;
      }

      if (!!values.id) {
        reject(new exceptions.DuplicateModel('Unable to create a new ' + service.model.modelName + ', identity already exists.'));
      }

      service
        .model
        .create(values, queryOptions)
        .then(resolve)
        .catch(reject);
    });
  },

  // Find one record using either id or a where {}
  find: function(idOrFindOptions, queryOptions) {
    idOrFindOptions  = (typeof idOrFindOptions !== 'object') ? {where:{id: idOrFindOptions}} : idOrFindOptions;
    if (!idOrFindOptions.where) {
      idOrFindOptions = {where: idOrFindOptions};
    }
    queryOptions    = queryOptions || {};

    return new Promise(function find(resolve, reject) {
      if (!this.model) {
        reject('Model not found, either set ' + this._name + '.model or implement ' + this._name + '.find()');
        return;
      }

      if (typeof idOrFindOptions === 'object' && this.model.type === 'ORM') {
        Object.keys(idOrFindOptions.where).forEach(function(name) {
          if (idOrFindOptions.where[ name ] === 'null') {
            idOrFindOptions.where[ name ] = null;
          }
        });
      }

      this
        .model
        .find(idOrFindOptions, queryOptions)
        .then(this.callback(function(model) {
          if (model !== null && !!model) {
            resolve(model);
          } else {
            reject(new exceptions.ModelNotFound(this.model.modelName + ' doesn\'t exist.'));
          }
        }))
        .catch(reject);

    }
    .bind(this));
  },

  findOrCreate: function(idOrFindOptions, queryOptions) {
    idOrFindOptions  = (typeof idOrFindOptions !== 'object') ? {where:{id: idOrFindOptions}} : idOrFindOptions;
    queryOptions = queryOptions || {};

    if (!queryOptions.where) {
      queryOptions.where = queryOptions;
    }

    return new Promise(function findOrCreate(resolve, reject) {
      if (!this.model) {
        reject('Model not found, either set ' + this._name + '.model or implement ' + this._name + '.find()');
        return;
      }

      if (typeof idOrFindOptions === 'object' && typeof idOrFindOptions.where === 'object' && this.model.type === 'ORM') {
        Object.keys(idOrFindOptions.where).forEach(function(name) {
          if (idOrFindOptions.where[ name ] === 'null') {
            idOrFindOptions.where[ name ] = null;
          }
        });
      }

      this.model
        .findOrCreate(idOrFindOptions, queryOptions)
        .then(resolve)
        .catch(reject);
    }
    .bind(this));
  },

  // Find more than one record using using a where {}
  findAll: function(idOrFindOptions, queryOptions) {
    idOrFindOptions  = (typeof idOrFindOptions !== 'object') ? {where:{id: idOrFindOptions}} : idOrFindOptions;
    if (!idOrFindOptions.where) {
      idOrFindOptions = {where: idOrFindOptions};
    }
    queryOptions    = queryOptions || {};

    return new Promise(function findAll(resolve, reject) {
      if (!this.model) {
        reject('Model not found, either set ' + this._name + '.model or implement ' + this._name + '.find()');
        return;
      }

      if (typeof idOrFindOptions === 'object' && this.model.type === 'ORM') {
        Object.keys(idOrFindOptions.where).forEach(function(name) {
          if (idOrFindOptions.where[ name ] === 'null') {
            idOrFindOptions.where[ name ] = null;
          }
        });
      }

      this
        .model
        .findAll(idOrFindOptions, queryOptions)
        .then(resolve)
        .catch(reject);
    }
    .bind(this));
  },

  update: function(values, queryOptions) {
    values = (typeof values === 'object') ? values : {};
    queryOptions = (typeof queryOptions !== 'object') ? {where:{id: queryOptions}} : queryOptions;
    if (!queryOptions.where) {
      queryOptions = {where: queryOptions};
    }

    return new Promise(function update(resolve, reject) {
      if (!this.model) {
        reject('Model not found, either set ' + this._name + '.model or implement ' + this._name + '.find()');
        return;
      }

      if (!queryOptions || queryOptions === null || !queryOptions.where) {
        reject(new exceptions.ModelNotFound('Unable to update ' + this.model.modelName + ', unable to determine identity.'));
      }

      if (!values || !Object.keys(values).length) {
        reject(new exceptions.InvalidData('Unable to update ' + this.model.modelName + ', you did not provide any data.'));
      }

      this
        .model
        .update(values, queryOptions)
        .then(this.proxy(function(user) {
          if (!!user) {
            resolve(user);
          } else {
            reject(new exceptions.ModelNotFound(this.model.modelName + ' doesn\'t exist.'));
          }
        }))
        .catch(reject);
    }
    .bind(this));
  },

  // Find one record and update it using either id or a where {}
  findAndUpdate: function(values, queryOptions) {
    var service = this;

    queryOptions = queryOptions || {};
    if (!queryOptions.where) {
      queryOptions.where = {
        id: queryOptions
      };
    }

    return new Promise(function update(resolve, reject) {
      if (!service.model) {
        reject('Model not found, either set ' + service._name + '.model or implement ' + service._name + '.find()');
        return;
      }

      if (!queryOptions || queryOptions === null) {
        reject(new exceptions.ModelNotFound('Unable to update ' + service.model.modelName + ', unable to determine identity.'));
      }

      if (!values) {
        reject(new exceptions.InvalidData('Unable to update ' + service.model.modelName + ', you did not provide any data.'));
      }

      service.model
        .find(underscore.pick(queryOptions, 'where'), values)
        .then(function(user) {
          if (!!user) {
            
            user.save(values, queryOptions)
              .then(resolve)
              .catch(reject);

          } else {
            reject(new exceptions.ModelNotFound(service.model.modelName + ' doesn\'t exist.'));
          }
        })
        .catch(reject);

    });
  },

  // Find one record and delete it using either id or a where {}
  destroy: function(queryOptions) {
    queryOptions = (typeof queryOptions !== 'object') ? {where:{id: queryOptions}} : queryOptions;
    if (!queryOptions.where) {
      queryOptions = {where: queryOptions};
    }

    return new Promise(function destroy(resolve, reject) {
      if (!this.model) {
        reject('Model not found, either set ' + this._name + '.model or implement ' + this._name + '.find()');
        return;
      }

      if (!queryOptions || queryOptions === null || !queryOptions.where) {
        reject(new exceptions.ModelNotFound('Unable to delete ' + this.model.modelName + ', unable to determine identity.'));
      }

      this
        .model
        .destroy(queryOptions)
        .then(resolve)
        .catch(reject);
    }
    .bind(this));
  }
});

module.exports.Class  = Service;

module.exports.extend = function() {
  var extendingArgs   = [].slice.call(arguments)
    , serviceName     = (typeof extendingArgs[ 0 ] === 'string') ? extendingArgs.shift() : false
    , Klass           = (extendingArgs.length === 2) ? extendingArgs.shift() : {}
    , Proto           = extendingArgs.shift();
  
  if (!serviceName) {
    if ((serviceName = utils.helpers.getClassName(3)) === false) {
      throw new Error('Unable to determine services location and name.');
    }
  }

  if (services[ serviceName ] !== undefined) {
    debug('Returning previously defined service ' + serviceName + '...');
    return services[ serviceName ];
  }

  debug('Setting up models this.debug() helper...');
  Proto.debug         = Klass.debug = require('debug')('cleverstack:services:' + serviceName);
  Proto.debug.enabled = Klass.debug.enabled;

  debug('Setting up ' + serviceName + '...');
  Proto._name = Klass._name = serviceName;

  if (!!Proto.model) {
    if (Proto.model.extend === Model.extend) {
      debug('Using the ' + Proto.model.modelName + ' model for default (restful) CRUD on this service...');

      Proto.db = Proto.model.connection;
      Klass.db = Proto.db;
      Klass.model = Proto.model;

    } else {
      debug(util.inspect(Proto));
      throw new Error('Unknown model type passed to Service.extend(), set environment variable DEBUG=Services for more information.');
    }
  } else if (!!Proto.db) {
    debug('Setting db adapter for service...');
    Klass.db = Proto.db;
  }

  debug('Creating service class...');
  Klass         = Service.callback('extend')(Klass, Proto);
  var instance  = Klass.callback('newInstance')();

  debug('Parsing templated event handlers...');
  
  injector.getInstance('moduleLoader').on('routesInitialized', function() {
    debug( 'Parsing templated event handlers...' );
    Object.keys(Proto).forEach(function(propName) {
      if (propName.indexOf(' ') !== -1) {
        var parts       = propName.split(' ')
          , resource    = parts.shift()
          , eventName   = parts.shift();

        injector.getInstance(resource).on(eventName, instance.proxy(propName));
      }
    });
  });

  services[ serviceName ] = instance;

  return instance;
};

module.exports.getDefinedServices = function() {
  return services;
};