UNPKG

express-oas-generator

Version:

Module to automatically generate OpenAPI (Swagger) specification for existing ExpressJS 4.x REST API applications

471 lines (397 loc) 16.4 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>index.js - Documentation</title> <script src="scripts/prettify/prettify.js"></script> <script src="scripts/prettify/lang-css.js"></script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/prettify.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc.css"> <script src="scripts/nav.js" defer></script> <meta name="viewport" content="width=device-width, initial-scale=1.0"> </head> <body> <input type="checkbox" id="nav-trigger" class="nav-trigger" /> <label for="nav-trigger" class="navicon-button x"> <div class="navicon"></div> </label> <label for="nav-trigger" class="overlay"></label> <nav > <h2><a href="index.html">Home</a></h2><h2><a href="https://github.com/mpashkovskiy/express-oas-generator" target="_blank" class="menu-item" >Source code</a></h2><h2><a href="https://github.com/mpashkovskiy/express-oas-generator/issues" target="_blank" class="menu-item" >Issues</a></h2><h3>Modules</h3><ul><li><a href="module-index.html">index</a><ul class='methods'><li data-type='method' style='display: none;'><a href="module-index.html#~getMethod">getMethod</a></li><li data-type='method' style='display: none;'><a href="module-index.html#~getPathKey">getPathKey</a></li><li data-type='method' style='display: none;'><a href="module-index.html#~getSpec">getSpec</a></li><li data-type='method' style='display: none;'><a href="module-index.html#~handleRequests">handleRequests</a></li><li data-type='method' style='display: none;'><a href="module-index.html#~handleResponses">handleResponses</a></li><li data-type='method' style='display: none;'><a href="module-index.html#~init">init</a></li><li data-type='method' style='display: none;'><a href="module-index.html#~patchSpec">patchSpec</a></li><li data-type='method' style='display: none;'><a href="module-index.html#~serveApiDocs">serveApiDocs</a></li><li data-type='method' style='display: none;'><a href="module-index.html#~setPackageInfoPath">setPackageInfoPath</a></li><li data-type='method' style='display: none;'><a href="module-index.html#~updateSchemesAndHost">updateSchemesAndHost</a></li><li data-type='method' style='display: none;'><a href="module-index.html#~updateSpecFromPackage">updateSpecFromPackage</a></li></ul></li><li><a href="module-lib_processors.html">lib/processors</a><ul class='methods'><li data-type='method' style='display: none;'><a href="module-lib_processors.html#.processBody">processBody</a></li><li data-type='method' style='display: none;'><a href="module-lib_processors.html#.processHeaders">processHeaders</a></li><li data-type='method' style='display: none;'><a href="module-lib_processors.html#.processPath">processPath</a></li><li data-type='method' style='display: none;'><a href="module-lib_processors.html#.processQuery">processQuery</a></li><li data-type='method' style='display: none;'><a href="module-lib_processors.html#.processResponse">processResponse</a></li><li data-type='method' style='display: none;'><a href="module-lib_processors.html#~appendChunkIfNeeded">appendChunkIfNeeded</a></li><li data-type='method' style='display: none;'><a href="module-lib_processors.html#~isCompressed">isCompressed</a></li><li data-type='method' style='display: none;'><a href="module-lib_processors.html#~updateProduces">updateProduces</a></li><li data-type='method' style='display: none;'><a href="module-lib_processors.html#~updateResponses">updateResponses</a></li><li data-type='method' style='display: none;'><a href="module-lib_processors.html#~updateSecurity">updateSecurity</a></li><li data-type='method' style='display: none;'><a href="module-lib_processors.html#~updateSecurityDefinitions">updateSecurityDefinitions</a></li></ul></li><li><a href="module-lib_utils.html">lib/utils</a><ul class='methods'><li data-type='method' style='display: none;'><a href="module-lib_utils.html#.getSchema">getSchema</a></li><li data-type='method' style='display: none;'><a href="module-lib_utils.html#.getType">getType</a></li><li data-type='method' style='display: none;'><a href="module-lib_utils.html#.sortObject">sortObject</a></li><li data-type='method' style='display: none;'><a href="module-lib_utils.html#~fillExamples">fillExamples</a></li></ul></li></ul> </nav> <div id="main"> <h1 class="page-title">index.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>/** * @fileOverview main file * @module index */ const _merge = require('lodash.merge'); const fs = require('fs'); const path = require('path'); const swaggerUi = require('swagger-ui-express'); const utils = require('./lib/utils'); const processors = require('./lib/processors'); const listEndpoints = require('express-list-endpoints'); const WRONG_MIDDLEWARE_ORDER_ERROR = ` Express oas generator: you miss-placed the **response** and **request** middlewares! Please, make sure to: 1. place the RESPONSE middleware FIRST, right after initializing the express app, 2. and place the REQUEST middleware LAST, inside the app.listen callback For more information, see https://github.com/mpashkovskiy/express-oas-generator#Advanced-usage-recommended `; let packageJsonPath = `${process.cwd()}/package.json`; let packageInfo; let app; /** * @param {function|object} predefinedSpec either the Swagger specification * or a function with one argument producing it. */ let swaggerUiServePath; let predefinedSpec; let spec = {}; let lastRecordTime = new Date().getTime(); let firstResponseProcessing = true; /** * @param {boolean} [responseMiddlewareHasBeenApplied=false] * * @note make sure to reset this variable once you've done your checks. * * @description used make sure the *order* of which the middlewares are applied is correct * * The `response` middleware MUST be applied FIRST, * before the `request` middleware is applied. * * We'll use this to make sure the order is correct. * If not - we'll throw an informative error. */ let responseMiddlewareHasBeenApplied = false; /** * */ function updateSpecFromPackage() { /* eslint global-require : off */ packageInfo = fs.existsSync(packageJsonPath) ? require(packageJsonPath) : {}; spec.info = spec.info || {}; if (packageInfo.name) { spec.info.title = packageInfo.name; } if (packageInfo.version) { spec.info.version = packageInfo.version; } if (packageInfo.license) { spec.info.license = { name: packageInfo.license }; } if (packageInfo.baseUrlPath) { spec.info.description = '[Specification JSON](' + packageInfo.baseUrlPath + '/api-spec) , base url : ' + packageInfo.baseUrlPath; } else { packageInfo.baseUrlPath = ''; spec.info.description = '[Specification JSON](' + packageInfo.baseUrlPath + '/api-spec)'; } if (packageInfo.description) { spec.info.description += `\n\n${packageInfo.description}`; } } /** * @description serve the openAPI docs with swagger at a specified path / url * * @returns void */ function serveApiDocs() { spec = { swagger: '2.0', paths: {} }; const endpoints = listEndpoints(app); endpoints.forEach(endpoint => { const params = []; let path = endpoint.path; const matches = path.match(/:([^/]+)/g); if (matches) { matches.forEach(found => { const paramName = found.substr(1); path = path.replace(found, `{${paramName}}`); params.push(paramName); }); } if (!spec.paths[path]) { spec.paths[path] = {}; } endpoint.methods.forEach(m => { spec.paths[path][m.toLowerCase()] = { summary: path, consumes: ['application/json'], parameters: params.map(p => ({ name: p, in: 'path', required: true, })) || [], responses: {} }; }); }); updateSpecFromPackage(); spec = patchSpec(predefinedSpec); app.use(packageInfo.baseUrlPath + '/api-spec', (req, res, next) => { res.setHeader('Content-Type', 'application/json'); res.send(JSON.stringify(patchSpec(predefinedSpec), null, 2)); next(); }); app.use(packageInfo.baseUrlPath + '/' + swaggerUiServePath, swaggerUi.serve, (req, res) => { swaggerUi.setup(patchSpec(predefinedSpec))(req, res); }); } /** * * @param predefinedSpec * @returns {{}} */ function patchSpec(predefinedSpec) { return !predefinedSpec ? spec : typeof predefinedSpec === 'object' ? utils.sortObject(_merge(spec, predefinedSpec || {})) : predefinedSpec(spec); } /** * * @param req * @returns {string|undefined|*} */ function getPathKey(req) { if (!req.url) { return undefined; } if (spec.paths[req.url]) { return req.url; } const url = req.url.split('?')[0]; const pathKeys = Object.keys(spec.paths); for (let i = 0; i &lt; pathKeys.length; i += 1) { const pathKey = pathKeys[i]; if (url.match(`${pathKey.replace(/{([^/]+)}/g, '(?:([^\\\\/]+?))')}/?$`)) { return pathKey; } } return undefined; } /** * * @param req * @returns {{method: *, pathKey: *}|undefined} */ function getMethod(req) { if (req.url.startsWith('/api-')) { return undefined; } const m = req.method.toLowerCase(); if (m === 'options') { return undefined; } const pathKey = getPathKey(req); if (!pathKey) { return undefined; } return { method: spec.paths[pathKey][m], pathKey }; } /** * * @param req */ function updateSchemesAndHost(req) { spec.schemes = spec.schemes || []; if (spec.schemes.indexOf(req.protocol) === -1) { spec.schemes.push(req.protocol); } if (!spec.host) { spec.host = req.get('host'); } } /** TODO - type defs for Express don't work (I tried @external) */ /** * Apply this **first**! * * (straight after creating the express app (as the very first middleware)) * * @description apply the `response` middleware. * * @param {Express} expressApp - the express app * * @param {Object} [options] optional configuration options * @param {string|undefined} [options.specOutputPath=undefined] where to write the openAPI specification to. * Specify this to create the openAPI specification file. * @param {number} [options.writeIntervalMs=10000] how often to write the openAPI specification to file * * @returns void */ function handleResponses(expressApp, options = { swaggerUiServePath: 'api-docs', specOutputPath: undefined, predefinedSpec: {}, writeIntervalMs: 1000 * 10 }) { responseMiddlewareHasBeenApplied = true; /** * save the `expressApp` to our local `app` variable. * Used here, but not in `handleRequests`, * because this comes before it. */ app = expressApp; swaggerUiServePath = options.swaggerUiServePath || 'api-docs'; predefinedSpec = options.predefinedSpec || {}; const { specOutputPath, writeIntervalMs } = options; /** middleware to handle RESPONSES */ app.use((req, res, next) => { try { const methodAndPathKey = getMethod(req); if (methodAndPathKey &amp;&amp; methodAndPathKey.method) { processors.processResponse(res, methodAndPathKey.method); } const ts = new Date().getTime(); if (firstResponseProcessing || specOutputPath &amp;&amp; ts - lastRecordTime > writeIntervalMs) { firstResponseProcessing = false; lastRecordTime = ts; fs.writeFile(specOutputPath, JSON.stringify(spec, null, 2), 'utf8', err => { const fullPath = path.resolve(specOutputPath); if (err) { /** * TODO - this is broken - the error will be caught and ignored in the catch below. * See https://github.com/mpashkovskiy/express-oas-generator/pull/39#discussion_r340026645 */ throw new Error(`Cannot store the specification into ${fullPath} because of ${err.message}`); } }); } } catch (e) { /** TODO - shouldn't we do something here? */ } finally { /** always call the next middleware */ next(); } }); } /** TODO - type defs for Express don't work (I tried @external) */ /** * Apply this **last**! * * (as the very last middleware of your express app) * * @description apply the `request` middleware * Applies to the `app` you provided in `handleResponses` * * Also, since this is the last function you'll need to invoke, * it also initializes the specification and serves the api documentation. * The options are for these tasks. * * @returns void */ function handleRequests() { /** make sure the middleware placement order (by the user) is correct */ if (responseMiddlewareHasBeenApplied !== true) { throw new Error(WRONG_MIDDLEWARE_ORDER_ERROR); } /** everything was applied correctly; reset the global variable. */ responseMiddlewareHasBeenApplied = false; /** middleware to handle REQUESTS */ app.use((req, res, next) => { try { const methodAndPathKey = getMethod(req); if (methodAndPathKey &amp;&amp; methodAndPathKey.method &amp;&amp; methodAndPathKey.pathKey) { const method = methodAndPathKey.method; updateSchemesAndHost(req); processors.processPath(req, method, methodAndPathKey.pathKey); processors.processHeaders(req, method, spec); processors.processBody(req, method); processors.processQuery(req, method); } } catch (e) { /** TODO - shouldn't we do something here? */ } finally { next(); } }); /** forward options to `serveApiDocs`: */ serveApiDocs(); } /** * TODO * * 1. Rename the parameter names * (this will require better global variable naming to allow re-assigning properly) * * 2. the `aPath` is ignored only if it's `undefined`, * but if it's set to an empty string `''`, then the tests fail. * I think we should be checking for falsy values, not only `undefined` ones. * * 3. (Breaking) Use object for optional parameters: * https://github.com/mpashkovskiy/express-oas-generator/issues/35 * */ /** * @warn it's preferred that you use `handleResponses`, * `handleRequests` and `serveApiDocs` **individually** * and not directly from this `init` function, * because we need `handleRequests` to be placed as the * very last middleware and we cannot guarantee this here, * since we're only using an arbitrary setTimeout of `1000` ms. * * See * https://github.com/mpashkovskiy/express-oas-generator/pull/32#issuecomment-546807216 * * @description initialize the `express-oas-generator`. * * This will apply both `handleResponses` and `handleRequests` * and also will call `serveApiDocs`. * * @param {Express} aApp - the express app * @param {*} [aPredefinedSpec={}] * @param {string|undefined} [aSpecOutputPath=undefined] where to write the openAPI specification to. * Specify this to create the openAPI specification file. * @param {number} [aWriteInterval=10000] how often to write the openAPI specification to file * @param {string} [aSwaggerUiServePath=api-docs] where to serve the openAPI docs. Defaults to `api-docs` */ function init(aApp, aPredefinedSpec = {}, aSpecOutputPath = undefined, aWriteInterval = 1000 * 10, aSwaggerUiServePath = 'api-docs') { handleResponses(aApp, { swaggerUiServePath: aSwaggerUiServePath, specOutputPath: aSpecOutputPath, predefinedSpec: aPredefinedSpec, writeIntervalMs: aWriteInterval, }); setTimeout(() => { handleRequests(); }, 1000); } /** * * @returns {{}} */ const getSpec = () => { return patchSpec(predefinedSpec); }; /** * * @param pkgInfoPath - path to package.json */ const setPackageInfoPath = pkgInfoPath => { packageJsonPath = `${process.cwd()}/${pkgInfoPath}/package.json`; }; module.exports = { handleResponses, handleRequests, init, getSpec, setPackageInfoPath }; </code></pre> </article> </section> </div> <br class="clear"> <footer> Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.6.3</a> on Fri Dec 27 2019 14:41:23 GMT+0200 (Eastern European Standard Time) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme. </footer> <script>prettyPrint();</script> <script src="scripts/polyfill.js"></script> <script src="scripts/linenumber.js"></script> <script src="scripts/collapse.js" defer></script> </body> </html>