UNPKG

compound

Version:

CompoundJS - MVC framework for NodeJS

355 lines (316 loc) 11.9 kB
var fs = require('fs'); var path = require('path'); var cache = {}; var ctlSuffix = /_controller\.(js|coffee)/g; var debug = function(){}; /** * @export ControllerBridge */ module.exports = ControllerBridge; /** * Bridge to kontroller package * * @param {Compound} rw - compound descriptor. * @constructor */ function ControllerBridge(rw) { this.compound = rw; this.root = rw.root + '/app/controllers'; this.pool = {}; if (process.env.NODE_DEBUG && /controller/.test(process.env.NODE_DEBUG)) { debug = function(x) { rw.log(x); }; } } /** * Bridge config * * @deprecated Should be removed to instance. */ ControllerBridge.config = { subdomain: { tld: 2 } }; /** * Prints information about current pool size */ ControllerBridge.prototype.poolSize = function() { var bridge = this; Object.keys(bridge.pool).forEach(function(i) { console.log(i, bridge.pool[i].length); }); }; /** * Caller for request handler * * @param {String} ns - namespace. * @param {String} controller - name of controller. * @param {String} action - name of action. * @param {Object} conf - optional params (comes from route definition). * * TODO: remove NS param, move subdomain logic to compound-routes * * @return {Function(req, res, next)} request handler. */ ControllerBridge.prototype.uniCaller = function(ns, controller, action, conf) { var app = this.compound.app; return function(req, res, next) { // Request hostname, or null if unknown var hostname = (req.headers && req.headers.host && req.headers.host.length) ? req.headers.host.toLowerCase() : null; var subdomain = req.headers && req.headers.host && req.headers.host .split('.') .slice(0, -1 * ControllerBridge.config.subdomain.tld); req.subdomain = subdomain && subdomain.join('.'); // Virtual Host match? if (conf && conf.vhost && conf.vhost.length && hostname) { // Virtual host is a wildcard? var wildcard = (conf.vhost.charAt(0) === '.'); // Virtual host name (without leading wildcard character, if any) var vhost = (wildcard) ? conf.vhost.substring(1).toLowerCase() : conf.vhost.toLowerCase(); // Compare without regard to port number if the configuration vhost // does not specify a port number. This lets us work with a proxy- // server with little complication. if (vhost.indexOf(":") < 0) { // Strip port number from the request hostname var portIndex = hostname.indexOf(":"); if (portIndex >= 0) { hostname = hostname.substring(0, portIndex); } } // Does the request match the Virtual Host? var match; // Wildcard virtual host: ".example.com" (12 chars) should match: // 1. "www.example.com" (15 chars) // 2. "example.com" (11 chars) if (wildcard) { // Condition #1 above match = (hostname.length > vhost.length) && (hostname.indexOf("." + vhost) === (hostname.length - conf.vhost.length)); // Condition #2 above match = match || (vhost === hostname); } // Virtual Host exact match else { match = (vhost === hostname); } if (!match) { // Virtual Host mismatch: go to the next route return next(); } } // Subdomain match? else if (conf && conf.subdomain && req.subdomain) { if (conf.subdomain !== req.subdomain) { if (conf.subdomain.match(/\*/)) { var matched = true; conf.subdomain.split('.').forEach(function(part, i) { if (part === '*') return; if (part !== subdomain[i]) matched = false; }); if (!matched) return next(); // next route } else return next(); } } var controllerName = ns + (controller || req.params.controller); this.callControllerAction(controllerName, action || req.params.action, req, res, next); }.bind(this); }; ControllerBridge.prototype.callControllerAction = function (controller, action, req, res, next) { debug('call controller ' + controller + ' action ' + action); var ctl = this.loadController(controller); var resEnd = res.end; var endCalled = false; res.end = function () { endCalled = true; resEnd.apply(res, [].slice.call(arguments)); }; // TODO: move all before-processing calls to separate method if (ctl && ctl._helpers) { delete ctl._helpers; } this.compound.emit('before controller', ctl, action, res, res, next); debug('perform ' + controller + '#' + action); ctl.perform(action, req, res, function(err) { debug(controller + '#' + action + ' completed'); // console.log((new Error).stack); if (ctl && ctl._backToPool) { ctl._backToPool(); } else { ctl = null; } if (err) { next(err); } else if (!endCalled) { next(); } }); } /** * Load controller (from pool, or create new) * * @param {String} controllerFullName - name of controller including namespace. * @return {Controller} - controller instance. */ ControllerBridge.prototype.loadController = function(controllerFullName) { if (this.compound.app.enabled('watch')) { return this.getInstance(controllerFullName); } var pool = this.pool; if (!pool[controllerFullName]) { pool[controllerFullName] = []; } if (pool[controllerFullName].length) { debug('found controller ' + controllerFullName + ' from pool'); return pool[controllerFullName].shift(); } debug('creating new controller'); var ctl = this.getInstance(controllerFullName); ctl._backToPool = function() { debug('return controller ' + controllerFullName + ' to pool'); pool[controllerFullName].push(ctl); }; return ctl; }; /** * Create new instance of controller * * @param {String} name - name of controller. * @return {Controller} - instance of controller. */ ControllerBridge.prototype.getInstance = function getInstance(name) { var compound = this.compound; var app = compound.app; var exts = this.compound.controllerExtensions; var controllers = compound.structure.controllers; var Controller, buildFromEval; if (name in controllers && !name.match(/_controller$/)) { Controller = compound.controller.constructClass(name, controllers[name]); Controller.skipLogging = controllers[name].skipLogging; } else { Controller = compound.controller.constructClass(name); buildFromEval = controllers[name + '_controller']; } // add controller extensions if (exts) { Object.keys(exts).forEach(function(k) { Controller.prototype[k] = exts[k]; }); } Controller.prototype.compound = compound; Controller.prototype.app = app; Controller.prototype.pathTo = compound.map.pathTo; // TODO: deprecate path_to Controller.prototype.path_to = compound.map.pathTo; // add models for (var m in this.compound.models) { Controller.prototype[m] = compound.models[m]; } // instantiate var ctl = new Controller; if (buildFromEval) { ctl.reset(); // and run through configurator code // TODO: pass patched compound.rootModule for more accurate require('./...') ctl.build(buildFromEval, { id: compound.rootModule.filename, filename: compound.rootModule.filename, paths: [path.join(compound.root, 'node_modules')] }); } compound.emit('controller instance', ctl); if (compound.app.disabled('quiet') || compound.app.get('quiet') === 'all') { var $ = compound.utils.stylize.$; var log = compound.utils.debug; var skipLogging = ctl.constructor.skipLogging; var logger = ctl.getLogger(); logger.on('beforeProcessing', function(ctl) { if (skipLogging && skipLogging.indexOf(ctl.actionName) > -1) { return; } var req = ctl.context.req; log( '\n' + $((new Date).toString()).yellow + ' ' + $(ctl.id || '').bold + '\n' + $(req.method).bold + ' ' + $(req.app && req.app.path()).blue + $(req.url).grey + ' controller: ' + $(ctl.controllerName).cyan + ' action: ' + $(ctl.actionName).blue ); if (req.query && Object.keys(req.query).length) { log($('Query: ').bold + JSON.stringify(req.query)); } if (req.body && req.method !== 'GET') { var filterParams = ctl.constructor.filterParams || []; var filteredBody = (function filter(obj) { if (typeof obj !== 'object' || obj == null) { return obj; } var filtered = {}; Object.keys(obj).forEach(function(param) { if (!filterParams.some(function(f) { return param.search(f) !== -1; })) { filtered[param] = filter(obj[param]); } else { filtered[param] = '[FILTERED]'; } }); return filtered; })(req.body); log($('Body: ').bold + JSON.stringify(filteredBody)); } if (req.params && typeof req.params === 'object' && Object.keys(req.params).length) { var filteredParams = {}; Object.keys(req.params).forEach(function (param) { filteredParams[param] = req.params[param]; }); log($('Params: ').bold + JSON.stringify(filteredParams)); } }); logger.on('afterProcessing', function(ctl, duration) { if (skipLogging && skipLogging.indexOf(ctl.actionName) > -1) { return; } var req = ctl.context.req; log('Handling ' + $(req.method).bold + ' ' + $(req.url).grey + ' completed in ' + duration + ' ms'); }); logger.on('beforeHook', function(ctl, action) { if (skipLogging && skipLogging.indexOf(ctl.actionName) > -1) { return; } if (ctl.context.inAction) { log('>>> perform ' + $(action).bold.cyan); } else { log('>>> perform ' + $(action).bold.grey); } }); logger.on('afterHook', function(ctl, action, duration) { if (skipLogging && skipLogging.indexOf(ctl.actionName) > -1) { return; } log('<<< ' + action + ' [' + duration + ' ms]'); }); logger.on('render', function(file, layout, duration) { if (skipLogging && skipLogging.indexOf(ctl.actionName) > -1) { return; } log('Rendered ' + $(file).grey + ' using layout ' + $(layout).grey + ' in ' + duration + 'ms'); }); } return ctl; };