lib_integrators_ExpressMiddleware.js
import _ from 'lodash';
import debug from 'debug';
import EventEmitter from 'events';
import { Router } from 'express';
import { getControllers, getDefinition } from './IntegratorHelpers.js';
import Endpoint from '../Endpoint.js';
import MiddlewareError from '../errors/MiddlewareError.js';
/**
* Express router integrator
* @module ExpressMiddleware
* @extends {EventEmitter}
*/
export default class ExpressMiddleware extends EventEmitter {
/**
* Options used when creating the `ExpressMiddleware`.
*
* @typedef {object} ConfigOptions
*
* @property {object|string} definition - The OpenAPI 3.0 definition location (location string for file type of json/yaml) or structure (json/yaml)
* @property {Map<String, Function>} controllers - The controllers location or structure
* @property {Map<String, Function>} securitySchemes - The security schemes validation functions
* @property {boolean} enforceResponseValidation - Flag for failing invalid responses (that don't match the endpoint's responseScheme)
* @example
* new ExpressMiddleware({
* definition: './openapi-sample-v3.yaml',
* controllers: {
* greetingGet: (req, res, next) => { res.send({ ok: true }) }
* },
* securitySchemes: {
* basicAuth: (authHeaderValue) => {},
* },
* enforceResponseValidation: false
* })
*
* @memberof module:ExpressMiddleware
*/
/**
* Router can now be attached to the express app
*
* @event ExpressMiddleware#ready
* @type {Object}
* @property {Express.Router} router - Express router object
*/
/**
* Router can now be attached to the express app
*
* @event ExpressMiddleware#error
* @type {Object}
* @property {Error} error object - Error class
*/
/**
* Router can now be attached to the express app
*
* @event ExpressMiddleware#invalidResponse
* @type {Object}
* @property {Error} error object - Error class
*/
/**
* Express router integrator
* @param {ConfigOptions} configRaw
* @fires ExpressMiddleware#ready
* @fires ExpressMiddleware#error
* @fires ExpressMiddleware#invalidResponse
*/
constructor(configRaw) {
super();
/**
* debugging helper
* @private
*/
this.debug = debug('openapi:ExpressMiddleware');
/**
* operation controllers, being built internally
* @type Map<string, function>
* @protected
*/
this.endpoints = {};
/**
* user-provided configuration (immutable)
* @type Map<string, any>
* @private
*/
this.configRaw = configRaw;
this.setupSequence();
}
/**
* sequence for class initialization
* @private
*/
async setupSequence() {
try {
await this.parseConfig();
const router = await this.setupMiddleware();
this.emit('ready', { router });
} catch (error) {
this.emit('error', { error });
}
}
/**
* parse the raw config object to finalized config json
* @private
*/
async parseConfig() {
/**
* @typedef {object} ExpressMiddlewareParsedConfig
* @property {object} definition openapi json definition
* @property {Map<string, function>} controllers controller functions (operationId as key, and controller function as function)
* @property {Object} securitySchemes openapi securitySchemes
* @property {boolean} enforceResponseValidation flag for failing invalid responses (that don't match the endpoint's responseScheme)
*/
/**
* Parsed configuration
* @type {ExpressMiddlewareParsedConfig}
* @memberof module:ExpressMiddleware
*/
this.config = {};
this.config.definition = getDefinition(this.configRaw);
this.config.controllers = await getControllers(this.configRaw);
this.config.securitySchemes = this.configRaw.securitySchemes;
this.config.enforceResponseValidation = this.configRaw.enforceResponseValidation;
this.debug('setup config finished');
}
/**
* Setup endpoint handler
* @private
* @param {Express.Request} req
* @param {Express.Response} res
* @param {Express.NextFunction} next
*/
setupEndpoint(req, res, next) {
this.debug('catched request, starting the middleware sequence');
const { route: { path: endpointPattern }, method } = req;
const { config: { definition: { paths } } } = this;
const { [endpointPattern]: { [method.toLowerCase()]: { operationId } } } = paths;
const { endpoints: { [operationId]: endpoint } } = this;
req.openApi = {
operationId,
};
endpoint.testIncoming(req.params, req.headers, req.query, req.headers.contentType || 'application/json', req.body);
endpoint.controllerFunc(req, res, next);
}
/**
* Global response catcher + validator
* @private
*/
responseValidator() {
const resSendInterceptor = (res, send) => (content) => {
res.contentBody = content;
res.send = send;
res.send(content);
};
this.router.use((req, res, next) => {
res.send = resSendInterceptor(res, res.send);
res.on('finish', async () => {
const { openApi } = req;
if (!openApi) {
// unknown endpoint
return;
}
const { _header: resHeaders, statusCode, contentBody } = res;
const { 'Content-Type': contentTypeWithCarset } = resHeaders.split('\n').reduce((all, line) => {
const [key, val] = line.split(':');
return {
...all,
[key]: val,
};
}, {});
const [contentType] = contentTypeWithCarset.trim().split(';');
const silentError = await this.endpoints[openApi.operationId]?.testOutgoing(statusCode, contentType, contentBody);
if (silentError instanceof Error) {
this.emit('invalidResponse', { error: silentError });
}
});
next();
});
}
/**
* Setup router (returns the router to attach to the express app)
* @return {Express.Router}
*/
setupMiddleware() {
/**
* Express.Router object to be attached to the express api server
* @type {Express.Router}
* @see {@link https://expressjs.com/en/4x/api.html#router}
*/
this.router = Router();
this.responseValidator();
const { config: { definition: { paths: allPaths } } } = this;
_.map(allPaths, (pathMethods, path) => {
_.map(pathMethods, (endpointDef, method) => {
try {
const { operationId } = endpointDef;
const {
config: { controllers: { [operationId]: endpointFn } },
} = this;
// setup new endpoint class for this particular operationId
this.endpoints[operationId] = new Endpoint(this.config.definition.securitySchemes, this.config?.securitySchemes, pathMethods[method], endpointFn, this.config.enforceResponseValidation);
// setup new express route
this.router[method](path, (...args) => this.setupEndpoint(...args));
} catch (e) {
this.debug('endpoint setup failed', e);
throw new MiddlewareError(`could not setup endpoint because ${e.message}`);
}
});
});
this.debug('setup router finished');
return this.router;
}
}