/**
* @version 1.1
* @author Boris GBAHOUE
* @file Generic Route framework exposing various utility methods
*/
// =======================================================================
// BASE SETUP
// =======================================================================
// call the packages we need
var debug = require('debug')('amiwo:session');
var util = require('util');
var when = require('when');
var u = require('../util');
// load our Objects
var PerformanceMonitor = require('../perf/PerformanceMonitor');
var GenericError = require('../error/GenericError');
// =======================================================================
// CONSTRUCTOR
// =======================================================================
/**
* @class module:session~Route
*/
function Route(verbose) {
this._verbose = false;
this._debug = this.$initDebug();
// Set 'this' context for our middlewares
this.startPerformanceMonitorTimer = this.startPerformanceMonitorTimer.bind(this);
this.activateDebug = this.activateDebug.bind(this);
}
// =======================================================================
// METHOD TO OVERRIDE
// =======================================================================
/**
* Create routes
*
* @param {any}
*/
Route.prototype.init = function() {
// To be overriden
}
// =======================================================================
// PUBLIC METHODS
// =======================================================================
/**
* Set the current Session object for this Route
* @public
*/
Route.prototype.setSession = function(session) {
this._session = session;
}
/**
* Generic middleware to start PerformanceMonitor's timer and save id in Session's Request object
* @public
*/
Route.prototype.startPerformanceMonitorTimer = function(req, res, next) {
if (this._session == null) {
debug("::AMIWO::%s::STARTPERFORMANCEMONITORTIMER::ERROR This Route's Session object has not been set", this.constructor.name.toUpperCase());
return next();
}
this._session.setRequest(req, this.constructor.name + ".performanceMonitor.id", PerformanceMonitor.addTimer());
next();
}
/**
* Get the PerformanceMonitor timer ID associated with this Route from the Session request object
*
* @param {Express.Request} req
* @param {Boolean} [delId=true] set to true to delete the ID from the Session Request object
*
* @returns {String}
*
* @public
*/
Route.prototype.getPerformanceMonitorTimerId = function(req, delId) {
if (this._session == null) {
debug("::AMIWO::%s::GETPERFORMANCEMONITORTIMERID::ERROR This Route's Session object has not been set", this.constructor.name.toUpperCase());
return;
}
delId = !(delId == false);
return this._session.getRequest(req, this.constructor.name + ".performanceMonitor.id", delId);
}
/**
* Get the PerformanceMonitor timer associated with this Route
*
* @param {Express.Request} req
* @param {Boolean} [del=true] set to true to delete the ID from the Session Request object and the associated PerformanceTimer
*
* @return {Number} timer's timestamp
*
* @public
*/
Route.prototype.getPerformanceMonitorTimer = function(req, del) {
if (this._session == null) {
debug("::AMIWO::%s::GETPERFORMANCEMONITORTIMER::ERROR This Route's Session object has not been set", this.constructor.name.toUpperCase());
return;
}
del = !(del == false);
var id = this._session.getRequest(req, this.constructor.name + ".performanceMonitor.id", del);
return PerformanceMonitor.getTimer(id, del);
}
/**
* Allow HTTP methods matching 'authorizedMethods' to be executed and block others
*
* @param {String} authorizedMethods
*/
Route.prototype.keepMethod = function(...authorizedMethods) {
return function $keepMethod(req, res, next) {
if (u.isEmpty(authorizedMethods)) next();
if ((authorizedMethods.length == 1) && Array.isArray(authorizedMethods[0])) authorizedMethods = authorizedMethods[0];
if (authorizedMethods.includes(req.method)) {
return next();
} else {
debug("::JUPITER::%s::KEEPMETHOD::ERROR Cannot %s %s", this.constructor.name.toUpperCase(), req.method, req.url);
return res.send(util.format("Cannot %s %s", req.method, req.originalUrl));
}
}
}
/**
* Display log informations on 'parameters'
*
* @param {String|String[]} parameters : list of expected parameters ("body" to list the parameters from req.body)
*
* @return {function} Express compatible (req, res, next) middleware
*/
Route.prototype.log = function(parameters) {
var self = this;
if (!Array.isArray(parameters)) parameters = [parameters];
return function $log(req, res, next) {
if (self._debug._log) return next(); // Already called in debug middleware
if (u.isEmpty(parameters)) return next();
debug("::AMIWO::ROUTE::LOG * Parameters");
var param;
var printBody = false;
for (var i = 0; i < parameters.length; i++) {
if (parameters[i] == "body") {
printBody = true; // Print body last
} else {
param = u.expressParam(req, parameters[i]);
debug("::AMIWO::ROUTE::LOG\t* %s = %s", parameters[i], (param == null) ? "<no such parameter found>" : JSON.stringify(param));
}
}
if (printBody) {
debug("::AMIWO::ROUTE::LOG\t* body = %s", JSON.stringify(req.body));
}
next();
}
}
/**
* Return an Express compatible middleware that will asynchronuously calls all the middlewares passed as argument and that will be completed once all middlewares are resolved.
* If any of the middleware fails (i.e., calls next(Error)), this middleware will be rejected with the Error of the first middleware that was rejected.
*
* @param {function} middleware: a middleware that takes req, res, next as parameter
*
* @returns {function} an Express compatible middleware
*
* @public
*/
Route.prototype.parallel = function() {
var self = this;
var middlewares = arguments;
return function $parallel(req, res, next) {
var promiseArray = [];
for (var i=0; i < middlewares.length; i++) {
if (typeof middlewares[i] == 'function') {
promiseArray.push(when.resolve(middlewares[i]).then(function(middleware) {
var deferredPromise = when.defer();
function $nextParallel(err) {
if (err == null) {
deferredPromise.resolve(true);
} else {
deferredPromise.reject(err);
}
};
var result = middleware(req, res, $nextParallel);
if (typeof result === 'function') {
// Middleware that returns a middleware => ok, call it again
process.nextTick(function() {
result(req, res, $nextParallel);
});
}
return deferredPromise.promise;
}));
} // end of inner if
} // end of for loop
when.settle(promiseArray).then(function(descriptors) {
var errors = [];
for (var i = 0; i < descriptors.length; i++) {
if (descriptors[i].state === 'rejected') {
var err = descriptors[i].reason;
debug("::AMIWO::%s::PARALLEL::ERROR Middleware '%s' threw an Error => err=%s, stack=%s", self.constructor.name, middlewares[i].name, err, (err instanceof Error) ? err.stack : "<not an Error>");
errors.push(err);
}
}
if (errors.length > 0) return when.resolve(next(errors[0]));
// We reach here if all descriptors were fulfilled => call next()
return when.resolve(next());
}).catch(function(err) {
debug("::AMIWO::%s::PARALLEL::ERROR Unexpected Error => err=%s, stack=%s", self.constructor.name, err, err.stack);
return when.resolve(next(err));
});
}
}
/**
* Express compatible middleware to activate the debug mode for that route until the next call.
* Superseeds original config file (thus preventing to have to relaunch the server to modify config options)
*
* @param {Boolean|String} req.*.debug
*/
Route.prototype.activateDebug = function(req, res, next) {
var self = this;
var debugParam = u.expressParam(req, "debug");
// Init
if (self._debug.verbose) self._verbose = self._debug.orgVerbose;
self._debug = this.$initDebug();
// Activate debug mode for this call only
if ((debugParam == "true") || (debugParam == true) || (Number(debugParam) == 1)) {
debug("::AMIWO::%s::ACTIVATEDEBUG::DEBUG * Debug mode", self.constructor.name);
self._debug.activated = true;
self._debug.verbose = u.expressParam(req, "verbose", null);
// Force displaying request parameters regardless of whether log middleware is used or not
var parameters = [];
var container = (req.method.toUpperCase() == "GET") ? req.query : req.body;
// - Process req.params
if (u.isNotEmpty(req.params)) {
for (var key in req.params) {
if (req.params && req.params[key] && (parameters.indexOf(key) == -1)) parameters.push(key);
}
}
// - Process req.body or req.query
if (u.isNotEmpty(container)) {
for (var key in container) {
if (container && container[key] && (parameters.indexOf(key) == -1)) parameters.push(key);
}
}
// - Call log middleware
self.log(parameters)(req, res, function $next() {
self._debug._log = true;
self._verbose = true;
self._debug.orgVerbose = self._verbose;
self._session.setRequest(req, "common.debug", self._debug);
return next();
});
} else {
self._session.deleteRequest(req, "common.debug");
return next();
}
}
// =======================================================================
// PRIVATE METHODS
// =======================================================================
/**
* Initialize the debug object
*
* @return {Object}
*
* @private
*/
Route.prototype.$initDebug = function() {
return {
_log: false,
activated: false,
verbose: 0,
count: (this._debug ? this._debug.count+1 : 0)
};
}
module.exports = Route;