UNPKG

express-service-bootstrap

Version:

This is a convenience package for starting a express API with security, health checks, process exits etc.

384 lines 21.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ApplicationBuilder = exports.ApplicationTypes = void 0; const express_1 = __importStar(require("express")); const enum_application_life_cycle_status_1 = require("./enum-application-life-cycle-status"); const disposable_singleton_container_1 = require("./disposable-singleton-container"); const null_probe_1 = require("./null-probe"); const node_apparatus_1 = require("node-apparatus"); var ApplicationTypes; (function (ApplicationTypes) { ApplicationTypes[ApplicationTypes["Main"] = 0] = "Main"; ApplicationTypes[ApplicationTypes["Health"] = 1] = "Health"; ApplicationTypes[ApplicationTypes["Both"] = 2] = "Both"; })(ApplicationTypes || (exports.ApplicationTypes = ApplicationTypes = {})); /** * The ApplicationBuilder class is responsible for building an express application. * It sets up the application's health status, ports, middlewares, routers, and error handling. */ class ApplicationBuilder { /** * Creates an instance of ApplicationBuilder. * * @param {string} applicationName - The name of the application. * @param startupHandler - The startup handler that has to be invoked before application starts, used to indicate the application's startup status. * @param shutdownHandler - The shutdown handler that has to be invoked before application shutdowns, used to indicate the application's liveliness status. * @param {IProbe} livenessProbe - The liveness probe used to indicate the application's liveness status. * @param {IProbe} readinessProbe - The readiness probe used to indicate the application's readiness status. * @param {NodeJS.Process} currentProcess - The current process. * @param {NodeJS.Signals[]} exitSignals - The exit signals. * @param {DisposableSingletonContainer} container - The container for disposable singletons. */ constructor(applicationName = 'Application', startupHandler = () => __awaiter(this, void 0, void 0, function* () { return ({ status: enum_application_life_cycle_status_1.ApplicationStartupStatus.UP, data: {} }); }), shutdownHandler = () => __awaiter(this, void 0, void 0, function* () { return ({ status: enum_application_life_cycle_status_1.ApplicationShutdownStatus.STOPPED, data: {} }); }), livenessProbe = new null_probe_1.NullProbe(enum_application_life_cycle_status_1.ApplicationStatus.UP), readinessProbe = new null_probe_1.NullProbe(enum_application_life_cycle_status_1.ApplicationStatus.UP), currentProcess = process, exitSignals = ['SIGINT', 'SIGTERM'], container = new disposable_singleton_container_1.DisposableSingletonContainer()) { this.applicationName = applicationName; this.startupHandler = startupHandler; this.shutdownHandler = shutdownHandler; this.livenessProbe = livenessProbe; this.readinessProbe = readinessProbe; this.currentProcess = currentProcess; this.exitSignals = exitSignals; this.container = container; this.applicationStatus = { status: enum_application_life_cycle_status_1.ApplicationDefaultStatus.UNKNOWN, data: {} }; this.applicationPort = 3000; this.healthPort = 5678; this.appHandlers = new node_apparatus_1.SortedMap(); this.healthHandlers = new node_apparatus_1.SortedMap(); this.exitHandler = this[Symbol.asyncDispose].bind(this); this.exitSignals.forEach(signal => { this.currentProcess.once(signal, this.exitHandler); }); this.catchAllErrorResponseTransformer = (req, error) => ({ apistatus: 500, err: [{ errcode: 500, errmsg: `Unhandled exception occurred, please retry your request.` }] }); } /** * Used to override the startup handler. * @param startupHandler Handler to be invoked before application starts, used to indicate the application's startup status. * @returns {ApplicationBuilder} ApplicationBuilder instance. */ overrideStartupHandler(startupHandler) { this.startupHandler = startupHandler; return this; } /** * Used to override the shutdown handler. * @param shutdownHandler Handler to be invoked before application shutdowns, used to cleanup resources. * @returns {ApplicationBuilder} ApplicationBuilder instance. */ overrideShutdownHandler(shutdownHandler) { this.shutdownHandler = shutdownHandler; return this; } /** * Used to override the liveness probe. * @param livenessProbe Probe used to indicate the application's liveness status. * @returns {ApplicationBuilder} ApplicationBuilder instance. */ overrideLivenessProbe(livenessProbe) { this.livenessProbe = livenessProbe; return this; } /** * Used to override the readiness probe. * @param readinessProbe Probe used to indicate the application's readiness status. * @returns {ApplicationBuilder} ApplicationBuilder instance. */ overrideReadinessProbe(readinessProbe) { this.readinessProbe = readinessProbe; return this; } /** * Used to overrides the application port default(3000). * @param {number} port new application port. * @returns {ApplicationBuilder} ApplicationBuilder instance. */ overrideAppPort(port) { this.applicationPort = port; return this; } /** * Used to overrides the health port default(5678). * @param {number} port new health port. * @returns {ApplicationBuilder} ApplicationBuilder instance. */ overrideHealthPort(port) { this.healthPort = port; return this; } /** * Used to overrides the catchAllErrorResponseTransformer configuration. * @param {(request: Request, error: unknown) => unknown} transformer new catchAllErrorResponseTransformer configured middleware. * @returns {ApplicationBuilder} ApplicationBuilder instance. */ overrideCatchAllErrorResponseTransformer(transformer) { this.catchAllErrorResponseTransformer = transformer; return this; } /** * Used to register a SYNC/ASYNC middleware. * @param {ApplicationBuilderMiddleware} handler handler to be registered. * @param {HostingPath} hostingPath path where the handler has to be registered, use "*" for global. * @param {number} order order in which the handler has to be registered. * @param {ApplicationTypes} appliesTo type of application to which the handler has to be registered. * @returns {ApplicationBuilder} ApplicationBuilder instance. */ registerApplicationHandler(handler, hostingPath, order = undefined, appliesTo = ApplicationTypes.Main) { if (order != undefined && order < 1) throw new Error('Order must be greater than 0'); switch (appliesTo) { case ApplicationTypes.Main: this.setRoutes(this.appHandlers, handler, hostingPath, order); break; case ApplicationTypes.Health: this.setRoutes(this.healthHandlers, handler, hostingPath, order); break; case ApplicationTypes.Both: this.setRoutes(this.appHandlers, handler, hostingPath, order); this.setRoutes(this.healthHandlers, handler, hostingPath, order); break; default: throw new Error(`Unknown Application Type:${appliesTo}`); } return this; } /** * Used to start the application using the configured parameters. * @returns {Promise<void>} Promise that resolves when the application is started. */ start() { return __awaiter(this, void 0, void 0, function* () { const startTime = Date.now(); this.applicationStatus = { status: enum_application_life_cycle_status_1.ApplicationStartupStatus.STARTING, data: { "invokeTime": startTime } }; try { const rootRouter = this.container.bootstrap.createInstanceWithoutConstructor(express_1.Router); this.applicationStatus = yield this.startupHandler(rootRouter, this.container, this); if (this.applicationStatus.status === enum_application_life_cycle_status_1.ApplicationStartupStatus.UP) { this.registerApplicationHandler(rootRouter, "/", 1, ApplicationTypes.Main); yield this.container.createAsyncInstanceWithoutConstructor(ApplicationBuilder.DINAME_HealthExpress, this.healthExpressListen.bind(this)); yield this.container.createAsyncInstanceWithoutConstructor(ApplicationBuilder.DINAME_ApplicationExpress, this.appExpressListen.bind(this)); } else { this.applicationStatus = { status: enum_application_life_cycle_status_1.ApplicationStatus.DOWN, data: { "reason": `Application startup handler returned failure status: ${this.applicationStatus.status}.` } }; } } catch (error) { this.applicationStatus = { status: enum_application_life_cycle_status_1.ApplicationStatus.DOWN, data: { "reason": "Application startup handler caught error" } }; throw error; } finally { this.applicationStatus.data["startupTime"] = Date.now() - startTime; } }); } /** * Used to stop the application. This clears all the middlewares and routers.(all config is reset to default) * @returns {Promise<void>} Promise that resolves when the application is stopped. */ [Symbol.asyncDispose]() { return __awaiter(this, void 0, void 0, function* () { const startTime = Date.now(); this.applicationStatus = { status: enum_application_life_cycle_status_1.ApplicationShutdownStatus.STOPPING, data: { "invokeTime": startTime } }; const result = yield this.shutdownHandler(); if (result.status === enum_application_life_cycle_status_1.ApplicationShutdownStatus.STOPPED) { this.exitSignals.forEach(signal => { this.currentProcess.removeListener(signal, this.exitHandler); }); yield this.container.disposeAll(); this.appHandlers.clear(); this.healthHandlers.clear(); this.applicationStatus = result; this.applicationStatus.data["shutdownTime"] = Date.now() - startTime; } }); } //------Private Methods------// appExpressListen() { return __awaiter(this, void 0, void 0, function* () { const applicationExpressInstance = this.container.bootstrap.createInstanceWithoutConstructor(express_1.default); const applicationHttpServer = yield new Promise((a, r) => { try { for (const [path, handler] of this.appHandlers.sort()) { if (path === "*") { const globalMiddleWares = Array.from(handler.sort().values()); applicationExpressInstance.use(globalMiddleWares); } else { applicationExpressInstance.use(path, handler); } } applicationExpressInstance.use(this.errorHandler.bind(this)); const server = applicationExpressInstance.listen(this.applicationPort, () => { a(server); }); } catch (e) { r(e); } }); applicationExpressInstance[Symbol.asyncDispose] = () => __awaiter(this, void 0, void 0, function* () { var _a; const customDispose = () => __awaiter(this, void 0, void 0, function* () { applicationHttpServer.close(e => e == null ? Promise.resolve() : Promise.reject(e)); }); yield (((_a = applicationHttpServer[Symbol.asyncDispose]) === null || _a === void 0 ? void 0 : _a.bind(applicationHttpServer)) || customDispose)(); }); return applicationExpressInstance; }); } healthExpressListen() { return __awaiter(this, void 0, void 0, function* () { const healthExpressInstance = this.container.bootstrap.createInstanceWithoutConstructor(express_1.default); const healthServer = yield new Promise((a, r) => { try { for (const [path, handler] of this.healthHandlers.sort()) { if (path === "*") { const globalMiddleWares = Array.from(handler.sort().values()); healthExpressInstance.use(globalMiddleWares); } else { healthExpressInstance.use(path, handler); } } healthExpressInstance.get(`/health/startup`, (req, res) => __awaiter(this, void 0, void 0, function* () { return this.checkHealthStatus("startup", res); })); healthExpressInstance.get(`/health/readiness`, (req, res) => __awaiter(this, void 0, void 0, function* () { return this.checkHealthStatus("readiness", res); })); healthExpressInstance.get(`/health/liveliness`, (req, res) => __awaiter(this, void 0, void 0, function* () { return this.checkHealthStatus("liveliness", res); })); healthExpressInstance.use(this.errorHandler); const server = healthExpressInstance.listen(this.healthPort, () => { a(server); }); } catch (e) { r(e); } }); healthExpressInstance[Symbol.asyncDispose] = () => __awaiter(this, void 0, void 0, function* () { var _a; const customDispose = () => __awaiter(this, void 0, void 0, function* () { healthServer.close(e => e == null ? Promise.resolve() : Promise.reject(e)); }); yield (((_a = healthServer[Symbol.asyncDispose]) === null || _a === void 0 ? void 0 : _a.bind(healthServer)) || customDispose)(); }); return healthExpressInstance; }); } checkHealthStatus(lifecycleStage, res) { return __awaiter(this, void 0, void 0, function* () { try { switch (lifecycleStage) { case "startup": if (this.applicationStatus.status === enum_application_life_cycle_status_1.ApplicationStartupStatus.UP) { res.status(200) .json({ "status": this.applicationStatus.status, "checks": [{ "name": lifecycleStage, "state": this.applicationStatus.status, "data": this.applicationStatus.data }] }); } else { res.status(503) .json({ "status": this.applicationStatus.status, "checks": [{ "name": lifecycleStage, "state": this.applicationStatus.status, "data": this.applicationStatus.data }] }); } break; case "readiness": if (this.applicationStatus.status === enum_application_life_cycle_status_1.ApplicationStatus.UP || this.applicationStatus.status === enum_application_life_cycle_status_1.ApplicationStatus.DOWN) { const result = yield this.readinessProbe.check(); if (result.status === enum_application_life_cycle_status_1.ApplicationStatus.UP) { res.status(200) .json({ "status": result.status, "checks": [{ "name": lifecycleStage, "state": result.status, "data": result.data }] }); } else { res.status(503) .json({ "status": result.status, "checks": [{ "name": lifecycleStage, "state": result.status, "data": result.data }] }); } } else if (this.applicationStatus.status === enum_application_life_cycle_status_1.ApplicationShutdownStatus.STOPPING || this.applicationStatus.status === enum_application_life_cycle_status_1.ApplicationShutdownStatus.STOPPED) { res.status(503) .json({ "status": this.applicationStatus.status, "checks": [{ "name": lifecycleStage, "state": this.applicationStatus.status, "data": { reason: "Application received exit signal." } }] }); } else { throw new Error(`Unknown Application state:${this.applicationStatus.status}`); } break; case "liveliness": const result = yield this.livenessProbe.check(); if (result.status === enum_application_life_cycle_status_1.ApplicationStatus.UP) { res.status(200) .json({ "status": result.status, "checks": [{ "name": lifecycleStage, "state": result.status, "data": result.data }] }); } else { res.status(503) .json({ "status": result.status, "checks": [{ "name": lifecycleStage, "state": result.status, "data": result.data }] }); } break; default: throw new Error(`Unknown life cycle stage:${lifecycleStage}`); } } catch (err) { res.status(500) .json({ "status": enum_application_life_cycle_status_1.ApplicationDefaultStatus.UNKNOWN, "checks": [{ "name": "global", "state": enum_application_life_cycle_status_1.ApplicationDefaultStatus.UNKNOWN, "data": { "reason": "Unhandled Exception" } }] }); } ; }); } errorHandler(err, req, res, next) { if (res.headersSent) { return next(err); } const errorResponse = this.catchAllErrorResponseTransformer(req, err); res.status(500) .send(errorResponse); } setRoutes(map, handler, hostingPath, order = undefined) { if (hostingPath === "*") { const existingMap = map.get(hostingPath) || new node_apparatus_1.SortedMap(); existingMap.set(`${hostingPath}-${order}`, handler, order); map.set(hostingPath, existingMap, 0); } else { map.set(hostingPath, handler, order); } } } exports.ApplicationBuilder = ApplicationBuilder; ApplicationBuilder.DINAME_ApplicationExpress = "AE"; ApplicationBuilder.DINAME_HealthExpress = "HE"; //# sourceMappingURL=application-builder.js.map