UNPKG

@eang/core

Version:

eang - model driven enterprise event processing

227 lines 8.26 kB
import * as fs from 'fs'; import { join, isAbsolute } from 'path'; import { FunctionInstanceObj } from './objects.js'; import { NatsStreamService } from './NatsStreamingService.js'; import { filter } from 'rxjs/operators'; import { logger } from './logger.js'; import { FunctionStartContext } from './context.js'; import { EntityEvent } from './entity.js'; export class EangBase { _env = {}; _secrets = {}; _natConf; _nss = new NatsStreamService(); _natsConnection; log = logger; constructor(env, secrets, isService = false) { this.configure(env, secrets, isService); } configure(env, secrets, isService = false) { this._env = env || this.loadEnv(process.env); this.validateEnv(isService); this._secrets = secrets || this.loadSecrets(); this.validateSecrets(); this._natConf = { servers: this._env.EANG_NATS_SERVERS, user: this._env.EANG_USER, pass: this._secrets.EANG_PASSWORD }; } loadEnv(env) { const result = {}; // Load all environment variables for (const [key, value] of Object.entries(env)) { if (value !== undefined) { result[key] = value; } } return result; } loadSecrets() { const secretsDir = isAbsolute(this._env.EANG_SECRETS_DIR) ? this._env.EANG_SECRETS_DIR : join(process.cwd(), this._env.EANG_SECRETS_DIR); const secretsFile = join(secretsDir, 'secrets.json'); try { if (!fs.existsSync(secretsFile)) { this.log.error('secrets.json file not found in secrets directory', secretsFile); process.exit(1); } const secretsContent = fs.readFileSync(secretsFile, 'utf-8'); const secrets = JSON.parse(secretsContent); if (!secrets || typeof secrets !== 'object' || Object.keys(secrets).length === 0) { this.log.error('secrets.json is empty or invalid', secretsFile); process.exit(1); } // Ensure all values are strings const processedSecrets = {}; for (const [key, value] of Object.entries(secrets)) { processedSecrets[key] = String(value); } return processedSecrets; } catch (error) { if (error instanceof SyntaxError) { this.log.error('Failed to parse secrets.json: Invalid JSON format'); } else { console.log(error); this.log.error('Failed to load secrets:', error); } process.exit(1); } } validateEnv(isService = false) { // Required variables for both service and function const requiredVars = ['EANG_USER', 'EANG_NATS_SERVERS', 'EANG_SECRETS_DIR']; if (!isService) { requiredVars.push('EANG_TENANT', 'EANG_INSTANCE_OF'); } // Check required variables for (const key of requiredVars) { const value = this._env[key]; if (!value) { this.log.error(`Required environment variable ${key} is not set`); process.exit(1); } } // Validate log level const validLogLevels = ['trace', 'debug', 'info', 'warn', 'error', 'silent']; const logLevel = this._env.EANG_LOG_LEVEL || 'trace'; if (!validLogLevels.includes(logLevel)) { this.log.error(`Invalid log level: ${logLevel}. Must be one of: ${validLogLevels.join(', ')}`); process.exit(1); } } validateSecrets() { if (!this._secrets.EANG_PASSWORD) { this.log.error('Required secret EANG_PASSWORD not found in secrets directory'); process.exit(1); } } async ensureStream(streamName, subjects) { if (!this._natConf) { this.configure(); } if (!this._natConf) { throw new Error('Nats configuration not found'); } if (!this._natsConnection) { const { nc, js, jsm } = await this._nss.initialize(this._natConf); this._natsConnection = { nc, js, jsm }; } const stream = await this._nss.ensureStream(streamName, subjects); return stream; } async publish(events, options) { const opts = options || {}; if (!opts.tenant) { opts.tenant = this._env.EANG_TENANT; } if (!opts.organizationalUnit && this._env.EANG_ORGANIZATIONAL_UNIT) { opts.organizationalUnit = this._env.EANG_ORGANIZATIONAL_UNIT; } if (!opts.user) { opts.user = this._env.EANG_USER; } if (!this._natsConnection) { this._natsConnection = await this._nss.initialize(this._natConf); } const pubAcks = await this._nss.publish(events, opts); return pubAcks; } async shutdown() { this.log.debug('In eang shutdown'); if (this._natsConnection) { const { nc } = this._natsConnection; await nc.drain(); await nc.close(); this._natsConnection = undefined; } } get env() { return this._env; } get secrets() { return this._secrets; } } export class EangPublisher extends EangBase { constructor(env, secrets) { super(env, secrets, true); } } export class EangFunction extends EangBase { _subscription; _handler; constructor(opts) { super(opts.env, opts.secrets, false); this._handler = opts.handler; } // Ensure env property is accessible - inherit from base class get env() { return super.env; } get secrets() { return super.secrets; } async start() { if (!this._natsConnection) { this._natsConnection = await this._nss.initialize(this._natConf); } const stream = await this._nss.ensureConsumerStream(`${this._env.EANG_TENANT}_Function_Instance_${this._env.EANG_INSTANCE_OF}`, [ `eang.obj.${this._env.EANG_TENANT}.*.*.Function_Instance.${this._env.EANG_INSTANCE_OF}.>` ]); this._subscription = stream.event$ .pipe(filter((m) => m.subjectData.eventType === 'start' && m.subjectData.typeOf === 'Function_Instance')) .subscribe(async (e) => { let startContext = undefined; if (e.context) { startContext = new FunctionStartContext(e.context); this.log.debug(startContext); const input = startContext?.data; const stopContext = await this._handler(input ?? {}, this, startContext); this.log.debug(stopContext); const subjectData = e.subjectData; const functionStopEvent = EntityEvent.stop(new FunctionInstanceObj({ key: subjectData.key, instanceOf: subjectData.instanceOf }), stopContext); const pubAcks = await this.publish([functionStopEvent]); this.log.debug(pubAcks); const ackAck = await e.msg.ackAck(); this.log.debug(ackAck); } else { throw new Error('No context found'); } }); return stream.startToConsume(); // return stream } async shutdown() { await super.shutdown(); if (this._subscription) { this._subscription.unsubscribe(); this._subscription = undefined; } } } export class EangService extends EangBase { _consumerName; _subscriptions; constructor(opts) { super(opts.env, opts.secrets, true); this._consumerName = opts.consumerName; this._subscriptions = opts.subscriptions; } async start() { if (!this._natsConnection) { this._natsConnection = await this._nss.initialize(this._natConf); } const stream = await this._nss.ensureConsumerStream(this._consumerName, this._subscriptions); return stream; } } //# sourceMappingURL=eang.js.map