UNPKG

dino-express

Version:

DinO enabled REST framework based on express

382 lines 19.3 kB
"use strict"; // Copyright 2018 Quirino Brizi [quirino.brizi@gmail.com] // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. Object.defineProperty(exports, "__esModule", { value: true }); exports.RoutingConfigurer = void 0; const tslib_1 = require("tslib"); const swagger_parser_1 = tslib_1.__importDefault(require("@apidevtools/swagger-parser")); const apicache_plus_1 = tslib_1.__importDefault(require("apicache-plus")); const cors_1 = tslib_1.__importDefault(require("cors")); const dino_core_1 = require("dino-core"); const express_1 = tslib_1.__importDefault(require("express")); const express_http_context2_1 = tslib_1.__importDefault(require("express-http-context2")); const https_1 = tslib_1.__importDefault(require("https")); const swagger_ui_express_1 = require("swagger-ui-express"); const api_validator_1 = require("./api.validator"); const error_handler_1 = require("./error.handler"); const ApplicationEvent_1 = require("./events/ApplicationEvent"); const EventProducer_1 = require("./events/EventProducer"); const EventQueue_1 = require("./events/EventQueue"); const LogEventEmitter_1 = require("./events/LogEventEmitter"); const NoopEventProducer_1 = require("./events/NoopEventProducer"); const GlobalErrorHandler_1 = require("./GlobalErrorHandler"); const Helper_1 = require("./Helper"); const after_handler_middleware_1 = require("./middlewares/after.handler.middleware"); const before_handler_middleware_1 = require("./middlewares/before.handler.middleware"); const ProjectionsMiddleware_1 = require("./middlewares/impl/after/ProjectionsMiddleware"); const SendResponseMiddleware_1 = require("./middlewares/impl/after/SendResponseMiddleware"); const ScopeCreatorMiddleware_1 = require("./middlewares/impl/before/ScopeCreatorMiddleware"); const SecurityMiddleware_1 = require("./middlewares/impl/before/SecurityMiddleware"); const RequestAdaptorMiddleware_1 = require("./middlewares/RequestAdaptorMiddleware"); const application_state_monitor_1 = require("./monitoring/application.state.monitor"); const cache_performance_monitor_1 = require("./monitoring/cache.performance.monitor"); const co2_emission_monitor_1 = require("./monitoring/co2.emission.monitor"); const filesystem_descriptor_provider_1 = require("./openapi/filesystem.descriptor.provider"); const PolicyFactory_1 = require("./policies/PolicyFactory"); const RequestAdaptor_1 = require("./request/RequestAdaptor"); const RouteHandler_1 = require("./RouteHandler"); const RuntimeContext_1 = require("./RuntimeContext"); const DESCRIPTOR_PROVIDER_MAP = Object.freeze({ filesystem: filesystem_descriptor_provider_1.FilesystemDescriptorProvider }); /** * Configure express routing based on swagger configuration. * * @typedef RoutingConfigurer * @public */ class RoutingConfigurer extends dino_core_1.Component { server; applicationStateMonitor; environment; applicationContext; runtimeContext; policyFactory; eventProducer; constructor({ applicationContext, environment }) { super(); this.server = undefined; this.environment = environment; this.applicationContext = applicationContext; this.runtimeContext = new RuntimeContext_1.RuntimeContext(environment); this.policyFactory = new PolicyFactory_1.PolicyFactory(); } async postConstruct() { this.applicationContext.add(dino_core_1.ComponentDescriptor.createFromValue('dinoRuntimeContext', this.runtimeContext, dino_core_1.Scope.SINGLETON)); const app = (0, express_1.default)(); app.disable('x-powered-by'); app.use(express_http_context2_1.default.middleware); const payloadSize = this.environment.getOrDefault('dino:server:payloadSize', '100kb'); app.use(express_1.default.json({ limit: payloadSize })); const requestAdaptorMiddleware = new RequestAdaptorMiddleware_1.RequestAdaptorMiddleware(new RequestAdaptor_1.RequestAdaptor(this.runtimeContext, this.environment, this.applicationContext)); // a middleware function with no mount path. This code is executed for every request to the router app.use(requestAdaptorMiddleware.handle.bind(requestAdaptorMiddleware)); this.extend(app); const parser = new swagger_parser_1.default(); const apis = await parser.dereference(await this.resolveDescriptorProvider().provide()); const apiValidator = new api_validator_1.ApiValidator(this.environment); apiValidator.init(apis); this.eventProducer = this.configureApplicationObservability(apis.info); this.applicationContext.add(dino_core_1.ComponentDescriptor.createFromValue('errorHandler', this.getGlobalErrorHandler(), dino_core_1.Scope.SINGLETON)); const router = express_1.default.Router(); if (apis.paths !== undefined) { const docsPath = this.environment.getOrDefault('dino:server:docs', '/api-docs'); router.use(docsPath, swagger_ui_express_1.serve); router.use(docsPath, (0, swagger_ui_express_1.setup)(apis, { explorer: false })); this.setupMonitoringEndpointsIfRequired(router); const monitoringMiddlewares = this.defineTopLevelMonitorsIfRequired(); const errorHandler = this.errorHandler(); const promises = Object.keys(apis.paths).map((path) => this.configureApiPath(apis, path, apiValidator, monitoringMiddlewares, errorHandler, router)); await Promise.allSettled(promises); } this.startServer(app, router); } preDestroy() { if (dino_core_1.ObjectHelper.isDefined(this.server)) { this.server.close(); } this.eventProducer.send(ApplicationEvent_1.ApplicationEvent.create('applicationStopped')); } async configureApiPath(apis, path, apiValidator, monitoringMiddlewares, errorHandler, router) { const currentPath = apis.paths[path]; if (currentPath !== undefined) { const promises = Object.keys(currentPath).map((method) => { this.configureApiMethod(path, currentPath, method, apiValidator, apis, monitoringMiddlewares, errorHandler, router); }); await Promise.allSettled(promises); } } configureApiMethod(path, currentPath, method, apiValidator, apis, monitoringMiddlewares, errorHandler, router) { const apiPath = Helper_1.Helper.normalizePath(path); const api = currentPath[method]; const responseValidator = apiValidator.getResponseValidator(method, path); const requestHandler = this.requestHandler(path, api, responseValidator); // defines middlewares proposing built-ins and concatenating lesser ones // as per express style the user will be able to interrupt the chain just // sending a response, this at current stage is not a concern but a better // middleware structure that avoid this kind if hacks should be put in place. // this is tracked on https://gitlab.com/codesketch/dino-express/issues/7 const userDefinedMiddlewares = this.middlewares(api, apis) || { before: [], after: [] }; const corsMiddleware = this.getCorsMiddlewareIfConfigured(api); if (corsMiddleware !== undefined) { // if cors is defined, enable the option method on the specific route router.options(apiPath, corsMiddleware); } try { const requestValidator = apiValidator.getRequestValidator(method, path); const beforeMiddlewares = [corsMiddleware, requestValidator] .concat(userDefinedMiddlewares.before) .concat(monitoringMiddlewares.before); const afterMiddlewares = [] .concat(userDefinedMiddlewares.after) .concat(monitoringMiddlewares.after) .concat([new SendResponseMiddleware_1.SendResponseMiddleware()]); const middlewares = [] .concat(beforeMiddlewares) .concat([requestHandler, errorHandler]) .concat(afterMiddlewares) .map((middleware) => { if (Helper_1.Helper.instanceOf(middleware, after_handler_middleware_1.AfterHandlerMiddleware) || Helper_1.Helper.instanceOf(middleware, before_handler_middleware_1.BeforeHandlerMiddleware)) { return middleware.handle.bind(middleware); } return middleware; }); router[method](apiPath, middlewares); } catch (e) { dino_core_1.Logger.error(`unable to configure the router from the provided OpenAPI definition, ${e.type ?? ''} - ${e.name} - ${e.message}... stopping the application`); process.exit(1); } } startServer(app, router) { app.use(this.environment.getOrDefault('dino:server:path', '/'), router); if (this.runtimeContext.isStandalone()) { const port = this.environment.getOrDefault('dino:server:port', 3030); const httpsConfig = this.environment.getOrDefault('dino:security:https', { enabled: false }); if (httpsConfig.enabled) { this.server = https_1.default.createServer(httpsConfig.config, app).listen(port, () => { dino_core_1.Logger.info(`application started, listening on port ${port}`); this.eventProducer.send(ApplicationEvent_1.ApplicationEvent.create('applicationStarted')); }); } else { this.server = app.listen(port, () => { dino_core_1.Logger.info(`application started, listening on port ${port}`); this.eventProducer.send(ApplicationEvent_1.ApplicationEvent.create('applicationStarted')); }); } } else { // if serverless register the router so that it can be used at later stages this.applicationContext.add(dino_core_1.ComponentDescriptor.createFromValue('dinoApiRouter', router, dino_core_1.Scope.SINGLETON)); this.eventProducer.send(ApplicationEvent_1.ApplicationEvent.create('applicationStarted')); } } resolveDescriptorProvider() { let answer; try { dino_core_1.Logger.info('try locate user defined descriptor provider from the application context'); answer = this.applicationContext.resolve('descriptorProvider'); } catch (e) { dino_core_1.Logger.info('user defined descriptor provider not found using default file system based one'); const oldStyle = this.environment.get('dino:descriptor:provider'); const newStyle = this.environment.getOrDefault('dino:openapi:descriptor:provider', 'filesystem'); const DescriptorProvider = DESCRIPTOR_PROVIDER_MAP[oldStyle ?? newStyle]; answer = new DescriptorProvider(this.environment); } return answer; } setupMonitoringEndpointsIfRequired(router) { let monitoringEnabled = this.runtimeContext.isMonitoringEnabled(); if (monitoringEnabled) { this.applicationStateMonitor = new application_state_monitor_1.ApplicationStateMonitor(this.applicationContext); router.use('/monitor', this.applicationStateMonitor.middleware()); } } /** * Allows to define top level monitors. The returned objects contains two arrays * before and after middlewares, the before middlewares are added at the very start * of the middleware chain while the after are added at the very end of the middleware * chain, both in the order that are defined. * * @returns {Object} * ```{ * before: [], * after: [] * }``` */ defineTopLevelMonitorsIfRequired() { const answer = { before: [], after: [] }; let co2MonitoringEnabled = this.runtimeContext.isCo2MonitoringEnabled(); if (co2MonitoringEnabled) { if (co2MonitoringEnabled) { const co2Monitor = new co2_emission_monitor_1.CO2EmissionMonitor(); this.addMonitor(co2Monitor); answer.before.push(co2Monitor.requestMiddleware.bind(co2Monitor)); answer.after.push(co2Monitor.responseMiddleware.bind(co2Monitor)); } } return answer; } /** * Allows to add a monitor * @param {Monitor} monitor */ addMonitor(monitor) { if (dino_core_1.ObjectHelper.isDefined(this.applicationStateMonitor)) { this.applicationStateMonitor.addMonitor(monitor); } return this; } /** * Hook method allow to customize the express instance. by default it add the json middleware only. * This method can be overwritten in order to provide more tailored configuration of express instance. * * @param express the express instance * @protected */ extend(_express) { } /** * Hook allows to provide an express error handler, by default a JSON error handler is used. * @protected */ errorHandler() { return error_handler_1.ErrorHandler.instance(this.environment); } /** * Allows users to provide custom middlewares the middlewares will be inserted on the * chain as provided by the user but between the built-in API validation and route * handler and error handler. * @param {any} api the swagger API definition * @param {any} components the components OpenAPI definition * @returns a collection of middlewares split between before and after request handler * * @protected */ middlewares(api, components) { const policies = this.getPolicies(api['x-dino-express-policies']); return { before: policies.concat([ new SecurityMiddleware_1.SecurityMiddleware(this.applicationContext, api, components), this.getCache(api, components), new ScopeCreatorMiddleware_1.ScopeCreatorMiddleware() ]), after: [new ProjectionsMiddleware_1.ProjectionsMiddleware()] }; } /** * Hook allow to define a custom request handler middleware. If no middleware is * returned the default will be used. * @param {any} api the OpenAPI definition * @param {function} responseValidator the validator for the response expected by the OpenAPI definition. * @returns {Function} * * @protected */ requestHandler(path, api, responseValidator) { const routeHandler = new RouteHandler_1.RouteHandler(path, api, this.applicationContext, responseValidator, this.environment, this.eventProducer); return routeHandler.handle.bind(routeHandler); } /** * Configures application observability events. * @returns the event producer */ configureApplicationObservability(apiInfo) { let eventProducer; const config = this.environment.getOrDefault('dino:observability', { enabled: false, eventEmitter: '', batchSize: 1 }); if (config.enabled) { let eventEmitter; if (dino_core_1.ObjectHelper.isDefined(config.eventEmitter)) { eventEmitter = this.applicationContext.resolve(config.eventEmitter); } else { eventEmitter = new LogEventEmitter_1.LogEventEmitter(); } const eventQueue = new EventQueue_1.EventQueue(eventEmitter, config.batchSize); eventProducer = new EventProducer_1.EventProducer(eventQueue, this.environment, apiInfo.title, apiInfo.version); } else { eventProducer = new NoopEventProducer_1.NoopEventProducer(); } this.applicationContext.add(dino_core_1.ComponentDescriptor.createFromValue('eventProducer', eventProducer, dino_core_1.Scope.SINGLETON)); return eventProducer; } getCorsMiddlewareIfConfigured(api) { const corsConfiguration = api['x-dino-express-cors']; if (dino_core_1.ObjectHelper.isDefined(corsConfiguration)) { return (0, cors_1.default)(corsConfiguration); } return (0, cors_1.default)({ origin: false }); } /** * Get the policies defined for this route * @param {Array<String>} policies the policies defined for this route * @returns {Array<AbstractPolicy>} the policies * * @private */ getPolicies(policies = []) { return policies.reduce((answer, policy) => { let _policy = this.policyFactory.getPolicy(policy.name); _policy.configure(policy.configuration); answer.push(_policy.middleware.bind(_policy)); return answer; }, []); } /** * Get the cache middleware configured as requested for the current route * @param {any} cache the cache configuration * @returns {Function} the cache middleware. * * @private */ getCache(api, _components) { dino_core_1.Logger.debug(`configuring cache for API ${api.operationId}`); let answer = (_req, _res, next) => next(); const cache = api['x-dino-express-cache']; if (cache !== undefined) { dino_core_1.Logger.debug(`configuring cache ttl to ${cache.ttl}`); const cacheInstance = apicache_plus_1.default.newInstance({ debug: cache.debug || false, trackPerformance: cache.trackPerformance || true, isBypassable: cache.isBypassable || true }); answer = cacheInstance.middleware(cache.ttl || '30 minutes', this.cacheOnlySuccessResponsesForGet); this.addMonitor(new cache_performance_monitor_1.CachePerformanceMonitor().setApiName(api.operationId).setCacheInstance(cacheInstance)); } return answer; } /** * Hook method that allows to define a global error handle, by default the * register global error handler will log the error and send an error event. * * @see {@link GlobalErrorHandler} for more info * @returns the error handler to register */ getGlobalErrorHandler() { return new GlobalErrorHandler_1.GlobalErrorHandler(this.eventProducer); } /** * Define a function that discriminate what responses should be cached. In this case only responses * with 200 response code from GET requests. * @returns {Function} the cache toggle function * * @private */ cacheOnlySuccessResponsesForGet(req, res) { dino_core_1.Logger.debug(`${res.statusCode} ${req.method}`); return res.statusCode == 200 && req.method.toLowerCase() == 'get'; } } exports.RoutingConfigurer = RoutingConfigurer; //# sourceMappingURL=RoutingConfigurer.js.map