UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

311 lines 13.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const node_path_1 = __importDefault(require("node:path")); const express_1 = __importDefault(require("express")); const helmet_1 = __importDefault(require("helmet")); const body_parser_1 = __importDefault(require("body-parser")); const cookie_parser_1 = __importDefault(require("cookie-parser")); const compression_1 = __importDefault(require("compression")); const models_1 = require("./models"); class App { constructor(services) { this.express = null; this.server = null; this.isAlive = false; this.services = services; } bind() { process.once('SIGTERM', this.signalHandler('SIGTERM')); process.once('SIGINT', this.signalHandler('SIGINT')); process.once('uncaughtException', this.errorHandler('uncaughtException')); process.once('unhandledRejection', this.errorHandler('unhandledRejection')); return this; } async start() { if (this.server) { return; } const tic = Date.now(); const { logger } = this.services.telemetry; logger.debug('[App] Starting telemetry'); await this.services.telemetry.start(); this.services.metrics.incrementProcessStatus({ state: 'starting' }); logger.info('[App] Starting'); this.express = (0, express_1.default)(); this.isAlive = false; logger.debug('[App] Configured features', { features: this.services.config.features, }); this.express .use((req, res, next) => { res.locals.tic = Date.now(); next(); }) .use((0, helmet_1.default)({ contentSecurityPolicy: this.services.config.mode === 'development' ? false : undefined, })) .disable('x-powered-by'); // Configure CORS headers this.express.use((req, res, next) => { if (this.services.config.features.cors.isEnabled === false) { res.header({ 'Access-Control-Allow-Credentials': 'true', 'Access-Control-Allow-Headers': '*', 'Access-Control-Allow-Origin': req.get('origin'), 'Access-Control-Allow-Methods': '*', 'Access-Control-Expose-Headers': '*', 'Access-Control-Request-Headers': '*', 'Access-Control-Request-Method': '*', }); } else { this.services.config.features.cors.allowCredentials !== undefined && res.header('Access-Control-Allow-Credentials', this.services.config.features.cors.allowCredentials); res.header('Access-Control-Allow-Headers', `${this.services.config.features.cors.allowHeaders},authorization,csrf-token,page,page-size,decrypt,content-type,cache-control,cursor-last-id,cursor-last-correlation-id`); this.services.config.features.cors.allowMethods !== undefined && res.header('Access-Control-Allow-Methods', this.services.config.features.cors.allowMethods); res.header('Access-Control-Allow-Origin', this.services.config.features.cors.allowOrigin ?? req.get('origin')); this.services.config.features.cors.exposeHeaders !== undefined && res.header('Access-Control-Expose-Headers', this.services.config.features.cors.exposeHeaders); this.services.config.features.cors.requestHeaders !== undefined && res.header('Access-Control-Request-Headers', this.services.config.features.cors.requestHeaders); this.services.config.features.cors.requestMethod !== undefined && res.header('Access-Control-Request-Method', this.services.config.features.cors.requestMethod); } if (req.method === 'OPTIONS') { return res.send(); } return next(); }); /** * Heartbeat route (Unauthenticated) */ this.express .use((0, compression_1.default)()) .get('/ready', (_req, res) => { res.json({ is_ready: true }); }) .use(async (_req, res, next) => { if (this.isAlive === false) { logger.debug('[App] Readiness - Service unavailable'); return res.status(503).json({ status: 503, message: 'Service Unavailable', }); } logger.debug('[App] Readiness - Service ready'); return next(); }) .get('/heartbeat', async (req, res) => { res.json({ is_alive: this.isAlive }); }); // Listen logger.debug('[App] Starting HTTP server', { port: this.services.config.port, }); this.server = this.express.listen(this.services.config.port, () => { logger.info('[App] Listening', { port: this.services.config.port, pid: process.pid, }); }); logger.debug('[App] Connecting MongoDb...', { configs: this.services.config.mongodb.databases.map((d) => ({ name: d.name, options: d.options, })), }); await this.services.mongodb.connect(); logger.info('[App] MongoDb connected'); logger.debug('[App] Models initialization...'); this.services.models = (0, models_1.init)(this.services.config.models, this.services); if (this.services.config.features.initInternalModels === true) { logger.debug('[App] Internal models indexes creation'); this.services.models .initInternalModels() .then(() => { logger.debug('[App] Internal models indexes created'); }) .catch((err) => { logger.warn('[App] Internal models indexes creation failed', err); }); } logger.debug('[App] Loading models definitions'); await this.services.models.load(); logger.info('[App] Models initialized', { count: this.services.models.MODELS.size, models: Array.from(this.services.models.MODELS.keys()), }); if (this.services.config.features.api.templates === true) { this.express.use('/templates', express_1.default.static(node_path_1.default.resolve(__dirname, '../templates'))); } if (this.services.config.security.accessTokenByCookie === true) { this.express.use((0, cookie_parser_1.default)()); // Authentication layer this.express .use(body_parser_1.default.json({ limit: this.services.config.features.api.json.limit, })) .post('/auth', (req, res) => { const body = req.body; for (const key in body) { const maxAge = (this.services.config.features.cookies.maxAges[key] ?? this.services.config.features.cookies.options.maxAge) * 1000; res.cookie(key, body[key], { maxAge, httpOnly: this.services.config.features.cookies.options.httpOnly, secure: this.services.config.features.cookies.options.secure, sameSite: this.services.config.features.cookies.options.sameSite, domain: this.services.config.features.cookies.options.domain ?? req.header('host'), }); } res.json({ is_authenticated: true }); }) .get('/auth', (req, res) => { const response = {}; const cookies = (req.query.cookies ?? []).filter((c) => c !== 'token'); for (const cookie of cookies) { response[cookie] = req.cookies[cookie]; res.cookie(cookie, '', { httpOnly: this.services.config.features.cookies.options.httpOnly, secure: this.services.config.features.cookies.options.secure, sameSite: this.services.config.features.cookies.options.sameSite, domain: this.services.config.features.cookies.options.domain ?? req.header('host'), maxAge: 0, }); } res.json(response); }); } // OpenAPI 3.0 middleware this.express.use('/api', await this.services.api(this.services)); this.isAlive = true; logger.info('[App] Datastore is up', { tic_time_in_milliseconds: Date.now() - tic, }); let withEvents = false; if (this.services.config.features?.mqtt?.isEnabled === true) { logger.debug('[App] Connecting to MQTT...'); await this.services.mqtt.connect(); logger.info('[App] Connected to MQTT'); withEvents = true; } if (this.services.config.features?.amqp?.isEnabled === true) { logger.debug('[App] Connecting to AMQP...'); await this.services.amqp.connect(); logger.info('[App] Connected to AMQP'); withEvents = true; } withEvents === true && (await this.services.events(this.services)); this.services.metrics.incrementProcessStatus({ state: 'started' }); // Build Data Model graph this.services.models.getGraph(); return this; } stop() { this.services.signals.emit('stop'); return new Promise((resolve, reject) => { if (this.server) { this.server.close((err) => { if (err) { return reject(err); } this.server = null; this.express = null; if (this.services.config.features?.mqtt?.isEnabled === true) { this.services.mqtt .end() .then(() => { this.services.telemetry.logger.info('[destroy] MQTT client stopped', err); }) .catch((err) => { this.services.telemetry.logger.error('[destroy] MQTT client stopping error', err); }); } if (this.services.config.features?.amqp?.isEnabled === true) { this.services.amqp .end() .then(() => { this.services.telemetry.logger.info('[destroy] AMQP client stopped', err); }) .catch((err) => { this.services.telemetry.logger.error('[destroy] AMQP client stopping error', err); }); } this.services.mongodb .disconnect() .then(() => { this.isAlive === true && resolve(this); }) .catch(() => { this.isAlive === true && resolve(this); }); this.isAlive === false && resolve(this); }); } else { return resolve(this); } }); } async restart() { await this.stop(); await this.start(); return this; } signalHandler(signal) { return () => { return this.destroy(signal); }; } errorHandler(signal) { return (err) => { return this.destroy(signal, err); }; } /** * Cleanup and stop the process properly, then exit the process. * @param signal - Signal to stop the process with * @param err - Error that caused the destruction of the process */ destroy(signal, err) { this.services.metrics.incrementProcessStatus({ state: err ? 'crashing' : 'stopping', }); const { logger } = this.services.telemetry; if (err) { logger.error('[destroy] Application error', { err, signal }); } logger.info('[destroy] Stopping application', { err, signal }); return this.stop() .then(() => { logger.info('[destroy] Application stopped', err); this.services.metrics.incrementProcessStatus({ state: err ? 'crashed' : 'stopped', }); const timeout = setTimeout(() => { timeout.unref(); process.exit(err ? 1 : 0); }, this.services.config.exitTimeout); return timeout; }) .catch((stopErr) => { logger.error('[destroy] Application crashed', { err: stopErr, firstErr: err, }); const timeout = setTimeout(() => { timeout.unref(); process.exit(1); }, this.services.config.exitTimeout); return timeout; }); } } exports.default = App; //# sourceMappingURL=App.js.map