Source: session/Route.js

/**
 * @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;