diff --git a/index.js b/index.js index f315aed..50f53ea 100644 --- a/index.js +++ b/index.js @@ -17,6 +17,14 @@ env.app.configure(function() { env.app.use( cors( env.config.cors ) ); }); +// Add some classes for simplicity +var classes = require( 'classes' ); +injector.instance( 'Class', classes.Class ); +injector.instance( 'Model', classes.Model ); +injector.instance( 'Service', classes.Service ); +injector.instance( 'Controller', classes.Controller ); +injector.instance( 'Module', classes.Module ); + // Load all the modules env.moduleLoader.loadModules(); diff --git a/lib/classes/Class.js b/lib/classes/Class.js new file mode 100644 index 0000000..cf92bf2 --- /dev/null +++ b/lib/classes/Class.js @@ -0,0 +1,5 @@ +var Class = require( 'uberclass' ) + , EventEmitter = require( 'events' ).EventEmitter + , uberUtil = require( 'utils' ).uberUtil; + +module.exports = Class.extend( uberUtil.inherits( {}, EventEmitter ) ); \ No newline at end of file diff --git a/lib/classes/Controller.js b/lib/classes/Controller.js index ef4e817..dea6ca8 100644 --- a/lib/classes/Controller.js +++ b/lib/classes/Controller.js @@ -1,4 +1,4 @@ -var Controller = require('clever-controller'); +var Controller = require( 'clever-controller' ); module.exports = Controller.extend( /* @Static */ @@ -9,14 +9,11 @@ module.exports = Controller.extend( { listAction: function() { if ( this.Class.service !== null ) { - var query = this.req.query; - var options = {}; - if ( Object.keys( query ).length ) { - options.where = query; - } - this.Class.service.find( options ) - .then( this.proxy( 'send' ) ) - .fail( this.proxy( 'handleException' ) ); + this.Class + .service + .findAll( this.req.query ) + .then( this.proxy( 'handleServiceMessage' ) ) + .catch( this.proxy( 'handleException' ) ); } else { this.next(); } @@ -24,9 +21,11 @@ module.exports = Controller.extend( getAction: function() { if ( this.Class.service !== null ) { - this.Class.service.findById( this.req.params.id ) - .then( this.proxy( 'send' ) ) - .fail( this.proxy( 'handleException' ) ); + this.Class + .service + .findById( this.req.params.id ) + .then( this.proxy( 'handleServiceMessage' ) ) + .catch( this.proxy( 'handleException' ) ); } else { this.next(); } @@ -34,9 +33,11 @@ module.exports = Controller.extend( postAction: function() { if ( this.Class.service !== null ) { - this.Class.service.create( this.req.body ) - .then( this.proxy( 'send' ) ) - .fail( this.proxy( 'handleException' ) ); + this.Class + .service + .create( this.req.body ) + .then( this.proxy( 'handleServiceMessage' ) ) + .catch( this.proxy( 'handleException' ) ); } else { this.next(); } @@ -44,9 +45,11 @@ module.exports = Controller.extend( putAction: function() { if ( this.Class.service !== null ) { - this.Class.service.update( this.req.params.id, this.req.body ) - .then( this.proxy( 'send' ) ) - .fail( this.proxy( 'handleException' ) ); + this.Class + .service + .update( this.req.params.id, this.req.body ) + .then( this.proxy( 'handleServiceMessage' ) ) + .catch( this.proxy( 'handleException' ) ); } else { this.next(); } @@ -54,9 +57,11 @@ module.exports = Controller.extend( deleteAction: function() { if ( this.Class.service !== null ) { - this.Class.service.destroy( this.req.params.id ) - .then( this.proxy( 'send' ) ) - .fail( this.proxy( 'handleException' ) ); + this.Class + .service + .destroy( this.req.params.id ) + .then( this.proxy( 'handleServiceMessage' ) ) + .catch( this.proxy( 'handleException' ) ); } else { this.next(); } diff --git a/lib/classes/Model.js b/lib/classes/Model.js new file mode 100644 index 0000000..8f1ee18 --- /dev/null +++ b/lib/classes/Model.js @@ -0,0 +1,491 @@ +var Class = require( 'uberclass' ) + , Promise = require( 'bluebird' ) + , async = require( 'async' ) + , debuggr = require( 'debug' )( 'Models' ) + , injector = require( 'injector' ) + , moduleLdr = injector.getInstance( 'moduleLoader' ) + , models = {}; + +module.exports = Class.extend( +/* @Static */ +{ + // Either 'ORM' or 'ODM' + type: 'ORM', + + // Get the model cache + getDefinedModels: function() { + return models; + }, + + // Behaviours you can use on models you define + softDeletable: false, + versionable: false, + timeStampable: true, + + // The function you call to create a new model + extend: function() { + if ( models[ arguments[ 0 ] ] !== undefined ) { + debuggr( arguments[ 0 ] + 'Model: Returning model class from the cache...' ); + return models[ arguments[ 0 ] ]; + } + + var extendingArgs = [].slice.call( arguments ) + , modelName = extendingArgs.shift() + , Static = ( extendingArgs.length === 2 ) + ? extendingArgs.shift() + : {} + , Proto = extendingArgs.shift() + , extendingArgs = [ Static, Proto ] + , modelType = Static.type !== undefined + ? Static.type + : this.type + , moduleName = 'clever-' + modelType.toLowerCase() + , driver = null + , model = null; + + // Make sure no one can override our extend function + if ( Static.extend ) { + delete Static.extend; + } + + debuggr( [ modelName + 'Model: Defining model using', modelType, 'type...' ].join( ' ' ) ); + + Static._name = modelName; + Static.type = modelType; + + debuggr( modelName + 'Model: Checking to see if the driver is installed and enabled...' ); + if ( moduleLdr.moduleIsEnabled( moduleName ) !== true ) { + throw new Error( [ 'To use type', modelType, 'on your', modelName, 'model you need to enable the', moduleName, 'module!' ].join( ' ' ) ); + } else { + Static._driver = driver = injector.getInstance( modelType.toLowerCase() === 'orm' ? 'cleverOrm' : 'cleverOdm' ); + var debug = function( msg ) { + debuggr( modelName + 'Model: ' + msg ); + }; + } + + debug( 'Defining models this.debug() helper...' ); + Proto.debug = Static.debug = function( msg ) { + driver.debug( modelName + 'Model: ' + msg ); + }; + + debug( 'Checking for defined getters and setters...' ); + + if ( Proto.getters !== undefined ) { + Static._getters = Proto.getters; + delete Proto.getters; + } + + if ( Proto.setters !== undefined ) { + Static._setters = Proto.setters; + delete Proto.setters; + } + + debug( 'Defining schema...' ); + Object.keys( Proto ).forEach( this.callback( 'getSchemaFromProto', Proto, Static ) ); + + debug( 'Setting up behaviours...' ); + [ 'softDeletable', 'versionable', 'timeStampable' ].forEach(function( behaviour ) { + + Static[ behaviour ] = Static[ behaviour ] !== undefined + ? Static[ behaviour ] + : this[ behaviour ]; + + }.bind( this )); + + // Add the accessedAt field if we are timeStampable + if ( Static.timeStampable === true ) { + debug( 'Defining timeStampable behaviour schema fields...' ); + + Static._schema.createdAt = Date; + Static._schema.updatedAt = Date; + } + + // Add the deletedAt field if we are softDeletable + if ( !!Static.softDeletable ) { + debug( 'Defining softDeletable behaviour schema fields...' ); + + if ( modelType.toLowerCase() === 'odm' ) { + Static._schema.deletedAt = { + type: Date, + default: null + }; + } + } + + debug( 'Generating native model using driver.parseModelSchema()...' ); + Static._model = driver.parseModelSchema( Static, Proto ); + + debug( 'Creating model class...' ); + model = this._super.apply( this, extendingArgs ); + + models[ modelName ] = model; + return model; + }, + + // Private function used to build _schema so it can be passed to the _driver for schema creation + getSchemaFromProto: function( Proto, Static, key ) { + var prop = Proto[ key ]; + + if ( prop === Date || prop === String || prop instanceof String || prop === Number || prop === Boolean || ( typeof prop === 'object' ) ) { + if ( typeof Static._schema !== 'object' ) { + Static._schema = {}; + } + + if ( typeof Static._getters !== 'object' ) { + Static._getters = {}; + } + + if ( typeof Static._setters !== 'object' ) { + Static._setters = {}; + } + + Static._schema[ key ] = prop; + Static._getters[ key ] = function() { + if ( key === 'id' && Static.type.toLowerCase() === 'odm' ) { + return this._model._id; + } else { + return this._model[ key ]; + } + }; + Static._setters[ key ] = function( val ) { + this._model[ key ] = val; + }; + + delete Proto[ key ]; + } + }, + + find: function( id ) { + var modelType = this.type.toUpperCase + ? this.type.toUpperCase() + : this.type + , isModel = !!this._model && this._model !== null + , that = this + , options = !/^[0-9a-fA-F]{24}$/.test( id ) && isNaN( id ) + ? id + : {}; + + return new Promise(function( resolve, reject ) { + + // Configure options + if ( !!id && ( /^[0-9a-fA-F]{24}$/.test( id ) || !isNaN( id ) ) ) { + if ( modelType === 'ODM' ) { + try { + options._id = that._driver.mongoose.Types.ObjectId.fromString( id ); + } catch( err ) { + resolve( null ); + return; + } + } else { + options.id = id; + } + } + + // Make sure we have either an id or options to find by models with + if ( !!isModel && !id && !options ) { + reject( [ 'You must specify either an id or an object containing fields to find a', that._name ].join( ' ' ) ); + return; + } + + async.waterfall( + [ + function validateModel( callback ) { + callback( !!isModel ? null : 'You cannot call Model.find() directly on the model class.' ); + }, + + function softDeletable( callback ) { + if ( !!that.softDeletable ) { + options.deletedAt = null; + } + callback( null ); + }, + + function findModel( callback ) { + that.debug( 'find(' + ( typeof options === 'object' ? JSON.stringify( options ) : options ) + ')' ); + + if ( modelType === 'ORM' ) { + + that._model + .find( { where: options } ) + .success( that.callback( callback, null ) ) + .error( callback ); + + } else if ( modelType === 'ODM' ) { + + that._model.findOne( options, callback ); + + } else { + + callback( 'Unsupported Model Type(' + modelType + ')' ); + + } + } + ], + function returnFoundModel( err, _model ) { + !!err + ? reject( [ 'Unable to find the', that._name, 'model because of', err ].join( ' ' ) ) + : resolve( !!_model && _model !== null ? new that( _model ) : null ); + } + ); + }); + }, + + findById: function( id ) { + return this.find( id ); + }, + + findOne: function( id ) { + return this.find( id ); + }, + + findAll: function( options ) { + var modelType = this.type.toUpperCase + ? this.type.toUpperCase() + : this.type + , isModel = !!this._model && this._model !== null + , that = this; + + return new Promise(function( resolve, reject ) { + async.waterfall( + [ + function validateModel( callback ) { + callback( !!isModel ? null : 'You cannot call Model.findAll() directly on the model class.' ); + }, + + function softDeletable( callback ) { + if ( !!that.softDeletable ) { + options.deletedAt = null; + } + callback( null ); + }, + + function findModels( callback ) { + that.debug( 'findAll(' + ( typeof options === 'object' ? JSON.stringify( options ) : options ) + ')' ); + + if ( modelType === 'ORM' ) { + + that._model + .find( { where: options } ) + .success( that.callback( callback, null ) ) + .error( callback ); + + } else if ( modelType === 'ODM' ) { + + that._model.find( options, callback ); + + } else { + + callback( 'Unsupported Model Type(' + modelType + ')' ); + + } + } + ], + function returnFoundModels( err, _models ) { + var models = [] + , _models = _models instanceof Array + ? _models + : [ _models ]; + + if ( !err ) { + _models.forEach(function( model ) { + if ( model !== null ) { + models.push( new that( model ) ); + } + }); + + resolve( models ); + } else { + reject( [ 'Unable to find any', that._name, 'models because of', err ].join( ' ' ) ); + } + } + ); + }); + }, + + create: function( data ) { + var modelType = this.type.toUpperCase + ? this.type.toUpperCase() + : this.type + , isModel = !!this._model && this._model !== null + , that = this; + + return new Promise(function( resolve, reject ) { + async.waterfall( + [ + function validateModel( callback ) { + callback( !!isModel ? null : 'You cannot call Model.create() directly on the model class.' ); + }, + + function timeStampable( callback ) { + if ( modelType === 'ODM' && !!that.timeStampable ) { + data.createdAt = Date.now(); + data.updatedAt = Date.now(); + } + callback( null ); + }, + + function createModel( callback ) { + that.debug( 'create(' + JSON.stringify( data ) + ')' ); + + if ( modelType === 'ORM' ) { + + that._model + .create( data ) + .success( that.callback( callback, null ) ) + .error( callback ) + + } else if ( modelType === 'ODM' ) { + + that._model.create( data, callback ); + + } else { + + callback( 'Unsupported Model Type(' + modelType + ')' ); + + } + } + ], + function returnCreatedModel( err, _model ) { + !!err + ? reject( [ 'Unable to create', that._name, 'because of', err ].join( ' ' ) ) + : resolve( new that( _model instanceof Array ? _model[ 0 ] : _model ) ); + } + ); + }); + } +}, +/* @Prototype */ +{ + setup: function( model ) { + this._setModel( model ); + Object.keys( this.Class._getters ).forEach( this.proxy( '_setupProperty' ) ); + }, + + _setupProperty: function( propName ) { + Object.defineProperty( this, propName, { + get: this.proxy( this.Class._getters[ propName ] ), + set: this.proxy( this.Class._setters[ propName ] ), + enumerable: true + }); + }, + + _setModel: function( _model ) { + this._model = _model; + }, + + map: function() { + return this._model.map.apply( this, arguments ); + }, + + save: function() { + var that = this; + + this.debug( 'save(' + JSON.stringify( this ) + ')' ); + + return new Promise( function( resolve, reject ) { + if ( that.Class.type.toLowerCase() === 'orm' ) { + + that._model + .save() + .success( function( _model ) { + that._setModel( _model ); + resolve( that ); + }) + .error( reject ) + + } else if ( that.Class.type.toLowerCase() === 'odm' ) { + + if ( !!that.Class.timeStampable ) { + that._model.updatedAt = Date.now(); + } + + that._model + .save( function( err, _model ) { + if ( !err ) { + that._setModel( _model ); + resolve( that ); + } else { + reject( err ); + } + }); + + } else { + reject( 'Unsupported Model Type' ); + } + }); + }, + + destroy: function() { + var that = this; + + this.debug( 'destroy(' + JSON.stringify( this ) + ')' ); + + return new Promise( function( resolve, reject ) { + if ( that.Class.type.toLowerCase() === 'orm' ) { + + that._model + .destroy() + .success( function( _model ) { + delete that._model; + resolve( {} ); + }) + .error( reject ) + + } else if ( that.Class.type.toLowerCase() === 'odm' ) { + + if ( !!that.Class.softDeletable ) { + // Perform softDelete + that._model.deletedAt = Date.now(); + that._model + .save( function( err, _model ) { + if ( !err ) { + delete that._model; + resolve( {} ); + } else { + reject( err ); + } + }); + + } else { + that._model.remove(function( err ) { + if ( !err ) { + delete that._model; + resolve( {} ); + } else { + reject( err ); + } + }); + } + + } else { + reject( 'Unsupported Model Type' ); + } + }); + }, + + toJSON: function() { + var that = this + , json; + + if ( this.Class.type === 'ORM' ) { + json = this._model.values; + } else { + json = this._model.toObject(); + + // Add in the id if we have it defined + if ( !!json._id ) { + json.id = json._id; + delete json._id; + } + } + + // Add in getters + Object.keys( this.Class._getters ).forEach( function( getterName ) { + if ( json[ getterName ] === undefined ) { + json[ getterName ] = that[ getterName ]; + } + }); + + return json; + } +}); \ No newline at end of file diff --git a/lib/classes/ModuleClass.js b/lib/classes/Module.js similarity index 50% rename from lib/classes/ModuleClass.js rename to lib/classes/Module.js index db8a3c0..f57fc41 100644 --- a/lib/classes/ModuleClass.js +++ b/lib/classes/Module.js @@ -1,23 +1,26 @@ -var Class = require( 'uberclass' ) - , path = require( 'path' ) - , fs = require( 'fs' ) - , debug = require( 'debug' )( 'Modules' ) - , config = injector.getInstance( 'config' ) - , moduleLoader = injector.getInstance( 'moduleLoader' ); - -module.exports = Class.extend( +var Class = require( 'uberclass' ) + , path = require( 'path' ) + , fs = require( 'fs' ) + , injector = require( 'injector' ) + , i = require( 'i' )() + , moduleDebug = require('debug')( 'Modules' ) + , config = injector.getInstance( 'config' ) + , modules = {} + , Module; + +Module = Class.extend( { moduleFolders: [ 'exceptions', 'classes', - 'models/orm', - 'models/odm', + 'models', 'services', 'controllers', 'tasks' ], injectableFolders: [ + 'models', 'controllers', 'services' ], @@ -32,29 +35,34 @@ module.exports = Class.extend( { name: null, - paths: null, - config: null, - setup: function( name, injector ) { - debug( 'setup called for module ' + name ); - - // Set our module name - this.name = name; + path: null, - // Allow some code to be executed before the main setup - this.hook( 'preSetup' ); + pkg: null, + + paths: null, + + setup: function( _name, _path, _pkg ) { + // Set our module name + this.name = _name; // Set our config if there is any - this.config = typeof config[ name ] === 'object' - ? config[ name ] + this.config = typeof config[ _name ] === 'object' + ? config[ _name ] : {}; // Set the modules location - this.modulePath = [ path.dirname( path.dirname( __dirname ) ), 'modules', this.name ].join( path.sep ); + this.path = _path; - // Add the modulePath to our list of paths - this.paths = [ this.modulePath ]; + // Set the modules package.json + this.pkg = _pkg; + + // Allow some code to be executed before the main setup + this.hook( 'preSetup' ); + + // Add the modules path to our list of paths + this.paths = [ _path ]; // Add our moduleFolders to the list of paths, and our injector paths this.Class.moduleFolders.forEach( this.proxy( 'addFolderToPath', injector ) ); @@ -65,13 +73,15 @@ module.exports = Class.extend( hook: function( hookName ) { if ( typeof this[ hookName ] === 'function' ) { - debug( hookName + ' hook called for module ' + this.name ); - this[ hookName ]( injector ); + this.debug( 'calling ' + hookName + '() hook...' ); + + // @TODO implement injector.injectSync() for use cases like this + this[ hookName ](); } }, addFolderToPath: function( injector, folder ) { - var p = [ this.modulePath, folder ].join( path.sep ) + var p = [ this.path, folder ].join( path.sep ) , obj = {} , folders = p.split( '/' ) , currentFolder = null @@ -101,12 +111,8 @@ module.exports = Class.extend( } } this[ rootFolder ] = obj; - - // Dont add paths for disabled model modules - if ( rootFolder !== 'models' || ( rootFolder === 'models' && moduleLoader.moduleIsEnabled( 'clever-' + currentFolder ) ) ) { - this.paths.push( p ); - injector._inherited.factoriesDirs.push( p ); - } + this.paths.push( p ); + injector._inherited.factoriesDirs.push( p ); }, loadResources: function() { @@ -148,37 +154,89 @@ module.exports = Class.extend( } } - if ( rootFolder === 'models' ) { - // Only include models for enabled modules - if ( moduleLoader.moduleIsEnabled( 'clever-' + currentFolder ) ) { - lastFolder[ name ] = require( 'clever-' + currentFolder ).getModel( [ pathToInspect, '/', file ].join( '' ) ); - } - } else { - // Load the resource - resource = require( [ pathToInspect, '/', file ].join( '' ) ); - - // Allow injection of certain dependencies - if ( typeof resource === 'function' && this.Class.injectableFolders.indexOf( rootFolder ) !== -1 ) { - debug( 'Injecting the ' + name + ' resource.' ); - resource = injector.inject( resource ); - } + // Load the resource + resource = require( [ pathToInspect, '/', file ].join( '' ) ); - // Add the resource to the injector - if ( name !== 'routes' ) { - debug( 'Adding ' + name + ' to the injector' ); - injector.instance( name, resource ); - } + // Allow injection of certain dependencies + if ( typeof resource === 'function' && this.Class.injectableFolders.indexOf( rootFolder ) !== -1 ) { + this.debug( 'Injecting the ' + name + ' resource.' ); + resource = injector.inject( resource ); + } - // Add the resource to the last object we found - lastFolder[ name ] = resource; + // Add the resource to the injector + if ( name !== 'routes' ) { + this.debug( 'Adding ' + name + ' to the injector' ); + injector.instance( name, resource ); } + + // Add the resource to the last object we found + lastFolder[ name ] = resource; } }, initRoutes: function() { if ( typeof this.routes === 'function' ) { - debug( 'initRoutes for module ' + this.name ); + this.debug( 'calling initRoutes() hook...' ); injector.inject( this.routes ); } } }); + +module.exports = { + Class: Module, + extend: function() { + var Reg = new RegExp( '\\)?.*\\(([^\\[\\:]+).*\\)', 'ig' ) + , stack = new Error().stack.split( '\n' ); + + // Get rid of the Error at the start + stack.shift(); + + if ( Reg.test( stack[ 1 ] ) ) { + var modulePath = RegExp.$1.split( path.sep ) + , modulePath = modulePath.splice( 0, modulePath.length - 1 ).join( path.sep ) + , moduleName = path.basename( modulePath ); + } else { + throw new Error( 'Error loading module, unable to determine modules location and name.' ); + } + + var extendingArgs = [].slice.call( arguments ) + , Static = ( extendingArgs.length === 2 ) + ? extendingArgs.shift() + : {} + , Proto = extendingArgs.shift() + , extendingArgs = [ Static, Proto ] + , pkg = [ modulePath, 'package.json' ]; + + if ( modules[ moduleName ] !== undefined ) { + moduleDebug( 'Returning previously defined module ' + moduleName + '...' ); + return modules[ moduleName ]; + } + + moduleDebug( 'Setting up ' + moduleName + ' module from path ' + modulePath + '...' ); + if ( Static.extend ) { + moduleDebug( 'You cannot override the extend() function provided by the CleverStack Module Class!' ); + delete Static.extend; + } + + if ( fs.existsSync( pkg ) ) { + moduleDebug( 'Loading ' + pkg + '...' ); + pkg = require( pkg ); + } else { + pkg = false; + } + + Proto._camelName = i.camelize( moduleName.replace( /\-/ig, '_' ), false ); + moduleDebug( 'Creating debugger with name ' + Proto._camelName + '...' ); + Proto.debug = require( 'debug' )( Proto._camelName ); + + moduleDebug( 'Creating module class...' ); + var Klass = Module.extend( Static, Proto ); + + modules[ moduleName ] = Klass; + + moduleDebug( 'Creating instance of module class...' ); + var instance = new Klass( moduleName, modulePath, pkg ); + + return instance; + } +} \ No newline at end of file diff --git a/lib/classes/Service.js b/lib/classes/Service.js new file mode 100644 index 0000000..253aca4 --- /dev/null +++ b/lib/classes/Service.js @@ -0,0 +1,229 @@ +var Class = require( 'uberclass' ) + , Promise = require( 'bluebird' ) + , path = require( 'path' ) + , util = require( 'util' ) + , injector = require( 'injector' ) + , debug = require( 'debug' )( 'Services' ) + , Model = injector.getInstance( 'Model' ) + , services = []; + +module.exports = Class.extend( +/** @Static **/ +{ + model: null, + + db: null, + + getDefinedServices: function() { + return services; + }, + + extend: function() { + var Reg = new RegExp( '\\)?.*\\(([^\\[\\:]+).*\\)', 'ig' ) + , stack = new Error().stack.split( '\n' ); + + // Get rid of the Error at the start + stack.shift(); + + // Use regular expression to get the name of this service + if ( Reg.test( stack[ 2 ] ) ) { + var serviceName = RegExp.$1.split( path.sep ).pop().replace( '.js', '' ); + } else { + throw new Error( 'Unable to determine services location and name.' ); + } + + var extendingArgs = [].slice.call( arguments ) + , Static = ( extendingArgs.length === 2 ) + ? extendingArgs.shift() + : {} + , Proto = extendingArgs.shift() + , extendingArgs = [ Static, Proto ]; + + if ( services[ serviceName ] !== undefined ) { + debug( 'Returning previously defined service ' + serviceName + '...' ); + return services[ serviceName ]; + } + + debug( 'Setting up ' + serviceName + '...' ); + + // Set the name of this service + Proto._name = Static._name = serviceName; + + if ( Static.extend ) { + debug( 'You cannot override the extend() function provided by the CleverStack Module Class!' ); + delete Static.extend; + } + + if ( !!Proto.model ) { + if ( Proto.model.extend === Model.extend ) { + debug( 'Using the ' + Proto.model._name + ' model for default (restful) CRUD on this service...' ); + + Proto.db = Proto.model._db; + Static.db = Proto.db; + Static.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...' ); + Static.db = Proto.db; + } + + debug( 'Creating service class...' ); + var service = this._super.apply( this, extendingArgs ); + + debug( 'Creating instance of service class...' ); + var instance = new service(); + + debug( 'Caching...' ); + services[ serviceName ] = instance; + + debug( 'Completed.' ); + return instance; + } +}, +/** @Prototype */ +{ + db: false, + + model: false, + + // Currently only supports Sequelize + query: function( query ) { + this.db.query( sql, null, { raw: true } ); + }, + + // Create a new model + create: function( data ) { + var service = this; + + return new Promise( function( resolve, reject ) { + if ( !service.model ) { + reject( 'Model not found, either set ' + service._name + '.model or implement ' + service._name + '.find()' ); + return; + } + + if ( !!data.id ) { + resolve( { statuscode: 400, message: 'Unable to create a new ' + service.model._name + ', identity already exists.' } ); + } + + service.model + .create( data ) + .then( resolve ) + .catch( reject ); + }); + }, + + // Find one record using either id or a where {} + find: function( idOrWhere ) { + var service = this; + + return new Promise( function( resolve, reject ) { + if ( !service.model ) { + reject( 'Model not found, either set ' + service._name + '.model or implement ' + service._name + '.find()' ); + return; + } + + service.model + .find( idOrWhere ) + + .then( function( model ) { + if ( !!model && !!model.id ) { + resolve( model ); + } else { + resolve( { statuscode: 403, message: service.model._name + " doesn't exist." } ); + } + }) + .catch( reject ); + + }); + }, + + // Find more than one record using using a where {} + findAll: function( where ) { + var service = this; + + return new Promise( function( resolve, reject ) { + if ( !service.model ) { + reject( 'Model not found, either set ' + service._name + '.model or implement ' + service._name + '.find()' ); + return; + } + + service.model + .findAll( where ) + .then( function( models ) { + resolve( models ); + }) + .catch( reject ); + + }); + }, + + // Find one record and update it using either id or a where {} + update: function( idOrWhere, data ) { + var service = this; + + return new Promise( function( resolve, reject ) { + if ( !service.model ) { + reject( 'Model not found, either set ' + service._name + '.model or implement ' + service._name + '.find()' ); + return; + } + + if ( !idOrWhere || idOrWhere === null ) { + resolve( { statuscode: 400, message: 'Unable to update ' + service.model._name + ', unable to determine identity.' } ); + } + + service.model + .find( idOrWhere ) + .then( function( user ) { + if ( !!user && !!user.id ) { + + data.forEach(function( i ) { + user[ i ] = data[ i ]; + }); + + user.save() + .then( resolve ) + .catch( reject ); + + } else { + resolve( { statuscode: 403, message: service.model._name + " doesn't exist." } ); + } + }) + .catch( reject ); + + }); + }, + + // Find one record and delete it using either id or a where {} + destroy: function( idOrWhere ) { + var service = this; + + return new Promise( function( resolve, reject ) { + if ( !service.model ) { + reject( 'Model not found, either set ' + service._name + '.model or implement ' + service._name + '.find()' ); + return; + } + + if ( !idOrWhere || idOrWhere === null ) { + resolve( { statuscode: 400, message: 'Unable to delete ' + service.model._name + ', unable to determine identity.' } ); + } + + service.model + .find( idOrWhere ) + .then( function( user ) { + if ( !!user && !!user.id ) { + user.destroy() + .then( resolve ) + .catch( reject ) + } else { + resolve( { statuscode: 403, message: service.model._name + " doesn't exist." } ); + } + }) + .catch( reject ); + + }); + } +}); \ No newline at end of file diff --git a/lib/injector/index.js b/lib/injector/index.js new file mode 100644 index 0000000..6c72449 --- /dev/null +++ b/lib/injector/index.js @@ -0,0 +1 @@ +module.exports = require( 'clever-injector' )(); \ No newline at end of file diff --git a/lib/models/index.js b/lib/models/index.js index e875341..e8bde08 100644 --- a/lib/models/index.js +++ b/lib/models/index.js @@ -1,18 +1,4 @@ var path = require( 'path' ) - , dbModules = [ 'orm', 'odm' ] - , moduleLoader = require( 'utils' ).moduleLoader.getInstance( ) - , models = {}; + , Model = require( 'classes' ).Model; -dbModules.forEach( function( type ) { - if ( moduleLoader.moduleIsEnabled( 'clever-' + type ) ) { - models[ type ] = {}; - moduleLoader.modules.forEach( function( theModule ) { - Object.keys( theModule.models[ type ] ).forEach( function( key ) { - models[ type ][ key ] = theModule.models[ type ][ key ]; - models[ type ][ key ][ type.toUpperCase() ] = true; - }); - }); - } -}); - -module.exports = models; \ No newline at end of file +module.exports = Model.getDefinedModels(); \ No newline at end of file diff --git a/lib/services/BaseService.js b/lib/services/BaseService.js deleted file mode 100644 index 19c1e08..0000000 --- a/lib/services/BaseService.js +++ /dev/null @@ -1,208 +0,0 @@ -var Class = require('uberclass') - , Q = require('q'); - -module.exports = Class.extend({ - instance: null, - Model: null -}, { - db: null, - - setup: function(dbAdapter) { - this.db = dbAdapter; - }, - - startTransaction: function() { - return this.db.startTransaction(); - }, - - query: function(sql) { - console.log('Running SQL: ' + sql); - return this.db.query(sql, null, { raw: true }); - }, - - findById: function (id) { - var deferred = Q.defer(); - - if (this.Class.Model !== null) { - if( this.Class.Model.ORM ){ - this.Class.Model.find(id).success(deferred.resolve).error(deferred.reject); - } else { - this.Class.Model.findById(id, function(err, result){ - if ( err ) { - process.nextTick(function() { - deferred.reject(); - }); - } else { - process.nextTick(function() { - deferred.resolve(result); - }); - } - }); - } - } else { - process.nextTick(function() { - deferred.reject('Function not defined and no Model provided'); - }); - } - - return deferred.promise; - }, - - findAll: function (options) { - options = options || {}; - var deferred = Q.defer(); - - if (this.Class.Model !== null) { - if ( this.Class.Model.ORM ) { - this.Class.Model.findAll().success(deferred.resolve).error(deferred.reject); - } else { - this.Class.Model.find(function(err, result){ - if ( err ) { - process.nextTick(function() { - deferred.reject(); - }); - } else { - process.nextTick(function() { - deferred.resolve(result); - }); - } - }); - } - - } else { - process.nextTick(function() { - deferred.reject('Function not defined and no Model provided.'); - }); - } - - return deferred.promise; - }, - - find: function (options) { - options = options || {}; - var deferred = Q.defer(); - - if (this.Class.Model !== null) { - if ( this.Class.Model.ORM ) { - this.Class.Model.findAll(options).success(deferred.resolve).error(deferred.reject); - } else { - this.Class.Model.find(options, function(err, result){ - if ( err ) { - process.nextTick(function() { - deferred.reject(); - }); - } else { - process.nextTick(function() { - deferred.resolve(result); - }); - } - }); - } - } else { - process.nextTick(function() { - deferred.reject('Function not defined and no Model provided.'); - }); - } - - return deferred.promise; - }, - - create: function (data) { - var deferred = Q.defer(); - - if (this.Class.Model !== null) { - if ( this.Class.Model.ORM ) { - this.Class.Model.create(data) - .success(deferred.resolve) - .error(deferred.reject); - } else { - new this.Class.Model(data).save(function(err, result){ - if ( err ) { - process.nextTick(function() { - deferred.reject(); - }); - } else { - process.nextTick(function() { - deferred.resolve(result); - }); - } - }); - } - - } else { - process.nextTick(function() { - deferred.reject('Function not defined and no Model provided.'); - }); - } - - return deferred.promise; - }, - - update: function (id, data) { - var deferred = Q.defer(); - - if (this.Class.Model !== null) { - if ( this.Class.Model.ORM ) { - this.Class.Model.find(id) - .success(function ( model ) { - model.updateAttributes(data) - .success(deferred.resolve) - .error(deferred.reject); - }) - .error(deferred.reject); - } else { - this.Class.Model.findOneAndUpdate({_id: id}, data, function(err, result){ - if ( err ) { - process.nextTick(function() { - deferred.reject(); - }); - } else { - process.nextTick(function() { - deferred.resolve(result); - }); - } - }); - } - } else { - process.nextTick(function() { - deferred.reject('Function not defined and no Model provided.'); - }); - } - - return deferred.promise; - }, - - destroy: function (id) { - var deferred = Q.defer(); - - if (this.Class.Model !== null) { - if ( this.Class.Model.ORM ) { - this.Class.Model.find(id) - .success(function ( model ) { - model.destroy() - .success(deferred.resolve) - .error(deferred.reject); - }) - .error(deferred.reject); - } else { - this.Class.Model.findById(id).remove(function(err, result){ - if ( err ) { - process.nextTick(function() { - deferred.reject(); - }); - } else { - process.nextTick(function() { - deferred.resolve(result); - }); - } - }); - } - } else { - process.nextTick(function() { - deferred.reject('Function not defined and no Model provided.'); - }); - } - - return deferred.promise; - } -}); \ No newline at end of file diff --git a/lib/services/index.js b/lib/services/index.js index 50eb4bb..5e24d0d 100644 --- a/lib/services/index.js +++ b/lib/services/index.js @@ -1 +1,4 @@ -module.exports = require( 'utils' ).magicModule( 'services' ); \ No newline at end of file +var path = require( 'path' ) + , Model = require( 'classes' ).Service; + +module.exports = Model.getDefinedServices(); \ No newline at end of file diff --git a/lib/utils/bootstrapEnv.js b/lib/utils/bootstrapEnv.js index bbfd61a..85e623a 100644 --- a/lib/utils/bootstrapEnv.js +++ b/lib/utils/bootstrapEnv.js @@ -14,7 +14,7 @@ module.exports = function( env ) { , bootstrappedEnv; // Bootstrap our DI - GLOBAL.injector = require( 'clever-injector' )(); + GLOBAL.injector = require( 'injector' ); injector.instance( 'express', express ); injector.instance( 'app', app ); diff --git a/lib/utils/moduleLoader.js b/lib/utils/moduleLoader.js index cd1611f..040f973 100644 --- a/lib/utils/moduleLoader.js +++ b/lib/utils/moduleLoader.js @@ -1,14 +1,16 @@ 'use strict'; -var Class = require( 'uberclass' ) - , path = require( 'path' ) +var Class = require( 'uberclass' ) + , path = require( 'path' ) , packageJson = require( path.resolve( __dirname + '/../../' ) + '/package.json' ) - , fs = require( 'fs' ) - , async = require( 'async' ) - , debug = require( 'debug' )( 'moduleLoader' ) - , moduleClass; - -var Module = module.exports = Class.extend( + , fs = require( 'fs' ) + , async = require( 'async' ) + , debug = require( 'debug' )( 'ModuleLoader' ) + , i = require( 'i' )() + , injector = require( 'injector' ) + , Module; + +var ModuleLoader = module.exports = Class.extend( { instance: null, @@ -32,8 +34,8 @@ var Module = module.exports = Class.extend( }, preShutdownHook: function( module ) { - if ( module instanceof moduleClass && typeof module.preShutdown === 'function' ) { - debug( [ 'preShutdown (hook) for module', module.name ].join( ' ' ) ); + if ( module instanceof Module.Class && typeof module.preShutdown === 'function' ) { + module.debug( 'Module.preShutdown() hook...' ); module.preShutdown(); } }, @@ -49,6 +51,8 @@ var Module = module.exports = Class.extend( }, loadModules: function( env ) { + Module = injector.getInstance( 'Module' ); + var self = this; if ( this.modulesLoaded === false ) { @@ -81,37 +85,47 @@ var Module = module.exports = Class.extend( process.env = env; } - debug( [ 'Loading the', moduleName, 'module' ].join( ' ' ) ); + debug( [ 'Loading the', moduleName, 'module' ].join( ' ' ) + '...' ); + var module = require( moduleName ); - // Get a copy of the module class - moduleClass = require( 'classes' ).ModuleClass; + var moduleLowerCamelCase = i.camelize( moduleName.replace( /\-/ig, '_' ), false ); - // Load (require) the module and add to our modules array - this.modules.push( require( moduleName ) ); + debug( [ 'Adding the', moduleLowerCamelCase, 'module to the injector' ].join( ' ' ) + '...' ); + injector.instance( moduleLowerCamelCase, module ); + + // Add the module into our modules array so we can keep track of them internally and call hooks in their module.js file + this.modules.push( module ); }, configureAppHook: function( module ) { - if ( module instanceof moduleClass && typeof module.configureApp === 'function' ) { - debug( 'configureApp hook called for module ' + module.name ); + if ( module instanceof Module.Class && typeof module.configureApp === 'function' ) { + module.debug( 'Module.configureApp() hook...' ); + injector.getInstance( 'app' ).configure( module.proxy( 'configureApp', injector.getInstance( 'app' ), injector.getInstance( 'express' ) ) ); } }, preResourcesHook: function( module ) { - if ( module instanceof moduleClass && typeof module.preResources === 'function' ) { + if ( module instanceof Module.Class && typeof module.preResources === 'function' ) { + module.debug( 'Module.preResources() hook...' ); + module.hook( 'preResources' ); } }, loadModuleResources: function( module ) { - if ( module instanceof moduleClass && typeof module.loadResources === 'function' ) { + if ( module instanceof Module.Class && typeof module.loadResources === 'function' ) { + module.debug( 'Module.loadResources() hook...' ); debug( [ 'loadResources for module', module.name ].join( ' ' ) ); + module.loadResources(); } }, modulesLoadedHook: function( module ) { - if ( module instanceof moduleClass && typeof module.modulesLoaded === 'function' ) { + if ( module instanceof Module.Class && typeof module.modulesLoaded === 'function' ) { + module.debug( 'Module.modulesLoaded() hook...' ); + module.hook( 'modulesLoaded' ); } }, @@ -121,7 +135,7 @@ var Module = module.exports = Class.extend( // Give the modules notice that we are about to add our routes to the app this.modules.forEach( this.proxy( 'preRouteHook' ) ); - // Initialize all the modules routes + debug( 'Initializing routes...' ); this.modules.forEach( this.proxy( 'initializeModuleRoutes' ) ); // We only want to do this once @@ -132,15 +146,15 @@ var Module = module.exports = Class.extend( }, preRouteHook: function( module ) { - if ( module instanceof moduleClass && typeof module.preRoute === 'function' ) { - debug( [ 'preRoute (hook) for module', module.name ].join( ' ' ) ); + if ( module instanceof Module.Class && typeof module.preRoute === 'function' ) { + module.debug( 'Module.configureApp() hook...' ); module.preRoute(); } }, initializeModuleRoutes: function( module ) { - if ( module instanceof moduleClass ) { - debug( [ 'Initializing the', module.name, 'modules routes.' ].join( ' ' ) ); + if ( module instanceof Module.Class ) { + module.debug( [ 'Initializing routes...' ].join( ' ' ) ); module.initRoutes(); } } diff --git a/package.json b/package.json index f13d69e..6dd7e72 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "main": "app.js", "name": "node-seed", "description": "Cleverstack Node-Seed", - "version": "0.1.5", + "version": "1.0.0", "author": { "name": "CleverStack", "email": "admin@cleverstack.io", @@ -20,20 +20,21 @@ "test": "grunt test --verbose" }, "dependencies": { - "express": "~3.4.7", - "clever-controller": "~1.1.2", + "async": "~0.2.9", + "bluebird": "^2.0.2", + "clever-controller": "~1.1.3", "clever-injector": "~1.0.0", - "nconf": "~0.6.9", - "uberclass": "~1.0.1", + "cors": "~2.1.1", "debug": "~0.7.2", + "deepmerge": "~0.2.7", + "express": "~3.4.7", + "i": "~0.3.2", "matchdep": "~0.3.0", - "async": "~0.2.9", - "underscore": "~1.5.2", + "nconf": "~0.6.9", "q": "~1.0.0", - "i": "~0.3.2", - "deepmerge": "~0.2.7", "should": "~3.1.2", - "cors": "~2.1.1" + "uberclass": "~1.0.1", + "underscore": "~1.5.2" }, "devDependencies": { "grunt-contrib-watch": "",