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
JavaScript
"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