diff --git a/.github/workflows/runTests.yml b/.github/workflows/runTests.yml index 95f6800..0b1a9ba 100644 --- a/.github/workflows/runTests.yml +++ b/.github/workflows/runTests.yml @@ -24,19 +24,19 @@ jobs: - run: npm ci - run: npm run lint - run: npm test - # coverage: - # name: coverage - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@master - # - uses: actions/setup-node@master - # with: - # node-version: '12' - # - run: npm ci - # - uses: paambaati/codeclimate-action@v2.6.0 - # env: - # CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} - # with: - # coverageCommand: npm test -- --coverage - # coverageLocations: | - # ${{github.workspace}}/coverage/lcov.info:lcov + coverage: + name: coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: actions/setup-node@master + with: + node-version: '12' + - run: npm ci + - uses: paambaati/codeclimate-action@v2.6.0 + env: + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + with: + coverageCommand: npm test -- --coverage + coverageLocations: | + ${{github.workspace}}/coverage/lcov.info:lcov diff --git a/README.md b/README.md index 9f7a8dc..082108a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,223 @@ +[![Build](https://github.com/BRIKEV/express-oas-validator/actions/workflows/runTests.yml/badge.svg)](https://github.com/BRIKEV/express-oas-validator/actions/workflows/runTests.yml) +[![Known Vulnerabilities](https://snyk.io/test/github/BRIKEV/express-oas-validator/badge.svg)](https://snyk.io/test/github/BRIKEV/express-oas-validator) +[![Maintainability](https://api.codeclimate.com/v1/badges/13aa6d75c21855b8857c/maintainability)](https://codeclimate.com/github/BRIKEV/express-oas-validator/maintainability) +[![Test Coverage](https://api.codeclimate.com/v1/badges/13aa6d75c21855b8857c/test_coverage)](https://codeclimate.com/github/BRIKEV/express-oas-validator/test_coverage) +![License: MIT](https://img.shields.io/badge/License-MIT-green.svg) + # express-oas-validator -Express OpenAPI Specification (OAS) middleware validator + +Express OpenAPI Specification (OAS) middleware validator and response validator. + +This package will expose an express middleware that will validate your endpoint based on your OpenAPI docs, and a response validator to do the same with your responses payload. + +## Installation +Install using the node package registry: + +``` +npm install --save express-oas-validator +``` + +## Usage + +This is a basic usage of this package. + +```js +const express = require('express'); +// We recommed to install "body-parser" to validate request body +const bodyParser = require('body-parser'); +const { init, validateMiddleware, responseValidation } = require('express-oas-validator'); +const swaggerDefinition = require('./swaggerDefinition.json'); + +const app = express(); + +init(swaggerDefinition); +app.use(bodyParser.urlencoded({ extended: true })); +app.use(bodyParser.json()); + +// Middleware validator +app.post('/api/v1/songs', validateMiddleware(), (req, res) => res.send('You save a song!')); + +// Middleware validator with custom configuration +app.get('/api/v1/albums/:id', validateMiddleware({ headers: false }), (req, res) => ( + res.json([{ + title: 'abum 1', + }]) +)); + +// Middleware validator with custom configuration +app.get('/api/v1/authors', validateMiddleware({ body: false, query: false }), (req, res) => ( + res.json([{ + title: 'abum 1', + }]) +)); + +// Response validator +app.post('/api/v1/name', (req, res, next) => { + try { + responseValidation('Error string', req, 200); + return res.send('Hello World!'); + } catch (error) { + return next(error); + } +}); + +// Express default error handler +app.use((err, req, res, next) => { + res.status(err.status).json(err); +}); +``` + +## methods + +### init(openApiDef, options) + +This methods initiates the validator so that `validateMiddleware` and `responseValidation` can be used in different files. + +**Parameters** + +| Name | Type | Description | +| ------------|:------:| ------------------:| +| openApiDef | object | OpenAPI definition | +| options | object | Options to extend the errorHandler or Ajv configuration | + +```js +const swaggerDefinition = require('./swaggerDefinition.json'); + +init(swaggerDefinition); +``` + + +## validateMiddleware(endpointConfig) + + +Express middleware that receives this configuration options and validates each of the options. + +```js +const DEFAULT_CONFIG = { + body: true, + params: true, + headers: true, + query: true, + required: true, +}; +``` + +**Example** + +```js +// This one uses the DEFAULT_CONFIG +app.get('/api/v1/albums/:id', validateMiddleware(), (req, res) => ( + res.json([{ + title: 'abum 1', + }]) +)); + +// With custom configuration +app.get('/api/v1/albums/:id', validateMiddleware({ headers: false }), (req, res) => ( + res.json([{ + title: 'abum 1', + }]) +)); +``` + +## responseValidation(payload, req, status) + +Method to validate response payload based on the docs and the status we want to validate. + +**Parameters** + +| Name | Type | Description | +| ------------|:------:| ------------------:| +| payload | * | response we want to validate | +| req | object | Options to extend the errorHandler or Ajv configuration | +| status | number | esponse status we want to validate | + + +**Example** + +```js +responseValidation('Error string', req, 200); +``` + +## Example with express-jsdoc-swagger + +This is an example using [express-jsdoc-swagger](https://www.npmjs.com/package/express-jsdoc-swagger). + +```js +const express = require('express'); +const bodyParser = require('body-parser'); +const expressJSDocSwagger = require('express-jsdoc-swagger'); +const { init, validateMiddleware, responseValidation } = require('express-oas-validator'); + +const options = { + info: { + version: '1.0.0', + title: 'Albums store', + license: { + name: 'MIT', + }, + }, + filesPattern: './fake-server.js', + baseDir: __dirname, +}; + +const app = express(); +const instance = expressJSDocSwagger(app)(options); + +const serverApp = () => new Promise(resolve => { + instance.on('finish', data => { + init(data); + resolve(app); + }); + app.use(bodyParser.urlencoded({ extended: true })); + app.use(bodyParser.json()); + /** + * A song + * @typedef {object} Song + * @property {string} title.required - The title + * @property {string} artist - The artist + * @property {integer} year - The year + */ + + /** + * POST /api/v1/songs + * @param {Song} request.body.required - song info + * @return {object} 200 - song response + */ + app.post('/api/v1/songs', validateMiddleware(), (req, res) => res.send('You save a song!')); + + /** + * POST /api/v1/name + * @param {string} request.body.required - name body description + * @return {object} 200 - song response + */ + app.post('/api/v1/name', (req, res, next) => { + try { + responseValidation('Error string', req); + return res.send('Hello World!'); + } catch (error) { + return next(error); + } + }); + + /** + * GET /api/v1/authors + * @summary This is the summary or description of the endpoint + * @param {string} name.query.required - name param description - enum:type1,type2 + * @param {array} license.query - name param description + * @return {object} 200 - success response - application/json + */ + app.get('/api/v1/authors', validateMiddleware({ headers: false }), (req, res) => ( + res.json([{ + title: 'abum 1', + }]) + )); + + // eslint-disable-next-line no-unused-vars + app.use((err, req, res, next) => { + res.status(err.status).json(err); + }); +}); + +module.exports = serverApp; +``` \ No newline at end of file diff --git a/index.js b/index.js index 00e3a20..c1242e4 100644 --- a/index.js +++ b/index.js @@ -9,6 +9,18 @@ const getConfig = require('./utils/config'); let instance = null; +/** + * Validator methods + * @typedef {object} Options + * @property {function()} errorHandler custom error handler + * @property {object} ajvConfig Ajv config object + */ + +/** + * Init method to instantiate the OpenAPI validator + * @param {object} openApiDef OpenAPI definition + * @param {Options} options Options to extend the errorHandler or Ajv configuration + */ const init = (openApiDef, options = {}) => { if (instance === null) { instance = openapiValidatorUtils(openApiDef, options); @@ -16,6 +28,20 @@ const init = (openApiDef, options = {}) => { return instance; }; +/** + * Validator methods + * @typedef {object} EndpointConfig + * @property {boolean} body custom error handler + * @property {boolean} params Ajv config object + * @property {boolean} headers Ajv config object + * @property {boolean} query Ajv config object + * @property {boolean} required Ajv config object + */ + +/** + * Endpoint configuration + * @param {EndpointConfig} endpointConfig middleware validator options + */ const validateMiddleware = endpointConfig => (req, res, next) => { try { const config = getConfig(endpointConfig); @@ -55,7 +81,13 @@ const validateMiddleware = endpointConfig => (req, res, next) => { } }; -const responseValidation = (payload, status, req) => { +/** + * Method to validate response payload + * @param {*} payload response we want to validate + * @param {object} req express request object + * @param {number} status response status we want to validate + */ +const responseValidation = (payload, req, status = 200) => { try { const { contentType, diff --git a/test/fake-server.js b/test/fake-server.js index 67f20fc..6bc0697 100644 --- a/test/fake-server.js +++ b/test/fake-server.js @@ -54,7 +54,7 @@ const serverApp = () => new Promise(resolve => { */ app.post('/api/v1/name', (req, res, next) => { try { - responseValidation('Error string', 200, req); + responseValidation('Error string', req); return res.send('Hello World!'); } catch (error) { return next(error); @@ -68,7 +68,7 @@ const serverApp = () => new Promise(resolve => { */ app.post('/api/v2/name', (req, res, next) => { try { - responseValidation('Error string', 200, req); + responseValidation('Error string', req, 200); return res.send('Hello World!'); } catch (error) { return next(error); diff --git a/utils/config.js b/utils/config.js index 70ade01..1dadad7 100644 --- a/utils/config.js +++ b/utils/config.js @@ -6,6 +6,9 @@ const DEFAULT_CONFIG = { required: true, }; +/** + * @param {object} config + */ const getConfig = config => { if (!config || Object.keys(config).length === 0) return DEFAULT_CONFIG; return { diff --git a/utils/index.js b/utils/index.js index 813f5b3..d21c6d0 100644 --- a/utils/index.js +++ b/utils/index.js @@ -1,14 +1,33 @@ +/** @module Utils */ + +/** + * This method get keys of a param object and filter them with exceptions + * @param {object} paramObject + * @param {string[]} exceptions + * @return {string[]} + */ const getKeys = (paramObject, exceptions = []) => ( Object.keys(paramObject) .filter(key => !exceptions.includes(key)) ); +/** + * @param {string} endpoint + * @param {string} method + */ const paramsValidator = (endpoint, method) => (payload, keys, validate) => { keys.forEach(key => { validate(payload[key], key, endpoint, method); }); }; +/** + * This methods format URL + * input: /test/:id + * output: /test/{id} + * @param {object} req express request object + * @return {string} + */ const formatURL = req => { const params = Object.keys(req.params); return params.reduce((acum, param) => ( @@ -16,6 +35,9 @@ const formatURL = req => { ), req.route.path); }; +/** + * @param {object} req express request object + */ const getParameters = req => { const contentType = req.headers['content-type'] || 'application/json'; const method = req.method.toLowerCase(); @@ -30,10 +52,17 @@ const getParameters = req => { }; }; +/** + * @param {object} paramObject + */ const formatParam = paramObject => paramKey => ({ [paramKey]: paramObject[paramKey], }); +/** + * @param {object} req express request object + * @return {object[]} + */ const paramsArray = req => ([ ...Object.keys(req.query).map(formatParam(req.query)), ...Object.keys(req.params).map(formatParam(req.params)),