@eang/core
Version:
eang - model driven enterprise event processing
227 lines • 8.26 kB
JavaScript
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