menu

SFRA / Server-side JS / Source: modules/server/server.js

/* globals request:false, response:false, customer:false, session:false */

'use strict';

var HookMgr = require('dw/system/HookMgr');
var middleware = require('./middleware');
var Request = require('./request');
var Response = require('./response');
var Route = require('./route');
var render = require('./render');

//--------------------------------------------------
// Private helpers
//--------------------------------------------------

/**
 * Validate that first item is a string and all of the following items are functions
 * @param {string} name - Arguments that were passed into function
 * @param {Array} middlewareChain - middleware chain
 * @returns {void}
 */
function checkParams(name, middlewareChain) {
    if (typeof name !== 'string') {
        throw new Error('First argument should be a string');
    }

    if (!middlewareChain.every(function (item) { return typeof item === 'function'; })) {
        throw new Error('Middleware chain can only contain functions');
    }
}

//--------------------------------------------------
// Public Interface
//--------------------------------------------------

/**
 * @constructor
 * @classdesc Server is a routing solution
 */
function Server() {
    this.routes = {};
}

Server.prototype = {
    /**
     * Creates a new route with a name and a list of middleware
     * @param {string} name - Name of the route
     * @param {Function[]} arguments - List of functions to be executed
     * @returns {void}
     */
    use: function use(name) {
        var args = Array.isArray(arguments) ? arguments : Array.prototype.slice.call(arguments);
        var middlewareChain = args.slice(1);
        var rq = new Request(
            typeof request !== 'undefined' ? request : {},
            typeof customer !== 'undefined' ? customer : {},
            typeof session !== 'undefined' ? session : {});
        checkParams(args[0], middlewareChain);

        var rs = new Response(typeof response !== 'undefined' ? response : {});

        if (this.routes[name]) {
            throw new Error('Route with this name already exists');
        }

        var route = new Route(name, middlewareChain, rq, rs);
        // Add event handler for rendering out view on completion of the request chain
        route.on('route:Complete', function onRouteCompleteHandler(req, res) {
            // compute cache value and set on response when we have a non-zero number
            if (res.cachePeriod && typeof res.cachePeriod === 'number') {
                var currentTime = new Date(Date.now());
                if (res.cachePeriodUnit && res.cachePeriodUnit === 'minutes') {
                    currentTime.setMinutes(currentTime.getMinutes() + res.cachePeriod);
                } else {
                    // default to hours
                    currentTime.setHours(currentTime.getHours() + res.cachePeriod);
                }
                res.base.setExpires(currentTime);
            }
            // add vary by
            if (res.personalized) {
                res.base.setVaryBy('price_promotion');
            }

            if (res.redirectUrl) {
                // if there's a pending redirect, break the chain
                route.emit('route:Redirect', req, res);
                if (res.redirectStatus) {
                    res.base.redirect(res.redirectUrl, res.redirectStatus);
                } else {
                    res.base.redirect(res.redirectUrl);
                }
                return;
            }

            render.applyRenderings(res);
        });

        this.routes[name] = route;

        if (HookMgr.hasHook('app.server.registerRoute')) {
            // register new route, allowing route events to be registered against
            HookMgr.callHook('app.server.registerRoute', 'registerRoute', route);
        }

        return route;
    },
    /**
     * Shortcut to "use" method that adds a check for get request
     * @param {string} name - Name of the route
     * @param {Function[]} arguments - List of functions to be executed
     * @returns {void}
     */
    get: function get() {
        var args = Array.prototype.slice.call(arguments);
        args.splice(1, 0, middleware.get);
        return this.use.apply(this, args);
    },
    /**
     * Shortcut to "use" method that adds a check for post request
     * @param {string} name - Name of the route
     * @param {Function[]} arguments - List of functions to be executed
     * @returns {void}
     */
    post: function post() {
        var args = Array.prototype.slice.call(arguments);
        args.splice(1, 0, middleware.post);
        return this.use.apply(this, args);
    },
    /**
     * Output an object with all of the registered routes
     * @returns {Object} Object with properties that match registered routes
     */
    exports: function exports() {
        var exportStatement = {};
        Object.keys(this.routes).forEach(function (key) {
            exportStatement[key] = this.routes[key].getRoute();
            exportStatement[key].public = true;
        }, this);
        if (!exportStatement.__routes) {
            exportStatement.__routes = this.routes;
        }
        return exportStatement;
    },
    /**
     * Extend existing server object with a list of registered routes
     * @param {Object} server - Object that corresponds to the output of "exports" function
     * @returns {void}
     */
    extend: function (server) {
        var newRoutes = {};
        if (!server.__routes) {
            throw new Error('Cannot extend non-valid server object');
        }
        if (Object.keys(server.__routes).length === 0) {
            throw new Error('Cannot extend server without routes');
        }

        Object.keys(server.__routes).forEach(function (key) {
            newRoutes[key] = server.__routes[key];
        });

        this.routes = newRoutes;
    },
    /**
     * Modify a given route by prepending additional middleware to it
     * @param {string} name - Name of the route to modify
     * @param {Function[]} arguments - List of functions to be appended
     * @returns {void}
     */
    prepend: function prepend(name) {
        var args = Array.prototype.slice.call(arguments);
        var middlewareChain = Array.prototype.slice.call(arguments, 1);

        checkParams(args[0], middlewareChain);

        if (!this.routes[name]) {
            throw new Error('Route with this name does not exist');
        }

        this.routes[name].chain = middlewareChain.concat(this.routes[name].chain);
    }, /**
    * Modify a given route by appending additional middleware to it
    * @param {string} name - Name of the route to modify
    * @param {Function[]} arguments - List of functions to be appended
    * @returns {void}
    */
    append: function append(name) {
        var args = Array.prototype.slice.call(arguments);
        var middlewareChain = Array.prototype.slice.call(arguments, 1);

        checkParams(args[0], middlewareChain);

        if (!this.routes[name]) {
            throw new Error('Route with this name does not exist');
        }

        this.routes[name].chain = this.routes[name].chain.concat(middlewareChain);
    },

    /**
     * Replace a given route with the new one
     * @param {string} name - Name of the route to replace
     * @param {Function[]} arguments - List of functions for the route
     * @returns {void}
     */
    replace: function replace(name) {
        var args = Array.prototype.slice.call(arguments);
        var middlewareChain = Array.prototype.slice.call(arguments, 1);
        checkParams(args[0], middlewareChain);

        if (!this.routes[name]) {
            throw new Error('Route with this name does not exist');
        }

        delete this.routes[name];

        this.use.apply(this, arguments);
    },

    /**
     * Returns a given route from the server
     * @param {string} name - Name of the route
     * @returns {Object} Route that matches the name that was passed in
     */
    getRoute: function getRoute(name) {
        return this.routes[name];
    }
};

module.exports = new Server();