From 5c5924c203bcc0456da51c54687f0d46d7761f6f Mon Sep 17 00:00:00 2001 From: Carmine DiMascio Date: Thu, 4 Apr 2019 16:56:41 -0400 Subject: [PATCH] Openapi 3 support (#32) * initial openapi_3 integration * update readme * add support for openapi 3.x * Update README.md * add error handler for openapi3 * update deps * Update error.handler.js --- README.md | 2 +- app/index.js | 49 +++++++++--- app/templates/.env | 6 +- app/templates/package.json | 4 + .../server/api/middlewares/error.handler.js | 16 ++++ .../common/{swagger/Api.yaml => api.v2.yml} | 0 app/templates/server/common/api.yml | 74 +++++++++++++++++++ app/templates/server/common/server.js | 20 ++++- .../common/{swagger/index.js => swagger.js} | 15 ++-- package-lock.json | 2 +- package.json | 2 +- 11 files changed, 166 insertions(+), 24 deletions(-) create mode 100644 app/templates/server/api/middlewares/error.handler.js rename app/templates/server/common/{swagger/Api.yaml => api.v2.yml} (100%) create mode 100644 app/templates/server/common/api.yml rename app/templates/server/common/{swagger/index.js => swagger.js} (79%) diff --git a/README.md b/README.md index 326acff..e9cd403 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![](https://img.shields.io/badge/status-stable-green.svg) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/56c006ccc44c47f49d12b6b35fcf35da)](https://www.codacy.com/app/cdimascio/generator-express-no-stress?utm_source=github.com&utm_medium=referral&utm_content=cdimascio/generator-express-no-stress&utm_campaign=Badge_Grade) ![](https://img.shields.io/badge/license-MIT-blue.svg) -Create awesome [Express.js](http://www.expressjs.com) applications with best of breed tech including ES.next via [Babel.js](https://babeljs.io/), structured logging with [Pino](https://github.com/pinojs/pino), API validation and interactive documentation via [Swagger](http://swagger.io/), environment based config with [dotenv](https://github.com/motdotla/dotenv), and linting with [ESLint](http://eslint.org/). +Create awesome [Express.js](http://www.expressjs.com) applications with best of breed tech including ES.next via [Babel.js](https://babeljs.io/), structured logging with [Pino](https://github.com/pinojs/pino), API validation and interactive documentation using an [OpenAPI 3](https://swagger.io/specification/) or [Swagger 2](http://swagger.io/) spec, environment based config with [dotenv](https://github.com/motdotla/dotenv), and linting with [ESLint](http://eslint.org/).

diff --git a/app/index.js b/app/index.js index 095c9f9..a5098b0 100644 --- a/app/index.js +++ b/app/index.js @@ -20,6 +20,7 @@ module.exports = class extends Generator { this.description = 'My cool app'; this.version = '1.0.0'; this.apiRoot = '/api/v1'; + this.specification = 'swagger_2'; } initializing() {} @@ -41,6 +42,15 @@ module.exports = class extends Generator { name: 'apiVersion', message: `Version [${this.version}]`, }, + { + type: 'list', + name: 'specification', + message: `OpenAPI spec version`, + choices: [ + { name: 'Swagger 2', value: 'swagger_2' }, + { name: 'OpenApi 3 (beta)', value: 'openapi_3' }, + ], + }, { type: 'list', name: 'linter', @@ -66,6 +76,7 @@ module.exports = class extends Generator { this.version = r.version ? r.version : this.version; this.apiRoot = r.apiRoot ? r.apiRoot.replace(/^\/?/, '/') : this.apiRoot; this.linter = r.linter; + this.specification = r.specification; }); } @@ -86,20 +97,32 @@ module.exports = class extends Generator { '.eslintrc.json', 'server/routes.js', 'test/examples.controller.js', - 'server/common/swagger/Api.yaml', + 'server/common/api.yml', + 'server/common/server.js', + 'server/api/middlewares/error.handler.js', 'public/api-explorer/index.html', 'public/api-explorer/swagger-ui-standalone-preset.js', 'public/index.html', 'gitignore', ]; - const copyOpts = this.docker - ? null - : { - globOptions: { - ignore: ['**/+(Dockerfile|.dockerignore)'], - }, - }; + const copyOpts = { + globOptions: { + ignore: [], + }, + }; + + if (this.specification === 'openapi_3') { + copyOpts.globOptions.ignore.push('**/server/common/swagger.js'); + copyOpts.globOptions.ignore.push('**/server/common/api.v2.yml'); + } else { + files.push('server/common/api.v2.yml'); + copyOpts.globOptions.ignore.push('**/server/common/api.yml'); + } + if (this.docker) { + copyOpts.globOptions.ignore.push('**/+(Dockerfile|.dockerignore)'); + } + this.fs.copy(src, dest, copyOpts); this.fs.copy(this.templatePath('.*'), dest, copyOpts); @@ -110,13 +133,15 @@ module.exports = class extends Generator { version: this.version, apiRoot: this.apiRoot, linter: this.linter, + specification: this.specification, }; files.forEach(f => { this.fs.copyTpl( this.templatePath(f), this.destinationPath(`${this.name}/${f}`), - opts + opts, + copyOpts ); }); @@ -124,6 +149,12 @@ module.exports = class extends Generator { this.destinationPath(`${this.name}`, 'gitignore'), this.destinationPath(`${this.name}`, '.gitignore') ); + if (this.specification !== 'openapi_3') { + this.fs.move( + this.destinationPath(`${this.name}`, 'server/common/api.v2.yml'), + this.destinationPath(`${this.name}`, 'server/common/api.yml') + ); + } }, }; } diff --git a/app/templates/.env b/app/templates/.env index c1425a1..bccac56 100644 --- a/app/templates/.env +++ b/app/templates/.env @@ -4,5 +4,9 @@ LOG_LEVEL=debug REQUEST_LIMIT=100kb SESSION_SECRET=mySecret +<% if (specification === 'openapi_3') { %> +OPENAPI_SPEC=<%= apiRoot %>/spec +<% } else { %> #Swagger -SWAGGER_API_SPEC=/spec \ No newline at end of file +SWAGGER_API_SPEC=/spec +<% } %> \ No newline at end of file diff --git a/app/templates/package.json b/app/templates/package.json index c9a060e..7a4970d 100644 --- a/app/templates/package.json +++ b/app/templates/package.json @@ -19,7 +19,11 @@ "dotenv": "^6.2.0", "express": "^4.16.4", "pino": "^5.11.1", + <% if (specification === 'openapi_3') { %> + "express-openapi-validator": "^0.10.2" + <% } else { %> "swagger-express-middleware": "^2.0.1" + <% } %> }, "devDependencies": { "@babel/cli": "^7.2.3", diff --git a/app/templates/server/api/middlewares/error.handler.js b/app/templates/server/api/middlewares/error.handler.js new file mode 100644 index 0000000..c8817bc --- /dev/null +++ b/app/templates/server/api/middlewares/error.handler.js @@ -0,0 +1,16 @@ +<% if (specification === 'openapi_3') { %> +// eslint-disable-next-line no-unused-vars, no-shadow +export default function errorHandler(err, req, res, next) { + const errors = err.errors || [{ message: err.message }]; + res.status(err.status || 500).json({ errors }) +} +<% } else { %> +// Error handler to display the error as HTML +// eslint-disable-next-line no-unused-vars, no-shadow +export default function errorHandler(err, req, res, next) { + res.status(err.status || 500); + res.send( + `

${err.status || 500} Error

` + + `
${err.message}
`); +} +<% } %> diff --git a/app/templates/server/common/swagger/Api.yaml b/app/templates/server/common/api.v2.yml similarity index 100% rename from app/templates/server/common/swagger/Api.yaml rename to app/templates/server/common/api.v2.yml diff --git a/app/templates/server/common/api.yml b/app/templates/server/common/api.yml new file mode 100644 index 0000000..a7840a6 --- /dev/null +++ b/app/templates/server/common/api.yml @@ -0,0 +1,74 @@ +openapi: 3.0.1 +info: + title: <%= title %> + description: <%= description %> + version: <%= version %> +servers: +- url: <%= apiRoot %> +tags: +- name: Examples + description: Simple example endpoints +- name: Specification + description: The swagger API specification +paths: + /examples: + get: + tags: + - Examples + description: Fetch all examples + responses: + 200: + description: Returns all examples + content: {} + post: + tags: + - Examples + description: Create a new example + requestBody: + description: an example + content: + application/json: + schema: + $ref: '#/components/schemas/ExampleBody' + required: true + responses: + 200: + description: Returns all examples + content: {} + /examples/{id}: + get: + tags: + - Examples + parameters: + - name: id + in: path + description: The id of the example to retrieve + required: true + schema: + type: integer + responses: + 200: + description: Return the example with the specified id + content: {} + 404: + description: Example not found + content: {} + /spec: + get: + tags: + - Specification + responses: + 200: + description: Return the API specification + content: {} +components: + schemas: + ExampleBody: + title: example + required: + - name + type: object + properties: + name: + type: string + example: no_stress diff --git a/app/templates/server/common/server.js b/app/templates/server/common/server.js index 4c8e411..86c01fe 100644 --- a/app/templates/server/common/server.js +++ b/app/templates/server/common/server.js @@ -4,7 +4,12 @@ import * as bodyParser from 'body-parser'; import * as http from 'http'; import * as os from 'os'; import cookieParser from 'cookie-parser'; +<% if (specification === 'openapi_3') { %> +import { OpenApiValidator } from 'express-openapi-validator'; +import errorHandler from '../api/middlewares/error.handler'; +<% } else { %> import swaggerify from './swagger'; +<% } %> import l from './logger'; const app = new Express(); @@ -17,10 +22,23 @@ export default class ExpressServer { app.use(bodyParser.urlencoded({ extended: true, limit: process.env.REQUEST_LIMIT || '100kb' })); app.use(cookieParser(process.env.SESSION_SECRET)); app.use(Express.static(`${root}/public`)); + + <% if (specification === 'openapi_3') { %> + const apiSpecPath = path.join(__dirname, 'api.yml'); + app.use(process.env.OPENAPI_SPEC || '/spec', Express.static(apiSpecPath)); + new OpenApiValidator({ + apiSpecPath, + }).install(app); + <% } %> } router(routes) { - swaggerify(app, routes); + <% if (specification === 'openapi_3') { %> + routes(app); + app.use(errorHandler); + <% } else { %> + swaggerify(app, routes); + <% } %> return this; } diff --git a/app/templates/server/common/swagger/index.js b/app/templates/server/common/swagger.js similarity index 79% rename from app/templates/server/common/swagger/index.js rename to app/templates/server/common/swagger.js index dab9ac7..dd87d4d 100644 --- a/app/templates/server/common/swagger/index.js +++ b/app/templates/server/common/swagger.js @@ -1,8 +1,9 @@ import middleware from 'swagger-express-middleware'; import * as path from 'path'; +import errorHandler from '../api/middlewares/error.handler'; export default function (app, routes) { - middleware(path.join(__dirname, 'Api.yaml'), app, (err, mw) => { + middleware(path.join(__dirname, 'api.yml'), app, (err, mw) => { // Enable Express' case-sensitive and strict options // (so "/entities", "/Entities", and "/Entities/" are all different) app.enable('case sensitive routing'); @@ -37,15 +38,9 @@ export default function (app, routes) { mw.CORS(), mw.validateRequest()); - // Error handler to display the validation error as HTML - // eslint-disable-next-line no-unused-vars, no-shadow - app.use((err, req, res, next) => { - res.status(err.status || 500); - res.send( - `

${err.status || 500} Error

` + - `
${err.message}
`); - }); - routes(app); + + // eslint-disable-next-line no-unused-vars, no-shadow + app.use(errorHandler); }); } diff --git a/package-lock.json b/package-lock.json index 7acf8a2..9fbae0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "generator-express-no-stress", - "version": "5.3.3", + "version": "7.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9cd6b9f..51ef231 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "generator-express-no-stress", - "version": "6.0.0", + "version": "7.0.1", "description": "Awesome APIs with ExpressJS and Swagger.", "main": "app/index.js", "scripts": {