UNPKG

@axway/api-builder-runtime

Version:

API Builder Runtime

137 lines (129 loc) 4.84 kB
const { oasPathToExpress } = require('@axway/api-builder-uri-utils'); const { pathToRegexp } = require('path-to-regexp'); const utils = require('../utils'); /** * The intention of PathManager is to handle the way we register paths accessible * through http/s calls. Currently the implmentation is based on Express but the * interface this class expose should be stable enough to be implemented via different * http framework. The PathManager does not currently handle all path binding in * API Builder but it will do it eventually. * * Things to consider: * 1. We may want to disable something when a path clashes? Can we know what to * disable, or should bindPath throw and let whatever is using it disable itself? * https://jira.axway.com/browse/RDPP-7117 * 2. Consider that we could return a router and bind it to the apiPrefix? * 3. Other things: * - should defer binding until a specific time by default * - could handle options for when to bind at different times (i.e. instantly) * - could create express routers to hand out to unique places (i.e. individual plugins) * - could handle teardown and unbinding from express * - should probably handle async/await in middleware * */ class PathManager { constructor(apibuilder) { this.apibuilder = apibuilder; this.paths = {}; this.pathSignatures = {}; } /** * Binds a path to a handler. * * @param {string} method - the HTTP method. * @param {string} path - the path to be bound. This is a valid Express path. * @param {Object} options - options needed to bind the path. * @param {string[]} [options.headers] - all supported response headers for * all the methods supported for this path. * @param {function} options.cb - handler that is executed when HTTP * method/path is hit. */ bindPath(method, path, options) { const lcMethod = method.toLowerCase(); this.apibuilder.logger.debug(`binding api (${lcMethod}) ${path}`); this._cachePath(method, path, options.headers); this.apibuilder.app[lcMethod](path, options.cb); } /** * Cache of all bound paths and holds info about them. The structure is: * - methods: methods allowed with the path * - responseHeaders: aggregation list of all response headers allowed for * all the methods for this path. * @param {string} method - the allowed method to cache * @param {string} path - the path to store * @param {string[]} headers - the response headers for the method/path */ _cachePath(method, path, headers = []) { const pathTransformed = utils.pathTransform(path); const pathSignature = `${method}:${pathTransformed}`; const ucMethod = method.toUpperCase(); if (!this.pathSignatures[pathSignature]) { this.pathSignatures[pathSignature] = { path, ucMethod }; } else { this.apibuilder.logger.debug(`binding api failed (${method.toLowerCase()}) ${path}`); const msg = `Duplicate path: ${ucMethod} ${path}. Multiple APIs registered for the same path and method`; throw new Error(msg); } if (!this.paths[path]) { this.paths[path] = { methods: new Set(), responseHeaders: new Set() }; } this.paths[path].methods.add(ucMethod); for (const header of headers) { this.paths[path].responseHeaders.add(header.toLowerCase()); } } /** * Binds an OpenAPI path to a handler. Leverage bindPath for the actual * binding. * @param {string} method - the HTTP method. * @param {string} openAPIPath - the path to be bound. This is a valid * OpenAPI path. * @param {Object} options - options needed to bind the path. * @param {string[]} options.headers - all supported response headers for * all the methods supported for this path. * @param {function} options.cb - handler that is executed when HTTP * method/path is hit. */ bindOpenAPIPath(method, openAPIPath, options) { const path = oasPathToExpress(openAPIPath); this.bindPath(method, path, options); } /** * Returns an object containing information about the provided path, namely * `methods`, an aggregated list of methods bound to the `path`, and * `responseHeaders`, an aggregated list of response headers allowed for all * the methods for this path. * * @param {string} path - The requested path * @returns {Object} Returns `{ methods: [], responseHeaders: [] }` */ getPathInfo(path) { let pathInfo = this.paths[path]; if (!pathInfo) { // search for parametrised path const paths = Object.keys(this.paths); for (let i = 0; i < paths.length; i++) { const newRegexPath = pathToRegexp(paths[i], [], { end: true }); if (newRegexPath.test(path)) { pathInfo = this.paths[paths[i]]; break; } } } if (pathInfo) { return { methods: Array.from(pathInfo.methods), responseHeaders: Array.from(pathInfo.responseHeaders) }; } } } module.exports = { PathManager };