twee
Version:
Twee Framework - the most powerful, elegant and extensive framework for Node.js based on Express.js
1,275 lines (1,089 loc) • 57 kB
JavaScript
/**
* Twee Framework Functionality
*/
"use strict";
var express = require('express')
, debug = require('debug')('twee.io')
, path = require('path')
, colors = require('colors/safe')
, fs = require('fs')
, extend = require('./utils/extend')
, events = require('events');
/**
* twee Framework Class
* @constructor
*/
function twee() {
/**
* Express Application Instance
* @type express()
* @private
*/
this.__app = express();
/**
* Flag that shows that framework already bootstrapped
* @type {boolean}
* @private
*/
this.__bootstraped = false;
/**
* Base Directory for including all the modules
* @type {string}
* @private
*/
this.__baseDirectory = '';
/**
* Environment
* @type {string}
* @private
*/
this.__env = 'production';
/**
* Configuration object. Stores all the modules configs and core config
* @type {{}}
* @private
*/
this.__config = {};
/**
* Default Module Options
* @type {{disabled: boolean, prefix: string, disableViewEngine: boolean}}
* @private
*/
this.__defaultModuleOptions = {
disabled: false,
prefix: '/'
};
/**
* Registry of extensions to avoid infinity recursion
* @type {{}}
* @private
*/
this.__extensionsRegistry = {};
/**
* View helpers registry
* @type {}
*/
this.helper = {};
/**
* It allows us to call in views:
* {{ helper.foo(..) }} or {{ helper['foo'](...) }}
* BUT! NOT: {{ foo(...) }} because it can be overwritten by usual passed variables.
* So we should protect each of them. We don't want to care about this. So we'll protect only `helper` name.
* @type {*}
*/
this.__app.locals.helper = this.helper;
/**
* Extending one config from another
* @type {*|exports}
*/
this.extend = extend;
/**
* HTTP Server instance
* @type {null}
* @private
*/
this.__http = null;
/**
* HTTPS Server instance
* @type {null}
* @private
*/
this.__https = null;
/**
* For recursy control
* @type {number}
* @private
*/
this.__extensionsRecursyDeepness = 0;
/**
* Registry of different objects
* @type {{}}
* @private
*/
this.__registry = {};
/**
* Registry of middleware lists that are used in dispatch process of Express
* @type {{}}
* @private
*/
this.__middlewareListRegistry = {};
}
/**
* Setting prototype of framework
*/
twee.prototype.__proto__ = new events.EventEmitter();
/**
* Getting Application Instance
*/
twee.prototype.getApplication = function() {
return this.__app;
};
/**
* Logging message to console
* @param message
* @returns {twee}
*/
twee.prototype.log = function(message) {
debug(colors.cyan('[WORKER:' + process.pid + '] ') + colors.yellow(message));
return this;
};
/**
* Logging error to console
* @param message
* @returns {twee}
*/
twee.prototype.error = function(message) {
debug(colors.cyan('[WORKER:' + process.pid + '][ERROR] ') + colors.red(message.stack || message.toString()));
return this;
};
/**
* Bootstrapping application
* @param options Object
* @returns {twee}
*/
twee.prototype.Bootstrap = function(options) {
if (this.__bootstraped) {
return this;
}
var self = this;
options = options || {};
// This is default config state. It can be overwritten before running
options = extend(true, {
modules: 'configs/modules',
tweeConfig: 'configs/twee'
}, options);
process.on('uncaughtException', function(err) {
self.error('Caught exception: ' + err.stack || err.toString());
//console.trace();
self.emit('twee.Exception', err, self);
});
process.on('SIGINT', function(){
// Generate event for all the modules to free some resources
self.emit('twee.Exit');
self.log('Exiting');
self.__http && self.__http.close();
self.__https && self.__http.close();
process.exit(0);
});
try {
this.__bootstrap(options);
} catch (err) {
throw new Error('Bootstrap Error: ' + err.stack || err.toString());
}
this.__bootstraped = true;
return this;
};
/**
* Common bootstrap process is wrapped with exception catcher
* @param options
* @returns {twee}
* @private
*/
twee.prototype.__bootstrap = function(options) {
var self = this;
this.emit('twee.Bootstrap.Start');
if (!options || !options.modules) {
throw new Error('Modules field should not be empty!');
}
var modules = options.modules;
// If this is file path with modules configuration - then load it
if (typeof modules == 'string') {
modules = this.Require(modules);
this.emit('twee.Bootstrap.ModulesList', modules);
}
if (typeof modules != 'object') {
throw new Error('Modules should be file path or Object');
}
// Loading default framework configuration
var tweeConfig = require('./configs/default');
this.emit('twee.Bootstrap.DefaultConfig', tweeConfig);
// Extending framework configuration during Bootstrapping
if (options.tweeConfig) {
if (typeof options.tweeConfig == 'string') {
var tweeConfigFullPath = path.join(this.__baseDirectory, options.tweeConfig);
try {
var loadedTweeConfig = require(tweeConfigFullPath);
tweeConfig = extend(true, tweeConfig, loadedTweeConfig);
this.emit('twee.Bootstrap.ExtendedConfig', tweeConfig);
} catch (e) {
this.log('[WARNING] No valid twee main config specified! Using default values.');
}
// Extending config with environment-specified configuration
var directory = path.dirname(tweeConfigFullPath)
, configFile = path.basename(tweeConfigFullPath)
, environmentConfig = directory + '/' + this.__env + '/' + configFile;
try {
var envTweeConfig = require(environmentConfig);
tweeConfig = extend(true, tweeConfig, envTweeConfig);
this.emit('twee.Bootstrap.ExtendedEnvConfig', tweeConfig);
} catch (err) {
// Nothing to do here. Just no config for environment
}
}
}
// Setting up framework config
this.__config.twee = tweeConfig;
this.emit('twee.Bootstrap.Config', tweeConfig);
// Setting package information
this.__config.twee.package = this.Require('package');
this.emit('twee.Bootstrap.PackageInfo');
// Extension specific configs
this.__config.twee.extension = this.__config.twee.extension || {};
// Setting framework object as global
global.twee = this;
// Pre-loading all the modules configs, routes, patterns and other stuff
this.LoadModulesInformation(modules);
this.emit('twee.Bootstrap.ModulesInformationLoaded');
// Load enabled twee core extensions
this.emit('twee.Bootstrap.TweeExtensionsPreLoad');
this.LoadExtensions(this.getConfig('twee:extensions', {}), null);
this.emit('twee.Bootstrap.TweeExtensionsLoaded');
// All the extensions that execute random not-standard or standard code - runs before everything
this.LoadModulesExtensions();
this.emit('twee.Bootstrap.ModulesExtensionsLoaded');
// Lifting the server because some extensions could require http-server object
// before all the routes has been setup. for example socket.io
this.__createServer();
// Head middlewares are module-specific and used to initialize something into req or res objects
this.LoadModulesMiddleware('head');
this.emit('twee.Bootstrap.ModulesHeadMiddlewareLoaded');
// Controllers is the place where all the business logic is concentrated
this.LoadModulesControllers();
this.emit('twee.Bootstrap.ModulesControllersLoaded');
// Tail middleware is used for logging and doing post-calculations, post-stuff
this.LoadModulesMiddleware('tail');
this.emit('twee.Bootstrap.ModulesTailMiddlewareLoaded');
// This route will be used to write user that he did not sat up any configuration file for framework
this.__handle404();
this.emit('twee.Bootstrap.End');
return this;
};
/**
* Loading turned on twee extensions
*
* @param extensions Object - Extensions object where keys are the names of extensions
* @param moduleName String The name of current module
* @returns {twee}
*/
twee.prototype.LoadExtensions = function(extensions, moduleName) {
for (var extension_name in extensions) {
extensions[extension_name].name = extension_name;
this.__resolveDependencies(extensions[extension_name], extensions, moduleName);
}
return this;
};
/**
* Generating extension unique ID for registry
* @param extension
* @param moduleName
* @returns {string}
* @private
*/
twee.prototype.__getExtensionUniqueID = function(extension, moduleName) {
return 'module:' + (moduleName || 'twee')
+ (extension.file ? '|file:' + extension.file : '')
+ (extension.module ? '|npm-module:' + extension.module : '')
+ (extension.applicationModule ? '|appModule:' + extension.applicationModule : '');
};
/**
* Loading all the extensions and it's dependencies tree
*
* @param currentExtension
* @param extensions
* @param moduleName
* @private
*/
twee.prototype.__resolveDependencies = function(currentExtension, extensions, moduleName) {
this.emit('twee.LoadExtensions.PreLoad', currentExtension, moduleName);
var extensionID = this.__getExtensionUniqueID(currentExtension, moduleName);
if (this.__extensionsRegistry[extensionID]) {
return;
}
this.__extensionsRegistry[extensionID] = {options: currentExtension, extension: null};
var moduleLog = moduleName ? '[MODULE::' + moduleName + ']' : '';
// Dependencies are loaded only when needed by another extensions
if (currentExtension.dependency || (currentExtension.disabled && !currentExtension.dependency)) {
return;
}
var currentExtensionDependencies;
currentExtensionDependencies = {};
// First of all trying to import extension and load it's internal dependencies definition
if (!currentExtension.module && !currentExtension.file) {
moduleLog += ('[EXTENSION' + (moduleLog ? '' : '::GLOBAL') + '] ');
throw new Error(moduleLog + colors.cyan(currentExtension.name) + '` has wrong configuration. `module` AND `file` are not correct');
}
// Loading extension module
var extensionModule = ''
, extensionModuleFolder = '';
try {
// This is simply local file or module
if (currentExtension.file) {
if (currentExtension.applicationModule) {
try {
extensionModuleFolder = this.__config['__folders__'][currentExtension.applicationModule]['moduleExtensionsFolder'];
extensionModule = require(extensionModuleFolder + currentExtension.file);
} catch (err) {
//noinspection ExceptionCaughtLocallyJS
throw new Error('Module `' + currentExtension.applicationModule
+ '` is not installed. Needed as dependency provider for extension: '
+ currentExtension.name + '. ' + err.stack || err.toString());
}
} else if (moduleName) {
extensionModuleFolder = this.__config['__folders__'][moduleName]['moduleExtensionsFolder'];
extensionModule = require(extensionModuleFolder + currentExtension.file);
} else {
//noinspection ExceptionCaughtLocallyJS
throw new Error('Extension is wrong configured: ' + JSON.stringify(currentExtension));
}
// This is npm module
} else if (currentExtension.module) {
extensionModule = require(currentExtension.module);
}
} catch (err) {
throw err;
}
if (!extensionModule.extension || typeof extensionModule.extension !== 'function') {
moduleLog += ('[EXTENSION' + (moduleLog ? '' : '::GLOBAL') + '] ');
throw new Error(moduleLog + extensionID + ' should export .extension as `function`');
}
this.__extensionsRegistry[extensionID].extension = extensionModule.extension;
currentExtensionDependencies = extensionModule.dependencies || {};
if (currentExtension.dependencies && typeof currentExtension.dependencies == 'object' && Object.keys(currentExtension.dependencies).length) {
// Overwrite dependencies configuration if local configuration presents. It has greater priority
currentExtensionDependencies = currentExtension.dependencies;
}
for (var dep in currentExtensionDependencies) {
var dependency = currentExtensionDependencies[dep];
if (dependency.disabled) {
continue;
}
try {
if (!dependency || typeof dependency !== 'object' || !Object.keys(dependency).length) {
// It means that dependency is empty object or we have only it's name
// And should search in global extensions namespace
if (!extensions[dep]) {
//noinspection ExceptionCaughtLocallyJS
throw new Error('Dependency info does not exists neither in dependency config nor in global extensions namespace');
}
dependency = extensions[dep];
dependency.dependency = false;
}
dependency.name = dep;
this.__extensionsRecursyDeepness++;
if (this.__extensionsRecursyDeepness > 100) {
throw new Error('It seems we have dependencies recursy infinity loop');
}
this.__resolveDependencies(dependency, extensions, moduleName);
this.__extensionsRecursyDeepness--;
} catch (err) {
throw new Error('Current Extension: `' + currentExtension.name + '`, dependency: `' + dep + '` exception: ' + err.stack || err.toString());
}
}
moduleLog += ('[EXTENSION::' + currentExtension.name + '] ');
if (extensionModule.config && typeof extensionModule.config === 'object') {
var configNamespace = extensionModule.configNamespace || '';
if (configNamespace) {
// Rewrite extension's config with application
this.__config['twee']['extension'][configNamespace] = this.__config['twee']['extension'][configNamespace] || {};
this.__config['twee']['extension'][configNamespace] = this.extend(true, extensionModule.config, this.__config['twee']['extension'][configNamespace]);
}
}
extensionModule.extension();
this.log(moduleLog + 'Installed (configNamespace: ' + configNamespace + ')');
this.emit('twee.LoadExtensions.Loaded', currentExtension, moduleName);
};
/**
* Loading all the modules
*
* @returns {twee}
*/
twee.prototype.LoadModulesControllers = function() {
for (var moduleName in this.__config['__moduleOptions__']) {
this.setupRoutes(moduleName, this.__config['__moduleOptions__'][moduleName].prefix || '');
}
return this;
};
/**
* Loading all the middlewares from all modules that should be dispatched before any constructor
* Head middlewares are executed before all the controllers. It is like preDispatch.
* Tail middlewares are executed after all the controllers. It is like postDispatch.
*
* @param placement String Placement of middleware: head or tail.
* @returns {twee}
*/
twee.prototype.LoadModulesMiddleware = function(placement) {
placement = String(placement || '').trim();
if (placement !== 'head' && placement !== 'tail') {
throw new Error('Middleware type should be `head` or `tail`');
}
this.emit('twee.LoadModulesMiddleware.Start', placement);
for (var moduleName in this.__config['__moduleOptions__']) {
this.emit('twee.LoadModulesMiddleware.OnLoad', placement, moduleName);
var middlewareList = this.getConfig('__setup__:' + moduleName + ':middleware:' + placement) || []
, middlewareInstanceList = this.getMiddlewareInstanceArray(moduleName, middlewareList);
if (middlewareInstanceList.length) {
var prefix = String(this.__config['__moduleOptions__'][moduleName].prefix || '').trim();
if (prefix) {
this.__app.use(prefix, middlewareInstanceList);
} else {
this.__app.use(middlewareInstanceList);
}
}
this.emit('twee.LoadModulesMiddleware.Loaded', placement, moduleName);
}
this.emit('twee.LoadModulesMiddleware.End', placement);
return this;
};
/**
* Loading all extensions from all the modules by order:
* ModulesOrder -> ExtensionsOrderInEveryModule
*
* @returns {twee}
*/
twee.prototype.LoadModulesExtensions = function() {
this.emit('twee.LoadModulesExtensions.Start');
for (var moduleName in this.__config['__moduleOptions__']) {
if (this.__config['__setup__'][moduleName]['extensions']) {
if (typeof this.__config['__setup__'][moduleName]['extensions'] != 'object') {
continue;
}
var extensions = this.__config['__setup__'][moduleName]['extensions'];
this.emit('twee.LoadModulesExtensions.LoadExtensions.Start', moduleName, extensions);
this.LoadExtensions(extensions, moduleName);
this.emit('twee.LoadModulesExtensions.LoadExtensions.Stop', moduleName, extensions);
}
}
this.emit('twee.LoadModulesExtensions.Stop');
return this;
};
/**
* Default 404 route
* @private
*/
twee.prototype.__handle404 = function() {
var self = this;
// Here we can rewrite environment with framework extending
this.emit('twee.__handle404.Start');
function generate404(req, res, next) {
next(new Error('Not Found!'));
}
function errorHandler(err, req, res, next) {
var message = '404 - Not found!';
if (err) {
res.status(500);
if (self.__env == 'development') {
message = err.toString();
}
} else {
res.status(404);
err = new Error('The page you requested has not been found!');
}
if (req.xhr) {
var jsonMessage = {message: message, error_code: 404};
if (self.__env == 'development') {
jsonMessage['stack'] = err.stack || err.toString();
}
res.json(jsonMessage);
} else {
if (self.__app.get('view engine')) {
res.render(path.resolve(self.getConfig('twee:options:errorPages:404:viewTemplate')), {error: err});
} else {
res.send('<h1>' + message + '</h1>');
}
}
}
this.__app.use(generate404, errorHandler);
this.emit('twee.__handle404.End');
};
/**
* Loading modules information
*
* @param modules
* @return {twee}
*/
twee.prototype.LoadModulesInformation = function(modules) {
this.emit('twee.LoadModulesInformation.Start');
for (var moduleName in modules) {
var moduleOptions = modules[moduleName];
if (moduleOptions.disabled == true) {
this.log('Module `' + moduleName + '` disabled. Skipping.');
continue;
}
this.__config['__moduleOptions__'] = this.__config['__moduleOptions__'] || {};
this.__config['__moduleOptions__'][moduleName] = moduleOptions;
this.LoadModuleInformation(moduleName, moduleOptions);
}
this.emit('twee.LoadModulesInformation.End');
return this;
};
/**
* Loading one module, including all the configs, middlewares and controllers
* @param moduleName
* @param moduleOptions
* @returns {twee}
* @constructor
*/
twee.prototype.LoadModuleInformation = function(moduleName, moduleOptions) {
this.emit('twee.LoadModuleInformation.Start', moduleName, moduleOptions);
this.log('[MODULE] Loading: ' + colors.cyan(moduleName));
moduleName = String(moduleName || '').trim();
if (!moduleName) {
throw new Error('twee::LoadModuleInformation - `moduleName` is empty');
}
if (moduleName == 'twee') {
throw new Error('twee::LoadModuleInformation - `twee` name for modules is deprecated. It is used for framework');
}
var moduleFolder = path.join(this.__baseDirectory, 'modules', moduleName + '/')
, moduleSetupFolder = path.join(moduleFolder, 'setup/')
, moduleSetupFile = path.join(moduleFolder, 'setup/setup')
, moduleConfigsFolder = path.join(moduleFolder, 'setup/configs/')
, moduleControllersFolder = path.join(moduleFolder, 'controllers/')
, moduleModelsFolder = path.join(moduleFolder, 'models/')
, moduleMiddlewareFolder = path.join(moduleFolder, 'middleware/')
, moduleParamsFolder = path.join(moduleFolder, 'params/')
, moduleViewsFolder = path.join(moduleFolder, 'views/')
, moduleExtensionsFolder = path.join(moduleFolder, 'extensions/')
, moduleI18nFolder = path.join(moduleFolder, 'i18n/')
, moduleAssetsFolder = path.join(moduleFolder, 'assets/');
this.__config['__folders__'] = this.__config['__folders__'] || {};
this.__config['__folders__'][moduleName] = {
module: moduleFolder,
moduleSetupFolder: moduleSetupFolder,
moduleSetupFile: moduleSetupFile,
moduleConfigsFolder: moduleConfigsFolder,
moduleControllersFolder: moduleControllersFolder,
moduleModelsFolder: moduleModelsFolder,
moduleMiddlewareFolder: moduleMiddlewareFolder,
moduleParamsFolder: moduleParamsFolder,
moduleViewsFolder: moduleViewsFolder,
moduleExtensionsFolder: moduleExtensionsFolder,
moduleI18nFolder: moduleI18nFolder,
moduleAssetsFolder: moduleAssetsFolder
};
// Load base configs and overwrite them according to environment
this.loadConfigs(moduleName, moduleConfigsFolder);
// Loading Routes Information
this.__config['__setup__'] = this.__config['__setup__'] || {};
this.__config['__setup__'][moduleName] = require(moduleSetupFile);
this.emit(
'twee.LoadModuleInformation.End',
moduleName,
this.__config['__setup__'][moduleName],
this.__config['__folders__'][moduleName]
);
return this;
};
/**
* Loading all the bunch of configs from configuration folder according to environment
*
* @param configsFolder string - configurations folder
* @returns {twee}
* @param moduleName
*/
twee.prototype.loadConfigs = function(moduleName, configsFolder) {
this.emit('twee.loadConfigs.Start', moduleName, configsFolder);
var self = this
, configs = fs.readdirSync(configsFolder)
, configsObject = {};
configs.forEach(function(configFile){
var configFilePath = path.join(configsFolder, configFile)
, stats = fs.statSync(configFilePath);
if (stats.isFile()) {
var configData = self.loadConfig(configFilePath, moduleName);
var cD = {};
cD[configData["name"]] = configData.config;
configsObject = extend(true, configsObject, cD);
}
});
configsFolder = path.join(configsFolder, this.__env);
if (fs.existsSync(configsFolder)) {
configs = fs.readdirSync(configsFolder);
configs.forEach(function(configFile){
var configFilePath = path.join(configsFolder, configFile)
, stats = fs.statSync(configFilePath);
if (stats.isFile()) {
var configData = self.loadConfig(configFilePath, moduleName);
var cD = {};
cD[configData["name"]] = configData.config;
configsObject = extend(true, configsObject, cD);
}
});
} else {
this.log('[WARNING] No environment configs exists');
}
this.__config[moduleName.toLowerCase()] = configsObject;
this.emit('twee.loadConfigs.End', moduleName, this.__config[moduleName]);
return this;
};
/**
* Loading config file and returning it's name and contents
* @param configFile
* @param moduleName
* @returns {{name: string, config: *}}
*/
twee.prototype.loadConfig = function(configFile, moduleName) {
this.emit('twee.loadConfig.Start', configFile, moduleName);
var configName = path.basename(configFile).toLowerCase().replace('.json', '').replace('.js', '')
, config = require(configFile);
this.log('[MODULE::' + moduleName + '][CONFIGS::' + configName + '] Loaded: ' + configFile);
config = {name: configName, config: config};
this.emit('twee.loadConfig.Start', configFile, moduleName, config);
return config;
};
/**
* Setting base directory for including all the rest
* @param directory
* @returns {twee}
*/
twee.prototype.setBaseDirectory = function(directory) {
directory = String(directory || '');
this.__baseDirectory = this.__baseDirectory || directory || process.cwd();
// Fixing environment
this.__env = process.env.NODE_ENV;
if (!this.__env) {
this.log('No NODE_ENV sat up. Setting to `production`');
this.__env = process.env.NODE_ENV = 'production';
}
this.log('NODE_ENV: ' + this.__env);
this.__app.locals.env = this.__env;
return this;
};
/**
* Returning root application directory or full subfolder
*
* @param postfix String Postfix to add to base directory
* @returns {string}
*/
twee.prototype.getBaseDirectory = function(postfix) {
if (typeof postfix === 'string') {
postfix = String(postfix || '').trim();
return path.join(this.__baseDirectory, postfix);
}
return this.__baseDirectory;
};
/**
* Including local module
* @param module
* @returns {*}
*/
twee.prototype.Require = function(module) {
return require(path.join(this.__baseDirectory, module));
};
/**
* Setting up params for routes
* @param params
* @param router
* @param moduleName
*/
twee.prototype.setupParams = function(params, router, moduleName) {
if (!router.param) {
throw new Error('Router should be instance of express.Router()');
}
if (params && params instanceof Object) {
for (var param in params) {
// Regexp can be used too
//console.log(param, typeof params[param]);
if (params[param] instanceof RegExp) {
var paramContents = params[param];
router.param(param, function(req, res, next, p){
if (p.match(paramContents)) {
next();
} else {
next('route');
}
});
this.log('[MODULE::' + moduleName + '][PARAM::' + param + '] Installed as RegExp(' + params[param] + ')');
// If it is middleware function from setup.js file - it could be passed as is too
} else if (typeof params[param] === 'function') {
var paramContents = params[param];
router.param(param, paramContents);
this.log('[MODULE::' + moduleName + '][PARAM::' + param + '] Installed as inline middleware');
// Otherwise it should be an instance or middleware function from file or module or applicationModule/params folder
} else if (typeof params[param] === 'object') {
// This is module
var requireString = '';
if (params[param].module && typeof params[param].module === 'string') {
requireString = params[param].module;
} else if (params[param].applicationModule
&& typeof params[param].applicationModule === 'string'
&& this.__config['__folders__'][params[param].applicationModule]) {
if (params[param].file && typeof params[param].file === 'string') {
requireString = this.__config['__folders__'][params[param].applicationModule]['moduleParamsFolder'];
requireString += params[param].file;
}
} else if (params[param].file && typeof params[param].file === 'string') {
requireString = this.__config['__folders__'][moduleName]['moduleParamsFolder'] + params[param].file;
}
if (requireString) {
var _module = require(requireString);
// If method specified - try to go in needed deepness to get right object
if (params[param].method) {
var methodParts = params[param].method.split('.')
, neededMethod = _module[methodParts[0]]
, previousMethod = null;
for (var i = 1; i < methodParts.length; i++) {
if (typeof neededMethod === 'function') {
neededMethod = neededMethod();
}
previousMethod = neededMethod;
if (neededMethod[methodParts[i]]) {
neededMethod = neededMethod[methodParts[i]];
}
}
// If it is regexp - then just use it as is
if (neededMethod instanceof RegExp) {
router.param(param, function(req, res, next, p){
if (p.match(neededMethod)) {
next();
} else {
next('route');
}
});
this.log('[MODULE::' + moduleName + '][PARAM::' + param + '] Installed as RegExp(' + params[param] + ')');
continue;
}
if (typeof neededMethod !== 'function') {
throw new Error('Method for router.param() neither RegExp nor Middleware Function');
}
// If we need to bind function to parent reference - then do it
if (params[param].reference && previousMethod instanceof Object) {
neededMethod = neededMethod.bind(previousMethod);
}
router.param(param, neededMethod);
this.log('[MODULE::' + moduleName + '][PARAM::' + param + '] Installed as middleware');
// if we have no specified method - then in case when it is middleware or RegExp - setup it
} else if (typeof _module === 'function' || _module instanceof RegExp) {
router.param(param, _module);
this.log('[MODULE::' + moduleName + '][PARAM::' + param + '] Installed as middleware');
}
}
}
}
}
};
/**
* Format for controllers in configuration:
* <ControllerName>Controller:<MethodName>Action:<get[,post[,all[...]]]>
*
* By default HTTP method is set to `all`. It means that all the HTTP methods are acceptable
*
* Example of Config:
* {
* "routes": [
* {
* "description": "Entry point for application. Landing page",
* "pattern": "/",
* "controllers": ["IndexController:indexAction"]
* }
* }
*
* Bundles of middlewares can be sat as:
* ["IndexController:authAction", "IndexController:indexAction"]
*
* @param moduleName string Module Name
* @param prefix string Module request prefix
* @returns {twee}
*/
twee.prototype.setupRoutes = function(moduleName, prefix) {
var routesFile = this.__config['__folders__'][moduleName]['moduleSetupFile']
, routes = require(routesFile)
// TODO: use options: http://expressjs.com/api.html#router
, router = express.Router()
, controllersRegistry = {};
var self = this;
if (!routes.routes) {
throw Error('Module: `' + moduleName + '`. No `routes` field in file: ' + colors.red(routesFile));
}
self.emit('twee.setupRoutes.Start', moduleName, prefix, router, controllersRegistry);
self.emit('twee.setupRoutes.GlobalModuleParams.Start', routes.params, router, moduleName);
// Install route global params
this.setupParams(routes.params, router, moduleName);
self.emit('twee.setupRoutes.GlobalModuleParams.End', routes.params, router, moduleName);
routes.routes.forEach(function(route){
var pattern = route.pattern || ''
, controllers = route.controllers || []
, middleware = route.middleware || {}
, params = route.params || {};
if (!pattern) {
throw Error('Module: `' + moduleName + '`. No valid `pattern` sat for route');
}
if (!controllers.length) {
return;
}
// If route has been disabled - then don't process it
if (route.disabled) {
return;
}
self.emit('twee.setupRoutes.ControllerParams.Start', params, router, moduleName);
// Installing params for each controller set
self.setupParams(params, router, moduleName);
self.emit('twee.setupRoutes.ControllerParams.End', params, router, moduleName);
controllers.forEach(function(controller) {
var controller_info = controller.split('.');
if (controller_info.length == 0 || !controller_info[0].trim()) {
throw new Error('Controller does not have controller name, action and method');
}
var controller_name = controller_info[0].trim()
, action_name = ''
, methods = [];
if (controller_info.length === 1) {
// trying indexAction
action_name = 'indexAction';
methods.push('all');
} else if (controller_info.length === 2) {
action_name = controller_info[1].trim();
methods.push('all');
} else if (controller_info.length === 3) {
action_name = controller_info[1].trim();
var _methods = controller_info[2].trim().split(',')
, at_least_one_method = false;
// Iterating for all the methods and call appropriate router
_methods.forEach(function(requestMethod){
if (router[requestMethod.trim()]) {
methods.push(requestMethod.trim());
at_least_one_method = true;
}
});
if (!at_least_one_method) {
methods.push('all');
}
}
// Deprecate all the actions that are not endWith `Action`
if (action_name.indexOf('Action', action_name.length - 6) === -1) {
throw new Error(
"Action name for controller have to be in format: <ActionName>Action." +
' It is used to protect all the methods from calling if they are not for Public requests'
);
}
if (!controllersRegistry[controller_name]) {
self.log("[MODULE::" + moduleName + "][CONTROLLER::" + controller_name + "] Loading");
var ControllerClass = require(self.__config['__folders__'][moduleName]['moduleControllersFolder'] + controller_name);
controllersRegistry[controller_name] = new ControllerClass;
}
// For pre-initializing controller with it's own stuff
if (!controllersRegistry[controller_name].__initCalled) {
if (controllersRegistry[controller_name]['init']
&& typeof controllersRegistry[controller_name]['init'] == 'function')
{
self.emit('twee.setupRoutes.NewController.PreInit', moduleName, route, controller_name, action_name, methods, controllersRegistry[controller_name]);
controllersRegistry[controller_name].init();
self.log('[MODULE::' + moduleName + '][CONTROLLER][INIT] ' +
colors.cyan(controller_name + '.init()'));
self.emit('twee.setupRoutes.NewController.PostInit', moduleName, route, controller_name, action_name, methods, controllersRegistry[controller_name]);
}
// Setting parent class to Controller
controllersRegistry[controller_name].__initCalled = true;
}
// Iterating over all collected methods and setup controllers into stack
if (!controllersRegistry[controller_name][action_name]) {
throw new Error('No action: `' + action_name + '` for Controller: `' + controller_name + '`');
}
var middlewareList = [];
if (middleware && middleware.before && middleware.before.length && middleware.before instanceof Array) {
self.log('[MODULE::'+ moduleName +'][CONTROLLER:' + colors.cyan(controller_name) + '] Loading PreControllerAction Middleware List');
self.emit('twee.setupRoutes.ControllerActionMiddleware.Before.Start', moduleName, route, controller_name, action_name, methods, controllersRegistry[controller_name], middleware.before);
middlewareList = self.getMiddlewareInstanceArray(moduleName, middleware.before);
self.emit('twee.setupRoutes.ControllerActionMiddleware.Before.End', moduleName, route, controller_name, action_name, methods, controllersRegistry[controller_name], middlewareList);
}
// This is Controller.Action installaction after `before-middlewares` and before `after-middlewares`
middlewareList.push(controllersRegistry[controller_name][action_name]
.bind(controllersRegistry[controller_name]));
if (middleware && middleware.after && middleware.after.length && middleware.after instanceof Array) {
self.log('[MODULE::'+ moduleName +'][CONTROLLER:' + colors.cyan(controller_name) + '] Loading PostControllerAction Middleware List');
self.emit('twee.setupRoutes.ControllerActionMiddleware.After.Start', moduleName, route, controller_name, action_name, methods, controllersRegistry[controller_name], middleware.after);
var afterMiddlewareList = self.getMiddlewareInstanceArray(moduleName, middleware.after);
self.emit('twee.setupRoutes.ControllerActionMiddleware.After.End', moduleName, route, controller_name, action_name, methods, controllersRegistry[controller_name], afterMiddlewareList);
for (var i = 0; i < afterMiddlewareList.length; i++) {
middlewareList.push(afterMiddlewareList[i]);
}
}
methods.forEach(function(method){
// Setup router
router[method](
pattern,
middlewareList
);
self.log('[MODULE::' + moduleName + '][ROUTE] HTTP METHOD: '
+ colors.cyan(method)
+ '. '
+ 'ACTION: ' + colors.cyan(controller_name + '.' + action_name));
});
});
});
// Install all the routes as a bunch under prefix
this.emit('twee.setupRoutes.preAppUse', prefix, router, moduleName);
this.__app.use(prefix || '/', router);
this.emit('twee.setupRoutes.postAppUse', prefix, moduleName);
return this;
};
/**
* Setting up middleware stack.
* If placement is `head`, then middlewares list will be loaded from config by this key, and
* will be sat up before all the routes
* If placement is `tail`, then the same, but middlewares will be sat up after all the routes dispatching
* If middlewares param has been passed - then it seems some controller wants some middleware to be
* executed before controller.
* Middleware is simple list of files or modules that should return middleware function or an object that includes it.
*
* Middleware example:
* middleware: [
* {
* "name": "authMiddleware", // (not required)
*
* // If your middleware is simple file (first priority). It is application specified middleware (not in packages)
* "file": "myFolder/myMiddleware"
*
* // OR in module (second priority if both exists)
* "module": "express/some/middleware"
*
* // As additional you can pass field name of object that should contain needed middleware:
* "method": "myMethod"
* // Then it will be passed to router
*
* // If object is too complex with nested hierarchy, then it can be specified like this:
* "method": "mySubObject[.mySubObject2[...]].MyMethod"
*
* // If this is a class and you need to use it's method as middleware, then probably
* // you want to set up `this` reference to this class. This option will allow to do this:
* "reference": true
* // It will do something like this:
* // var ref = MyClass.MySubClass
* // middleware = ref[myMethod].bind(ref)
*
* // You can disable middleware by passing this value:
* "disabled": true
* }
* ]
*
* @param moduleName
* @param middlewareList
* @returns {Array}
*/
twee.prototype.getMiddlewareInstanceArray = function(moduleName, middlewareList) {
if (!middlewareList instanceof Array) {
throw new Error('Middleware list should be an Array');
}
var self = this
, middlewareInstanceArray = []
, middlewareModule = null
, middlewareIndex
, middlewareListLength = middlewareList.length;
for (middlewareIndex=0; middlewareIndex < middlewareListLength; middlewareIndex++) {
var middleware = middlewareList[middlewareIndex]
, middlewareName = middleware.name || ''
, uniqueMiddlewareId = JSON.stringify({m: moduleName, md: middlewareName});
if (this.__middlewareListRegistry[uniqueMiddlewareId]) {
middlewareModule = this.__middlewareListRegistry[uniqueMiddlewareId];
} else {
if (!middleware.file && !middleware.module) {
throw new Error('In module `' + moduleName + '` middleware `' + middlewareName + '` have to be specified with `file` or `module` filed');
}
// Check if it has been disabled
if (middleware.disabled) {
this.log('[MODULE::' + moduleName + '][MIDDLEWARE::' + middlewareName + '] Disabled.');
continue;
}
middlewareModule = null;
var middlewareModuleFolder = self.__config['__folders__'][moduleName]['moduleMiddlewareFolder']
, _construct = false;
// Instantiating middleware module
if (middleware.file) {
middlewareModule = require(middlewareModuleFolder + middleware.file);
middlewareName = middlewareName || middleware.file;
} else if (middleware.module) {
var mmLen = middleware.module.length;
if (middleware.module[mmLen - 1] == '@') {
middleware.module = middleware.module.substr(0, mmLen - 1);
_construct = true;
middlewareName = middlewareName || middleware.module;
}
middlewareModule = require(middleware.module);
if (_construct) {
if (middleware.params) {
var currParam = null;
if (typeof middleware.params !== 'object') {
middleware.params = [middleware.params];
}
if (middleware.params instanceof Array) {
for (var i = 0; i < middleware.params.length; i++) {
currParam = middleware.params[i];
if (typeof currParam === 'string') {
if (currParam[0] === '@') {
middleware.params[i] = self.getConfig(currParam.replace('@', ''));
}
}
}
} else if (middleware.params instanceof Object) {
for (var paramName in middleware.params) {
currParam = middleware.params[paramName];
if (typeof currParam === 'string') {
if (currParam[0] === '@') {
middleware.params[paramName] = self.getConfig(currParam.replace('@', ''));
}
}
}
}
if (!middleware.params instanceof Array) {
middleware.params = [middleware.params];
}
middlewareModule = middlewareModule.apply(null, middleware.params);
} else {
middlewareModule = middlewareModule();
}
}
}
// If method has been sat up - then lets use it
var method = middlewareModule
, methodParent = middlewareModule
, methodParts = String(middleware.method || '').trim().split('.');
if (methodParts.length && !_construct) {
// Going through hierarchy of object and finding out if the last method part is the middleware function
for (var index = 0; index < methodParts.length; index++) {
if (method[methodParts[index]]) {
methodParent = method;
method = method[methodParts[index]];
}
// Check if it is function and not the last - then instantiate it
if (index < methodParts.length - 1 && typeof method === 'function') {
method = new method;
}
}
if (typeof method !== 'function') {
throw new Error('Method should be a function, got: ' + (typeof method) + '. Method: ' + middleware.method + ', middlewareId: ' + middlewareId);
}
// Do we need to provide class reference to middleware function?
if (middleware.reference) {
middlewareModule = method.bind(methodParent);
} else {
middlewareModule = method;
}
}
if (typeof middlewareModule !== 'function') {
throw new Error('Middleware should be a function(req, res, [next]) or another app.use() valid middleware format');
}
}
middlewareInstanceArray.push(middlewareModule);
this.__middlewareListRegistry[uniqueMiddlewareId] = middlewareModule;
this.log('[MODULE::' + moduleName + '][MIDDLEWARE::' + middlewareName + '] Installed');
}
return middlewareInstanceArray;
};
/**
* Returning config by it's path
* Examples:
* twee.getConfig('twee:foo', 'bar')
* twee.getConfig('myModule:myConfigFile:myConfig', 'baz')
*
* @param key
* @param defaultValue
* @returns {*}
*/
twee.prototype.getConfig = function(key, defaultValue) {
key = String(key || '').trim();
var keyParts = key.split(':');
if (!keyParts.length) {
return defaultValue;
}
for (var i = 0; i < keyParts.length; i++) {
if (!keyParts[i].trim()) {
throw new Error('Config path is not correct: ' + colors.red(ke