dino-express
Version:
DinO enabled REST framework based on express
382 lines • 19.3 kB
JavaScript
"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