Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
language: node_js

node_js:
- '0.12'
- '4'
- '5'
- '6'
Expand Down
2 changes: 1 addition & 1 deletion route/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ module.exports = function (app, config) {

if (!router) {
expressRouter = express.Router();
router = new Router(expressRouter, config.defaultAuthHandler, config.unauthenticatedStatusCode);
router = new Router(expressRouter, config.defaultAuthHandler, config.unauthenticatedStatusCode, config.apiVersioning);

if (!testing) {
router = addLoggingToRouter(router, rootPath);
Expand Down
215 changes: 204 additions & 11 deletions route/router/Route.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
var _ = require('lodash')
, AccessError = require('dodo/errors').AccessError
, NotFoundError = require('dodo/errors').NotFoundError
, HTTPError = require('dodo/errors').HTTPError
, Promise = require('bluebird')
, log = require('dodo/logger').getLogger('dodo-core-features.router');

Expand Down Expand Up @@ -42,9 +43,13 @@ function Route(opt) {
*/
this.expressMiddleware = [];
/**
* @type {function (Request, Response, next)}
* @type {Object}
*/
this.handlerFunc = null;
this.handlerFuncs = {};
/**
* @type {Object}
*/
this.apiVersioningConfig = opt.apiVersioningConfig;
}

/**
Expand All @@ -54,7 +59,7 @@ function Route(opt) {
* @returns {Route}
*/
Route.prototype.middleware = function (middleware) {
if (this.handlerFunc) {
if (_.size(this.handlerFuncs) > 0) {
throw new Error('You must call middleware(func) before handler(func)');
}

Expand All @@ -70,7 +75,7 @@ Route.prototype.middleware = function (middleware) {
* @returns {Route}
*/
Route.prototype.auth = function (authHandler) {
if (this.handlerFunc) {
if (_.size(this.handlerFuncs) > 0) {
throw new Error('You must call auth(func) before handler(func)');
}

Expand Down Expand Up @@ -108,27 +113,200 @@ Route.prototype.customResponse = function () {
/**
* Installs a handler for the route.
*
* @see Router#get for examples.
* @private
* @param {function(IncomingMessage, ServerResponse, Next)} handler
* @param {number} apiVersion
* @param {boolean} isDefault
* @returns {Route}
*/
Route.prototype.handler = function (handler) {
if (this.handlerFunc) {
throw new Error('handler(func) can be called just once per Route instance');
Route.prototype.handler_ = function (handler, apiVersion, isDefault) {
var self = this;

if (self.isApiVersioningEnabled_() === false && !_.isNil(apiVersion)) {
throw new Error('cant define versioned handler because api versioning is not enabled');
}

// Set the default handler is needed (if called without apiVersion, or with isDefault=true)
if (_.isNil(apiVersion) || isDefault === true) {
var existingHandler = self.handlerFuncs['default'];

if (existingHandler) {
throw new Error('default handler func can be set only once per Route instance');
} else {
self.handlerFuncs['default'] = handler;
}
}

// Set the api version handler if needed (if apiVersion is defined)
if (!_.isNil(apiVersion)) {
self.validateApiVersion_(apiVersion);

var existingHandler = self.handlerFuncs[apiVersion];

if (existingHandler) {
throw new Error('apiVersion handler already exists, can be set only once.');
} else {
self.handlerFuncs[apiVersion] = handler;
}
}

this.handlerFunc = handler;
this.execute_();
return this;
};

/**
* Installs a handler for the route.
*
* @see Router#get for examples.
* @param {function(IncomingMessage, ServerResponse, Next)} handler
* @param {number} [apiVersion]
* @returns {Route}
*/
Route.prototype.handler = function (arg1, arg2) {
var self = this;
var numberOfArguments = arguments.length;
if (numberOfArguments <= 0 || numberOfArguments > 2) {
throw new Error('Wrong number of arguments passed to .handler()')
}
if (numberOfArguments == 1) {
return self.handler_(arg1, undefined, false);
} else {
return self.handler_(arg2, arg1, false);
}
};

/**
* Installs a default handler for the route.
*
* @see Router#get for examples.
* @param {function(IncomingMessage, ServerResponse, Next)} handler
* @param {number} [apiVersion]
* @returns {Route}
*/
Route.prototype.defaultHandler = function (arg1, arg2) {
var self = this;
var numberOfArguments = arguments.length;
if (numberOfArguments <= 0 || numberOfArguments > 2) {
throw new Error('Wrong number of arguments passed to .defaultHandler()')
}
if (numberOfArguments == 1) {
return self.handler_(arg1, undefined, true);
} else {
return self.handler_(arg2, arg1, true);
}
};

/**
* Finds handler for specified api version (optional)
* If apiVersion is not provided, returns the default handler
*
* @private
* @param {number} [apiVersion]
* @returns {function (Request, Response, next)}
*/
Route.prototype.findHandler_ = function (apiVersion, fallbackToDefault, fallbackToPrevious) {
var self = this;
var handler;

// If no api version is provided in request
if (_.isNil(apiVersion)) {
// Fallback to default handler, if configured
if (fallbackToDefault) {
handler = self.handlerFuncs['default'];
}
// Api version is provided
} else {
// Try to find handler for the api version
var handler = self.handlerFuncs[apiVersion];

// If handler is not found, and fallbackToPrevious config is true
if (!handler && fallbackToPrevious) {
// Find previous api version from handlerFuncs keys
var previousApiVersion = _.chain(self.handlerFuncs)
.omit('default') // Omit default handler
.keys() // Get apiVersions
.filter(function(key) { // We don't want to include newer api versions than initially requested
return key <= apiVersion;
})
.max() // Get the previous existing api version from the subset
.value();
// Get the handler from handlerFuncs
if (previousApiVersion !== undefined) {
handler = self.handlerFuncs[previousApiVersion];
}
// If no previous apiVersion handler is found, fallback to default handler if configured
if (handler === undefined && fallbackToDefault) {
handler = self.handlerFuncs['default'];
}
// At last resort, fallback to default if configured
} else if (!handler && fallbackToDefault) {
handler = self.handlerFuncs['default'];
}
}
return handler;
};

/**
* Tries to find apiVersion from request. Validates it if needed.
*
* @private
* @param {Object} req
* @returns {number}
*/
Route.prototype.findApiVersionFromRequest_ = function (req) {
var self = this;
var fallbackToDefault = _.get(self, 'apiVersioningConfig.fallbackToDefaultHandler', true);
var apiVersion = self.apiVersioningConfig.findApiVersionHandler(req);

// If apiVersion is not defined in request, and fallback is not allowed, thrown an Error.
if (apiVersion === undefined && fallbackToDefault === false) {
throw new NotFoundError('Api version must be defined');
// If apiVersion is defined, validate it
} else if (apiVersion !== undefined) {
self.validateApiVersion_(apiVersion);
} else {
// Api version is not defined in request, but fallback to default handler is allowed. Pass.
}

return apiVersion;
};

/**
* Validates provided api version against apiVersionConfig availableApiVersions array
*
* @private
* @param {Object} req
*/
Route.prototype.validateApiVersion_ = function (apiVersion) {
var self = this;
var availableApiVersions = _.get(self, 'apiVersioningConfig.availableApiVersions');
if (availableApiVersions && !_.includes(availableApiVersions, apiVersion)) {
throw new NotFoundError('specified apiVersion not available. Available api versions are: ' + availableApiVersions.join(', '));
}
};

/**
* Finds if api versioning is enabled
*
* @private
*/
Route.prototype.isApiVersioningEnabled_ = function () {
var self = this;
return _.get(self, 'apiVersioningConfig.enabled') === true;
};

/**
* @private
*/
Route.prototype.execute_ = function () {
var self = this;

this.expressRouter[this.method](this.path, function (req, res, next) {
var path = this.path;
if (self.isApiVersioningEnabled_() && _.isFunction(self.apiVersioningConfig.generateRoutePathHandler)) {
path = self.apiVersioningConfig.generateRoutePathHandler(path);
}

this.expressRouter[this.method](path, function (req, res, next) {
// return for testing purposes...
return self.handlerMiddleware_(req, res, next);
});
Expand Down Expand Up @@ -213,7 +391,22 @@ Route.prototype.handle_ = function (req, res, next) {
});

return promise.then(function () {
var result = self.handlerFunc.call(context, req, res, next);
var apiVersion = undefined;

if (self.isApiVersioningEnabled_()) {
// Try to find api version from request. This also validates it and may throw an error.
apiVersion = self.findApiVersionFromRequest_(req);
}
var handler = self.findHandler_(
apiVersion,
_.get(self, 'apiVersioningConfig.fallbackToDefaultHandler', true),
_.get(self, 'apiVersioningConfig.fallbackToPreviousApiVersion', true)
);
if (!handler) {
throw new NotFoundError('Handler not found');
}

var result = handler.call(context, req, res, next);
if (self._omitResultHandlers) {
return Promise.resolve(result).then(function () {
return NO_RESULT;
Expand Down
10 changes: 8 additions & 2 deletions route/router/Router.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ var Route = require('./Route');
* @constructor
* @param {express.Router} expressRouter
* @param {function} defaultAuthHandler
* @param {Object} apiVersioningConfig
*/
function Router(expressRouter, defaultAuthHandler, unauthenticatedStatusCode) {
function Router(expressRouter, defaultAuthHandler, unauthenticatedStatusCode, apiVersioningConfig) {
/**
* @type {express.Router}
*/
Expand All @@ -23,6 +24,10 @@ function Router(expressRouter, defaultAuthHandler, unauthenticatedStatusCode) {
* @type {Number}
*/
this.unauthenticatedStatusCode = unauthenticatedStatusCode || 401;
/**
* @type {Object}
*/
this.apiVersioningConfig = apiVersioningConfig
}

/**
Expand Down Expand Up @@ -165,7 +170,8 @@ Router.prototype._route = function (path, method) {
method: method,
expressRouter: this.expressRouter,
defaultAuthHandler: this.defaultAuthHandler,
unauthenticatedStatusCode: this.unauthenticatedStatusCode
unauthenticatedStatusCode: this.unauthenticatedStatusCode,
apiVersioningConfig: this.apiVersioningConfig
});
};

Expand Down
Loading