@axway/api-builder-runtime
Version:
API Builder Runtime
137 lines (129 loc) • 4.84 kB
JavaScript
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
};