Skip to content

Commit 6b4b8d1

Browse files
committed
Add support for API versioning
- Refactor Route to support multiple handlers and api versioning. - Add HeaderApiVersioning and RouteApiVersioning features. - Refactor Router to forward apiVersioningConfig from Router's config to Route. - Also resolve feature name in Router to the actual versioning feature. - Add tests for Router and both versioning features.
1 parent c671411 commit 6b4b8d1

File tree

4 files changed

+514
-14
lines changed

4 files changed

+514
-14
lines changed

route/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ module.exports = function (app, config) {
9191

9292
if (!router) {
9393
expressRouter = express.Router();
94-
router = new Router(expressRouter, config.defaultAuthHandler, config.unauthenticatedStatusCode);
94+
router = new Router(expressRouter, config.defaultAuthHandler, config.unauthenticatedStatusCode, config.apiVersioning);
9595

9696
if (!testing) {
9797
router = addLoggingToRouter(router, rootPath);

route/router/Route.js

Lines changed: 193 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
var _ = require('lodash')
44
, AccessError = require('dodo/errors').AccessError
55
, NotFoundError = require('dodo/errors').NotFoundError
6+
, HTTPError = require('dodo/errors').HTTPError
67
, Promise = require('bluebird')
78
, log = require('dodo/logger').getLogger('dodo-core-features.router');
89

@@ -42,9 +43,13 @@ function Route(opt) {
4243
*/
4344
this.expressMiddleware = [];
4445
/**
45-
* @type {function (Request, Response, next)}
46+
* @type {Object}
4647
*/
47-
this.handlerFunc = null;
48+
this.handlerFuncs = {};
49+
/**
50+
* @type {Object}
51+
*/
52+
this.apiVersioningConfig = opt.apiVersioningConfig;
4853
}
4954

5055
/**
@@ -54,7 +59,7 @@ function Route(opt) {
5459
* @returns {Route}
5560
*/
5661
Route.prototype.middleware = function (middleware) {
57-
if (this.handlerFunc) {
62+
if (_.size(this.handlerFuncs) > 0) {
5863
throw new Error('You must call middleware(func) before handler(func)');
5964
}
6065

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

@@ -108,27 +113,189 @@ Route.prototype.customResponse = function () {
108113
/**
109114
* Installs a handler for the route.
110115
*
111-
* @see Router#get for examples.
116+
* @private
112117
* @param {function(IncomingMessage, ServerResponse, Next)} handler
118+
* @param {number} apiVersion
119+
* @param {boolean} isDefault
113120
* @returns {Route}
114121
*/
115-
Route.prototype.handler = function (handler) {
116-
if (this.handlerFunc) {
117-
throw new Error('handler(func) can be called just once per Route instance');
122+
Route.prototype.handler_ = function (handler, apiVersion, isDefault) {
123+
var self = this;
124+
125+
if (self.isApiVersioningEnabled_() === false && !_.isNil(apiVersion)) {
126+
throw new Error('cant define versioned handler because api versioning is not enabled');
127+
}
128+
129+
// Set the default handler is needed (if called without apiVersion, or with isDefault=true)
130+
if (_.isNil(apiVersion) || isDefault === true) {
131+
var existingHandler = self.handlerFuncs['default'];
132+
133+
if (existingHandler) {
134+
throw new Error('default handler func can be set only once per Route instance');
135+
} else {
136+
self.handlerFuncs['default'] = handler;
137+
}
138+
}
139+
140+
// Set the api version handler if needed (if apiVersion is defined)
141+
if (!_.isNil(apiVersion)) {
142+
self.validateApiVersion_(apiVersion);
143+
144+
var existingHandler = self.handlerFuncs[apiVersion];
145+
146+
if (existingHandler) {
147+
throw new Error('apiVersion handler already exists, can be set only once.');
148+
} else {
149+
self.handlerFuncs[apiVersion] = handler;
150+
}
118151
}
119152

120-
this.handlerFunc = handler;
121153
this.execute_();
122154
return this;
123155
};
124156

157+
/**
158+
* Installs a handler for the route.
159+
*
160+
* @see Router#get for examples.
161+
* @param {function(IncomingMessage, ServerResponse, Next)} handler
162+
* @param {number} [apiVersion]
163+
* @returns {Route}
164+
*/
165+
Route.prototype.handler = function (arg1, arg2) {
166+
var self = this;
167+
var numberOfArguments = arguments.length;
168+
if (numberOfArguments <= 0 || numberOfArguments > 2) {
169+
throw new Error('Wrong number of arguments passed to .handler()')
170+
}
171+
if (numberOfArguments == 1) {
172+
return self.handler_(arg1, undefined, false);
173+
} else {
174+
return self.handler_(arg2, arg1, false);
175+
}
176+
};
177+
178+
/**
179+
* Installs a default handler for the route.
180+
*
181+
* @see Router#get for examples.
182+
* @param {function(IncomingMessage, ServerResponse, Next)} handler
183+
* @param {number} [apiVersion]
184+
* @returns {Route}
185+
*/
186+
Route.prototype.defaultHandler = function (arg1, arg2) {
187+
var self = this;
188+
var numberOfArguments = arguments.length;
189+
if (numberOfArguments <= 0 || numberOfArguments > 2) {
190+
throw new Error('Wrong number of arguments passed to .defaultHandler()')
191+
}
192+
if (numberOfArguments == 1) {
193+
return self.handler_(arg1, undefined, true);
194+
} else {
195+
return self.handler_(arg2, arg1, true);
196+
}
197+
};
198+
199+
/**
200+
* Finds handler for specified api version (optional)
201+
* If apiVersion is not provided, returns the default handler
202+
*
203+
* @private
204+
* @param {number} [apiVersion]
205+
* @returns {function (Request, Response, next)}
206+
*/
207+
Route.prototype.findHandler_ = function (apiVersion, fallbackToDefault, fallbackToPrevious) {
208+
var self = this;
209+
var handler;
210+
211+
if (_.isNil(apiVersion)) {
212+
if (fallbackToDefault) {
213+
handler = self.handlerFuncs['default'];
214+
}
215+
} else {
216+
// Try to find handler for api version
217+
var handler = self.handlerFuncs[apiVersion];
218+
219+
// If handler is not found, and fallbackToPrevious config is true
220+
if (!handler && fallbackToPrevious) {
221+
// Find previous api version from handlerFuncs keys
222+
var previousApiVersion = _.chain(self.handlerFuncs)
223+
.omit('default') // Omit default handler
224+
.keys() // Get apiVersions
225+
.filter(function(key) { // We don't want to include newer api versions than initially requested
226+
return key <= apiVersion;
227+
})
228+
.max() // Get the previous existing api version from the subset
229+
.value();
230+
if (previousApiVersion !== undefined) {
231+
handler = self.handlerFuncs[previousApiVersion];
232+
}
233+
}
234+
}
235+
return handler;
236+
};
237+
238+
/**
239+
* Tries to find apiVersion from request. Validates it if needed.
240+
*
241+
* @private
242+
* @param {Object} req
243+
* @returns {number}
244+
*/
245+
Route.prototype.findApiVersionFromRequest_ = function (req) {
246+
var self = this;
247+
var fallbackToDefault = _.get(self, 'apiVersioningConfig.fallbackToDefaultHandler', true);
248+
var apiVersion = self.apiVersioningConfig.findApiVersionHandler(req);
249+
250+
// If apiVersion is not defined in request, and fallback is not allowed, thrown an Error.
251+
if (apiVersion === undefined && fallbackToDefault === false) {
252+
throw new NotFoundError('Api version must be defined');
253+
// If apiVersion is defined, validate it
254+
} else if (apiVersion !== undefined) {
255+
self.validateApiVersion_(apiVersion);
256+
} else {
257+
// Api version is not defined in request, but fallback to default handler is allowed. Pass.
258+
}
259+
260+
return apiVersion;
261+
};
262+
263+
/**
264+
* Validates provided api version against apiVersionConfig availableApiVersions array
265+
*
266+
* @private
267+
* @param {Object} req
268+
*/
269+
Route.prototype.validateApiVersion_ = function (apiVersion) {
270+
var self = this;
271+
var availableApiVersions = _.get(self, 'apiVersioningConfig.availableApiVersions');
272+
if (availableApiVersions && !availableApiVersions.includes(apiVersion)) {
273+
throw new NotFoundError('specified apiVersion not available. Available api versions are: ' + availableApiVersions.join(', '));
274+
}
275+
};
276+
277+
/**
278+
* Finds if api versioning is enabled
279+
*
280+
* @private
281+
*/
282+
Route.prototype.isApiVersioningEnabled_ = function () {
283+
var self = this;
284+
return _.get(self, 'apiVersioningConfig.enabled') === true;
285+
};
286+
125287
/**
126288
* @private
127289
*/
128290
Route.prototype.execute_ = function () {
129291
var self = this;
130292

131-
this.expressRouter[this.method](this.path, function (req, res, next) {
293+
var path = this.path;
294+
if (self.isApiVersioningEnabled_() && _.isFunction(self.apiVersioningConfig.generateRoutePathHandler)) {
295+
path = self.apiVersioningConfig.generateRoutePathHandler(path);
296+
}
297+
298+
this.expressRouter[this.method](path, function (req, res, next) {
132299
// return for testing purposes...
133300
return self.handlerMiddleware_(req, res, next);
134301
});
@@ -213,7 +380,22 @@ Route.prototype.handle_ = function (req, res, next) {
213380
});
214381

215382
return promise.then(function () {
216-
var result = self.handlerFunc.call(context, req, res, next);
383+
var apiVersion = undefined;
384+
385+
if (self.isApiVersioningEnabled_()) {
386+
// Try to find api version from request. This also validates it and may throw an error.
387+
apiVersion = self.findApiVersionFromRequest_(req);
388+
}
389+
var handler = self.findHandler_(
390+
apiVersion,
391+
_.get(self, 'apiVersioningConfig.fallbackToDefaultHandler', true),
392+
_.get(self, 'apiVersioningConfig.fallbackToPreviousApiVersion', true)
393+
);
394+
if (!handler) {
395+
throw new NotFoundError('Handler not found');
396+
}
397+
398+
var result = handler.call(context, req, res, next);
217399
if (self._omitResultHandlers) {
218400
return Promise.resolve(result).then(function () {
219401
return NO_RESULT;

route/router/Router.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ var Route = require('./Route');
99
* @constructor
1010
* @param {express.Router} expressRouter
1111
* @param {function} defaultAuthHandler
12+
* @param {Object} apiVersioningConfig
1213
*/
13-
function Router(expressRouter, defaultAuthHandler, unauthenticatedStatusCode) {
14+
function Router(expressRouter, defaultAuthHandler, unauthenticatedStatusCode, apiVersioningConfig) {
1415
/**
1516
* @type {express.Router}
1617
*/
@@ -23,6 +24,10 @@ function Router(expressRouter, defaultAuthHandler, unauthenticatedStatusCode) {
2324
* @type {Number}
2425
*/
2526
this.unauthenticatedStatusCode = unauthenticatedStatusCode || 401;
27+
/**
28+
* @type {Object}
29+
*/
30+
this.apiVersioningConfig = apiVersioningConfig
2631
}
2732

2833
/**
@@ -165,7 +170,8 @@ Router.prototype._route = function (path, method) {
165170
method: method,
166171
expressRouter: this.expressRouter,
167172
defaultAuthHandler: this.defaultAuthHandler,
168-
unauthenticatedStatusCode: this.unauthenticatedStatusCode
173+
unauthenticatedStatusCode: this.unauthenticatedStatusCode,
174+
apiVersioningConfig: this.apiVersioningConfig
169175
});
170176
};
171177

0 commit comments

Comments
 (0)