@cap-js-community/event-queue 1.8.3 → 1.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "1.8.3",
3
+ "version": "1.8.5",
4
4
  "description": "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.",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -546,6 +546,16 @@ class EventQueueProcessorBase {
546
546
  });
547
547
  }
548
548
 
549
+ handleErrorTx(error) {
550
+ this.logger.error("Error in commit|rollback transaction, check handlers and constraints!", error, {
551
+ eventType: this.#eventType,
552
+ eventSubType: this.#eventSubType,
553
+ });
554
+ this.__queueEntries.forEach((queueEntry) => {
555
+ this.#determineAndAddEventStatusToMap(queueEntry.ID, EventProcessingStatus.Error);
556
+ });
557
+ }
558
+
549
559
  handleInvalidPayloadReturned(queueEntry) {
550
560
  this.logger.error(
551
561
  "Undefined payload is not allowed. If status should be done, nulls needs to be returned" +
package/src/config.js CHANGED
@@ -58,7 +58,6 @@ class Config {
58
58
  #isEventQueueActive;
59
59
  #configFilePath;
60
60
  #processEventsAfterPublish;
61
- #skipCsnCheck;
62
61
  #registerAsEventProcessor;
63
62
  #disableRedis;
64
63
  #env;
@@ -78,7 +77,8 @@ class Config {
78
77
  #cronTimezone;
79
78
  #publishEventBlockList;
80
79
  #crashOnRedisUnavailable;
81
- #tenantIdFilterCb;
80
+ #tenantIdFilterTokenInfoCb;
81
+ #tenantIdFilterEventProcessingCb;
82
82
  static #instance;
83
83
  constructor() {
84
84
  this.#logger = cds.log(COMPONENT_NAME);
@@ -94,7 +94,6 @@ class Config {
94
94
  this.#isEventQueueActive = true;
95
95
  this.#configFilePath = null;
96
96
  this.#processEventsAfterPublish = null;
97
- this.#skipCsnCheck = null;
98
97
  this.#disableRedis = null;
99
98
  this.#env = getEnvInstance();
100
99
  this.#blockedEvents = {};
@@ -293,7 +292,10 @@ class Config {
293
292
 
294
293
  addCAPOutboxEvent(serviceName, config) {
295
294
  if (this.#eventMap[this.generateKey(CAP_EVENT_TYPE, serviceName)]) {
296
- return;
295
+ const index = this.#config.events.findIndex(
296
+ (event) => event.type === CAP_EVENT_TYPE && event.subType === serviceName
297
+ );
298
+ this.#config.events.splice(index, 1);
297
299
  }
298
300
 
299
301
  const eventConfig = {
@@ -312,8 +314,12 @@ class Config {
312
314
  appNames: config.appNames,
313
315
  appInstances: config.appInstances,
314
316
  useEventQueueUser: config.useEventQueueUser,
317
+ retryFailedAfter: config.retryFailedAfter,
318
+ priority: config.priority,
319
+ multiInstanceProcessing: config.multiInstanceProcessing,
315
320
  internalEvent: true,
316
321
  };
322
+
317
323
  this.#basicEventTransformationAfterValidate(eventConfig);
318
324
  this.#config.events.push(eventConfig);
319
325
  this.#eventMap[this.generateKey(CAP_EVENT_TYPE, serviceName)] = eventConfig;
@@ -530,12 +536,20 @@ class Config {
530
536
  this.#crashOnRedisUnavailable = value;
531
537
  }
532
538
 
533
- get tenantIdFilterCb() {
534
- return this.#tenantIdFilterCb;
539
+ get tenantIdFilterTokenInfo() {
540
+ return this.#tenantIdFilterTokenInfoCb;
535
541
  }
536
542
 
537
- set tenantIdFilterCb(value) {
538
- this.#tenantIdFilterCb = value;
543
+ set tenantIdFilterTokenInfo(value) {
544
+ this.#tenantIdFilterTokenInfoCb = value;
545
+ }
546
+
547
+ get tenantIdFilterEventProcessing() {
548
+ return this.#tenantIdFilterEventProcessingCb;
549
+ }
550
+
551
+ set tenantIdFilterEventProcessing(value) {
552
+ this.#tenantIdFilterEventProcessingCb = value;
539
553
  }
540
554
 
541
555
  set globalTxTimeout(value) {
@@ -617,14 +631,6 @@ class Config {
617
631
  return this.#processEventsAfterPublish;
618
632
  }
619
633
 
620
- set skipCsnCheck(value) {
621
- this.#skipCsnCheck = value;
622
- }
623
-
624
- get skipCsnCheck() {
625
- return this.#skipCsnCheck;
626
- }
627
-
628
634
  set disableRedis(value) {
629
635
  this.#disableRedis = value;
630
636
  }
package/src/constants.js CHANGED
@@ -21,7 +21,7 @@ module.exports = {
21
21
  VeryHigh: "veryHigh",
22
22
  },
23
23
  TenantIdCheckTypes: {
24
- getAllTenantIds: "getAllTenantIds",
24
+ eventProcessing: "eventProcessing",
25
25
  getTokenInfo: "getTokenInfo",
26
26
  },
27
27
  };
package/src/index.d.ts CHANGED
@@ -12,7 +12,7 @@ declare type EventProcessingStatusKeysType = keyof typeof EventProcessingStatus;
12
12
  export declare type EventProcessingStatusType = (typeof EventProcessingStatus)[EventProcessingStatusKeysType];
13
13
 
14
14
  export declare const TenantIdCheckTypes: {
15
- getAllTenantIds: "getAllTenantIds";
15
+ eventProcessing: "eventProcessing";
16
16
  getTokenInfo: "getTokenInfo";
17
17
  };
18
18
 
@@ -182,54 +182,63 @@ export function triggerEventProcessingRedis(
182
182
  declare class Config {
183
183
  constructor();
184
184
 
185
- getEventConfig(type: string, subType: string): any;
186
- isCapOutboxEvent(type: string): boolean;
187
- hasEventAfterCommitFlag(type: string, subType: string): boolean;
188
- _checkRedisIsBound(): boolean;
189
- checkRedisEnabled(): boolean;
190
- publishEventBlockList(): boolean;
191
- crashOnRedisUnavailable(): boolean;
185
+ getEventConfig(type: any, subType: any): any;
186
+ isCapOutboxEvent(type: any): boolean;
187
+ hasEventAfterCommitFlag(type: any, subType: any): any;
188
+ shouldBeProcessedInThisApplication(type: any, subType: any): boolean;
189
+ checkRedisEnabled(): any;
192
190
  attachConfigChangeHandler(): void;
193
191
  attachRedisUnsubscribeHandler(): void;
194
- executeUnsubscribeHandlers(tenantId: string): void;
195
- handleUnsubscribe(tenantId: string): void;
196
- attachUnsubscribeHandler(cb: Function): void;
197
- publishConfigChange(key: string, value: any): void;
198
- blockEvent(type: string, subType: string, isPeriodic: boolean, tenant?: string): void;
192
+ executeUnsubscribeHandlers(tenantId: any): void;
193
+ handleUnsubscribe(tenantId: any): void;
194
+ attachUnsubscribeHandler(cb: any): void;
195
+ publishConfigChange(key: any, value: any): void;
196
+ blockEvent(type: any, subType: any, isPeriodic: any, tenant?: string): void;
199
197
  clearPeriodicEventBlockList(): void;
200
- unblockEvent(type: string, subType: string, isPeriodic: boolean, tenant?: string): void;
201
- addCAPOutboxEvent(serviceName: string, config: any): void;
202
- isEventBlocked(type: string, subType: string, isPeriodicEvent: boolean, tenant: string): boolean;
203
- get isEventQueueActive(): boolean;
198
+ unblockEvent(type: any, subType: any, isPeriodic: any, tenant?: string): void;
199
+ addCAPOutboxEvent(serviceName: any, config: any): void;
200
+ isEventBlocked(type: any, subType: any, isPeriodicEvent: any, tenant: any): any;
204
201
  set isEventQueueActive(value: boolean);
202
+ get isEventQueueActive(): boolean;
205
203
  set fileContent(config: any);
206
204
  get fileContent(): any;
207
- get events(): any[];
208
- get periodicEvents(): any[];
209
- isPeriodicEvent(type: string, subType: string): boolean;
210
- get allEvents(): any[];
211
- get forUpdateTimeout(): number;
212
- get globalTxTimeout(): number;
205
+ generateKey(type: any, subType: any): string;
206
+ removeEvent(type: any, subType: any): void;
207
+ isTenantUnsubscribed(tenantId: any): any;
208
+ get events(): any;
209
+ get periodicEvents(): any;
210
+ isPeriodicEvent(type: any, subType: any): any;
211
+ get allEvents(): any;
213
212
  set forUpdateTimeout(value: number);
213
+ get forUpdateTimeout(): number;
214
214
  set globalTxTimeout(value: number);
215
- get runInterval(): number | null;
216
- set runInterval(value: number);
217
- get redisEnabled(): boolean | null;
218
- set redisEnabled(value: boolean | null);
219
- get initialized(): boolean;
215
+ get globalTxTimeout(): number;
216
+ set publishEventBlockList(value: any);
217
+ get publishEventBlockList(): any;
218
+ set crashOnRedisUnavailable(value: any);
219
+ get crashOnRedisUnavailable(): any;
220
+ set tenantIdFilterTokenInfo(value: any);
221
+ get tenantIdFilterTokenInfo(): any;
222
+ set tenantIdFilterEventProcessing(value: any);
223
+ get tenantIdFilterEventProcessing(): any;
224
+ set runInterval(value: any);
225
+ get runInterval(): any;
226
+ set redisEnabled(value: any);
227
+ get redisEnabled(): any;
220
228
  set initialized(value: boolean);
221
- get instanceLoadLimit(): number;
229
+ get initialized(): boolean;
230
+ set cronTimezone(value: any);
231
+ get cronTimezone(): any;
222
232
  set instanceLoadLimit(value: number);
223
- get isEventBlockedCb(): any;
233
+ get instanceLoadLimit(): number;
224
234
  set isEventBlockedCb(value: any);
235
+ get isEventBlockedCb(): any;
225
236
  get tableNameEventQueue(): string;
226
237
  get tableNameEventLock(): string;
227
- set configFilePath(value: string | null);
228
- get configFilePath(): string | null;
238
+ set configFilePath(value: any);
239
+ get configFilePath(): any;
229
240
  set processEventsAfterPublish(value: any);
230
241
  get processEventsAfterPublish(): any;
231
- set skipCsnCheck(value: any);
232
- get skipCsnCheck(): any;
233
242
  set disableRedis(value: any);
234
243
  get disableRedis(): any;
235
244
  set updatePeriodicEvents(value: any);
@@ -248,6 +257,8 @@ declare class Config {
248
257
  get redisOptions(): any;
249
258
  set insertEventsBeforeCommit(value: any);
250
259
  get insertEventsBeforeCommit(): any;
260
+ set enableCAPTelemetry(value: any);
261
+ get enableCAPTelemetry(): any;
251
262
  get isMultiTenancy(): boolean;
252
263
  }
253
264
 
package/src/initialize.js CHANGED
@@ -42,7 +42,6 @@ const CONFIG_VARS = [
42
42
  ["cronTimezone", null],
43
43
  ["publishEventBlockList", true],
44
44
  ["crashOnRedisUnavailable", false],
45
- ["tenantIdFilterCb", null],
46
45
  ];
47
46
 
48
47
  /**
@@ -66,7 +65,6 @@ const CONFIG_VARS = [
66
65
  * @param {string} [options.cronTimezone=null] - Default timezone for cron jobs.
67
66
  * @param {string} [options.publishEventBlockList=true] - If redis is available event blocklist is distributed to all application instances
68
67
  * @param {string} [options.crashOnRedisUnavailable=true] - If enabled an error is thrown if the redis connection check is not successful
69
- * @param {function} [options.tenantIdFilterCb=null] - Allows to set customer filter function to filter the tenants ids which should be processed in the event-queue
70
68
  */
71
69
  const initialize = async (options = {}) => {
72
70
  if (config.initialized) {
@@ -188,7 +186,7 @@ const mixConfigVarsWithEnv = (options) => {
188
186
 
189
187
  const registerCdsShutdown = () => {
190
188
  const isTestProfile = cds.env.profiles.find((profile) => profile.includes("test"));
191
- if (isTestProfile) {
189
+ if (isTestProfile || !config.redisEnabled) {
192
190
  return;
193
191
  }
194
192
  cds.on("shutdown", async () => {
@@ -12,7 +12,13 @@ const COMPONENT_NAME = "/eventQueue/eventQueueAsOutbox";
12
12
  const EVENT_QUEUE_SPECIFIC_FIELDS = ["startAfter", "referenceEntity", "referenceEntityKey"];
13
13
 
14
14
  function outboxed(srv, customOpts) {
15
- // outbox max. once
15
+ if (!(new.target || customOpts)) {
16
+ const former = srv[OUTBOXED];
17
+ if (former) {
18
+ return former;
19
+ }
20
+ }
21
+
16
22
  const logger = cds.log(COMPONENT_NAME);
17
23
  const outboxOpts = Object.assign(
18
24
  {},
@@ -21,14 +27,8 @@ function outboxed(srv, customOpts) {
21
27
  customOpts || {}
22
28
  );
23
29
 
24
- if (!new.target) {
25
- const former = srv[OUTBOXED];
26
- if (former) {
27
- if (outboxOpts.kind === "persistent-outbox") {
28
- config.addCAPOutboxEvent(srv.name, outboxOpts);
29
- }
30
- return former;
31
- }
30
+ if (outboxOpts.kind === "persistent-outbox") {
31
+ config.addCAPOutboxEvent(srv.name, outboxOpts);
32
32
  }
33
33
 
34
34
  const originalSrv = srv[UNBOXED] || srv;
@@ -36,11 +36,9 @@ function outboxed(srv, customOpts) {
36
36
  outboxedSrv[UNBOXED] = originalSrv;
37
37
 
38
38
  if (!new.target) {
39
- Object.defineProperty(srv, OUTBOXED, { value: outboxedSrv });
40
- }
41
-
42
- if (outboxOpts.kind === "persistent-outbox") {
43
- config.addCAPOutboxEvent(srv.name, outboxOpts);
39
+ if (!srv[OUTBOXED]) {
40
+ Object.defineProperty(srv, OUTBOXED, { value: outboxedSrv });
41
+ }
44
42
  }
45
43
  outboxedSrv.handle = async function (req) {
46
44
  const context = req.context || cds.context;
@@ -201,56 +201,67 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
201
201
  }
202
202
  };
203
203
 
204
- const processEventMap = async (eventTypeInstance) => {
205
- eventTypeInstance.startPerformanceTracerEvents();
206
- await eventTypeInstance.beforeProcessingEvents();
207
- eventTypeInstance.logStartMessage();
208
- if (eventTypeInstance.commitOnEventLevel) {
209
- eventTypeInstance.txUsageAllowed = false;
204
+ const processEventMap = async (instance) => {
205
+ instance.startPerformanceTracerEvents();
206
+ await instance.beforeProcessingEvents();
207
+ instance.logStartMessage();
208
+ if (instance.commitOnEventLevel) {
209
+ instance.txUsageAllowed = false;
210
210
  }
211
211
  await limiter(
212
- eventTypeInstance.parallelEventProcessing,
213
- Object.entries(eventTypeInstance.eventProcessingMap),
212
+ instance.parallelEventProcessing,
213
+ Object.entries(instance.eventProcessingMap),
214
214
  async ([key, { queueEntries, payload }]) => {
215
- if (eventTypeInstance.commitOnEventLevel) {
215
+ if (instance.commitOnEventLevel) {
216
216
  let statusMap;
217
217
  await executeInNewTransaction(
218
- eventTypeInstance.baseContext,
219
- `eventQueue-processEvent-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
218
+ instance.baseContext,
219
+ `eventQueue-processEvent-${instance.eventType}##${instance.eventSubType}`,
220
220
  async (tx) => {
221
- statusMap = await _processEvent(eventTypeInstance, tx.context, key, queueEntries, payload);
222
- if (
223
- eventTypeInstance.statusMapContainsError(statusMap) ||
224
- eventTypeInstance.shouldRollbackTransaction(key)
225
- ) {
221
+ statusMap = await _processEvent(instance, tx.context, key, queueEntries, payload);
222
+ const shouldRollback =
223
+ instance.statusMapContainsError(statusMap) || instance.shouldRollbackTransaction(key);
224
+ if (shouldRollback) {
226
225
  await tx.rollback();
226
+ await _commitStatusInNewTx(instance, statusMap);
227
+ } else {
228
+ await instance.persistEventStatus(tx, {
229
+ skipChecks: true,
230
+ statusMap,
231
+ });
227
232
  }
228
233
  }
229
234
  );
230
- await executeInNewTransaction(
231
- eventTypeInstance.baseContext,
232
- `eventQueue-persistStatus-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
233
- async (tx) => {
234
- eventTypeInstance.processEventContext = tx.context;
235
- await eventTypeInstance.persistEventStatus(tx, {
236
- skipChecks: true,
237
- statusMap,
238
- });
239
- }
240
- );
241
235
  } else {
242
- await _processEvent(eventTypeInstance, eventTypeInstance.context, key, queueEntries, payload);
236
+ await _processEvent(instance, instance.context, key, queueEntries, payload);
243
237
  }
244
238
  }
245
- ).finally(() => {
246
- eventTypeInstance.clearEventProcessingContext();
247
- if (eventTypeInstance.commitOnEventLevel) {
248
- eventTypeInstance.txUsageAllowed = true;
249
- }
250
- });
251
- eventTypeInstance.endPerformanceTracerEvents();
239
+ )
240
+ .catch((err) => {
241
+ instance.handleErrorTx(err);
242
+ })
243
+ .finally(() => {
244
+ instance.clearEventProcessingContext();
245
+ if (instance.commitOnEventLevel) {
246
+ instance.txUsageAllowed = true;
247
+ }
248
+ });
249
+ instance.endPerformanceTracerEvents();
252
250
  };
253
251
 
252
+ const _commitStatusInNewTx = async (eventTypeInstance, statusMap) =>
253
+ await executeInNewTransaction(
254
+ eventTypeInstance.baseContext,
255
+ `eventQueue-persistStatus-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
256
+ async (tx) => {
257
+ eventTypeInstance.processEventContext = tx.context;
258
+ await eventTypeInstance.persistEventStatus(tx, {
259
+ skipChecks: true,
260
+ statusMap,
261
+ });
262
+ }
263
+ );
264
+
254
265
  const _checkEventIsBlocked = async (baseInstance) => {
255
266
  const isEventBlockedCb = config.isEventBlockedCb;
256
267
  let eventBlocked;
@@ -10,6 +10,7 @@ const config = require("../config");
10
10
  const common = require("../shared/common");
11
11
  const { runEventCombinationForTenant } = require("../runner/runnerHelper");
12
12
  const trace = require("../shared/openTelemetry");
13
+ const { TenantIdCheckTypes } = require("../constants");
13
14
 
14
15
  const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
15
16
  const COMPONENT_NAME = "/eventQueue/redisPub";
@@ -59,6 +60,10 @@ const broadcastEvent = async (tenantId, events, forceBroadcast = false) => {
59
60
  events = Array.isArray(events) ? events : [events];
60
61
  try {
61
62
  if (!config.redisEnabled) {
63
+ const tenantShouldBeProcessed = await common.isTenantIdValidCb(TenantIdCheckTypes.eventProcessing, tenantId);
64
+ if (!tenantShouldBeProcessed) {
65
+ return;
66
+ }
62
67
  await _processLocalWithoutRedis(tenantId, events);
63
68
  return;
64
69
  }
@@ -66,6 +71,9 @@ const broadcastEvent = async (tenantId, events, forceBroadcast = false) => {
66
71
  await trace(context, "broadcast-inserted-events", async () => {
67
72
  for (const { type, subType } of events) {
68
73
  const eventConfig = config.getEventConfig(type, subType);
74
+ if (!eventConfig) {
75
+ continue;
76
+ }
69
77
  for (let i = 0; i < TRIES_FOR_PUBLISH_PERIODIC_EVENT; i++) {
70
78
  const result = eventConfig.multiInstanceProcessing
71
79
  ? false
@@ -6,6 +6,7 @@ const redis = require("../shared/redis");
6
6
  const config = require("../config");
7
7
  const runnerHelper = require("../runner/runnerHelper");
8
8
  const common = require("../shared/common");
9
+ const { TenantIdCheckTypes } = require("../constants");
9
10
 
10
11
  const EVENT_MESSAGE_CHANNEL = "EVENT_QUEUE_MESSAGE_CHANNEL";
11
12
  const COMPONENT_NAME = "/eventQueue/redisSub";
@@ -22,6 +23,10 @@ const _messageHandlerProcessEvents = async (messageData) => {
22
23
  const logger = cds.log(COMPONENT_NAME);
23
24
  try {
24
25
  const { lockId, tenantId, type, subType } = JSON.parse(messageData);
26
+ const tenantShouldBeProcessed = await common.isTenantIdValidCb(TenantIdCheckTypes.eventProcessing, tenantId);
27
+ if (!tenantShouldBeProcessed) {
28
+ return;
29
+ }
25
30
  logger.debug("received redis event", {
26
31
  tenantId,
27
32
  type,
@@ -111,17 +111,19 @@ const _executeEventsAllTenantsRedis = async (tenantIds) => {
111
111
  // NOTE: do checks for all tenants on the same app instance --> acquire lock tenant independent
112
112
  // distribute from this instance to all others
113
113
  const dummyContext = new cds.EventContext({});
114
- const couldAcquireLock = await trace(
115
- dummyContext,
116
- "acquire-lock-master-runner",
117
- async () => {
118
- return await distributedLock.acquireLock(dummyContext, EVENT_QUEUE_RUN_REDIS_CHECK, {
119
- expiryTime: eventQueueConfig.runInterval * 0.95,
120
- tenantScoped: false,
121
- });
122
- },
123
- { newRootSpan: true }
124
- );
114
+ const couldAcquireLock = config.tenantIdFilterEventProcessing
115
+ ? true
116
+ : await trace(
117
+ dummyContext,
118
+ "acquire-lock-master-runner",
119
+ async () => {
120
+ return await distributedLock.acquireLock(dummyContext, EVENT_QUEUE_RUN_REDIS_CHECK, {
121
+ expiryTime: eventQueueConfig.runInterval * 0.95,
122
+ tenantScoped: false,
123
+ });
124
+ },
125
+ { newRootSpan: true }
126
+ );
125
127
  if (!couldAcquireLock) {
126
128
  return;
127
129
  }
@@ -237,7 +239,7 @@ const _executePeriodicEventsAllTenants = async (tenantIds) => {
237
239
  };
238
240
  await cds.tx(tenantContext, async ({ context }) => {
239
241
  await trace(tenantContext, "update-periodic-events-for-tenant", async () => {
240
- if (!config.redisEnabled) {
242
+ if (!config.redisEnabled && !config.tenantIdFilterEventProcessing) {
241
243
  const couldAcquireLock = await distributedLock.acquireLock(context, EVENT_QUEUE_UPDATE_PERIODIC_EVENTS, {
242
244
  expiryTime: eventQueueConfig.runInterval * 0.95,
243
245
  });
@@ -433,7 +435,7 @@ const _multiTenancyPeriodicEvents = async (tenantIds) => {
433
435
  dummyContext,
434
436
  "update-periodic-events",
435
437
  async () => {
436
- if (config.redisEnabled) {
438
+ if (config.redisEnabled && !config.tenantIdFilterEventProcessing) {
437
439
  const couldAcquireLock = await distributedLock.acquireLock(dummyContext, EVENT_QUEUE_UPDATE_PERIODIC_EVENTS, {
438
440
  expiryTime: 60 * 1000, // short living lock --> assume we do not have 2 onboards within 1 minute
439
441
  tenantScoped: false,
@@ -23,6 +23,7 @@ const COMPONENT_NAME = "/eventQueue/cdsHelper";
23
23
  async function executeInNewTransaction(context = {}, transactionTag, fn, args, { info = {} } = {}) {
24
24
  const parameters = Array.isArray(args) ? args : [args];
25
25
  const logger = cds.log(COMPONENT_NAME);
26
+ let transactionRollbackPromise = Promise.resolve(false);
26
27
  try {
27
28
  const user = new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(context.tenant) });
28
29
  if (cds.db.kind === "hana") {
@@ -36,7 +37,15 @@ async function executeInNewTransaction(context = {}, transactionTag, fn, args, {
36
37
  },
37
38
  async (tx) => {
38
39
  tx.context._ = context._ ?? {};
39
- return await fn(tx, ...parameters);
40
+ return new Promise((outerResolve, outerReject) => {
41
+ transactionRollbackPromise = new Promise((resolve) => {
42
+ tx.context.on("succeeded", () => resolve(false));
43
+ tx.context.on("failed", () => resolve(true));
44
+ fn(tx, ...parameters)
45
+ .then(outerResolve)
46
+ .catch(outerReject);
47
+ });
48
+ });
40
49
  }
41
50
  );
42
51
  } else {
@@ -51,7 +60,10 @@ async function executeInNewTransaction(context = {}, transactionTag, fn, args, {
51
60
  user,
52
61
  headers: context.headers,
53
62
  },
54
- async (tx) => fn(tx, ...parameters)
63
+ async (tx) => {
64
+ await fn(tx, ...parameters);
65
+ transactionRollbackPromise = false;
66
+ }
55
67
  );
56
68
  } else {
57
69
  contextTx.context.user = user;
@@ -68,17 +80,24 @@ async function executeInNewTransaction(context = {}, transactionTag, fn, args, {
68
80
  // change rollback to no opt - closing tx would cause follow-up usage to fail.
69
81
  // the process that opened the tx needs to manage it
70
82
  };
71
- await fn(contextTx, ...parameters).finally(() => (contextTx.rollback = txRollback));
83
+ await fn(contextTx, ...parameters)
84
+ .then(() => (transactionRollbackPromise = false))
85
+ .finally(() => (contextTx.rollback = txRollback));
72
86
  }
73
87
  }
74
88
  } catch (err) {
89
+ const transactionRollback = await transactionRollbackPromise;
75
90
  if (err instanceof VError) {
76
91
  Object.assign(err.jse_info, {
77
92
  newTx: info,
78
93
  });
79
- throw err;
94
+ if (transactionRollback) {
95
+ throw err;
96
+ } else {
97
+ logger.error("business transaction commited but succeeded|done|failed threw a error!", err);
98
+ }
80
99
  } else {
81
- throw new VError(
100
+ const nestedError = new VError(
82
101
  {
83
102
  name: VERROR_CLUSTER_NAME,
84
103
  cause: err,
@@ -86,6 +105,11 @@ async function executeInNewTransaction(context = {}, transactionTag, fn, args, {
86
105
  },
87
106
  "Execution in new transaction failed"
88
107
  );
108
+ if (transactionRollback) {
109
+ throw err;
110
+ } else {
111
+ logger.error("business transaction commited but succeeded|done|failed threw a error!", nestedError);
112
+ }
89
113
  }
90
114
  } finally {
91
115
  logger.debug("Execution in new transaction finished", info);
@@ -109,7 +133,13 @@ const getAllTenantIds = async () => {
109
133
  const response = await ssp.get("/tenant");
110
134
  return response
111
135
  .map((tenant) => tenant.subscribedTenantId ?? tenant.tenant)
112
- .filter((tenantId) => common.isTenantIdValidCb(TenantIdCheckTypes.getAllTenantIds, tenantId));
136
+ .reduce(async (result, tenantId) => {
137
+ result = await result;
138
+ if (await common.isTenantIdValidCb(TenantIdCheckTypes.eventProcessing, tenantId)) {
139
+ result.push(tenantId);
140
+ }
141
+ return result;
142
+ }, []);
113
143
  };
114
144
 
115
145
  module.exports = {
@@ -4,6 +4,8 @@ const crypto = require("crypto");
4
4
 
5
5
  const cds = require("@sap/cds");
6
6
  const xssec = require("@sap/xssec");
7
+ const VError = require("verror");
8
+
7
9
  const config = require("../config");
8
10
  const { TenantIdCheckTypes } = require("../constants");
9
11
 
@@ -44,7 +46,22 @@ const limiter = async (limit, payloads, iterator) => {
44
46
  }
45
47
  }
46
48
  }
47
- return Promise.allSettled(returnPromises);
49
+ return promiseAllDone(returnPromises);
50
+ };
51
+
52
+ const promiseAllDone = async (iterable) => {
53
+ const results = await Promise.allSettled(iterable);
54
+ const rejects = results.filter((entry) => {
55
+ return entry.status === "rejected";
56
+ });
57
+ if (rejects.length === 1) {
58
+ return Promise.reject(rejects[0].reason);
59
+ } else if (rejects.length > 1) {
60
+ return Promise.reject(new VError.MultiError(rejects.map((reject) => reject.reason)));
61
+ }
62
+ return results.map((entry) => {
63
+ return entry.value;
64
+ });
48
65
  };
49
66
 
50
67
  const isValidDate = (value) => {
@@ -115,10 +132,23 @@ const getTokenInfo = async (tenantId) => {
115
132
  return await tokenInfoCache[tenantId].value;
116
133
  };
117
134
 
118
- const isTenantIdValidCb = (checkType, tenantId) => {
119
- if (config.tenantIdFilterCb) {
120
- return config.tenantIdFilterCb(checkType, tenantId);
121
- } else {
135
+ const isTenantIdValidCb = async (checkType, tenantId) => {
136
+ let cb;
137
+ switch (checkType) {
138
+ case TenantIdCheckTypes.getTokenInfo:
139
+ cb = config.tenantIdFilterTokenInfo;
140
+ break;
141
+ case TenantIdCheckTypes.eventProcessing:
142
+ cb = config.tenantIdFilterEventProcessing;
143
+ break;
144
+ default:
145
+ cb = async () => true;
146
+ }
147
+
148
+ try {
149
+ return cb ? await cb(tenantId) : true;
150
+ } catch (err) {
151
+ cds.log(COMPONENT_NAME).error("failed in custom tenant id filter callback. Returning true.", err);
122
152
  return true;
123
153
  }
124
154
  };
@@ -131,6 +161,7 @@ module.exports = {
131
161
  hashStringTo32Bit,
132
162
  getTokenInfo,
133
163
  isTenantIdValidCb,
164
+ promiseAllDone,
134
165
  __: {
135
166
  clearTokenInfoCache: () => (getTokenInfo._tokenInfoCache = {}),
136
167
  },