Skip to content

Commit

Permalink
Merge branch 'release/3.1.1'
Browse files Browse the repository at this point in the history
  • Loading branch information
davesag committed Oct 24, 2019
2 parents 0f4e16b + 1fc53ff commit 5a008b4
Show file tree
Hide file tree
Showing 17 changed files with 440 additions and 339 deletions.
91 changes: 54 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ Connect [`Express`](http://www.expressjs.com) route controllers to restful paths

## Prerequisites

This library assumes:
This library assumes you are using:

1. You are using [`expressjs`](http://www.expressjs.com)
2. You are using [`swagger`](http://swagger.io) _version 2_ or [`OpenAPI`](https://www.openapis.org) _version 3_
1. [NodeJS](https://nodejs.org) _version 6.4.0_ or better,
2. [`expressjs`](http://www.expressjs.com) _any version_, and
3. [`swagger`](http://swagger.io) _version 2_, or [`OpenAPI`](https://www.openapis.org) _version 3_.

## Install

Expand Down Expand Up @@ -49,9 +50,9 @@ const ping = (req, res) => {
module.exports = { ping, versions }
```

### Swagger Version 2 Example
### Swagger Version 2 example

Given a Swagger (v2) YAML file `my-api.yml` along the lines of:
Given a Swagger (v2) YAML file `api.yml` along the lines of:

```yml
swagger: '2.0'
Expand Down Expand Up @@ -185,9 +186,9 @@ components:
$ref: '#/components/schemas/APIVersion'
```
## Your Express Server
## Connecting your Express server
You could set up your server as follows:
You can `connect` your `Express` app or router as follows:

```js
const express = require('express')
Expand All @@ -196,15 +197,16 @@ const { connector } = require('swagger-routes-express')
const api = require('./api')
const makeApp = () => {
const apiDefinition = YAML.load('api.yml')
const connect = connector(api, apiDefinition)
const apiDefinition = YAML.load('api.yml') // load the api as json
const connect = connector(api, apiDefinition) // make the connector
const app = express() // make the app
const app = express()
// do any other app stuff, such as wire in passport, use cors etc
// then attach the routes
connect(app)
connect(app) // attach the routes
// add any error handlers last
return app
}
```
Expand All @@ -213,11 +215,11 @@ With the result that requests to `GET /` will invoke the `versions` controller a

## Adding security middleware handlers

You can pass in a range of options, so if your swagger document defines security scopes you can pass in via a `security` option:
If your swagger document defines security, you can map this to your own Auth Middleware by passing in a `security` option to the `connector`.

### With scopes
### Security with scopes

For example if your path has a `security` block like
For example if your path defines oAuth style `security` like:

```yml
paths:
Expand All @@ -238,15 +240,15 @@ Supply a `security` option as follows
```js
const options = {
security: {
'read-write': readWriteAuthMiddlewareFunction,
'read,write': readWriteAuthMiddlewareFunction,
admin: adminAuthMiddlewareFunction
}
}
```

### Without scopes
### Security without scopes

If your paths supply a `security` block but its `scopes` array is empty, you can just use its name instead in the `security` option.
If your path defines `security`, and its `scopes` array is empty, you use its name in the `security` option.

Given:

Expand All @@ -269,10 +271,31 @@ const options = {
}
```

### Global security definition

Both Swagger V2 and OpenAPI V3 allow you to define global `security`. The global `security` definition will be applied if there is no path-specific one defined.

### Exempting a path from global security

If you've defined global `security` but wish to exempt a specific path, then you can configure the path like:

```yml
paths:
/my-route
get:
summary: some route that is exempt from the default security
security: []
```

### Further reading on Swagger and security

- [Swagger V2 Authentication](https://swagger.io/docs/specification/2-0/authentication/), and
- [Open API V3 Authentication](https://swagger.io/docs/specification/authentication/) docs.

### Notes

- The scopes, if supplied, are sorted alphabetically.
- Only the **first** security option is used, the others are ignored.
- Only the **first** security option is used, the others are ignored. Your Auth Middleware function must handle any alternative authentication schemes.
- Scopes, if supplied, are sorted alphabetically.

### What's an Auth Middleware function?

Expand All @@ -295,13 +318,9 @@ async function correspondingMiddlewareFunction(req, res, next) {

- [More information…](https://duckduckgo.com/?q=express+auth+middleware) (via DuckDuckGo)

### OpenAPI V3 Global Security Blocks

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
You can add your own path specific middleware by passing in a `middleware` option:

```js
{
Expand All @@ -311,7 +330,7 @@ You can add your own path specific middleware by passing in a `middleware` optio
}
```

and then in the path specification adding an `x-middleware` option
With either Swagger v2 or OpenAPI v3, add an `x-middleware` option in the path specification:

```yml
paths:
Expand All @@ -322,9 +341,7 @@ paths:
- myMiddleware
```

The `someMiddlewareFunction` will be inserted **after** any auth middleware.

This works for both Swagger v2 and OpenAPI v3 documents.
The `someMiddlewareFunction` will be inserted **after** any Auth Middleware.

## Adding hooks

Expand All @@ -339,7 +356,7 @@ const onCreateRoute = (method, descriptor) => {

The method will be one of 'get', 'post', 'patch', 'put', or 'delete'.

The descriptor is an array of
The `descriptor` is an array of:

```js
;[
Expand All @@ -364,23 +381,23 @@ You can supply your own `apiSeparator` option in place of `_` to map from `/`.

## Missing Route Controllers

If a route controller is defined as an `operationId` in swagger but there is no corresponding controller, a default `notImplemented` controller will be inserted that simply responds with a `501` error. You can also specify your own `notImplemented` controller in `options`.
If a route controller is defined as an `operationId` in Swagger but there is no corresponding controller, a default `notImplemented` controller will be inserted that simply responds with a `501` error. You can also specify your own `notImplemented` controller in `options`.

If no `operationId` is supplied for a path then a default `notFound` controller that responds with a `404` status will be inserted. You can also specify your own `notFound` controller in `options`.

## Base paths

### Swagger Version 2

For the root path `/` we check the route's `tags`. If the first tag defined for a path is `'root'` we don't inject the api basePath, otherwise we do. You can define your own `rootTag` option to override this.
For the root path `/` we check the route's `tags`. If the first `tag` defined for a path is `'root'` we don't inject the api `basePath`, otherwise we do. You can define your own `rootTag` option to override this behaviour.

### OpenAPI Version 3

The OpenAPI format allows you to define both a default `servers` array, and `path` specific `servers` arrays. The `url` fields in those arrays are parsed, ignoring any absolute URLS (as they are deemed to refer to controllers external to this API Server).
The OpenAPI V3 format allows you to define both a default `servers` array, and `path` specific `servers` arrays. The `url` fields in those arrays are parsed, ignoring any absolute URLS (as they are deemed to refer to controllers external to this API Server).

The spec allows you to include template variables in the `servers`' `url` field. To accomodate this you can supply a `variables` option in `options`. Any variables you specify will be substituted.

## Default Options
## Default options

If you don't pass in any options the defaults are:

Expand All @@ -398,7 +415,7 @@ If you don't pass in any options the defaults are:
}
```

## Generating API Summary information
## Generating API summary information

You can generate a summary of your Swagger v2 or OpenAPI v3 API specification in the form:

Expand All @@ -419,7 +436,7 @@ const apiDefinition = YAML.load('api.yml')
const apiSummary = summarise(apiDefinition)
```

## Upgrading from Swagger Routes Express V2 to V3.
## Upgrading from Swagger Routes Express V2 to V3

These docs refer to Version 3 of Swagger Routes Express which changed the way you invoke the `connector`.

Expand Down Expand Up @@ -449,7 +466,7 @@ const { connector } = require('swagger-routes-express')

### Prerequisites

- [NodeJS](https://nodejs.org) — Ideally version `10.16.3 (LTS)` or better.
- [NodeJS](https://nodejs.org) — Ideally you will develop with version `12.13.0 (LTS)` or better, but it will work with node versions going back to version `6.4.0`.

### Test it

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "swagger-routes-express",
"version": "3.1.0",
"version": "3.1.1",
"description": "Connect Express route controllers to restful paths using a Swagger 2 or OpenAPI 3 definition file",
"main": "src/index.js",
"engines": {
Expand Down
6 changes: 6 additions & 0 deletions src/extract/extractVersion.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
const { major, coerce } = require('semver')

/**
* Determins whether to use version 2 or 3 given the version defined in the supplied swagger document.
* @param an object with keys `swagger` or `openapi`
* @returns 2 if the document is a swagger version 2 document,
* 3 if it's an openapi version 3 document, or undefined otherwise.
*/
const extractVersion = ({ swagger, openapi }) =>
swagger && parseInt(major(coerce(swagger))) === 2
? 2
Expand Down
8 changes: 5 additions & 3 deletions src/extract/v2/extractPaths.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { METHODS } = require('../../constants')
const normaliseSecurity = require('../../normalise/v2/normaliseSecurity')
const normaliseSecurity = require('../../normalise/normaliseSecurity')
const normaliseOperationId = require('../../normalise/normaliseOperationId')
const normaliseMiddleware = require('../../normalise/normaliseMiddleware')
const normaliseRoute = require('../../normalise/normaliseRoute')
Expand All @@ -17,13 +17,15 @@ const normaliseRoute = require('../../normalise/normaliseRoute')
* ]
*
*/
const extractPaths = ({ basePath, paths }, options = {}) => {
const extractPaths = ({ security, 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
middleware = {}
} = options

const defaultSecurity = normaliseSecurity(security)

const reduceRoutes = (acc, elem) => {
METHODS.forEach(method => {
const op = paths[elem][method]
Expand All @@ -33,7 +35,7 @@ const extractPaths = ({ basePath, paths }, options = {}) => {
method,
route: normaliseRoute(`${isRoot ? '' : basePath}${elem}`),
operationId: normaliseOperationId(op.operationId, apiSeparator),
security: normaliseSecurity(op.security),
security: normaliseSecurity(op.security, defaultSecurity),
middleware: normaliseMiddleware(middleware, op['x-middleware'])
})
}
Expand Down
14 changes: 7 additions & 7 deletions src/extract/v3/extractPaths.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { METHODS } = require('../../constants')
const normaliseSecurity = require('../../normalise/v3/normaliseSecurity')
const normaliseSecurity = require('../../normalise/normaliseSecurity')
const normaliseOperationId = require('../../normalise/normaliseOperationId')
const normaliseMiddleware = require('../../normalise/normaliseMiddleware')
const normaliseRoute = require('../../normalise/normaliseRoute')
Expand Down Expand Up @@ -30,11 +30,11 @@ const extractPaths = ({ security, servers, paths }, options = {}) => {
const defaultBasePath = basePath(servers, variables)
const defaultSecurity = normaliseSecurity(security)

const pathSecurity = (opSecurity, defaultSecurity) => {
const pathSecurity = normaliseSecurity(opSecurity)
if (pathSecurity === null) return // the security was an empty array.
return pathSecurity || defaultSecurity
}
// const pathSecurity = (opSecurity, defaultSecurity) => {
// const pathSecurity = normaliseSecurity(opSecurity)
// if (pathSecurity === null) return // the security was an empty array.
// return pathSecurity || defaultSecurity
// }

const reduceRoutes = (acc, elem) => {
METHODS.forEach(method => {
Expand All @@ -45,7 +45,7 @@ const extractPaths = ({ security, servers, paths }, options = {}) => {
method,
route: normaliseRoute(`${trimBase(base)}${elem}`),
operationId: normaliseOperationId(op.operationId, apiSeparator),
security: pathSecurity(op.security, defaultSecurity),
security: normaliseSecurity(op.security, defaultSecurity),
middleware: normaliseMiddleware(middleware, op['x-middleware'])
})
}
Expand Down
40 changes: 40 additions & 0 deletions src/normalise/keyOrScopes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Given a block of security data, either return the key, if there are no scopes,
* or return the sorted, joined up scopes.
*
* From the official docs
* > `security` is an array of hashmaps, where each hashmap contains
* > one or more named security schemes.
*
* @param security — The array of security blocks
* @return the key if there are no scopes, or
* the joined up sorted scopes if there are scopes, or
* undefined if the array of blocks is empty
*/
const keyOrScopes = ([data]) => {
if (!data) return // there is no security
const [key] = Object.keys(data) // we only care about the first key
const scopes = data[key] // there must be at least one key
if (!scopes.length) return key // if there are no scopes use the key instead
return scopes.sort().join(',')
}

module.exports = keyOrScopes

/*
security: []
=> undefined
security:
- someKey: []
=> someKey
security:
- someKey:
- scope1
- scope2
=> scope1,scope2
*/
18 changes: 18 additions & 0 deletions src/normalise/normaliseSecurity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const keyOrScopes = require('./keyOrScopes')

/**
* Maps the supplied security block (if any) to a simple string representation
* that can in turn be used as a key for the appropriate security middleware.
*
* Refs:
* - V2 https://swagger.io/docs/specification/2-0/authentication
* - V3 https://swagger.io/docs/specification/authentication
*
* @param security — A swagger security block
* @param globalSecurity — A previously computed global security key.
* @return a string representation used as a key for the appropriate security middleware.
*/
const normaliseSecurity = (security, globalSecurity) =>
security ? keyOrScopes(security) : globalSecurity

module.exports = normaliseSecurity
16 changes: 0 additions & 16 deletions src/normalise/v2/keyOrScopes.js

This file was deleted.

Loading

0 comments on commit 5a008b4

Please sign in to comment.