@getanthill/datastore
Version:
Event-Sourced Datastore
312 lines • 13.8 kB
JavaScript
"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
const corsMiddleware = async (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();
};
this.express.use(corsMiddleware);
/**
* Heartbeat route (Unauthenticated)
*/
const availabilityMiddleware = 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();
};
this.express
.use((0, compression_1.default)())
.get('/ready', (_req, res) => {
res.json({ is_ready: true });
})
.use(availabilityMiddleware)
.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((err_) => {
logger.error('[destroy] Application crashed', {
err: err_,
firstErr: err,
});
const timeout = setTimeout(() => {
timeout.unref();
process.exit(1);
}, this.services.config.exitTimeout);
return timeout;
});
}
}
exports.default = App;
//# sourceMappingURL=App.js.map