UNPKG

@cap-js-community/event-queue

Version:

An event queue that enables secure transactional processing of asynchronous and periodic events, featuring instant event processing with Redis Pub/Sub and load distribution across all application instances.

940 lines (790 loc) 27 kB
"use strict"; const cds = require("@sap/cds"); const { CronExpressionParser } = require("cron-parser"); const { getEnvInstance } = require("./shared/env"); const EventQueueError = require("./EventQueueError"); const { Priorities } = require("./constants"); const FOR_UPDATE_TIMEOUT = 10; const GLOBAL_TX_TIMEOUT = 30 * 60 * 1000; const REDIS_PREFIX = "EVENT_QUEUE"; const COMPONENT_NAME = "/eventQueue/config"; const MIN_INTERVAL_SEC = 10; const DEFAULT_LOAD = 1; const DEFAULT_PRIORITY = Priorities.Medium; const DEFAULT_INCREASE_PRIORITY = true; const DEFAULT_KEEP_ALIVE_INTERVAL = 60; const DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL = 3.5; const DEFAULT_INHERIT_TRACE_CONTEXT = true; const DEFAULT_CHECK_FOR_NEXT_CHUNK = true; const SUFFIX_PERIODIC = "_PERIODIC"; const CAP_EVENT_TYPE = "CAP_OUTBOX"; const CAP_PARALLEL_DEFAULT = 5; const CAP_MAX_ATTEMPTS_DEFAULT = 5; const DELETE_TENANT_BLOCK_AFTER_MS = 5 * 60 * 1000; const DEFAULT_RETRY_AFTER = 5 * 60 * 1000; const PRIORITIES = Object.values(Priorities); const UTC_DEFAULT = false; const USE_CRON_TZ_DEFAULT = true; const BASE_TABLES = { EVENT: "sap.eventqueue.Event", LOCK: "sap.eventqueue.Lock", }; const ALLOWED_EVENT_OPTIONS_BASE = [ "type", "subType", "load", "impl", "transactionMode", "deleteFinishedEventsAfterDays", "useEventQueueUser", "priority", "keepAliveInterval", "increasePriorityOverTime", "keepAliveMaxInProgressTime", "appNames", "appInstances", "namespace", "internalEvent", ]; const ALLOWED_EVENT_OPTIONS_AD_HOC = [ ...ALLOWED_EVENT_OPTIONS_BASE, "inheritTraceContext", "selectMaxChunkSize", "parallelEventProcessing", "retryAttempts", "processAfterCommit", "checkForNextChunk", "retryFailedAfter", "propagateHeaders", "retryOpenAfter", "multiInstanceProcessing", "kind", "timeBucket", ]; const ALLOWED_EVENT_OPTIONS_PERIODIC_EVENT = [ ...ALLOWED_EVENT_OPTIONS_BASE, "interval", "cron", "utc", "useCronTimezone", "randomOffset", ]; class Config { #logger; #config; #forUpdateTimeout; #globalTxTimeout; #runInterval; #redisEnabled; #initialized; #instanceLoadLimit; #tableNameEventQueue; #tableNameEventLock; #isEventQueueActive; #configFilePath; #processEventsAfterPublish; #registerAsEventProcessor; #disableRedis; #env; #eventMap; #updatePeriodicEvents; #isEventBlockedCb; #thresholdLoggingEventProcessing; #useAsCAPOutbox; #userId; #cleanupLocksAndEventsForDev; #redisOptions; #insertEventsBeforeCommit; #enableTelemetry; #unsubscribeHandlers = []; #unsubscribedTenants = {}; #cronTimezone; #randomOffsetPeriodicEvents; #crashOnRedisUnavailable; #tenantIdFilterAuthContextCb; #tenantIdFilterEventProcessingCb; #configEvents; #configPeriodicEvents; #enableAdminService; #disableProcessingOfSuspendedTenants; #namespace; #processingNamespaces; static #instance; constructor() { this.#logger = cds.log(COMPONENT_NAME); this.#config = null; this.#forUpdateTimeout = FOR_UPDATE_TIMEOUT; this.#globalTxTimeout = GLOBAL_TX_TIMEOUT; this.#runInterval = null; this.#redisEnabled = null; this.#initialized = false; this.#instanceLoadLimit = 100; this.#tableNameEventQueue = null; this.#tableNameEventLock = null; this.#isEventQueueActive = true; this.#configFilePath = null; this.#processEventsAfterPublish = null; this.#disableRedis = null; this.#env = getEnvInstance(); } getEventConfig(type, subType, namespace = this.namespace) { return this.#eventMap[this.generateKey(namespace, type, subType)] ? { ...this.#eventMap[this.generateKey(namespace, type, subType)] } : undefined; } isCapOutboxEvent(type) { return [CAP_EVENT_TYPE, [CAP_EVENT_TYPE, SUFFIX_PERIODIC].join("")].includes(type); } hasEventAfterCommitFlag(type, subType, namespace = this.namespace) { return this.#eventMap[this.generateKey(namespace, type, subType)]?.processAfterCommit ?? true; } _checkRedisIsBound() { return !!this.#env.redisRequires?.credentials; } #parseRegexOrString(str) { const regexLiteralPattern = /^\/((?:\\.|[^\\/])*)\/([gimsuy]*)$/; const match = str.match(regexLiteralPattern); if (match) { try { return { type: "regex", value: new RegExp(match[1], match[2]) }; } catch { return { type: "string", value: str }; } } return { type: "string", value: str }; } normalizeSubType(type, rawSubType) { if (![CAP_EVENT_TYPE, [CAP_EVENT_TYPE, SUFFIX_PERIODIC].join("")].includes(type)) { return { subType: rawSubType }; } const serviceParts = rawSubType.split("."); let srvName = serviceParts.shift(); while (!cds.env.requires[srvName] && serviceParts.length) { srvName = [srvName, serviceParts.shift()].join("."); } const actionName = serviceParts.shift(); const actionSpecificCall = this.getCdsOutboxEventSpecificConfig(srvName, actionName); return { subType: actionSpecificCall ? rawSubType : srvName, actionName, srvName, }; } shouldBeProcessedInThisApplication(type, rawSubType, namespace = this.namespace) { const { subType } = this.normalizeSubType(type, rawSubType); const config = this.#eventMap[this.generateKey(namespace, type, subType)]; const appNameConfig = config._appNameMap; const appInstanceConfig = config._appInstancesMap; let result = true; if (!appNameConfig && !appInstanceConfig) { return result; } if (appNameConfig) { if (config._appNameContainsRegex) { for (const configKey in appNameConfig) { const config = appNameConfig[configKey]; if (config.type === "regex") { result = config.value.test(this.#env.applicationName); } else { const shouldBeProcessedBasedOnAppName = appNameConfig[this.#env.applicationName]; result = !!shouldBeProcessedBasedOnAppName; } if (result) { break; } } } else { const shouldBeProcessedBasedOnAppName = appNameConfig[this.#env.applicationName]; if (!shouldBeProcessedBasedOnAppName) { return false; } } } if (appInstanceConfig) { const shouldBeProcessedBasedOnAppInstance = appInstanceConfig[this.#env.applicationInstance]; if (!shouldBeProcessedBasedOnAppInstance) { return false; } } return result; } checkRedisEnabled() { this.#redisEnabled = !this.#disableRedis && this._checkRedisIsBound(); return this.#redisEnabled; } executeUnsubscribeHandlers(tenantId) { this.#unsubscribedTenants[tenantId] = true; setTimeout(() => delete this.#unsubscribedTenants[tenantId], DELETE_TENANT_BLOCK_AFTER_MS); for (const unsubscribeHandler of this.#unsubscribeHandlers) { try { unsubscribeHandler(tenantId); } catch (err) { this.#logger.error("could executing unsubscribe handler", err, { tenantId, }); } } } attachUnsubscribeHandler(cb) { this.#unsubscribeHandlers.push(cb); } addCAPOutboxEventBase(serviceName, config, currentEvents) { const namespace = config.namespace ?? this.#namespace; // NOTE: CAP outbox defaults are injected by cds.requires.outbox // cds.requires.queue const eventConfig = this.#sanitizeParamsAdHocEvent({ type: CAP_EVENT_TYPE, subType: serviceName, impl: "./outbox/EventQueueGenericOutboxHandler", kind: config.kind ?? "persistent-queue", ...this.#mixCAPPropertyNamesWithEventQueueNames(config), namespace, ...config, }); eventConfig.retryAttempts ??= CAP_MAX_ATTEMPTS_DEFAULT; eventConfig.internalEvent = true; this.#basicEventTransformation(eventConfig); this.#validateAdHocEvents(currentEvents, eventConfig, false); this.#basicEventTransformationAfterValidate(eventConfig); return [this.generateKey(namespace, CAP_EVENT_TYPE, serviceName), eventConfig]; } #mixCAPPropertyNamesWithEventQueueNames(config) { return this.#cleanUndefined({ selectMaxChunkSize: config.selectMaxChunkSize ?? config.chunkSize, parallelEventProcessing: config.parallelEventProcessing ?? (config.parallel && CAP_PARALLEL_DEFAULT), retryAttempts: config.retryAttempts ?? config.maxAttempts, ...config, }); } addCAPOutboxEventSpecificAction(baseServiceConfig, serviceName, actionName, currentEvents) { const subType = [serviceName, actionName].join("."); const eventConfig = this.#sanitizeParamsAdHocEvent({ ...baseServiceConfig, ...this.getCdsOutboxEventSpecificConfig(serviceName, actionName), subType, }); eventConfig.internalEvent = true; this.#basicEventTransformation(eventConfig); this.#validateAdHocEvents(currentEvents, eventConfig, false); this.#basicEventTransformationAfterValidate(eventConfig); return [this.generateKey(eventConfig.namespace, CAP_EVENT_TYPE, subType), eventConfig]; } findBaseCAPServiceWithoutNamespace(serviceName) { return Object.values(this.#eventMap).find( (event) => event.type === CAP_EVENT_TYPE && event.subType === serviceName ); } get isEventQueueActive() { return this.#isEventQueueActive; } set isEventQueueActive(value) { this.#isEventQueueActive = value; } mixFileContentWithEnv(fileContent) { fileContent.events ??= []; fileContent.periodicEvents ??= []; const events = this.#configEvents ?? {}; const periodicEvents = this.#configPeriodicEvents ?? {}; const { adHoc: adHocCAP, periodicEvents: periodicEventsCAP } = this.#cdsQueueEventsFromEnv(); fileContent.events = fileContent.events.concat(this.#mapEnvEvents(events)).concat(Object.values(adHocCAP)); fileContent.periodicEvents = fileContent.periodicEvents .concat(this.#mapEnvEvents(periodicEvents)) .concat(Object.values(periodicEventsCAP)); this.fileContent = fileContent; } #getBasicCapOutboxEventConfig(serviceName) { return Object.assign( {}, (typeof cds.requires.outbox === "object" && cds.requires.outbox) || {}, (typeof cds.requires.queue === "object" && cds.requires.queue) || {}, (typeof cds.env.requires[serviceName]?.outbox === "object" && cds.env.requires[serviceName].outbox) || {}, (typeof cds.env.requires[serviceName]?.queued === "object" && cds.env.requires[serviceName].queued) || {} ); } #cdsQueueEventsFromEnv() { return Object.entries(cds.env.requires).reduce( (result, [name, value]) => { const config = value.outbox ?? value.queued; if (!config) { return result; } // make sure service is known in general // only if not known const basicServiceConfig = this.#getBasicCapOutboxEventConfig(name); const [key, srvConfig] = this.addCAPOutboxEventBase(name, basicServiceConfig, result.adHoc); result.adHoc[key] = srvConfig; for (const fnName in config.events) { const base = { ...config }; const fnConfig = config.events[fnName]; if (fnConfig.interval || fnConfig.cron) { if ("interval" in base || "cron" in base) { this.#logger.error( "The properties interval|cron must be defined in the event section and will be ignored in the outbox section.", { serviceName: name } ); delete base.cron; delete base.interval; } const subType = `${name}.${fnName}`; result.periodicEvents[subType] = Object.assign( { type: CAP_EVENT_TYPE, subType, impl: "./outbox/EventQueueGenericOutboxHandler", internalEvent: true, }, base, fnConfig ); } else { const [key, specificEventConfig] = this.addCAPOutboxEventSpecificAction( srvConfig, name, fnName, result.adHoc ); result.adHoc[key] = specificEventConfig; } } return result; }, { adHoc: {}, periodicEvents: {} } ); } addCAPServiceWithoutEnvConfig(serviceName, srv, customOpts = {}) { const queueOptions = srv.options.queued ?? srv.options.outbox ?? {}; const basicServiceConfig = this.#getBasicCapOutboxEventConfig(serviceName); const [key, srvConfig] = this.addCAPOutboxEventBase( serviceName, { ...basicServiceConfig, ...queueOptions, ...customOpts }, this.#eventMap ); this.#eventMap[key] = srvConfig; } getCdsOutboxEventSpecificConfig(serviceName, action) { const srv = cds.env.requires[serviceName]; const config = srv?.outbox ?? srv?.queued; if (config?.events?.[action]) { return this.#mixCAPPropertyNamesWithEventQueueNames(config.events[action]); } else { return null; } } #mapEnvEvents(events) { return Object.entries(events) .map(([key, event]) => { if (!event) { return; } const [type, subType] = key.split("/"); event.type ??= type; event.subType ??= subType; return { ...event }; }) .filter((a) => a); } set fileContent(config) { config.events = config.events ?? []; config.periodicEvents = config.periodicEvents ?? []; this.#eventMap = config.events.reduce((result, event) => { const eventSanitized = this.#sanitizeParamsAdHocEvent(event); this.#basicEventTransformation(eventSanitized); this.#validateAdHocEvents(result, eventSanitized); this.#basicEventTransformationAfterValidate(eventSanitized); result[this.generateKey(eventSanitized.namespace, eventSanitized.type, eventSanitized.subType)] = eventSanitized; return result; }, {}); this.#eventMap = config.periodicEvents.reduce((result, event) => { const eventSanitized = this.#sanitizeParamsPeriodicEventEvent(event); eventSanitized.type = `${eventSanitized.type}${SUFFIX_PERIODIC}`; eventSanitized.isPeriodic = true; this.#basicEventTransformation(eventSanitized); this.#validatePeriodicConfig(result, eventSanitized); this.#basicEventTransformationAfterValidate(eventSanitized); result[this.generateKey(eventSanitized.namespace, eventSanitized.type, eventSanitized.subType)] = eventSanitized; return result; }, this.#eventMap); this.#config = config; } #basicEventTransformation(event) { event.load = event.load ?? DEFAULT_LOAD; event.priority = event.priority ?? DEFAULT_PRIORITY; event.increasePriorityOverTime = event.increasePriorityOverTime ?? DEFAULT_INCREASE_PRIORITY; event.keepAliveInterval = event.keepAliveInterval ?? DEFAULT_KEEP_ALIVE_INTERVAL; event.keepAliveMaxInProgressTime = event.keepAliveInterval * DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL; event.checkForNextChunk = event.checkForNextChunk ?? DEFAULT_CHECK_FOR_NEXT_CHUNK; event.namespace = event.namespace === undefined ? this.#namespace : event.namespace; } #sanitizeParamsBase(config, allowList) { return Object.entries(config).reduce((result, [name, value]) => { if (allowList.includes(name)) { result[name] = value; } return result; }, {}); } #sanitizeParamsAdHocEvent(config) { return this.#sanitizeParamsBase(config, ALLOWED_EVENT_OPTIONS_AD_HOC); } #sanitizeParamsPeriodicEventEvent(config) { return this.#sanitizeParamsBase(config, ALLOWED_EVENT_OPTIONS_PERIODIC_EVENT); } #basicEventTransformationAfterValidate(event) { event._appNameMap = event.appNames ? Object.fromEntries(new Map(event.appNames.map((a) => [a, this.#parseRegexOrString(a)]))) : null; event._appNameContainsRegex = event.appNames ? event.appNames.some((appName) => this.#parseRegexOrString(appName).type === "regex") : null; event._appInstancesMap = event.appInstances ? Object.fromEntries(new Map(event.appInstances.map((a) => [a, true]))) : null; event.propagateHeaders = event.propagateHeaders ?? []; event.retryFailedAfter = event.retryFailedAfter ?? DEFAULT_RETRY_AFTER; event.retryOpenAfter = event.retryOpenAfter ?? DEFAULT_RETRY_AFTER; } #basicEventValidation(event) { if (!event.impl) { throw EventQueueError.missingImpl(event.type, event.subType); } if (!event.type) { throw EventQueueError.missingType(event); } if (!event.subType) { throw EventQueueError.missingSubType(event); } if (event.appNames) { if (!Array.isArray(event.appNames) || event.appNames.some((appName) => typeof appName !== "string")) { throw EventQueueError.appNamesFormat(event.type, event.subType, event.appNames); } } if (event.appInstances) { if ( !Array.isArray(event.appInstances) || event.appInstances.some((appInstance) => typeof appInstance !== "number") ) { throw EventQueueError.appInstancesFormat(event.type, event.subType, event.appInstances); } } if (!PRIORITIES.includes(event.priority)) { throw EventQueueError.priorityNotAllowed(event.priority, "initEvent"); } if (event.load > this.#instanceLoadLimit) { throw EventQueueError.loadHigherThanLimit(event.load, "initEvent"); } } #validatePeriodicConfig(eventMap, event) { const key = this.generateKey(event.namespace, event.type, event.subType); if (eventMap[key] && eventMap[key].isPeriodic) { throw EventQueueError.duplicateEventRegistration(event.type, event.subType); } if (!event.cron && !event.interval) { throw EventQueueError.noCronOrInterval(event.type, event.subType); } if (event.cron && event.interval) { throw EventQueueError.cronAndInterval(event.type, event.subType); } if (event.cron) { let cron; // NOTE: logic is as follows: // - if event.utc is true --> always use UTC (default is false) // - if event.useCronTimezone is false OR event.cronTimezone is not defined --> use UTC as well // - if event.utc is not true AND event.cronTimezone is set AND event.useCronTimezone is NOT set to false use event.cronTimezone event.utc = event.utc ?? UTC_DEFAULT; if (!this.cronTimezone) { event.useCronTimezone = false; } else { event.useCronTimezone = event.useCronTimezone ?? USE_CRON_TZ_DEFAULT; } event.tz = event.utc || !event.useCronTimezone ? "UTC" : this.cronTimezone; try { cron = CronExpressionParser.parse(event.cron); } catch { throw EventQueueError.cantParseCronExpression(event.type, event.subType, event.cron); } const next = cron.next(); const afterNext = cron.next(); const diffInSeconds = (afterNext.getTime() - next.getTime()) / 1000; if (diffInSeconds <= MIN_INTERVAL_SEC) { throw EventQueueError.invalidIntervalBetweenCron(event.type, event.subType, diffInSeconds); } return this.#basicEventValidation(event); } if (!event.interval || event.interval <= MIN_INTERVAL_SEC) { throw EventQueueError.invalidInterval(event.type, event.subType, event.interval); } this.#basicEventValidation(event); } #validateAdHocEvents(eventMap, event, checkForDuplication = true) { const key = this.generateKey(event.namespace, event.type, event.subType); if (eventMap[key] && !eventMap[key].isPeriodic && checkForDuplication) { throw EventQueueError.duplicateEventRegistration(event.type, event.subType); } if (this.isMultiTenancy && event.multiInstanceProcessing) { throw EventQueueError.multiInstanceProcessingNotAllowed(event.type, event.subType); } event.inheritTraceContext = event.inheritTraceContext ?? DEFAULT_INHERIT_TRACE_CONTEXT; if (event.timeBucket) { try { CronExpressionParser.parse(event.timeBucket); } catch { throw EventQueueError.cantParseCronExpression(event.type, event.subType, event.timeBucket); } } this.#basicEventValidation(event); } generateKey(namespace, type, subType) { return [namespace, type, subType].join("##"); } removeEvent(type, subType, namespace = this.namespace) { delete this.#eventMap[this.generateKey(namespace, type, subType)]; } isTenantUnsubscribed(tenantId) { return this.#unsubscribedTenants[tenantId]; } shouldProcessNamespace(namespace) { return this.#processingNamespaces.includes(namespace); } get fileContent() { return this.#config; } get events() { return Object.values(this.#eventMap).filter((e) => !e.isPeriodic); } set configEvents(value) { this.#configEvents = JSON.parse(JSON.stringify(value)); } get hasConfigEvents() { return !!(Object.keys(this.#configEvents ?? {}).length || Object.keys(this.#configPeriodicEvents ?? {}).length); } set configPeriodicEvents(value) { this.#configPeriodicEvents = JSON.parse(JSON.stringify(value)); } get periodicEvents() { return Object.values(this.#eventMap).filter((e) => e.isPeriodic); } isPeriodicEvent(type, subType, namespace = this.namespace) { return this.#eventMap[this.generateKey(namespace, type, subType)]?.isPeriodic; } get allEvents() { return Object.values(this.#eventMap); } get forUpdateTimeout() { return this.#forUpdateTimeout; } get globalTxTimeout() { return this.#globalTxTimeout; } set forUpdateTimeout(value) { this.#forUpdateTimeout = value; } get crashOnRedisUnavailable() { return this.#crashOnRedisUnavailable; } set crashOnRedisUnavailable(value) { this.#crashOnRedisUnavailable = value; } get tenantIdFilterAuthContext() { return this.#tenantIdFilterAuthContextCb; } set tenantIdFilterAuthContext(value) { this.#tenantIdFilterAuthContextCb = value; } get tenantIdFilterEventProcessing() { return this.#tenantIdFilterEventProcessingCb; } set tenantIdFilterEventProcessing(value) { this.#tenantIdFilterEventProcessingCb = value; } set globalTxTimeout(value) { this.#globalTxTimeout = value; } get runInterval() { return this.#runInterval; } set runInterval(value) { if (!Number.isInteger(value) || value <= 10 * 1000) { throw EventQueueError.invalidInterval(); } this.#runInterval = value; } get redisEnabled() { return this.#redisEnabled; } set redisEnabled(value) { this.#redisEnabled = value; } get initialized() { return this.#initialized; } set initialized(value) { this.#initialized = value; } get cronTimezone() { return this.#cronTimezone; } set cronTimezone(value) { this.#cronTimezone = value; } get randomOffsetPeriodicEvents() { return this.#randomOffsetPeriodicEvents; } set randomOffsetPeriodicEvents(value) { this.#randomOffsetPeriodicEvents = value; } get instanceLoadLimit() { return this.#instanceLoadLimit; } set instanceLoadLimit(value) { this.#instanceLoadLimit = value; } get isEventBlockedCb() { return this.#isEventBlockedCb; } set isEventBlockedCb(value) { this.#isEventBlockedCb = value; } get tableNameEventQueue() { return BASE_TABLES.EVENT; } get tableNameEventLock() { return BASE_TABLES.LOCK; } set configFilePath(value) { this.#configFilePath = value; } get configFilePath() { return this.#configFilePath; } set processEventsAfterPublish(value) { this.#processEventsAfterPublish = value; } get processEventsAfterPublish() { return this.#processEventsAfterPublish; } set disableRedis(value) { this.#disableRedis = value; } get disableRedis() { return this.#disableRedis; } set updatePeriodicEvents(value) { this.#updatePeriodicEvents = value; } get updatePeriodicEvents() { return this.#updatePeriodicEvents; } set registerAsEventProcessor(value) { this.#registerAsEventProcessor = value; } get registerAsEventProcessor() { return this.#registerAsEventProcessor; } set thresholdLoggingEventProcessing(value) { this.#thresholdLoggingEventProcessing = value; } get thresholdLoggingEventProcessing() { return this.#thresholdLoggingEventProcessing; } set useAsCAPOutbox(value) { if (this.#useAsCAPOutbox) { return; } this.#useAsCAPOutbox = value; } get useAsCAPOutbox() { return this.#useAsCAPOutbox; } set useAsCAPQueue(value) { this.useAsCAPOutbox = value; } get useAsCAPQueue() { return this.useAsCAPOutbox; } set userId(value) { this.#userId = value; } get userId() { return this.#userId; } set cleanupLocksAndEventsForDev(value) { this.#cleanupLocksAndEventsForDev = value; } get cleanupLocksAndEventsForDev() { return this.#cleanupLocksAndEventsForDev; } set redisOptions(value) { this.#redisOptions = value; } get redisOptions() { return { ...this.#redisOptions, }; } redisNamespace(addPublishNamespace = true) { return addPublishNamespace ? `${[REDIS_PREFIX, this.#namespace].filter((a) => a).join("##")}` : REDIS_PREFIX; } set insertEventsBeforeCommit(value) { this.#insertEventsBeforeCommit = value; } get insertEventsBeforeCommit() { return this.#insertEventsBeforeCommit; } set enableTelemetry(value) { this.#enableTelemetry = value; } get enableTelemetry() { return this.#enableTelemetry; } get isMultiTenancy() { return !!cds.requires.multitenancy; } get _rawEventMap() { return this.#eventMap; } get developmentMode() { return cds.env.profiles.find((profile) => profile === "development"); } get enableAdminService() { return this.#enableAdminService; } set enableAdminService(value) { this.#enableAdminService = value; } get disableProcessingOfSuspendedTenants() { return this.#disableProcessingOfSuspendedTenants; } set disableProcessingOfSuspendedTenants(value) { this.#disableProcessingOfSuspendedTenants = value; } get namespace() { return this.#namespace; } set namespace(value) { this.#namespace = value; } get processingNamespaces() { return this.#processingNamespaces; } set processingNamespaces(value) { this.#processingNamespaces = value; } #cleanUndefined(input) { return Object.entries(input).reduce((acc, [key, value]) => { if (value !== undefined) { acc[key] = value; } return acc; }, {}); } /** @return { Config } **/ static get instance() { if (!Config.#instance) { Config.#instance = new Config(); } return Config.#instance; } } const instance = Config.instance; module.exports = instance;