Giter Club home page Giter Club logo

swagger-routes's Introduction

Swagger Routes

Build Status npm version

A tool to generate and register Restify or Express route handlers from a Swagger 2.0 (OpenAPI) specification.

Usage

Requires Node v4.0+

Express

const swaggerRoutes = require('swagger-routes')
const express = require('express')
const app = express()

swaggerRoutes(app, {
    api: './api.yml',
    handlers:  './src/handlers',
    authorizers: './src/handlers/security'
})
app.listen(8080)

Restify

const swaggerRoutes = require('swagger-routes')
const restify = require('restify')
const server = restify.createServer()

swaggerRoutes(server, {
    api: './api.yml',
    handlers:  './src/handlers',
    authorizers: './src/handlers/security'
})
server.listen(8080)
Options
  • api: path to your Swagger spec, or the loaded spec reference.
  • docsPath: url path to serve your swagger api json. Defaults to /api-docs.
  • docsMiddleware: An optional middleware function that can be used to secure the api docs endpoint.
  • handlers: directory where your handler files reside. Defaults to ./handlers. Can alternatively be a function to return a handler function given an operation.
  • authorizers: directory where your authorizer files reside. Defaults to ./security. Can alternatively be a function to return an authorizer middleware given a swagger security scheme.
  • maintainHeaders: Keeps your generated handler doc headers in sync with your Swagger api. Default is false.

Operation Handlers

You have the option to define and maintain a handler file for each Swagger operation, or alternatively provide a factory function which creates a handler function given an operation.

Handler Files

Using individual handler files is a good choice if each handler needs unique logic to deal with an operation request.

A handler file must be named after the Swagger operation it handles e.g. listPets.js.

All handler files must reside in the same directory, unless the group option is enabled, in which case the handler file should sit under a folder of its primary tag name (see Generating Handler Files below).

File Contents

A function called handler should be exported to deal with an incoming operation request.

exports.handler = function listPets(req, res, next) {

}

You also have the option to export a middleware function to be executed before the handler.

exports.middleware = preprocess

function preprocess(req, res, next) {
    next()
}

Middleware can be an ordered list.

exports.middleware = [
    function preprocess1(req, res, next) { next() },
    function preprocess2(req, res, next) { next() }
]
Generating Handler Files

To save you some time there's a bundled tool to generate handler files based on operations in your Swagger spec, together with a Mustache template.

This tool is on by default so check your handlers folder the first time you run swaggerRoutes and it should be poulated with handler stubs for each operation defined in your Swagger document.

Each time you start your app swaggerRoutes will see if you have any missing operation handlers and generate stub handler for any which are. If a handler file exists it won't be touched, i.e. this is non-destructive so you are free to edit them.

Note that if you turn on the syncHeaders option then the header of your handler files will be updated each run based on your Swagger api. This keeps your handler documentation up to date so you can easily see what parameters accompany a request for a given operation. It will overwrite any edits you make to the header so only turn on if you don't plan on manually editing them.

When a re-run finds handlers no longer in use they will be renamed with an _ prefix, so listPets.js would become _listPets.js. This allows you to identify handlers no longer in use and remove / rename them if you wish.

If you later enable a handler again in your spec and re-run, then the underscore will be removed.

Note that this feature of prefixing removed handlers is only currently supported when the group options is not enabled.

The default template is defined here but you can supply your own by expanding the handlers option e.g.

{
    ...
    handlers: {
        path: './src/handlers',
        template: './template/handler.mustache', // can also be set with a loaded template
        getTemplateView: operation => operation, // define the object to be rendered by your template
        create: operation => (req, res) => {}, // see Handler Factory section for details
        generate: true, // hander file generation on by default
        group: false // when true each handler file will be placed under a directory named after its primary tag
    }
}

Handler Factory

The factory function is a better option to a file if handlers are quite similar e.g. delegate their request processing onto service classes.

Creating a Handler Factory

You can define handlers as a function when registering your routes. It receives a Swagger operation and returns the request handler responsible for dealing with it.

const swaggerRoutes = require('swagger-routes')

swaggerRoutes(app, {
    api: './api.yml',
    handlers:  createHandler
})

function createHandler(operation) {
    return function handler(req, res, next) {
        res.send(operation.id)
    }
}

If a handler function is returned then it will take precedence over a handler file for the same operation.

Route Middleware

Just as a file handler can define route middleware, so can createHandler.

function createHandler(operation) {
    return {
        middleware: function preprocess(req, res, next) { next() },
        handler: function handler(req, res, next) { res.send(operation.id) }
    }
}

As before, route middleware can be an ordered list.

function createHandler(operation) {
    return {
        middleware: [
            function preprocess1(req, res, next) { next() },
            function preprocess2(req, res, next) { next() }
        ],
        handler: function handler(req, res, next) { res.send(operation.id) }
    }
}

Authorizers

When your Swagger api specifies one or more security schemes then routes which opt into one or more of these schemes can be protected by authorizer middleware.

Just like handlers, you can define an authorizer in a file or via a factory.

File Authorizer

The file should be named after the security scheme it protects e.g. petstore_auth.js, and reside in the directory path defined by the authorizers option. It should export a single middleware function to authorize a request.

module.exports = function petstore_auth(req, res, next) {
    const token = decodeToken(req.headers.authorization)
    if (token) {
        const scopes = getTokenScopes(token)
        next(req.verifyScopes(scopes))
    } else {
        const error = new Error('Unauthorized')
        error.status = error.statusCode = 401
        next(error)
    }
}

The above is one example of how this can work.

As you can see a verifyScopes function is supplied to the req if the security scheme is OAuth2. It takes an array of scopes you decode from the authenticated request and verifies that the required scope(s) defined be the scheme are present. If they're not a 403 Forbidden error is returned.

When multiple oauth scopes are defined for the security of an endpoint, Swagger expects all of them to be present for a call to proceed. As an extension to this, swagger-routes also supports the logical OR of token scopes, so if any exist then verifyScopes succeeds.

As an example, this definition below will pass the auth check if either a Catalog OR Playback scope exist.

  ...
  security:
    - accountAuth:
      - Catalog
      - Playback
  x-security:
    accountAuth:
      OR_scopes: true

Remember if no credentials are supplied a 401 Unauthorized should be returned.

Generating Authorizer Files

Much like handler files, authorizer file stubs will be generated and managed for you too.

The default template is defined here but you can supply your own by expanding the authorizers option e.g.

{
    ...
    authorizers: {
    	path: './src/handlers/security',
    	template: './template/authorizer.mustache', // can also be set with a loaded template
    	getTemplateView: operation => operation, // define the object to be rendered by your template
    	create: operation => (req, res) => {}, // see Authorizer Factory section for details
    	generate: true // authorizer file generation on by default
    }
}

Authorizer Factory

const swaggerRoutes = require('swagger-routes')

swaggerRoutes(app, {
    api: './api.yml',
    authorizers: createAuthorizer
})

function createAuthorizer(schemeId, securityScheme) {
    return function authorizer(req, res, next) {
        const token = decodeToken(req.headers.authorization)
        if (token) {
            const scopes = getTokenScopes(token)
            next(req.verifyScopes(scopes))
        } else {
            const error = new Error('Invalid access token')
            error.status = error.statusCode = 401
            next(error)
        }
    }
}

Request Validation

Each incoming request which makes it to a handler will be run through request validation middleware. This executes JSON Schema validation on the request to ensure it meets the Swagger specification you've defined. A failure to meet this requirement will cause the request to fail and the handler not to be executed.

Swagger Host

Statically setting the host property of your Swagger api can be error prone if you run the api in different environments (QA, Staging, Production), that's why I'd recommended removing its definition from your specification. This will by default then resolve to the host, including port, the spec is served from.

If this still isn't sufficient you have a couple of other options.

  1. Set API_HOST environment variable for your node instance. SwaggerRoutes will pick this up and use it.
  2. Set the app.swagger.host manually from within your app after you've called swaggerRoutes.
const server = app.listen(3000, '0.0.0.0', () => {
    app.swagger.host = `${server.address().address}:${server.address().port}`
})

Route Stack Execution Order

  1. authorizer middleware If there are security restrictions on a route then an authorizer for each will need to verify the rights attached to the request.
  2. custom middleware If the route defines one or more middleware these will be executed in order.
  3. validation middleware The incoming request will now be validated against the Swagger spec for the given operation.
  4. handler Assuming all previous steps pass, the handler is now executed.

Advanced Usage

Registering Multiple Swagger Apis

You may be in the situation where you have a Swagger definition for each major version of your api. If this is the case, and you want to handle each on the same server, then you are free to register more than one spec.

swaggerRoutes(server, {
    api: './api-v1.yml',
    handlers:  './src/handlers/v1',
    authorizers: './src/handlers/v1/security'
})

swaggerRoutes(server, {
    api: './api-v2.yml',
    handlers:  './src/handlers/v2',
    authorizers: './src/handlers/v2/security'
})

You'll need to ensure that there's no conflict in route paths between each. The best way to do that would be to add a unique basePath to each spec, say /v1, /v2 etc.

Operation Object

An operation object inherits its properties from those defined in the Swagger spec.

There are only a few differences / additions.

  • id: Replaces operationId.
  • path: The route path of this operation.
  • method: The http method
  • consumes: Populated with the top level consumes unless the operation defines its own.
  • produces: Populated with the top level produces unless the operation defines its own.
  • paramGroupSchemas: JSON Schema for each param group ('header', 'path', 'query', 'body', 'formData') relevant to the operation.

Acknowledgments

Inspiration for this library came primarily from time spent using swaggerize-express. It's a great library which you should check out.

My reasoning behind writing a new, alternate implementation was the wish to base all routing off operation ids and not paths. This aligns with how Swagger client code gen works, making it easier to see your client SDKs and server code base as a whole.

I also wanted to automate away much of the boilerplate code being written and support Restify and Express in a single library, given their similarities.

swagger-routes's People

Contributors

g-junming avatar jamidon avatar laszlo-vadasz-deltatre avatar mike-stead-deltatre avatar mikestead avatar mtribes-sdk-bot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

swagger-routes's Issues

Parameter with an array is not being validated and maybe not parsed correctly

I'm trying to track down a problem with passing arrays to some of my POST methods where validation isn't happening. I've attached part of a Swagger 2.0 schema JSON file: api.txt that illustrates the problem. I did verify that it is valid by checking it against Swagger 2.0 Parser.

The swagger-ui looks like this:
swagger-ui

As you can see, the page doesn't seem to know an array of values can be sent to the organizations/add interface. Also, if I pass something invalid like:

{ "name": "foo", "abbreviation": "bar" }

which is not an array or

[ { "name21": "foo", "abbreviation": "bar" } ]

where name21 is invalid.

My little bootstrap for hosting this is:

const swaggerRoutes = require('swagger-routes');
const restify = require('restify');

const server = restify.createServer();
server.use(restify.acceptParser(server.acceptable));
server.use(restify.authorizationParser());
server.use(restify.dateParser());
server.use(restify.queryParser({ mapParams: false }));
server.use(restify.jsonp());
server.use(restify.gzipResponse());
server.use(restify.bodyParser());
server.use(restify.CORS());
server.use(restify.fullResponse());

function createHandler(operation) {
  return function handler(req, res, next) {
    res.send(operation.id);
  };
}

swaggerRoutes(server, {
  api: './api.json',
  handlers: createHandler
});
server.listen(8080);

Any idea as to where to look to track this down?

Thanks.

path.id is not of a type(s) integer

Am getting a validation error path.id is not of a type(s) integer whenever I use a path value that contains an integer. Must all path values be defined as strings?

/artist/{id}:
  x-serviceId: 'artist'
  get:
    tags:
    - "event"
    summary: "getArtist"
    description: "Get an artist by ID"
    produces:
    - "application/json"
    parameters:
    - name: "id"
      in: "path"
      description: "artist identifier"
      required: true
      type: "integer"
      format: "int64"

Array items failing validation

yaml definition

testarray:
  type: "array"
  items:
    type: "integer"
    format: "int64"

sending this data in the body:

testarray: [1,5,9]

-or-

testarray: 1,5,9

returns this error:

body.testarray[0] is not of a type(s) integer, body.testarray[1] is not of a type(s) integer, body.testarray[2] is not of a type(s) integer

Of course, it may well be the format I am sending...

Errors from middleware swallowed by promise

The authorisation middleware that is added by this module here looks like this:

function authorize(req, res, next) {
    return Promise.all(...)
    .then(() => next())
    .catch(e => next(e))
  }

This causes errors that are thrown from middleware further down the chain to be swallowed due to this: restify/node-restify/issues/962

Essentially the .then(() => next()) means that the execution carries on inside the promise scope, so any errors then thrown will land in .catch(e => next(e)), but by this time the next function has already been called once and calling it again has no effect.

Resolving this I think means removing the promises or potentially something like:

function authorize(req, res, next) {
    return Promise.all(...)
    .then(() => setImmediate(next))
    .catch(e => next(e))
  }

`apiSpecs.js` broken after axios update

#22 updated axios from 0.9.1 to 0.18.1

Release 0.13.0 of axios contains a breaking change with error formats - errors are now errors rather than response objects. This is not handled by the apiSpecs.js code and we get failures in various places.

I will have a look at fixing this and opening a PR if I get a chance

Response validation

Currently swagger-routes only supports validating request structure; would be extremely useful if it could validate responses as well.

Order of excecution of security handlers

Is there a way to run the security handlers sequentially? I have two handlers A and B where B depends on A but due to the handlers being executed in parallel, that dependency can not be enforced. I looked through the docs but I could not find a flag that allows one to achieve that.

Unexpected fields validation

Currently swagger-routes silently accepts unsupported fields. It would be useful to be able to enable enforcement of only defined fields to be present.

Support for restify 7.x and 8.x

We are using this library for all routes on Restify 4.x and 6.x without issues. There is however change in path resolver in restify 7.x and it is causing this issue:

Generated 'api.yml'
13:44:51.878Z FATAL rocket: Uncaught exception, aborting Node (err.code=ERR_ASSERTION)
    AssertionError [ERR_ASSERTION]: The first character of a path should be `/` or `*`
        at Router.on (...\node_modules\find-my-way\index.js:69:3)
        at RouterRegistryRadix.add (...\node_modules\restify\lib\routerRegistryRadix.js:42:21)
        at Router.mount (...\node_modules\restify\lib\router.js:212:20)
        at Server.serverMethod [as get] (...\node_modules\restify\lib\server.js:1686:33)
        at registerDocsRoute (...\node_modules\swagger-routes\src\routeRegister.js:36:7)
        at Object.registerRoutes (...\node_modules\swagger-routes\src\routeRegister.js:12:3)
        at addHandlers (...\node_modules\swagger-routes\src\index.js:19:17)

It happens ^^ when we call swaggerRoutes(server, {

To see what exactly is happening I logged data from https://github.com/mikestead/swagger-routes/blob/master/src/routeRegister.js#L34

And if I update it with this:

function registerDocsRoute(app, options) {
  console.log(`/${options.api.basePath}/${options.docsPath}`)
  const docsPath = path.normalize(`/${options.api.basePath}/${options.docsPath}`)
  console.log(docsPath);
  app.get(docsPath, options.docsMiddleware, function handler(req, res) { res.json(options.api) })
}

Then this is logged before error:

////spec
\spec

For new restify resolver, path cannot start with \.

When I tried to just comment this line https://github.com/mikestead/swagger-routes/blob/master/src/routeRegister.js#L36 then everything else is working correctly and it loads all paths same as before.

Removing an API, adding an API using JSON and preventing handler generation.

Hi, I'm trying out the library. Really great work.

I'm going through the code now but have a few questions:

  1. Is there a way to dynamically remove an API from a running server (using restify in my case)?
  2. Is there a recommended way to pass a json swagger object instead of yaml?
  3. Is there a way to prevent handler generation completely?

Thank you!

formData required validation

Greetings everyone,
I'm having problem when validation formData file upload with multer and swaggerRoutes.
swagger.yml

parameters:
- in: "formData"
        type: "file"
        required: true
        name: "document"

The swaggerRoutes method returns this when validating the "required: true":
"message": "formData requires property \"document\""

If I set required as false, the swaggerRotues continues without any problem.

Anyone have any ideas how to solve this?

Split apiSpecs into separate module

At the moment the src/apiSpecs file is intended for testing only, yet it accounts for almost all of the dependencies of this module.

The upshot is we are pulling in modules like expect which has a shed load of its own dependencies into our production builds.

Ideally we could split the apiSpecs out somehow so this is not the case. Cant think of a really elegant solution but some ideas:

  1. Set the dependencies only needed for this file as optionalDependencies - then consumers can chose not to install. Don't really like this one as I don't know how other modules would use optional dependencies
  2. Move apiSpecs into a subdirectory with its own package.json - then somehow just do a local install on this dir if needed
  3. Move apiSpecs into a totally separate repo - bit of a hassle

Not initializing on Windows

Tried to initialize like this using a basic swagger json on Windows:
swaggerRoutes(server, { api: './definitions/openapi.json', handlers: { path: './dist/handlers/v1', template: './definitions/template/handler.mustache', getTemplateView: (operation) => operation,template generate: enableCodeGenerator, group: false, }, authorizers: './src/security/v1', });

However, getting error:

AssertionError [ERR_ASSERTION]: The first character of a path should be / or *

After doing some investigation found that this is caused by line 35 on routerRegister.js file

Changed path.normalize to path.posix.normalize and it fixed the issue.

Help on usage

Hi, really liking this library too, been looking for something to allow me to do some specific route handling in node express from a swagger file.

Use case: add the tag name to the route handler
It looks like the handlerfactory approach would work well for this, but the examples are a little above my understanding. Could you help me work out what a handler might look like for this case?

api.yaml: parameters and other stuff omitted for brevity

  /authtest/{id}:
    get:
      x-method-id: "1022"
      tags:
      - "system"
  post:
      x-method-id: "1023"
      tags:
      - "system"

required handler folder structure for both of these would be

/routes/system/authtest,js

methods in that handler would be

authtestPOST(req, res, next)
authtestGET(req, res, next)

And the x-method-id needs to be parsed and available in the request somewhere.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.