@cap-js-community/event-queue 2.0.0-beta.8 → 2.0.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": "2.0.0-beta.8",
3
+ "version": "2.0.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",
@@ -296,7 +296,7 @@ class EventQueueProcessorBase {
296
296
  const statusMap = this.commitOnEventLevel || returnMap ? {} : this.__statusMap;
297
297
  const errorHandler = (error) => {
298
298
  queueEntries.forEach((queueEntry) =>
299
- this.#determineAndAddEventStatusToMap(queueEntry.ID, EventProcessingStatus.Error, statusMap)
299
+ this.#determineAndAddEventStatusToMap(queueEntry.ID, EventProcessingStatus.Error, { statusMap })
300
300
  );
301
301
  this.logger.error(
302
302
  "The supplied status tuple doesn't have the required structure. Setting all entries to error.",
@@ -317,7 +317,7 @@ class EventQueueProcessorBase {
317
317
 
318
318
  try {
319
319
  queueEntryProcessingStatusTuple.forEach(([id, processingStatus]) =>
320
- this.#determineAndAddEventStatusToMap(id, processingStatus, statusMap)
320
+ this.#determineAndAddEventStatusToMap(id, processingStatus, { statusMap })
321
321
  );
322
322
  } catch (error) {
323
323
  errorHandler(error);
@@ -344,11 +344,15 @@ class EventQueueProcessorBase {
344
344
  }
345
345
  }
346
346
 
347
- #determineAndAddEventStatusToMap(id, statusOrUpdateData, statusMap = this.__statusMap) {
347
+ #determineAndAddEventStatusToMap(id, statusOrUpdateData, { statusMap = this.__statusMap, error } = {}) {
348
348
  if (typeof statusOrUpdateData === "number") {
349
349
  statusOrUpdateData = { status: statusOrUpdateData };
350
350
  }
351
351
 
352
+ if (error) {
353
+ statusOrUpdateData.error = error;
354
+ }
355
+
352
356
  if (!statusMap[id]) {
353
357
  statusMap[id] = statusOrUpdateData;
354
358
  return;
@@ -376,9 +380,17 @@ class EventQueueProcessorBase {
376
380
  }
377
381
  );
378
382
  queueEntries.forEach((queueEntry) =>
379
- this.#determineAndAddEventStatusToMap(queueEntry.ID, EventProcessingStatus.Error)
383
+ this.#determineAndAddEventStatusToMap(queueEntry.ID, EventProcessingStatus.Error, { error })
384
+ );
385
+ return Object.fromEntries(
386
+ queueEntries.map((queueEntry) => [
387
+ queueEntry.ID,
388
+ {
389
+ status: EventProcessingStatus.Error,
390
+ error,
391
+ },
392
+ ])
380
393
  );
381
- return Object.fromEntries(queueEntries.map((queueEntry) => [queueEntry.ID, EventProcessingStatus.Error]));
382
394
  }
383
395
 
384
396
  handleErrorDuringPeriodicEventProcessing(error, queueEntry) {
@@ -389,11 +401,12 @@ class EventQueueProcessorBase {
389
401
  });
390
402
  }
391
403
 
392
- async setPeriodicEventStatus(queueEntryIds, status) {
404
+ async setPeriodicEventStatus(queueEntryIds, status, error) {
393
405
  await this.tx.run(
394
406
  UPDATE.entity(this.#config.tableNameEventQueue)
395
407
  .set({
396
408
  status: status,
409
+ ...(error && { error: this.#error2String(error) }),
397
410
  })
398
411
  .where({
399
412
  ID: queueEntryIds,
@@ -154,6 +154,7 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
154
154
  }
155
155
 
156
156
  let status = EventProcessingStatus.Done;
157
+ let error;
157
158
  await executeInNewTransaction(
158
159
  eventTypeInstance.context,
159
160
  `eventQueue-periodic-process-${eventTypeInstance.eventType}##${eventTypeInstance.eventSubType}`,
@@ -166,6 +167,7 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
166
167
  eventTypeInstance.startPerformanceTracerPeriodicEvents();
167
168
  await eventTypeInstance.processPeriodicEvent(tx.context, queueEntry.ID, queueEntry);
168
169
  } catch (err) {
170
+ error = err;
169
171
  status = EventProcessingStatus.Error;
170
172
  eventTypeInstance.handleErrorDuringPeriodicEventProcessing(err, queueEntry);
171
173
  await tx.rollback();
@@ -190,7 +192,7 @@ const processPeriodicEvent = async (context, eventTypeInstance) => {
190
192
  async (tx) => {
191
193
  await trace(eventTypeInstance.context, "periodic-event-set-status", async () => {
192
194
  eventTypeInstance.processEventContext = tx.context;
193
- await eventTypeInstance.setPeriodicEventStatus(queueEntry.ID, status);
195
+ await eventTypeInstance.setPeriodicEventStatus(queueEntry.ID, status, error);
194
196
  });
195
197
  }
196
198
  );
@@ -165,6 +165,14 @@ const isTenantIdValidCb = async (checkType, tenantId) => {
165
165
  }
166
166
  };
167
167
 
168
+ const cleanUndefined = (input) =>
169
+ Object.entries(input).reduce((acc, [key, value]) => {
170
+ if (value !== undefined) {
171
+ acc[key] = value;
172
+ }
173
+ return acc;
174
+ }, {});
175
+
168
176
  module.exports = {
169
177
  arrayToFlatMap,
170
178
  limiter,
@@ -174,6 +182,7 @@ module.exports = {
174
182
  getAuthContext,
175
183
  isTenantIdValidCb,
176
184
  promiseAllDone,
185
+ cleanUndefined,
177
186
  __: {
178
187
  clearAuthContextCache: () => getAuthContext._cache?.clear(),
179
188
  },
@@ -42,4 +42,17 @@ service EventQueueAdminService {
42
42
  @mandatory
43
43
  subType: String) returns Boolean;
44
44
  }
45
+
46
+ action publishEvent(
47
+ @mandatory
48
+ tenants: array of String,
49
+ @mandatory
50
+ type: String,
51
+ @mandatory
52
+ subType: String,
53
+ referenceEntity: String,
54
+ referenceEntityKey: String,
55
+ @open
56
+ payload: {},
57
+ startAfter: String);
45
58
  }
@@ -1,26 +1,35 @@
1
1
  "use strict";
2
2
 
3
3
  const cds = require("@sap/cds");
4
- const { EventProcessingStatus } = require("../../src");
5
- const config = require("../../src/config");
4
+
5
+ const eventQueue = require("../../src");
6
6
  const distributedLock = require("../../src/shared/distributedLock");
7
7
  const redisPub = require("../../src/redis/redisPub");
8
+ const publishEventHelper = require("./publishEventHelper");
9
+ const commonHelper = require("../../src/shared/common");
10
+
11
+ const COMPONENT_NAME = "/eventQueue/admin";
8
12
 
9
13
  module.exports = class AdminService extends cds.ApplicationService {
10
14
  async init() {
11
15
  const { Event: EventService, Lock: LockService } = this.entities;
12
16
  const { Event: EventDb } = cds.db.entities("sap.eventqueue");
17
+ const { publishEvent } = this.actions;
13
18
  const { landscape, space } = this.getLandscapeAndSpace();
14
19
 
15
20
  this.before("*", (req) => {
16
- if (!config.enableAdminService) {
21
+ if (!eventQueue.config.enableAdminService) {
17
22
  req.reject(403, "Admin service is disabled by configuration");
18
23
  }
19
24
 
25
+ if (req.event === publishEvent.name.split(".")[1]) {
26
+ return;
27
+ }
28
+
20
29
  const headers = Object.assign({}, req.headers, req.req?.headers);
21
30
  const tenant = headers["z-id"] ?? req.data.tenant;
22
31
 
23
- if (config.isMultiTenancy && tenant == null) {
32
+ if (eventQueue.config.isMultiTenancy && tenant == null) {
24
33
  req.reject(400, "Missing tenant ID in request header (z-id)");
25
34
  }
26
35
  req.headers ??= {};
@@ -46,7 +55,7 @@ module.exports = class AdminService extends cds.ApplicationService {
46
55
  });
47
56
 
48
57
  this.on("READ", LockService, async () => {
49
- if (!config.redisEnabled) {
58
+ if (!eventQueue.config.redisEnabled) {
50
59
  return [];
51
60
  }
52
61
  const locks = await distributedLock.getAllLocksRedis();
@@ -59,14 +68,14 @@ module.exports = class AdminService extends cds.ApplicationService {
59
68
 
60
69
  this.on("setStatusAndAttempts", async (req) => {
61
70
  const tenant = req.headers["z-id"];
62
- cds.log("eventQueue").info("Restarting processing for event queue");
71
+ cds.log(COMPONENT_NAME).info("Restarting processing for event queue");
63
72
  const updateData = {};
64
73
 
65
74
  if (Number.isInteger(req.data.attempts)) {
66
75
  updateData.attempts = req.data.attempts;
67
76
  }
68
77
 
69
- if (Object.values(EventProcessingStatus).includes(req.data.status)) {
78
+ if (Object.values(eventQueue.EventProcessingStatus).includes(req.data.status)) {
70
79
  updateData.status = req.data.status;
71
80
  }
72
81
 
@@ -95,6 +104,32 @@ module.exports = class AdminService extends cds.ApplicationService {
95
104
  });
96
105
  });
97
106
 
107
+ this.on(publishEvent, async (req) => {
108
+ const logger = cds.log(COMPONENT_NAME);
109
+ try {
110
+ const { type, subType, referenceEntity, referenceEntityKey, payload, startAfter } = req.data;
111
+ const tenants = await publishEventHelper.resolveTenantInfos(req);
112
+ const eventOptions = commonHelper.cleanUndefined({
113
+ type,
114
+ subType,
115
+ referenceEntity,
116
+ referenceEntityKey,
117
+ payload,
118
+ startAfter,
119
+ });
120
+ const publishInfo = { count: tenants.length, type, subType, tenants: req.data.tenants };
121
+ logger.info("publishing event for tenant(s)", publishInfo);
122
+ for (const tenant of tenants) {
123
+ await cds.tx({ tenant }, async (tx) => {
124
+ await eventQueue.publishEvent(tx, { ...eventOptions });
125
+ });
126
+ }
127
+ logger.info("finished publishing event for tenant(s)", publishInfo);
128
+ } catch (err) {
129
+ logger.error("error publishing event", err);
130
+ }
131
+ });
132
+
98
133
  await super.init();
99
134
  }
100
135
 
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+
3
+ const cdsHelper = require("../../src/shared/cdsHelper");
4
+
5
+ /**
6
+ * @typedef TenantInfo
7
+ * @type object
8
+ * @property {string} tenantId
9
+ * @property {string} subdomain
10
+ */
11
+ /**
12
+ * For service action interfaces, with a tenants = [...] data field this code
13
+ * resolves the inputs into a tenantInfos array.
14
+ *
15
+ * Inputs:
16
+ * - ["all"] give me all tenants
17
+ * - ["self"] give me just the context tenant
18
+ * - [] give me no tenant
19
+ * - ["<tenantId1>", "<tenantId2>", "<subdomain3>", "<subdomain4>",...]
20
+ * give me those tenants where either tenantId or subdomain corresponds to the given inputs
21
+ *
22
+ * @returns {Promise<Array<TenantInfo>>} returns an array of {@link TenantInfo} objects.
23
+ */
24
+ const resolveTenantInfos = async (context, { sortByTenantId = true } = {}) => {
25
+ let result = await _resolveTenantInfos(context);
26
+ if (sortByTenantId) {
27
+ result = result.sort(_orderByTenantId);
28
+ }
29
+ return result;
30
+ };
31
+
32
+ const _resolveTenantInfos = async (context) => {
33
+ if (!Array.isArray(context.data.tenants)) {
34
+ return [];
35
+ }
36
+ for (const tenant of context.data.tenants) {
37
+ if (tenant === "self") {
38
+ return [context.tenant];
39
+ }
40
+ if (["all", "*"].includes(tenant)) {
41
+ return await cdsHelper.getAllTenantIds();
42
+ }
43
+ }
44
+ const contextTenantsMap = Object.fromEntries(context.data.tenants.map((tenant) => [tenant, true]));
45
+ return (await cdsHelper.getAllTenantIds()).filter((tenantId) => contextTenantsMap[tenantId]);
46
+ };
47
+
48
+ const _orderByTenantId = (a, b) => a.localeCompare(b);
49
+
50
+ module.exports = {
51
+ resolveTenantInfos,
52
+ };