From 9fc9a76e162d7c5e5e11fca2cdd6b61019bf3937 Mon Sep 17 00:00:00 2001 From: Dave Sag Date: Thu, 18 Apr 2019 17:21:20 +0530 Subject: [PATCH] #22 added path-specific middleware option --- README.md | 32 +++++++++++++- src/connector.js | 12 ++--- src/extract/v2/extractPaths.js | 10 +++-- src/extract/v3/extractPaths.js | 10 +++-- src/normalise/normaliseMiddleware.js | 7 +++ test/fixtures/exampleV2.json | 3 +- test/fixtures/exampleV3.json | 3 +- test/unit/connector.test.js | 44 ++++++++++++++----- test/unit/extract/v2/extractPaths.test.js | 9 ++-- test/unit/extract/v3/basePath.test.js | 4 +- test/unit/extract/v3/extractPaths.test.js | 9 ++-- .../normalise/normaliseMiddleware.test.js | 35 +++++++++++++++ 12 files changed, 145 insertions(+), 33 deletions(-) create mode 100644 src/normalise/normaliseMiddleware.js create mode 100644 test/unit/normalise/normaliseMiddleware.test.js diff --git a/README.md b/README.md index 542419e..1234e02 100644 --- a/README.md +++ b/README.md @@ -304,6 +304,33 @@ async function correspondingMiddlewareFunction(req, res, next) { OpenAPI V3 allows you to define a global `security` definition as well as path specific ones. The global `security` block will be applied if there is no path specific one defined. +### Adding other path-level middleware + +You can add your own path specific middleware by passing in a `middleware` option + +```js +{ + middleware: { + myMiddleware: someMiddlewareFunction + } +} +``` + +and then in the path specification adding an `x-middleware` option + +```yml +paths: + /special + get: + summary: some special route + x-middleware: + - myMiddleware +``` + +The `someMiddlewareFunction` will be inserted **after** any auth middleware. + +This works for both Swagger v2 and OpenAPI v3 documents. + ### Adding hooks You can supply an `onCreateRoute` handler function with the options with signature @@ -367,9 +394,10 @@ If you don't pass in any options the defaults are: notFound: : require('./routes/notFound'), notImplemented: require('./routes/notImplemented'), onCreateRoute: undefined, - rootTag: 'root', // unused in OpenAPI v3 docs + rootTag: 'root', // only used in Swagger V2 docs security: {}, - variables: {}, // unused in Swagger V2 docs + variables: {}, // only used in OpenAPI v3 docs + middleware: {}, INVALID_VERSION: require('./errors').INVALID_VERSION } ``` diff --git a/src/connector.js b/src/connector.js index 07fb618..cf14112 100644 --- a/src/connector.js +++ b/src/connector.js @@ -18,13 +18,13 @@ const connectController = require('./connectors/connectController') * onCreateRoute * rootTag = 'root' // ignored if using OpenAPI v3 * security = {} - * variables = {} + * variables = {}, + * middleware = {}, * INVALID_VERSION = errors.INVALID_VERSION * } */ const connector = (api, apiDoc, options = {}) => { const { INVALID_VERSION = ERRORS.INVALID_VERSION, onCreateRoute } = options - const version = extractVersion(apiDoc) if (!version) throw new Error(INVALID_VERSION) @@ -33,13 +33,15 @@ const connector = (api, apiDoc, options = {}) => { const paths = extractPaths(apiDoc, options) return app => { - paths.forEach(({ method, route, operationId, security }) => { - const middleware = connectSecurity(security, options) + paths.forEach(({ method, route, operationId, security, middleware }) => { + const auth = connectSecurity(security, options) const controller = connectController(api, operationId, options) const descriptor = [route] - if (middleware) descriptor.push(middleware) + if (auth) descriptor.push(auth) + if (middleware.length) descriptor.push(...middleware) descriptor.push(controller) + app[method](...descriptor) if (typeof onCreateRoute === 'function') onCreateRoute(method, descriptor) }) diff --git a/src/extract/v2/extractPaths.js b/src/extract/v2/extractPaths.js index 3d9abf0..d5bb9aa 100644 --- a/src/extract/v2/extractPaths.js +++ b/src/extract/v2/extractPaths.js @@ -1,6 +1,7 @@ const { METHODS } = require('../../constants') const normaliseSecurity = require('../../normalise/v2/normaliseSecurity') const normaliseOperationId = require('../../normalise/normaliseOperationId') +const normaliseMiddleware = require('../../normalise/normaliseMiddleware') const normaliseRoute = require('../../normalise/normaliseRoute') /* @@ -11,7 +12,8 @@ const normaliseRoute = require('../../normalise/normaliseRoute') method, route, (normalised and inclues basePath if not a root route) operationId, - security + security, + middleware } ] @@ -19,7 +21,8 @@ const normaliseRoute = require('../../normalise/normaliseRoute') const extractPaths = ({ basePath, paths }, options = {}) => { const { apiSeparator, // What to swap for `/` in the swagger doc - rootTag = 'root' // The tag that tells us not to prepend the basePath + rootTag = 'root', // The tag that tells us not to prepend the basePath + middleware = {} } = options const reduceRoutes = (acc, elem) => { @@ -31,7 +34,8 @@ const extractPaths = ({ basePath, paths }, options = {}) => { method, route: normaliseRoute(`${isRoot ? '' : basePath}${elem}`), operationId: normaliseOperationId(op.operationId, apiSeparator), - security: normaliseSecurity(op.security) + security: normaliseSecurity(op.security), + middleware: normaliseMiddleware(middleware, op['x-middleware']) }) } }) diff --git a/src/extract/v3/extractPaths.js b/src/extract/v3/extractPaths.js index 3b4c28d..92bb850 100644 --- a/src/extract/v3/extractPaths.js +++ b/src/extract/v3/extractPaths.js @@ -1,6 +1,7 @@ const { METHODS } = require('../../constants') const normaliseSecurity = require('../../normalise/v3/normaliseSecurity') const normaliseOperationId = require('../../normalise/normaliseOperationId') +const normaliseMiddleware = require('../../normalise/normaliseMiddleware') const normaliseRoute = require('../../normalise/normaliseRoute') const basePath = require('./basePath') @@ -12,7 +13,8 @@ const basePath = require('./basePath') method, route, (normalised and inclues basePath if not a root route) operationId, - security + security, + middleware } ] @@ -20,7 +22,8 @@ const basePath = require('./basePath') const extractPaths = ({ security, servers, paths }, options = {}) => { const { apiSeparator, // What to swap for `/` in the swagger doc - variables = {} + variables = {}, + middleware = {} } = options const defaultBasePath = basePath(servers, variables) @@ -38,7 +41,8 @@ const extractPaths = ({ security, servers, paths }, options = {}) => { method, route: normaliseRoute(`${trimmedBase}${elem}`), operationId: normaliseOperationId(op.operationId, apiSeparator), - security: normaliseSecurity(op.security) || defaultSecurity + security: normaliseSecurity(op.security) || defaultSecurity, + middleware: normaliseMiddleware(middleware, op['x-middleware']) }) } }) diff --git a/src/normalise/normaliseMiddleware.js b/src/normalise/normaliseMiddleware.js new file mode 100644 index 0000000..4f7faf9 --- /dev/null +++ b/src/normalise/normaliseMiddleware.js @@ -0,0 +1,7 @@ +const normaliseMiddleware = (handlers = {}, names = []) => + names.reduce((acc, elem) => { + if (typeof handlers[elem] === 'function') acc.push(handlers[elem]) + return acc + }, []) + +module.exports = normaliseMiddleware diff --git a/test/fixtures/exampleV2.json b/test/fixtures/exampleV2.json index 1b92705..959e1eb 100644 --- a/test/fixtures/exampleV2.json +++ b/test/fixtures/exampleV2.json @@ -5,7 +5,7 @@ "version": "1.0.0", "title": "Example API" }, - "basePath": "/api/v2", + "basePath": "/api/v1", "schemes": ["https", "http"], "paths": { "/": { @@ -51,6 +51,7 @@ "summary": "Just a test", "description": "Returns 200 Okay if the path is accessed with the correct token", "operationId": "v2/test", + "x-middleware": ["middleTest"], "produces": ["application/json"], "responses": { "200": { diff --git a/test/fixtures/exampleV3.json b/test/fixtures/exampleV3.json index 5aa4841..8fd9816 100644 --- a/test/fixtures/exampleV3.json +++ b/test/fixtures/exampleV3.json @@ -65,6 +65,7 @@ "summary": "Just a test", "description": "Returns 200 Okay if the path is accessed with the correct token", "operationId": "v2/test", + "x-middleware": ["middleTest"], "responses": { "200": { "description": "success" @@ -180,7 +181,7 @@ }, "path": { "type": "string", - "example": "/api/v2" + "example": "/api/v1" } } }, diff --git a/test/unit/connector.test.js b/test/unit/connector.test.js index 1dff46a..1bffa15 100644 --- a/test/unit/connector.test.js +++ b/test/unit/connector.test.js @@ -1,5 +1,5 @@ const { expect } = require('chai') -const { stub, spy } = require('sinon') +const { stub, spy, resetHistory } = require('sinon') const connector = require('src') const ERRORS = require('src/errors') @@ -22,13 +22,8 @@ describe('src/connector', () => { const onCreateRoute = spy() - const resetStubs = () => { - mockApp.get.resetHistory() - onCreateRoute.resetHistory() - } - context('given an invalid document', () => { - after(resetStubs) + after(resetHistory) it('throws an error', () => expect(() => { @@ -46,7 +41,7 @@ describe('src/connector', () => { connect(mockApp) }) - after(resetStubs) + after(resetHistory) it('returned a function', () => { expect(connect).to.be.a('function') @@ -66,7 +61,7 @@ describe('src/connector', () => { connect(mockApp) }) - after(resetStubs) + after(resetHistory) it('called app.get for each route', () => { expect(mockApp.get.callCount).to.equal(4) @@ -82,7 +77,7 @@ describe('src/connector', () => { connect(mockApp) }) - after(resetStubs) + after(resetHistory) it('called app.get for each route', () => { expect(mockApp.get.callCount).to.equal(4) @@ -92,6 +87,35 @@ describe('src/connector', () => { expect(onCreateRoute.callCount).to.equal(4) }) }) + + context('with middleware', () => { + const middleTest = () => {} + before(() => { + const connect = connector(mockApi, doc, { + security: fakeSecurity, + middleware: { middleTest } + }) + connect(mockApp) + }) + + after(resetHistory) + + it('called app.get for each route', () => { + expect(mockApp.get.callCount).to.equal(4) + }) + + it("called get('/') with the versions handler", () => { + expect(mockApp.get).to.have.been.calledWith('/', mockApi.versions) + }) + + it("passed the middleware into the call to get('/api/v1/test') after the security middleware", () => { + expect(mockApp.get).to.have.been.calledWith( + '/api/v1/test', + fakeSecurity['admin,identity.basic,identity.email'], + middleTest + ) + }) + }) }) } diff --git a/test/unit/extract/v2/extractPaths.test.js b/test/unit/extract/v2/extractPaths.test.js index ab9e2ee..58ba83d 100644 --- a/test/unit/extract/v2/extractPaths.test.js +++ b/test/unit/extract/v2/extractPaths.test.js @@ -36,19 +36,22 @@ describe('src/extract/v2/extractPaths', () => { method: 'get', route: '/', operationId: 'versions', - security: undefined + security: undefined, + middleware: [] }, { method: 'get', route: '/ping', operationId: 'ping', - security: undefined + security: undefined, + middleware: [] }, { method: 'get', route: '/api/v1/test', operationId: 'v1_test', - security: 'admin,identity.basic,identity.email' + security: 'admin,identity.basic,identity.email', + middleware: [] } ] diff --git a/test/unit/extract/v3/basePath.test.js b/test/unit/extract/v3/basePath.test.js index 21b068c..7e15552 100644 --- a/test/unit/extract/v3/basePath.test.js +++ b/test/unit/extract/v3/basePath.test.js @@ -16,13 +16,13 @@ describe('src/extract/v3/basePath', () => { url: `${faker.internet.url()}/` }, { - url: '/{base}/v2' + url: '/{base}/v3' } ] const variables = { base: 'test' } - const expected = '/test/v2' + const expected = '/test/v3' it('returns the expected result', () => { expect(basePath(servers, variables)).to.equal(expected) diff --git a/test/unit/extract/v3/extractPaths.test.js b/test/unit/extract/v3/extractPaths.test.js index a14d9be..b613f0c 100644 --- a/test/unit/extract/v3/extractPaths.test.js +++ b/test/unit/extract/v3/extractPaths.test.js @@ -39,19 +39,22 @@ describe('src/extract/v3/extractPaths', () => { method: 'get', route: '/', operationId: 'versions', - security: undefined + security: undefined, + middleware: [] }, { method: 'get', route: '/ping', operationId: 'ping', - security: undefined + security: undefined, + middleware: [] }, { method: 'get', route: '/api/v1/test', operationId: 'v1_test', - security: 'admin,identity.basic,identity.email' + security: 'admin,identity.basic,identity.email', + middleware: [] } ] diff --git a/test/unit/normalise/normaliseMiddleware.test.js b/test/unit/normalise/normaliseMiddleware.test.js new file mode 100644 index 0000000..4baecf0 --- /dev/null +++ b/test/unit/normalise/normaliseMiddleware.test.js @@ -0,0 +1,35 @@ +const { expect } = require('chai') + +const normaliseMiddleware = require('src/normalise/normaliseMiddleware') + +describe('src/normalise/normaliseMiddleware', () => { + let result + + context('given nothing to normalise', () => { + before(() => { + result = normaliseMiddleware() + }) + + it('returns an empty array', () => { + expect(result).to.be.an('array') + expect(result).to.have.length(0) + }) + }) + + context('given something to normalise', () => { + const test1 = () => {} + const test2 = () => {} + + const handlers = { test1, test2 } + const names = ['test1', 'test2', 'test3'] + const expected = [test1, test2] + + before(() => { + result = normaliseMiddleware(handlers, names) + }) + + it('returns an array containing the named middleware', () => { + expect(result).to.deep.equal(expected) + }) + }) +})