/* 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();