Skip to content

Commit

Permalink
#22 added path-specific middleware option
Browse files Browse the repository at this point in the history
  • Loading branch information
davesag committed Apr 18, 2019
1 parent 197bfdb commit 9fc9a76
Show file tree
Hide file tree
Showing 12 changed files with 145 additions and 33 deletions.
32 changes: 30 additions & 2 deletions README.md
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
```
Expand Down
12 changes: 7 additions & 5 deletions src/connector.js
Expand Up @@ -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)

Expand All @@ -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)
})
Expand Down
10 changes: 7 additions & 3 deletions 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')

/*
Expand All @@ -11,15 +12,17 @@ const normaliseRoute = require('../../normalise/normaliseRoute')
method,
route, (normalised and inclues basePath if not a root route)
operationId,
security
security,
middleware
}
]
*/
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) => {
Expand All @@ -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'])
})
}
})
Expand Down
10 changes: 7 additions & 3 deletions 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')

Expand All @@ -12,15 +13,17 @@ const basePath = require('./basePath')
method,
route, (normalised and inclues basePath if not a root route)
operationId,
security
security,
middleware
}
]
*/
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)
Expand All @@ -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'])
})
}
})
Expand Down
7 changes: 7 additions & 0 deletions 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
3 changes: 2 additions & 1 deletion test/fixtures/exampleV2.json
Expand Up @@ -5,7 +5,7 @@
"version": "1.0.0",
"title": "Example API"
},
"basePath": "/api/v2",
"basePath": "/api/v1",
"schemes": ["https", "http"],
"paths": {
"/": {
Expand Down Expand Up @@ -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": {
Expand Down
3 changes: 2 additions & 1 deletion test/fixtures/exampleV3.json
Expand Up @@ -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"
Expand Down Expand Up @@ -180,7 +181,7 @@
},
"path": {
"type": "string",
"example": "/api/v2"
"example": "/api/v1"
}
}
},
Expand Down
44 changes: 34 additions & 10 deletions 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')
Expand All @@ -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(() => {
Expand All @@ -46,7 +41,7 @@ describe('src/connector', () => {
connect(mockApp)
})

after(resetStubs)
after(resetHistory)

it('returned a function', () => {
expect(connect).to.be.a('function')
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
)
})
})
})
}

Expand Down
9 changes: 6 additions & 3 deletions test/unit/extract/v2/extractPaths.test.js
Expand Up @@ -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: []
}
]

Expand Down
4 changes: 2 additions & 2 deletions test/unit/extract/v3/basePath.test.js
Expand Up @@ -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)
Expand Down
9 changes: 6 additions & 3 deletions test/unit/extract/v3/extractPaths.test.js
Expand Up @@ -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: []
}
]

Expand Down
35 changes: 35 additions & 0 deletions 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)
})
})
})

0 comments on commit 9fc9a76

Please sign in to comment.