@cap-js-community/event-queue 1.5.3 → 1.6.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.5.3",
3
+ "version": "1.6.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",
@@ -44,12 +44,12 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@sap/xssec": "^3.6.1",
47
- "redis": "^4.6.14",
47
+ "redis": "^4.6.15",
48
48
  "verror": "^1.10.1",
49
49
  "yaml": "^2.4.2"
50
50
  },
51
51
  "devDependencies": {
52
- "@cap-js/hana": "^1.0.0",
52
+ "@cap-js/hana": "^1.1.0",
53
53
  "@cap-js/sqlite": "^1.7.2",
54
54
  "@sap/cds": "^7.9.2",
55
55
  "@sap/cds-dk": "^7.8.0",
@@ -18,8 +18,7 @@ const ERROR_CODES = {
18
18
  NO_MANUEL_INSERT_OF_PERIODIC: "NO_MANUEL_INSERT_OF_PERIODIC",
19
19
  LOAD_HIGHER_THAN_LIMIT: "LOAD_HIGHER_THAN_LIMIT",
20
20
  NOT_ALLOWED_PRIORITY: "NOT_ALLOWED_PRIORITY",
21
- SCHEMA_TENANT_MISMATCH: "SCHEMA_TENANT_MISMATCH",
22
- GLOBAL_CDS_CONTEXT_MISMATCH: "GLOBAL_CDS_CONTEXT_MISMATCH",
21
+ APP_NAMES_FORMAT: "APP_NAMES_FORMAT",
23
22
  };
24
23
 
25
24
  const ERROR_CODES_META = {
@@ -69,11 +68,8 @@ const ERROR_CODES_META = {
69
68
  [ERROR_CODES.NOT_ALLOWED_PRIORITY]: {
70
69
  message: "The supplied priority is not allowed. Only LOW, MEDIUM, HIGH is allowed!",
71
70
  },
72
- [ERROR_CODES.SCHEMA_TENANT_MISMATCH]: {
73
- message: "The db client associated to the tenant context does not match! Processing will be skipped.",
74
- },
75
- [ERROR_CODES.GLOBAL_CDS_CONTEXT_MISMATCH]: {
76
- message: "The global cds context does not match the local cds context.",
71
+ [ERROR_CODES.APP_NAMES_FORMAT]: {
72
+ message: "The app names property must be an array and only contain strings.",
77
73
  },
78
74
  };
79
75
 
@@ -244,23 +240,12 @@ class EventQueueError extends VError {
244
240
  );
245
241
  }
246
242
 
247
- static dbClientSchemaMismatch(tenantId, dbClientSchema, serviceManagerSchema) {
248
- const { message } = ERROR_CODES_META[ERROR_CODES.SCHEMA_TENANT_MISMATCH];
249
- return new EventQueueError(
250
- {
251
- name: ERROR_CODES.SCHEMA_TENANT_MISMATCH,
252
- info: { tenantId, dbClientSchema, serviceManagerSchema },
253
- },
254
- message
255
- );
256
- }
257
-
258
- static globalCdsContextNotMatchingLocal(globalProperties, localProperties) {
259
- const { message } = ERROR_CODES_META[ERROR_CODES.GLOBAL_CDS_CONTEXT_MISMATCH];
243
+ static appNamesFormat(type, subType, appNames) {
244
+ const { message } = ERROR_CODES_META[ERROR_CODES.APP_NAMES_FORMAT];
260
245
  return new EventQueueError(
261
246
  {
262
- name: ERROR_CODES.GLOBAL_CDS_CONTEXT_MISMATCH,
263
- info: { globalProperties, localProperties },
247
+ name: ERROR_CODES.APP_NAMES_FORMAT,
248
+ info: { type, subType, appNames },
264
249
  },
265
250
  message
266
251
  );
@@ -1108,6 +1108,10 @@ class EventQueueProcessorBase {
1108
1108
  return this.#eventType.replace(SUFFIX_PERIODIC, "");
1109
1109
  }
1110
1110
 
1111
+ get rawEventType() {
1112
+ return this.#eventType;
1113
+ }
1114
+
1111
1115
  get eventSubType() {
1112
1116
  return this.#eventSubType;
1113
1117
  }
package/src/config.js CHANGED
@@ -105,7 +105,12 @@ class Config {
105
105
  }
106
106
 
107
107
  _checkRedisIsBound() {
108
- return !!this.#env.getRedisCredentialsFromEnv();
108
+ return !!this.#env.redisCredentialsFromEnv;
109
+ }
110
+
111
+ shouldBeProcessedInThisApplication(type, subType) {
112
+ const config = this.#eventMap[this.generateKey(type, subType)];
113
+ return !config._appNameMap || config._appNameMap[this.#env.applicationName];
109
114
  }
110
115
 
111
116
  checkRedisEnabled() {
@@ -276,8 +281,12 @@ class Config {
276
281
  eventOutdatedCheck: config.eventOutdatedCheck,
277
282
  checkForNextChunk: config.checkForNextChunk,
278
283
  deleteFinishedEventsAfterDays: config.deleteFinishedEventsAfterDays,
284
+ appNames: config.appNames,
279
285
  internalEvent: true,
280
286
  };
287
+ eventConfig._appNameMap = eventConfig.appNames
288
+ ? Object.fromEntries(new Map(eventConfig.appNames.map((a) => [a, true])))
289
+ : null;
281
290
  this.#config.events.push(eventConfig);
282
291
  this.#eventMap[this.generateKey(CAP_EVENT_TYPE, serviceName)] = eventConfig;
283
292
  }
@@ -317,6 +326,7 @@ class Config {
317
326
  event.load = event.load ?? DEFAULT_LOAD;
318
327
  event.priority = event.priority ?? DEFAULT_PRIORITY;
319
328
  this.validateAdHocEvents(result, event);
329
+ event._appNameMap = event.appNames ? Object.fromEntries(new Map(event.appNames.map((a) => [a, true]))) : null;
320
330
  result[this.generateKey(event.type, event.subType)] = event;
321
331
  return result;
322
332
  }, {});
@@ -326,6 +336,7 @@ class Config {
326
336
  event.type = `${event.type}${SUFFIX_PERIODIC}`;
327
337
  event.isPeriodic = true;
328
338
  this.validatePeriodicConfig(result, event);
339
+ event._appNameMap = event.appNames ? Object.fromEntries(new Map(event.appNames.map((a) => [a, true]))) : null;
329
340
  result[this.generateKey(event.type, event.subType)] = event;
330
341
  return result;
331
342
  }, this.#eventMap);
@@ -355,6 +366,12 @@ class Config {
355
366
  if (!config.impl) {
356
367
  throw EventQueueError.missingImpl(config.type, config.subType);
357
368
  }
369
+
370
+ if (config.appNames) {
371
+ if (!Array.isArray(config.appNames) || config.appNames.some((appName) => typeof appName !== "string")) {
372
+ throw EventQueueError.appNamesFormat(config.type, config.subType, config.appNames);
373
+ }
374
+ }
358
375
  }
359
376
 
360
377
  generateKey(type, subType) {
@@ -15,9 +15,19 @@ const COMPONENT_NAME = "/eventQueue/eventQueueAsOutbox";
15
15
  function outboxed(srv, customOpts) {
16
16
  // outbox max. once
17
17
  const logger = cds.log(COMPONENT_NAME);
18
+ const outboxOpts = Object.assign(
19
+ {},
20
+ (typeof cds.requires.outbox === "object" && cds.requires.outbox) || {},
21
+ (typeof srv.options?.outbox === "object" && srv.options.outbox) || {},
22
+ customOpts || {}
23
+ );
24
+
18
25
  if (!new.target) {
19
26
  const former = srv[OUTBOXED];
20
27
  if (former) {
28
+ if (outboxOpts.kind === "persistent-outbox") {
29
+ config.addCAPOutboxEvent(srv.name, outboxOpts);
30
+ }
21
31
  return former;
22
32
  }
23
33
  }
@@ -30,14 +40,9 @@ function outboxed(srv, customOpts) {
30
40
  Object.defineProperty(srv, OUTBOXED, { value: outboxedSrv });
31
41
  }
32
42
 
33
- const outboxOpts = Object.assign(
34
- {},
35
- (typeof cds.requires.outbox === "object" && cds.requires.outbox) || {},
36
- (typeof srv.options?.outbox === "object" && srv.options.outbox) || {},
37
- customOpts || {}
38
- );
39
-
40
- config.addCAPOutboxEvent(srv.name, outboxOpts);
43
+ if (outboxOpts.kind === "persistent-outbox") {
44
+ config.addCAPOutboxEvent(srv.name, outboxOpts);
45
+ }
41
46
  outboxedSrv.handle = async function (req) {
42
47
  const context = req.context || cds.context;
43
48
  if (outboxOpts.kind === "persistent-outbox") {
@@ -269,6 +269,7 @@ const _checkEventIsBlocked = async (baseInstance) => {
269
269
  });
270
270
  }
271
271
  } else {
272
+ // TODO: we should be able to get rid of baseInstance.isPeriodicEvent with rawEventType
272
273
  eventBlocked = config.isEventBlocked(
273
274
  baseInstance.eventType,
274
275
  baseInstance.eventSubType,
@@ -283,11 +284,16 @@ const _checkEventIsBlocked = async (baseInstance) => {
283
284
 
284
285
  if (eventBlocked) {
285
286
  baseInstance.logger.info("skipping run because event is blocked by configuration", {
286
- type: baseInstance.eventType,
287
+ type: baseInstance.rawEventType,
287
288
  subType: baseInstance.eventSubType,
288
289
  tenantUnsubscribed: config.isTenantUnsubscribed(baseInstance.context.tenant),
289
290
  });
290
291
  }
292
+
293
+ if (!eventBlocked) {
294
+ eventBlocked = !config.shouldBeProcessedInThisApplication(baseInstance.rawEventType, baseInstance.eventSubType);
295
+ }
296
+
291
297
  return eventBlocked;
292
298
  };
293
299
 
@@ -35,14 +35,6 @@ const _messageHandlerProcessEvents = async (messageData) => {
35
35
  return;
36
36
  }
37
37
 
38
- const user = await cds.tx({ tenant: tenantId }, async () => {
39
- return new cds.User.Privileged({ id: config.userId, authInfo: await common.getAuthInfo(tenantId) });
40
- });
41
- const tenantContext = {
42
- tenant: tenantId,
43
- user,
44
- };
45
-
46
38
  if (!config.getEventConfig(type, subType)) {
47
39
  if (config.isCapOutboxEvent(type)) {
48
40
  try {
@@ -64,6 +56,23 @@ const _messageHandlerProcessEvents = async (messageData) => {
64
56
  }
65
57
  }
66
58
 
59
+ if (!(config.getEventConfig(type, subType) && config.shouldBeProcessedInThisApplication(type, subType))) {
60
+ logger.debug("event is not configured to be processed on this app-name", {
61
+ tenantId,
62
+ type,
63
+ subType,
64
+ });
65
+ return;
66
+ }
67
+
68
+ const user = await cds.tx({ tenant: tenantId }, async () => {
69
+ return new cds.User.Privileged({ id: config.userId, authInfo: await common.getAuthInfo(tenantId) });
70
+ });
71
+ const tenantContext = {
72
+ tenant: tenantId,
73
+ user,
74
+ };
75
+
67
76
  return await cds.tx(tenantContext, async ({ context }) => {
68
77
  return await runnerHelper.runEventCombinationForTenant(context, type, subType, { lockId, shouldTrace: true });
69
78
  });
@@ -29,18 +29,21 @@ const getOpenQueueEntries = async (tx) => {
29
29
 
30
30
  const result = [];
31
31
  for (const { type, subType } of entries) {
32
- if (type.startsWith("CAP_OUTBOX")) {
33
- if (cds.requires[subType]) {
34
- await cds.connect.to(subType).catch(() => {});
35
- result.push({ type, subType });
36
- } else {
37
- const service = await cds.connect.to(subType).catch(() => {});
38
- if (service) {
39
- result.push({ type, subType });
40
- }
41
- }
32
+ if (eventConfig.isCapOutboxEvent(type)) {
33
+ await cds.connect
34
+ .to(subType)
35
+ .then((service) => {
36
+ if (!service) {
37
+ return;
38
+ }
39
+ cds.outboxed(service);
40
+ if (eventConfig.shouldBeProcessedInThisApplication(type, subType)) {
41
+ result.push({ type, subType });
42
+ }
43
+ })
44
+ .catch(() => {});
42
45
  } else {
43
- if (eventConfig.getEventConfig(type, subType)) {
46
+ if (eventConfig.getEventConfig(type, subType) && eventConfig.shouldBeProcessedInThisApplication(type, subType)) {
44
47
  result.push({ type, subType });
45
48
  }
46
49
  }
package/src/shared/env.js CHANGED
@@ -3,34 +3,42 @@
3
3
  let instance;
4
4
 
5
5
  class Env {
6
- #isLocal;
7
6
  #vcapServices;
7
+ #vcapApplication;
8
8
 
9
9
  constructor() {
10
10
  try {
11
11
  this.#vcapServices = JSON.parse(process.env.VCAP_SERVICES);
12
+ this.#vcapApplication = JSON.parse(process.env.VCAP_APPLICATION);
12
13
  } catch {
13
14
  this.#vcapServices = {};
15
+ this.#vcapApplication = {};
14
16
  }
15
17
  }
16
18
 
17
- getRedisCredentialsFromEnv() {
19
+ get redisCredentialsFromEnv() {
18
20
  return this.#vcapServices["redis-cache"]?.[0]?.credentials;
19
21
  }
20
22
 
21
- set isLocal(value) {
22
- this.#isLocal = value;
23
- }
24
- get isLocal() {
25
- return this.#isLocal;
23
+ get applicationName() {
24
+ return this.#vcapApplication.application_name;
26
25
  }
27
26
 
28
27
  set vcapServices(value) {
29
28
  this.#vcapServices = value;
30
29
  }
30
+
31
31
  get vcapServices() {
32
32
  return this.#vcapServices;
33
33
  }
34
+
35
+ set vcapApplication(value) {
36
+ this.#vcapApplication = value;
37
+ }
38
+
39
+ get vcapApplication() {
40
+ return this.#vcapApplication;
41
+ }
34
42
  }
35
43
 
36
44
  module.exports = {
@@ -47,7 +47,7 @@ class EventScheduler {
47
47
  }
48
48
 
49
49
  clearForTenant(tenantId) {
50
- Object.values(this.#eventsByTenants[tenantId]).forEach((timeoutId) => clearTimeout(timeoutId));
50
+ Object.keys(this.#eventsByTenants[tenantId] ?? []).forEach((timeoutId) => clearTimeout(timeoutId));
51
51
  }
52
52
 
53
53
  calculateOffset(type, subType, startAfter) {
@@ -67,6 +67,14 @@ class EventScheduler {
67
67
  clearScheduledEvents() {
68
68
  this.#scheduledEvents = {};
69
69
  }
70
+
71
+ clearEventsByTenants() {
72
+ this.#eventsByTenants = {};
73
+ }
74
+
75
+ get eventsByTenants() {
76
+ return this.#eventsByTenants;
77
+ }
70
78
  }
71
79
 
72
80
  module.exports = {
@@ -30,7 +30,7 @@ const createMainClientAndConnect = (options) => {
30
30
  const _createClientBase = (redisOptions) => {
31
31
  const env = getEnvInstance();
32
32
  try {
33
- const credentials = env.getRedisCredentialsFromEnv();
33
+ const credentials = env.redisCredentialsFromEnv;
34
34
  const redisIsCluster = credentials.cluster_mode;
35
35
  const url = credentials.uri.replace(/(?<=rediss:\/\/)[\w-]+?(?=:)/, "");
36
36
  if (redisIsCluster) {