UNPKG

@bitblit/ratchet-epsilon-common

Version:

Tiny adapter to simplify building API gateway Lambda APIS

234 lines 12.6 kB
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