@cap-js-community/event-queue 1.9.0-beta.2 → 1.9.0

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.9.0-beta.2",
3
+ "version": "1.9.0",
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",
@@ -79,6 +79,17 @@
79
79
  "registerAsEventProcessor": false,
80
80
  "updatePeriodicEvents": false,
81
81
  "insertEventsBeforeCommit": false
82
+ },
83
+ "periodicEvents": {
84
+ "[production]": {
85
+ "EVENT_QUEUE_BASE/DELETE_EVENTS": {
86
+ "priority": "low",
87
+ "impl": "./housekeeping/EventQueueDeleteEvents",
88
+ "load": 20,
89
+ "interval": 86400,
90
+ "internalEvent": true
91
+ }
92
+ }
82
93
  }
83
94
  },
84
95
  "requires": {
@@ -18,6 +18,8 @@ const ERROR_CODES = {
18
18
  NO_INTERVAL_OR_CRON: "NO_INTERVAL_OR_CRON",
19
19
  INTERVAL_AND_CRON: "INTERVAL_AND_CRON",
20
20
  MISSING_IMPL: "MISSING_IMPL",
21
+ MISSING_TYPE: "MISSING_TYPE",
22
+ MISSING_SUBTYPE: "MISSING_SUBTYPE",
21
23
  DUPLICATE_EVENT_REGISTRATION: "DUPLICATE_EVENT_REGISTRATION",
22
24
  NO_MANUEL_INSERT_OF_PERIODIC: "NO_MANUEL_INSERT_OF_PERIODIC",
23
25
  LOAD_HIGHER_THAN_LIMIT: "LOAD_HIGHER_THAN_LIMIT",
@@ -62,6 +64,12 @@ const ERROR_CODES_META = {
62
64
  [ERROR_CODES.MISSING_IMPL]: {
63
65
  message: "Missing path to event class implementation.",
64
66
  },
67
+ [ERROR_CODES.MISSING_TYPE]: {
68
+ message: "Missing type event implementation.",
69
+ },
70
+ [ERROR_CODES.MISSING_SUBTYPE]: {
71
+ message: "Missing subtype event implementation.",
72
+ },
65
73
  [ERROR_CODES.DUPLICATE_EVENT_REGISTRATION]: {
66
74
  message: "Duplicate event registration, check the uniqueness of type and subType.",
67
75
  },
@@ -265,6 +273,28 @@ class EventQueueError extends VError {
265
273
  );
266
274
  }
267
275
 
276
+ static missingType(config) {
277
+ const { message } = ERROR_CODES_META[ERROR_CODES.MISSING_TYPE];
278
+ return new EventQueueError(
279
+ {
280
+ name: ERROR_CODES.MISSING_TYPE,
281
+ info: { config },
282
+ },
283
+ message
284
+ );
285
+ }
286
+
287
+ static missingSubType(config) {
288
+ const { message } = ERROR_CODES_META[ERROR_CODES.MISSING_SUBTYPE];
289
+ return new EventQueueError(
290
+ {
291
+ name: ERROR_CODES.MISSING_SUBTYPE,
292
+ info: { config },
293
+ },
294
+ message
295
+ );
296
+ }
297
+
268
298
  static duplicateEventRegistration(type, subType) {
269
299
  const { message } = ERROR_CODES_META[ERROR_CODES.DUPLICATE_EVENT_REGISTRATION];
270
300
  return new EventQueueError(
package/src/config.js CHANGED
@@ -30,18 +30,6 @@ const PRIORITIES = Object.values(Priorities);
30
30
  const UTC_DEFAULT = false;
31
31
  const USE_CRON_TZ_DEFAULT = true;
32
32
 
33
- const BASE_PERIODIC_EVENTS = [
34
- {
35
- type: "EVENT_QUEUE_BASE",
36
- subType: "DELETE_EVENTS",
37
- priority: Priorities.Low,
38
- impl: "./housekeeping/EventQueueDeleteEvents",
39
- load: 20,
40
- interval: 86400, // 1 day,
41
- internalEvent: true,
42
- },
43
- ];
44
-
45
33
  const BASE_TABLES = {
46
34
  EVENT: "sap.eventqueue.Event",
47
35
  LOCK: "sap.eventqueue.Lock",
@@ -82,6 +70,8 @@ class Config {
82
70
  #crashOnRedisUnavailable;
83
71
  #tenantIdFilterTokenInfoCb;
84
72
  #tenantIdFilterEventProcessingCb;
73
+ #configEvents;
74
+ #configPeriodicEvents;
85
75
  static #instance;
86
76
  constructor() {
87
77
  this.#logger = cds.log(COMPONENT_NAME);
@@ -357,13 +347,33 @@ class Config {
357
347
  this.#isEventQueueActive = value;
358
348
  }
359
349
 
350
+ mixFileContentWithEnv(fileContent) {
351
+ fileContent.events ??= [];
352
+ fileContent.periodicEvents ??= [];
353
+ const events = this.#configEvents ?? {};
354
+ const periodicEvents = this.#configPeriodicEvents ?? {};
355
+ fileContent.events = fileContent.events.concat(this.#mapEnvEvents(events));
356
+ fileContent.periodicEvents = fileContent.periodicEvents.concat(this.#mapEnvEvents(periodicEvents));
357
+ this.fileContent = fileContent;
358
+ }
359
+
360
+ #mapEnvEvents(events) {
361
+ return Object.entries(events)
362
+ .map(([key, event]) => {
363
+ if (!event) {
364
+ return;
365
+ }
366
+ const [type, subType] = key.split("/");
367
+ event.type ??= type;
368
+ event.subType ??= subType;
369
+ return { ...event };
370
+ })
371
+ .filter((a) => a);
372
+ }
373
+
360
374
  set fileContent(config) {
361
- this.#config = config;
362
375
  config.events = config.events ?? [];
363
- const shouldIncludeBaseEvents = cds.env.profiles.includes("production") || cds.env.profiles.includes("test");
364
- config.periodicEvents = (config.periodicEvents ?? []).concat(
365
- (shouldIncludeBaseEvents ? BASE_PERIODIC_EVENTS : []).map((event) => ({ ...event }))
366
- );
376
+ config.periodicEvents = config.periodicEvents ?? [];
367
377
  this.#eventMap = config.events.reduce((result, event) => {
368
378
  this.#basicEventTransformation(event);
369
379
  this.#validateAdHocEvents(result, event);
@@ -380,6 +390,7 @@ class Config {
380
390
  result[this.generateKey(event.type, event.subType)] = event;
381
391
  return result;
382
392
  }, this.#eventMap);
393
+ this.#config = config;
383
394
  }
384
395
 
385
396
  #basicEventTransformation(event) {
@@ -402,6 +413,14 @@ class Config {
402
413
  throw EventQueueError.missingImpl(event.type, event.subType);
403
414
  }
404
415
 
416
+ if (!event.type) {
417
+ throw EventQueueError.missingType(event);
418
+ }
419
+
420
+ if (!event.subType) {
421
+ throw EventQueueError.missingSubType(event);
422
+ }
423
+
405
424
  if (event.appNames) {
406
425
  if (!Array.isArray(event.appNames) || event.appNames.some((appName) => typeof appName !== "string")) {
407
426
  throw EventQueueError.appNamesFormat(event.type, event.subType, event.appNames);
@@ -506,6 +525,14 @@ class Config {
506
525
  return this.#config.events;
507
526
  }
508
527
 
528
+ set configEvents(value) {
529
+ this.#configEvents = JSON.parse(JSON.stringify(value));
530
+ }
531
+
532
+ set configPeriodicEvents(value) {
533
+ this.#configPeriodicEvents = JSON.parse(JSON.stringify(value));
534
+ }
535
+
509
536
  get periodicEvents() {
510
537
  return this.#config.periodicEvents;
511
538
  }
package/src/initialize.js CHANGED
@@ -26,6 +26,8 @@ const TIMEOUT_SHUTDOWN = 2500;
26
26
 
27
27
  const CONFIG_VARS = [
28
28
  ["configFilePath", null],
29
+ ["events", null, "configEvents"],
30
+ ["periodicEvents", null, "configPeriodicEvents"],
29
31
  ["registerAsEventProcessor", true],
30
32
  ["processEventsAfterPublish", true],
31
33
  ["isEventQueueActive", true],
@@ -49,6 +51,8 @@ const CONFIG_VARS = [
49
51
  *
50
52
  * @param {Object} options - The configuration options.
51
53
  * @param {string} [options.configFilePath=null] - Path to the configuration file.
54
+ * @param {string} [options.events={}] - Options to allow events in the configuration.
55
+ * @param {string} [options.periodicEvents={}] - Options to allow periodicEvents in the configuration.
52
56
  * @param {boolean} [options.registerAsEventProcessor=true] - Register the instance as an event processor.
53
57
  * @param {boolean} [options.processEventsAfterPublish=true] - Process events immediately after publishing.
54
58
  * @param {boolean} [options.isEventQueueActive=true] - Flag to activate/deactivate the event queue.
@@ -97,7 +101,8 @@ const initialize = async (options = {}) => {
97
101
  throw EventQueueError.redisConnectionFailure();
98
102
  }
99
103
  }
100
- config.fileContent = await readConfigFromFile(config.configFilePath);
104
+ const fileContent = await readConfigFromFile(config.configFilePath);
105
+ config.mixFileContentWithEnv(fileContent);
101
106
 
102
107
  monkeyPatchCAPOutbox();
103
108
  registerCdsShutdown();
@@ -178,9 +183,9 @@ const monkeyPatchCAPOutbox = () => {
178
183
  };
179
184
 
180
185
  const mixConfigVarsWithEnv = (options) => {
181
- CONFIG_VARS.forEach(([configName, defaultValue]) => {
186
+ CONFIG_VARS.forEach(([configName, defaultValue, mappingName]) => {
182
187
  const configValue = options[configName];
183
- config[configName] = configValue ?? cds.env.eventQueue?.[configName] ?? defaultValue;
188
+ config[mappingName ?? configName] = configValue ?? cds.env.eventQueue?.[configName] ?? defaultValue;
184
189
  });
185
190
  };
186
191
 
@@ -16,7 +16,6 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
16
16
  }
17
17
 
18
18
  async processEvent(processContext, key, queueEntries, payload) {
19
- let status = EventProcessingStatus.Done;
20
19
  try {
21
20
  const service = await cds.connect.to(this.eventSubType);
22
21
  const { useEventQueueUser } = this.eventConfig;
@@ -30,14 +29,37 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
30
29
  authInfo: await common.getTokenInfo(processContext.tenant),
31
30
  });
32
31
  processContext._eventQueue = { processor: this, key, queueEntries, payload };
33
- await cds.unboxed(service).tx(processContext)[invocationFn](msg);
32
+ const result = await cds.unboxed(service).tx(processContext)[invocationFn](msg);
33
+ return this.#determineResultStatus(result, queueEntries);
34
34
  } catch (err) {
35
- status = EventProcessingStatus.Error;
36
35
  this.logger.error("error processing outboxed service call", err, {
37
36
  serviceName: this.eventSubType,
38
37
  });
38
+ return queueEntries.map((queueEntry) => [queueEntry.ID, EventProcessingStatus.Error]);
39
+ }
40
+ }
41
+
42
+ #determineResultStatus(result, queueEntries) {
43
+ const validStatusValues = Object.values(EventProcessingStatus);
44
+ const validStatus = validStatusValues.includes(result);
45
+ if (validStatus) {
46
+ return queueEntries.map((queueEntry) => [queueEntry.ID, result]);
47
+ }
48
+
49
+ if (!Array.isArray(result)) {
50
+ return queueEntries.map((queueEntry) => [queueEntry.ID, EventProcessingStatus.Done]);
51
+ }
52
+
53
+ const valid = !result.some((entry) => {
54
+ const [, status] = entry;
55
+ return !validStatusValues.includes(status);
56
+ });
57
+
58
+ if (valid) {
59
+ return result;
60
+ } else {
61
+ return queueEntries.map((queueEntry) => [queueEntry.ID, EventProcessingStatus.Done]);
39
62
  }
40
- return queueEntries.map((queueEntry) => [queueEntry.ID, status]);
41
63
  }
42
64
  }
43
65