/**
* @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 < 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 && methodAndPathKey.method) {
processors.processResponse(res, methodAndPathKey.method);
}
const ts = new Date().getTime();
if (firstResponseProcessing || specOutputPath && 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 && methodAndPathKey.method && 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
};