Skip to content

Commit

Permalink
Added onRequest hook for non versioned routes
Browse files Browse the repository at this point in the history
  • Loading branch information
paulhobbel committed Aug 3, 2018
1 parent 4fc3de4 commit acb626d
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 48 deletions.
8 changes: 8 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Exclude everything
*

# Except the library itself
!lib/**

# And this file
!.npmignore
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2018 MovieCast
Copyright (c) 2018 MovieCast and all it's contributors.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
68 changes: 52 additions & 16 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
'use strict';

const Path = require('path');
const Joi = require('joi');
const Hoek = require('hoek');
const Boom = require('boom');
const Glob = require('glob');
const Semver = require('semver');
const debug = require('debug')('hapi-endpoint');

const internals = {
options: {
path: Path.join(process.cwd(), 'endpoints'),
prefix: '',
version: require(`${process.cwd()}/package.json`).version
options: Joi.object({
path: Joi.string().trim().default(`${process.cwd()}/endpoints`),
validVersions: Joi.array().items(Joi.number().integer()).min(1).required(),
version: Joi.number().integer().valid(Joi.ref('validVersions')).required(),
prefix: Joi.string().trim().default('')
}),

getRequestedApiVersion: (request) => {

const version = request.headers['api-version'];

if (/^[0-9]+$/.test(version)) {
return parseInt(version);
}

return null;
},

mapRoutes: (routes, prefix) => {
Expand All @@ -35,16 +48,21 @@ const internals = {
exports.register = (server, options) => {

debug('Setting up hapi-endpoint');
const settings = Object.assign({}, internals.options, options);

const { error, value: settings } = Joi.validate(options, internals.options);
Hoek.assert(!error, 'Invalid plugin options', error);

const folders = Glob.sync(`${settings.path}/v*`);

folders.forEach((folder) => {

const version = folder.split('/').pop();
if (!Hoek.contain(settings.validVersions, parseInt(version.replace('v', '')))) {
return;
}

const prefixRegex = new RegExp(`^.+?${version}(.*)\/([^/]*)$`);
const isServerVersion = Semver.satisfies(settings.version, version);
debug(`[endpoint] Found version ${version}, matches server version? '${isServerVersion}'`);
debug(`[endpoint] Found version ${version}`);

const endpoints = Glob.sync(`${folder}/**/*.js`);

Expand All @@ -60,20 +78,38 @@ exports.register = (server, options) => {

const prefixedRoutes = internals.mapRoutes(routes, prefix);

// TODO: Remove this and use the onRequest hook instead
if (isServerVersion) {
debug(`[endpoint][${version}] Adding routes without version prefix`);
const unVersionedPrefix = `${settings.prefix}${prefixPath}`;
prefixedRoutes.push(...internals.mapRoutes(routes, unVersionedPrefix));
}

if (prefixedRoutes.length > 0) {
server.route(prefixedRoutes);
debug(`[endpoint][${version}] Added %O`, prefixedRoutes);
}
});
});


const requestRegex = new RegExp(`^${settings.prefix}/v[0-9]`);

server.ext('onRequest', (request, h) => {

if (requestRegex.test(request.path)) {
h.continue;
}

const version = internals.getRequestedApiVersion(request) || settings.version;
const prefixedPath = `${settings.prefix}/v${version}`;

if (!Hoek.contain(settings.validVersions, version)) {
return Boom.badRequest(`Unsupported api version, valid versions are: ${settings.validVersions}`);
}

const route = server.match(request.method, prefixedPath + request.path.substring(settings.prefix.length));

if (route) {
request.setUrl(prefixedPath + request.url.path.substring(settings.prefix.length));
}

return h.continue;
});

debug('Finished setting up');
};

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@
},
"homepage": "https://github.com/MovieCast/hapi-endpoint#readme",
"dependencies": {
"boom": "^7.2.0",
"glob": "^7.1.2",
"semver": "^5.5.0"
"hoek": "^5.0.3",
"joi": "^13.5.2"
},
"devDependencies": {
"code": "^5.2.0",
Expand Down
10 changes: 10 additions & 0 deletions test/endpoints/v3/status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

module.exports = {
method: 'GET',
path: '/status',
handler: () => ({
version: 3,
uptime: process.uptime() | 0
})
};
124 changes: 98 additions & 26 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,84 @@ beforeEach(async () => {

describe('Plugin registration', () => {

it('should register properly', async () => {
it('should fail if no options are specified', async () => {

await expect(server.register({
plugin: require('../lib'),
options: {}
})).to.reject(Error, /Invalid plugin options/);
});

it('should fail if validVersions is not specified', async () => {

await expect(server.register({
plugin: require('../lib'),
options: {
version: 1
}
})).to.reject(Error, /Invalid plugin options/);
});

it('should fail if validversions is an empty array', async () => {

await expect(server.register({
plugin: require('../lib'),
options: {
validVersions: [],
version: 1
}
})).to.reject(Error, /Invalid plugin options/);
});

it('should fail if version is not specified', async () => {

await expect(server.register({
plugin: require('../lib'),
options: {
validVersions: [1]
}
})).to.reject(Error, /Invalid plugin options/);
});

it('should fail if version is not an element of validVersions', async () => {

await expect(server.register({
plugin: require('../lib'),
options: {
validVersions: [1],
version: 2
}
})).to.reject(Error, /Invalid plugin options/);
});

it('should succeed if all options are valid', async () => {

expect(await server.register({
plugin: require('../lib')
plugin: require('../lib'),
options: {
path: `${__dirname}/endpoints`,
validVersions: [1, 2],
version: 1
}
})).to.be.undefined();
});
});

describe('Versioning', () => {

describe(' -> version 1 default', () => {

beforeEach(async () => {
beforeEach(async () => {

await server.register({
plugin: require('../lib'),
options: {
path: `${__dirname}/endpoints`,
version: '1.0.0'
}
});
await server.register({
plugin: require('../lib'),
options: {
path: `${__dirname}/endpoints`,
validVersions: [1, 2],
version: 1
}
});
});

describe(' -> basic', () => {

it('should prefix endpoints properly', async () => {

Expand All @@ -63,6 +119,16 @@ describe('Versioning', () => {
expect(response2.result.version).to.equal(2);
});

it('should not load unsupported endpoints', async () => {

const response = await server.inject({
method: 'GET',
url: '/v3/status'
});

expect(response.statusCode).to.equal(404);
});

it('should not load faulty routes', async () => {

const response = await server.inject({
Expand All @@ -84,7 +150,11 @@ describe('Versioning', () => {
expect(response.result.message).to.equal('This is route 1');
});

it('should make v1 endpoints default', async () => {
});

describe(' -> header', () => {

it('should fall back to the version specified in options', async () => {

const response = await server.inject({
method: 'GET',
Expand All @@ -94,30 +164,32 @@ describe('Versioning', () => {
expect(response.statusCode).to.equal(200);
expect(response.result.version).to.equal(1);
});
});

describe(' -> version 2 default', () => {
it('should support the custom api-version header', async () => {

beforeEach(async () => {

await server.register({
plugin: require('../lib'),
options: {
path: `${__dirname}/endpoints`,
version: '2.0.0'
const response = await server.inject({
method: 'GET',
url: '/status',
headers: {
'api-version': 2
}
});

expect(response.statusCode).to.equal(200);
expect(response.result.version).to.equal(2);
});

it('should make v2 endpoints default', async () => {
it('should fail if an unsupported api-version is specified', async () => {

const response = await server.inject({
method: 'GET',
url: '/status'
url: '/status',
headers: {
'api-version': 3
}
});

expect(response.statusCode).to.equal(200);
expect(response.result.version).to.equal(2);
expect(response.statusCode).to.equal(400);
});
});
});
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ big-time@2.x.x:
version "2.0.1"
resolved "https://registry.yarnpkg.com/big-time/-/big-time-2.0.1.tgz#68c7df8dc30f97e953f25a67a76ac9713c16c9de"

boom@7.x.x:
boom@7.x.x, boom@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/boom/-/boom-7.2.0.tgz#2bff24a55565767fde869ec808317eb10c48e966"
dependencies:
Expand Down Expand Up @@ -700,7 +700,7 @@ heavy@6.x.x:
hoek "5.x.x"
joi "13.x.x"

hoek@5.x.x:
hoek@5.x.x, hoek@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-5.0.3.tgz#b71d40d943d0a95da01956b547f83c4a5b4a34ac"

Expand Down Expand Up @@ -818,7 +818,7 @@ isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"

joi@13.x.x:
joi@13.x.x, joi@^13.5.2:
version "13.5.2"
resolved "https://registry.yarnpkg.com/joi/-/joi-13.5.2.tgz#32207c85fa76d889f1e971c7eaaf69b232259a91"
dependencies:
Expand Down Expand Up @@ -1226,7 +1226,7 @@ seedrandom@2.4.x:
version "2.4.3"
resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-2.4.3.tgz#2438504dad33917314bff18ac4d794f16d6aaecc"

semver@^5.3.0, semver@^5.5.0:
semver@^5.3.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"

Expand Down

0 comments on commit acb626d

Please sign in to comment.