@bitblit/ratchet-epsilon-common
Version:
Tiny adapter to simplify building API gateway Lambda APIS
234 lines • 12.6 kB
JavaScript
import { MisconfiguredError } from '../error/misconfigured-error.js';
import { Logger } from '@bitblit/ratchet-common/logger/logger';
import { BooleanRatchet } from '@bitblit/ratchet-common/lang/boolean-ratchet';
import { NullReturnedObjectHandling } from '../../config/http/null-returned-object-handling.js';
import { BuiltInFilters } from '../../built-in/http/built-in-filters.js';
import { BuiltInHandlers } from '../../built-in/http/built-in-handlers.js';
import { BuiltInAuthFilters } from '../../built-in/http/built-in-auth-filters.js';
import { LogLevelManipulationFilter } from '../../built-in/http/log-level-manipulation-filter.js';
export class RouterUtil {
constructor() { }
static defaultAuthenticationHeaderParsingEpsilonPreFilters(webTokenManipulator) {
return [
(fCtx) => BuiltInAuthFilters.parseAuthorizationHeader(fCtx, webTokenManipulator),
(fCtx) => BuiltInAuthFilters.applyOpenApiAuthorization(fCtx),
].concat(RouterUtil.defaultEpsilonPreFilters());
}
static defaultEpsilonPreFilters() {
return [
(fCtx) => BuiltInFilters.autoRespondToOptionsRequestWithCors(fCtx),
(fCtx) => BuiltInFilters.ensureEventMaps(fCtx),
(fCtx) => LogLevelManipulationFilter.setLogLevelForTransaction(fCtx),
(fCtx) => BuiltInFilters.parseJsonBodyToObject(fCtx),
(fCtx) => BuiltInFilters.fixStillEncodedQueryParams(fCtx),
(fCtx) => BuiltInFilters.uriDecodeQueryParams(fCtx),
(fCtx) => BuiltInFilters.disallowStringNullAsPathParameter(fCtx),
(fCtx) => BuiltInFilters.disallowStringNullAsQueryStringParameter(fCtx),
(fCtx) => BuiltInFilters.validateInboundBody(fCtx),
(fCtx) => BuiltInFilters.validateInboundQueryParams(fCtx),
(fCtx) => BuiltInFilters.validateInboundQueryParams(fCtx),
];
}
static defaultEpsilonPostFilters() {
return [
(fCtx) => BuiltInFilters.validateOutboundResponse(fCtx),
(fCtx) => BuiltInFilters.addAWSRequestIdHeader(fCtx),
(fCtx) => BuiltInFilters.addAllowReflectionCORSHeaders(fCtx),
(fCtx) => BuiltInFilters.applyGzipIfPossible(fCtx),
(fCtx) => BuiltInFilters.checkMaximumLambdaBodySize(fCtx),
(fCtx) => LogLevelManipulationFilter.clearLogLevelForTransaction(fCtx),
];
}
static defaultEpsilonErrorFilters() {
return [
(fCtx) => BuiltInFilters.addAWSRequestIdHeader(fCtx),
(fCtx) => BuiltInFilters.addAllowReflectionCORSHeaders(fCtx),
(fCtx) => LogLevelManipulationFilter.clearLogLevelForTransaction(fCtx),
];
}
static defaultHttpMetaProcessingConfigWithAuthenticationHeaderParsing(webTokenManipulator) {
const defaults = {
configName: 'EpsilonDefaultHttpMetaProcessingConfig',
timeoutMS: 30_000,
overrideAuthorizerName: null,
preFilters: RouterUtil.defaultAuthenticationHeaderParsingEpsilonPreFilters(webTokenManipulator),
postFilters: RouterUtil.defaultEpsilonPostFilters(),
errorFilters: RouterUtil.defaultEpsilonErrorFilters(),
nullReturnedObjectHandling: NullReturnedObjectHandling.Return404NotFoundResponse,
};
return defaults;
}
static defaultHttpMetaProcessingConfig() {
const defaults = {
configName: 'EpsilonDefaultHttpMetaProcessingConfig',
timeoutMS: 30_000,
overrideAuthorizerName: null,
preFilters: RouterUtil.defaultEpsilonPreFilters(),
postFilters: RouterUtil.defaultEpsilonPostFilters(),
errorFilters: RouterUtil.defaultEpsilonErrorFilters(),
nullReturnedObjectHandling: NullReturnedObjectHandling.Return404NotFoundResponse,
};
return defaults;
}
static assignDefaultsOnHttpConfig(cfg) {
const defaults = {
handlers: new Map(),
authorizers: new Map(),
defaultMetaHandling: this.defaultHttpMetaProcessingConfig(),
staticContentRoutes: {},
prefixesToStripBeforeRouteMatch: [],
filterHandledRouteMatches: ['options .*'],
};
const rval = Object.assign({}, defaults, cfg || {});
return rval;
}
static findApplicableMeta(httpConfig, method, path) {
let rval = null;
if (httpConfig?.overrideMetaHandling) {
for (let i = 0; i < httpConfig.overrideMetaHandling.length && !rval; i++) {
const test = httpConfig.overrideMetaHandling[i];
if (!test.methods ||
test.methods.length === 0 ||
test.methods.map((s) => s.toLocaleLowerCase()).includes(method.toLocaleLowerCase())) {
const matches = !!path.match(test.pathRegex);
if ((matches && !test.invertPathMatching) || (!matches && test.invertPathMatching)) {
rval = test.config;
}
}
}
}
if (!rval) {
rval = httpConfig.defaultMetaHandling || RouterUtil.defaultHttpMetaProcessingConfig();
}
return rval;
}
static openApiYamlToRouterConfig(httpConfig, openApiDoc, modelValidator, backgroundHttpAdapterHandler) {
if (!openApiDoc || !httpConfig) {
throw new MisconfiguredError('Cannot configure, missing either yaml or cfg');
}
const rval = {
routes: [],
openApiModelValidator: modelValidator,
config: RouterUtil.assignDefaultsOnHttpConfig(httpConfig),
};
if (openApiDoc?.components?.securitySchemes) {
Object.keys(openApiDoc.components.securitySchemes).forEach((sk) => {
if (!rval.config.authorizers || !rval.config.authorizers.get(sk)) {
throw new MisconfiguredError().withFormattedErrorMessage('Doc requires authorizer %s but not found in map', sk);
}
});
}
const missingPaths = [];
const filterHandledPathMatches = httpConfig.filterHandledRouteMatches || [];
if (openApiDoc?.paths) {
Object.keys(openApiDoc.paths).forEach((path) => {
Object.keys(openApiDoc.paths[path]).forEach((method) => {
const convertedPath = RouterUtil.openApiPathToRouteParserPath(path);
const finder = method + ' ' + path;
const applicableMeta = RouterUtil.findApplicableMeta(httpConfig, method, path);
const entry = openApiDoc.paths[path][method];
const isBackgroundEndpoint = path.startsWith(backgroundHttpAdapterHandler.httpSubmissionPath);
const isBackgroundMetaEndpoint = path === backgroundHttpAdapterHandler.httpMetaEndpoint;
const isBackgroundStatusEndpoint = path === backgroundHttpAdapterHandler.httpStatusPath;
if (isBackgroundEndpoint) {
rval.config.handlers.set(finder, (evt, ctx) => backgroundHttpAdapterHandler.handleBackgroundSubmission(evt, ctx));
}
if (isBackgroundMetaEndpoint) {
rval.config.handlers.set(finder, (evt, ctx) => backgroundHttpAdapterHandler.handleBackgroundMetaRequest(evt, ctx));
}
if (isBackgroundStatusEndpoint) {
rval.config.handlers.set(finder, (evt, ctx) => backgroundHttpAdapterHandler.handleBackgroundStatusRequest(evt, ctx));
}
if (!rval.config.handlers || !rval.config.handlers.get(finder)) {
const match = filterHandledPathMatches.find((reg) => finder.match(reg));
if (match) {
Logger.debug('Adding filter-handled handler for %s', finder);
rval.config.handlers.set(finder, (evt) => BuiltInHandlers.expectedHandledByFilter(evt));
}
else {
missingPaths.push(finder);
}
}
if (entry && entry['security'] && entry['security'].length > 1) {
throw new MisconfiguredError('Epsilon does not currently support multiple security (path was ' + finder + ')');
}
const authorizerName = entry['security'] && entry['security'].length == 1 ? Object.keys(entry['security'][0])[0] : null;
const newRoute = {
path: convertedPath,
method: method,
function: rval.config.handlers.get(finder),
authorizerName: applicableMeta.overrideAuthorizerName || authorizerName,
metaProcessingConfig: applicableMeta,
validation: null,
outboundValidation: null,
};
if (entry['requestBody'] &&
entry['requestBody']['content'] &&
entry['requestBody']['content']['application/json'] &&
entry['requestBody']['content']['application/json']['schema']) {
const schema = entry['requestBody']['content'];
Logger.silly('Applying schema %j to %s', schema, finder);
const modelName = this.findAndValidateModelName(method, path, schema, rval.config.overrideModelValidator || rval.openApiModelValidator);
const required = BooleanRatchet.parseBool(entry['requestBody']['required']);
const validation = {
extraPropertiesAllowed: true,
emptyAllowed: !required,
modelName: modelName,
};
newRoute.validation = validation;
}
if (entry['responses'] &&
entry['responses']['200'] &&
entry['responses']['200']['content'] &&
entry['responses']['200']['content']['application/json'] &&
entry['responses']['200']['content']['application/json']['schema']) {
const schema = entry['responses']['200']['content'];
Logger.silly('Applying schema %j to %s', schema, finder);
const modelName = this.findAndValidateModelName(method, path, schema, rval.config.overrideModelValidator || rval.openApiModelValidator);
const validation = {
extraPropertiesAllowed: false,
emptyAllowed: false,
modelName: modelName,
};
newRoute.outboundValidation = validation;
}
rval.routes.push(newRoute);
});
});
}
if (missingPaths.length > 0) {
throw new MisconfiguredError().withFormattedErrorMessage('Missing expected handlers : %j', missingPaths);
}
return rval;
}
static findAndValidateModelName(method, path, schema, modelValidator) {
let rval = undefined;
const schemaPath = schema['application/json']['schema']['$ref'];
const inlinePath = schema['application/json']['schema']['type'];
if (schemaPath) {
rval = schemaPath.substring(schemaPath.lastIndexOf('/') + 1);
if (!modelValidator.fetchModel(rval)) {
throw new MisconfiguredError(`Path ${method} ${path} refers to schema ${rval} but its not in the schema section`);
}
}
else if (inlinePath) {
rval = `${method}-${path}-requestBodyModel`;
const model = schema['application/json']['schema'];
modelValidator.addModel(rval, model);
}
return rval;
}
static openApiPathToRouteParserPath(input) {
let rval = input;
if (rval) {
let sIdx = rval.indexOf('{');
while (sIdx > -1) {
const eIdx = rval.indexOf('}');
rval = rval.substring(0, sIdx) + ':' + rval.substring(sIdx + 1, eIdx) + rval.substring(eIdx + 1);
sIdx = rval.indexOf('{');
}
}
return rval;
}
}
//# sourceMappingURL=router-util.js.map