UNPKG

mvcx

Version:

A web based MVC framework for Node JS.

664 lines (552 loc) 23.7 kB
module.exports = function( configMetadata, options ){ var self = this; this.q = require('q'); this.lazyjs = require('lazy.js'); this.expressApp = null; this.logger = null; this.isInitializationSuccessful = false; this.responseTypes = { VoidResponse: require('./VoidResponse'), DownloadResponse: require('./DownloadResponse'), FileResponse: require('./FileResponse'), RedirectResponse: require('./RedirectResponse'), StreamResponse: require('./StreamResponse'), ViewResponse: require('./ViewResponse'), Response: require('./Response') } this.mvcxConfig = null; this.appConfig = mergeConfig(configMetadata); this.mvcxConfig = self.appConfig.mvcx; if(typeof(options) !== 'undefined' && options !== null){ if(options.expressApp) { this.expressApp = options.expressApp; } } if(this.expressApp === null){ var express = require('express'); this.expressApp = express(); } this.initialize = function(onCompleted){ self.q.Promise(function(resolve, reject, notify) { try{ console.log('info: [mvcx] Initializing...'); if(self.mvcxConfig.clusteringEnabled) { var cluster = require('cluster'); var http = require('http'); var numCPUs = require('os').cpus().length; var appConfig = null; if (cluster.isMaster) { console.log('info: [mvcx] Clustering for ' + numCPUs + ' CPU cores...'); // Fork workers. for (var i = 0; i < numCPUs; i++) { console.log('info: [mvcx] Spawning worker ' + (i + 1) + '...'); cluster.fork(); } console.log('info: [mvcx] Clustering intialized with ' + numCPUs + ' worker processes.'); cluster.on('exit', function (worker, code, signal) { console.log('info: [mvcx] Worker process with process id ' + worker.process.pid + ' terminated.'); }); } else { initializeCore(); } } else{ console.log('info: [mvcx] Clustering is disabled.'); initializeCore(); } var result = { expressApp: self.expressApp, config: self.appConfig }; return resolve(result); } catch(e){ var failureMessage = '[mvcx] Intialization failed.'; if(self.logger != null){ self.logger.error(failureMessage); } else{ console.log(failureMessage); } reject(e); } }).then(function(result){ onCompleted(null, result); }).catch(function(e){ onCompleted(e, null); }).done(); }; this.createHttpServer = function(options) { if(!self.isInitializationSuccessful){ throw new Error('[mvcx] Unable create server when mvcx has not been initialized successfully.'); } self.logger.info('[mvcx] Creating http server...'); var http = self.mvcxConfig.hooks.ioc.resolve('http').value; var server = http.createServer(self.expressApp); self.logger.info('[mvcx] Http server created.'); createServerCore(server); return server; }; this.createHttpsServer = function(options) { if(!self.isInitializationSuccessful){ throw new Error('[mvcx] Unable create server when mvcx has not been initialized successfully.'); } self.logger.info('[mvcx] Creating https server...'); var https = self.mvcxConfig.hooks.ioc.resolve('https').value; var server = https.createServer(options, self.expressApp); self.logger.info('[mvcx] Https server created.'); createServerCore(server); return server; }; this.createWebSocket = function(server){ if(!self.isInitializationSuccessful){ throw new Error('[mvcx] Unable create server when mvcx has not been initialized successfully.'); } self.logger.info('[mvcx] Creating web socket (socket.io) server...'); var socketio = self.mvcxConfig.hooks.ioc.resolve('socket.io').value; return socketio(server); self.logger.info('[mvcx] Web socket server created.'); } function createServerCore(server){ server.on('connection', function (socket) { if(self.mvcxConfig.keepAliveTimeoutSeconds > 0) { //logger.debug('Connection opened. Setting keep alive timeout to %s seconds', config.keepAliveTimeoutSeconds); socket.setKeepAlive(true); socket.setTimeout(self.mvcxConfig.keepAliveTimeoutSeconds * 1000, function () { //logger.debug('Connection closed after exceeding keep alive timeout.'); }); } else{ socket.setKeepAlive(false); } }); if(self.mvcxConfig.keepAliveTimeoutSeconds > 0) { self.logger.info('[mvcx] Server connection keep-alive timeout set to %s seconds.', self.mvcxConfig.keepAliveTimeoutSeconds); }else { self.logger.info('[mvcx] Server connection keep-alive is disabled.'); } } function initializeCore(){ self.logger = initializeLogging(); initializeIoc(); initializeExpress(); initializeRoutes(); self.logger.info('[mvcx] Intialization completed.'); self.isInitializationSuccessful = true; } function initializeRoutes(){ self.logger.info('[mvcx] Loading routes...'); var ModuleLoader = require('./ModuleLoader'); var moduleLoader = new ModuleLoader(); var path = require('path'); var controllers = moduleLoader.load(path.resolve(self.mvcxConfig.controllerPath), self.mvcxConfig.controllerSuffix); if(controllers == null){ self.logger.info('[mvcx] No controllers were found.'); } else{ self.logger.info('[mvcx] Found ' + controllers.length + ' controller(s).'); if(self.mvcxConfig.autoRoutesEnabled){ self.logger.info('[mvcx] Automatic routing enabled.'); } else{ self.logger.info('[mvcx] Automatic routing disabled.'); } var iocContainer = self.mvcxConfig.hooks.ioc; var routeIndex = createRouteIndex(); self.lazyjs(controllers).each(function(controller){ extendController(controller.moduleName, controller.module); if(routeIndex.controllerActionRoutes.has(controller.moduleName)){ var actionsForController = routeIndex.controllerActionRoutes.get(controller.moduleName); self.lazyjs(actionsForController.keys()).each(function(action){ extendAction(controller.module, action); }); } iocContainer.register(controller.moduleName, controller.module, 'perRequest'); var routeForGetDefined = false; var routeForPutDefined = false; var routeForPostDefined = false; var routeForDeleteDefined = false; var routeForPatchDefined = false; if(routeIndex.controllerBasedRoutes.has(controller.moduleName)){ var explicitRouteMethods = routeIndex.controllerBasedRoutes.get(controller.moduleName); routeForGetDefined = explicitRouteMethods.has('get'); routeForPutDefined = explicitRouteMethods.has('put'); routeForPostDefined = explicitRouteMethods.has('post'); routeForDeleteDefined = explicitRouteMethods.has('delete'); routeForPatchDefined = explicitRouteMethods.has('patch'); self.lazyjs(explicitRouteMethods.values()).each(function(routesArray){ self.lazyjs(routesArray).each(function(route){ registerControllerBasedRoute(route, controller.module.$type); }); }); } if(self.mvcxConfig.autoRoutesEnabled){ if(!routeForGetDefined) registerControllerBasedRoute(createAutoRoute('get', controller), controller.module.$type); if(!routeForPutDefined) registerControllerBasedRoute(createAutoRoute('put', controller), controller.module.$type); if(!routeForPostDefined) registerControllerBasedRoute(createAutoRoute('post', controller), controller.module.$type); if(!routeForDeleteDefined) registerControllerBasedRoute(createAutoRoute('delete', controller), controller.module.$type); if(!routeForPatchDefined) registerControllerBasedRoute(createAutoRoute('patch', controller), controller.module.$type); } }); self.lazyjs(routeIndex.viewBasedRoutes).each(function(route){ registerViewBasedRoute(route.method, route.route, route.view); }); } self.logger.info('[mvcx] Loading routes completed.'); }; function mergeConfig(configMetadata) { var environment; var config; console.log('info: [mvcx] Initializing configuration...'); var baseConfig = configMetadata.baseConfig; if (typeof(baseConfig) === 'undefined' || baseConfig == null) { baseConfig = {}; } var merge = require('merge'); console.log('info: [mvcx] Checking environment configuration indicator...'); var environment = null; if(!isEmpty(configMetadata.environmentIndicatorVariable)){ environment = process.env[configMetadata.environmentIndicatorVariable]; } if (!isEmpty(environment)) { console.log('info: [mvcx] Loading configuration override for ' + environment + ' environment.'); var overrideConfig = require(configMetadata.environmentConfigs[environment]); if (!(overrideConfig)) { throw new Error('[mvcx] The ' + env + ' environment configuration override is missing.'); } console.log('info: [mvcx] Merging configuration override for ' + environment + ' environment...'); config = merge.recursive(true, baseConfig, overrideConfig); } else { console.log('info: [mvcx] No environment indicator found. Continuing with the base configuration...'); config = baseConfig; } var overriddenMvcxConfig = config.mvcx; if(isEmpty(overriddenMvcxConfig)){ overriddenMvcxConfig = {}; } console.log('info: [mvcx] Merging mvcx default configuration with specified overrides from the application configuration...'); config.mvcx = merge.recursive(true, require('./DefaultConfig'), overriddenMvcxConfig); var path = require('path'); if(!isEmpty(config.mvcx.assets) && !isEmpty(config.mvcx.assets.paths) && config.mvcx.assets.paths.length > 0){ console.log('info: [mvcx] Resolving asset paths...'); var assetPaths = []; self.lazyjs(config.mvcx.assets.paths).each(function(assetPath){ assetPaths.push(path.resolve(assetPath)); }); config.mvcx.assets.paths = assetPaths; } config.internalViewPath = path.join(__dirname, 'views'); console.log('info: [mvcx] Configuration initialized.'); return config; } function isEmpty(val){ return (typeof (val) === 'undefined' || val == null); } function initializeIoc(){ self.logger.info('[mvcx] Registering dependencies...'); var ioc = self.mvcxConfig.hooks.ioc; ioc.register('expressApp', { value: self.expressApp }, 'singleton'); ioc.register('config', { value: self.appConfig }, 'singleton'); ioc.register('logger', { value: self.logger }, 'singleton'); ioc.register('q', { value: self.q }, 'singleton'); ioc.register('compression', { value: require('compression') }, 'singleton'); ioc.register('body-parser', { value: require('body-parser') }, 'singleton'); ioc.register('http', { value: require('http') }, 'singleton'); ioc.register('https', { value: require('https') }, 'singleton'); ioc.register('socket.io', { value: require('socket.io') }, 'singleton'); ioc.register('merge', { value: require('merge') }, 'singleton'); ioc.register('lazy.js', { value: require('lazy.js') }, 'singleton'); ioc.register('hashmap', { value: require('hashmap') }, 'unique'); ioc.register('connect-assets', { value: require('connect-assets') }, 'singleton'); ioc.register('tv4', { value: require('tv4') }, 'singleton'); self.logger.info('[mvcx] Dependency registration completed.'); } function initializeExpress(){ self.logger.info('[mvcx] Creating express app...'); var path = require('path'); var iocContainer = self.mvcxConfig.hooks.ioc; var expressApp = iocContainer.resolve('expressApp').value; expressApp.locals.config = self.appConfig; self.logger.info('[mvcx] Registering standard middleware...'); if(isEmpty(self.mvcxConfig.viewEngine)){ self.logger.info('[mvcx] No view engine specified.'); } else{ self.logger.info('[mvcx] Registering view engine...'); expressApp.set('view engine', self.mvcxConfig.viewEngine); expressApp.set('views', path.resolve(self.mvcxConfig.viewPath)); } if(self.mvcxConfig.compressionEnabled){ var compress = iocContainer.resolve('compression').value; expressApp.use(compress()); self.logger.info('[mvcx] Gzip compression is enabled.'); } else{ self.logger.info('[mvcx] Gzip compression is disabled.'); } if(isEmpty(self.mvcxConfig.assets)){ self.logger.info('[mvcx] Assets are disabled.'); } else { self.logger.info('[mvcx] Registering assets...'); var assets = iocContainer.resolve('connect-assets').value; expressApp.use(assets(self.mvcxConfig.assets)); } self.logger.info('[mvcx] Registering body parser with url encoding and json support...'); var bodyParser = iocContainer.resolve('body-parser').value; expressApp.use(bodyParser.urlencoded({ extended: false })); expressApp.use(bodyParser.json({limit:(self.mvcxConfig.requestLimitKB)+"kb"})); self.logger.info('[mvcx] Standard middleware registration completed.'); return expressApp; } function initializeLogging(){ var logger; var winston = require('winston'); winston.emitErrs = true; var winstonTransports = []; if(self.mvcxConfig.loggerAppenders && self.mvcxConfig.loggerAppenders.length > 0){ for(var i=0; i < self.mvcxConfig.loggerAppenders.length; i++){ var appender = self.mvcxConfig.loggerAppenders[i]; winstonTransports.push(new winston.transports[appender.type](appender.options)); } } else{ winstonTransports.push(new winston.transports.Console({ level: 'silly', handleExceptions: true, json: false, colorize: true })); } logger = new winston.Logger({ transports: winstonTransports, exitOnError: false }); logger.info('[mvcx] Overriding Console.log...'); console.log = logger.debug; logger.info('[mvcx] Logger initialized.'); return logger; } function createRouteIndex(){ var Hashmap = self.mvcxConfig.hooks.ioc.resolve('hashmap').value; var controllerBasedRoutes = new Hashmap(); var controllerActionRoutes = new Hashmap(); var viewBasedRoutes = []; if(!isEmpty(self.mvcxConfig.routes)){ self.lazyjs(self.mvcxConfig.routes).each(function(route){ if(!isEmpty(route.controller)){ if(!controllerActionRoutes.has(route.controller)){ var actionsForController = new Hashmap(); actionsForController.set(route.action, [route]); controllerActionRoutes.set(route.controller, actionsForController); } else{ var actionsForController = controllerActionRoutes.get(route.controller); if(!actionsForController.has(route.action)){ actionsForController.set(route.action, [route]) } else{ var routesArray = actionsForController.get(route.action); routesArray.push(route); } } if(!controllerBasedRoutes.has(route.controller)){ var routeMethods = new Hashmap(); routeMethods.set(route.method, [route]); controllerBasedRoutes.set(route.controller, routeMethods); } else{ var controllerRoute = controllerBasedRoutes.get(route.controller); if(!controllerRoute.has(route.method)){ controllerRoute.set(route.method, [route]) } else{ var routesArray = controllerRoute.get(route.method); routesArray.push(route); } } } else if(!isEmpty(route.view)){ viewBasedRoutes.push(route); } else{ throw new Error('[mvcx] Invalid route (' + route.route + ') encountered with no controller or view specified.') } }); } return { controllerBasedRoutes: controllerBasedRoutes, viewBasedRoutes: viewBasedRoutes, controllerActionRoutes: controllerActionRoutes }; } function createAutoRoute(method, controllerModule){ var route = { method: method, route: '/' + controllerModule.modulePrefix, controller: controllerModule.moduleName, action: method }; var requestModel = controllerModule.modulePrefix + method.charAt(0).toUpperCase() + method.substr(1) + self.mvcxConfig.requestModelSuffix; var path = require('path'); var fs = require('fs'); var modelPath = path.join(path.resolve(self.mvcxConfig.modelPath), requestModel + '.js') try{ fs.statSync(modelPath); route.requestModel = requestModel; } catch(e){ //File does not exist } return route; } function extendController(controllerModuleName, controllerModule){ if(isEmpty(controllerModule.$type)){ controllerModule.$type = 'mvc'; } var extensions = {}; var path = require('path'); if(controllerModule.$type === 'mvc'){ extensions.view = function(view, model){ if(view.indexOf('/') == -1){ view = path.join(controllerModuleName.substring(0, controllerModuleName.length - self.mvcxConfig.controllerSuffix.length), view); } return new self.responseTypes.ViewResponse(view, model); }; } extensions.redirect = function(route){ return new self.responseTypes.RedirectResponse(route); } extensions.file = function(filePath, options){ return new self.responseTypes.FileResponse(filePath, options); } extensions.download = function(filePath, downloadedFilename){ return new self.responseTypes.DownloadResponse(filePath, downloadedFilename); } extensions.stream = function(stream){ return new self.responseTypes.StreamResponse(stream); } extensions.void = function(){ return new self.responseTypes.VoidResponse(); } extensions.response = function(){ return new self.responseTypes.Response(); } controllerModule.prototype.mvcx = extensions; } function extendAction(controllerModule, actionName){ var controllerAction = controllerModule.prototype[actionName]; controllerModule.prototype[actionName] = function(model, req, res, next){ console.log('Extended action called'); return controllerAction(model, req, res, next); } } function registerControllerBasedRoute(route, controllerType){ var iocContainer = self.mvcxConfig.hooks.ioc; var controllerMetadata = iocContainer.resolve(route.controller); if(!isEmpty(controllerMetadata[route.action])){ var url = require('url'); var formattedRoute = url.resolve(url.resolve(url.resolve('/', self.mvcxConfig.baseUrlPrefix), '/'), route.route); self.logger.info('[mvcx] Registering controller action ' + route.controller + '.' + route.action + ' with route ' + formattedRoute + '...'); self.expressApp[route.method](formattedRoute, function(req, res, next){ var controller = iocContainer.resolve(route.controller); invokeControllerAction(route, controller, controllerType, req, res, next); }); } } function registerViewBasedRoute(method, route, view){ self.logger.info('[mvcx] Registering view ' + view + ' with route ' + route + '...'); self.expressApp[method](route, function(req, res, next){ res.render(view); }); } function invokeControllerAction(route, controller, controllerType, req, res, next){ try{ var merge = self.mvcxConfig.hooks.ioc.resolve('merge').value; var model = merge.recursive(true, req.query, req.params); model = merge.recursive(true, model, req.body); var result = controller[route.action](model, req, res, next); if(!isEmpty(result) && self.q.isPromise(result)){ result.then(function(response){ createSuccessResponse(controllerType, response, res); }).catch(function(e){ createErrorResponse(controllerType, e, res); }); } else{ createSuccessResponse(controllerType, result, res); } } catch(e){ createErrorResponse(controllerType, e, res); } } function createSuccessResponse(controllerType, response, res){ if(typeof(response) === 'undefined'){ createErrorResponse(controllerType, new Error('[mvcx] Controller action did not return a response.'), res); } else { if(response == null || response instanceof self.responseTypes.VoidResponse){ res.status(200); } else if(response instanceof self.responseTypes.ViewResponse){ if(response.model == null){ res.render(response.view); } else{ res.render(response.view, response.model); } } else if(response instanceof self.responseTypes.DownloadResponse){ if(response.downloadedFilename == null){ res.download(response.filePath); } else{ res.download(response.filePath, response.downloadedFilename); } } else if(response instanceof self.responseTypes.FileResponse){ if(response.options == null){ res.sendFile(response.filePath); } else{ res.sendFile(response.filePath, response.options) } } else if(response instanceof self.responseTypes.RedirectResponse){ if(response.status == null){ res.redirect(response.route); } else{ res.redirect(response.status, response.route); } } else if(response instanceof self.responseTypes.StreamResponse){ res.setHeader("content-type", response.contentType); res.stream.pipe(res); } else if(response instanceof self.responseTypes.Response){ response.handler(res); } else { res.status(200).json(response); } } } function createErrorResponse(controllerType, e, res){ var errorHandlerHook = self.mvcxConfig.hooks.errorHandlers[controllerType]; if(isEmpty(errorHandlerHook)){ throw new Error('[mvcx] No error handler specified for controller type ' + controllerType + '.'); } var hookOptions = { response: res, error: e, includeErrorStackInResponse: self.mvcxConfig.includeErrorStackInResponse }; errorHandlerHook.createResponse(self.appConfig, hookOptions); } };