@cap-js-community/event-queue 1.10.3 → 1.10.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/index.cds CHANGED
@@ -1 +1,2 @@
1
1
  using from './db';
2
+ using from './srv';
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@cap-js-community/event-queue",
3
- "version": "1.10.3",
3
+ "version": "1.10.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",
7
7
  "files": [
8
8
  "src",
9
+ "srv",
9
10
  "db",
10
11
  "cds-plugin.js",
11
12
  "index.cds"
@@ -21,6 +22,8 @@
21
22
  "multi-tenancy"
22
23
  ],
23
24
  "scripts": {
25
+ "start": "PORT=4005 cds-serve",
26
+ "watch": "PORT=4005 cds watch",
24
27
  "test:unit": "jest --testPathIgnorePatterns=\"/test-integration/\"",
25
28
  "test:integration": "jest --testPathIgnorePatterns=\"/test/\" --runInBand --forceExit",
26
29
  "voter:test:integration": "jest --testPathIgnorePatterns=\"/test/\" --forceExit",
@@ -45,7 +48,7 @@
45
48
  },
46
49
  "dependencies": {
47
50
  "@sap/xssec": "^4.6.0",
48
- "cron-parser": "^5.1.0",
51
+ "cron-parser": "^5.2.0",
49
52
  "redis": "^4.7.0",
50
53
  "verror": "^1.10.1",
51
54
  "yaml": "^2.7.1"
@@ -76,7 +76,7 @@ class EventQueueProcessorBase {
76
76
  this.__txMap = {};
77
77
  this.__txRollback = {};
78
78
  this.__queueEntries = [];
79
- this.#keepAliveRunner = new SetIntervalDriftSafe(this.#eventConfig.keepAliveInterval);
79
+ this.#keepAliveRunner = new SetIntervalDriftSafe(this.#eventConfig.keepAliveInterval * 1000);
80
80
  }
81
81
 
82
82
  /**
@@ -174,7 +174,12 @@ class EventQueueProcessorBase {
174
174
  eventSubType: this.#eventSubType,
175
175
  iterationCounter,
176
176
  });
177
- this.#eventSchedulerInstance.scheduleEvent(this.__context.tenant, this.#eventType, this.#eventSubType, new Date());
177
+ this.#eventSchedulerInstance.scheduleEvent(
178
+ this.__context.tenant,
179
+ this.#eventType,
180
+ this.#eventSubType,
181
+ new Date(Date.now() + 5 * 1000) // add some offset to make sure all locks are released
182
+ );
178
183
  }
179
184
 
180
185
  logStartMessage() {
@@ -612,7 +617,7 @@ class EventQueueProcessorBase {
612
617
  "OR lastAttemptTimestamp IS NULL ) OR ( status =",
613
618
  EventProcessingStatus.InProgress,
614
619
  "AND lastAttemptTimestamp <=",
615
- new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime).toISOString(),
620
+ new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime * 1000).toISOString(),
616
621
  ") )",
617
622
  ]
618
623
  : [
@@ -623,7 +628,7 @@ class EventQueueProcessorBase {
623
628
  ") OR ( status =",
624
629
  EventProcessingStatus.InProgress,
625
630
  "AND lastAttemptTimestamp <=",
626
- new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime).toISOString(),
631
+ new Date(baseDate - this.#eventConfig.keepAliveMaxInProgressTime * 1000).toISOString(),
627
632
  ") )",
628
633
  ])
629
634
  )
@@ -863,7 +868,7 @@ class EventQueueProcessorBase {
863
868
  }
864
869
 
865
870
  continuesKeepAlive() {
866
- if (Date.now() - this.lockAcquiredTime.getTime() >= this.#eventConfig.keepAliveInterval) {
871
+ if (Date.now() - this.lockAcquiredTime.getTime() >= this.#eventConfig.keepAliveInterval * 1000) {
867
872
  trace(this.baseContext, "keepAlive-between-iterations", async () => {
868
873
  await this.#renewDistributedLock();
869
874
  }).catch((err) => this.logger.error("renewing lock between intervals failed!", err));
@@ -956,7 +961,7 @@ class EventQueueProcessorBase {
956
961
  const lockAcquired = await distributedLock.acquireLock(
957
962
  this.__context,
958
963
  [this.#eventType, this.#eventSubType].join("##"),
959
- { keepTrackOfLock: true, expiryTime: this.#eventConfig.keepAliveMaxInProgressTime }
964
+ { keepTrackOfLock: true, expiryTime: this.#eventConfig.keepAliveMaxInProgressTime * 1000 }
960
965
  );
961
966
  if (!lockAcquired) {
962
967
  this.logger.debug("no lock available, exit processing", {
@@ -978,7 +983,7 @@ class EventQueueProcessorBase {
978
983
  const lockAcquired = await distributedLock.renewLock(
979
984
  this.__context,
980
985
  [this.#eventType, this.#eventSubType].join("##"),
981
- { expiryTime: this.#eventConfig.keepAliveMaxInProgressTime }
986
+ { expiryTime: this.#eventConfig.keepAliveMaxInProgressTime * 1000 }
982
987
  );
983
988
  if (!lockAcquired) {
984
989
  this.logger.error("renewing distributed lock failed!", {
package/src/config.js CHANGED
@@ -117,6 +117,7 @@ class Config {
117
117
  #tenantIdFilterEventProcessingCb;
118
118
  #configEvents;
119
119
  #configPeriodicEvents;
120
+ #enableAdminService;
120
121
  static #instance;
121
122
  constructor() {
122
123
  this.#logger = cds.log(COMPONENT_NAME);
@@ -549,7 +550,7 @@ class Config {
549
550
  event.load = event.load ?? DEFAULT_LOAD;
550
551
  event.priority = event.priority ?? DEFAULT_PRIORITY;
551
552
  event.increasePriorityOverTime = event.increasePriorityOverTime ?? DEFAULT_INCREASE_PRIORITY;
552
- event.keepAliveInterval = (event.keepAliveInterval ?? DEFAULT_KEEP_ALIVE_INTERVAL) * 1000;
553
+ event.keepAliveInterval = event.keepAliveInterval ?? DEFAULT_KEEP_ALIVE_INTERVAL;
553
554
  event.keepAliveMaxInProgressTime = event.keepAliveInterval * DEFAULT_MAX_FACTOR_STUCK_2_KEEP_ALIVE_INTERVAL;
554
555
  event.checkForNextChunk = event.checkForNextChunk ?? DEFAULT_CHECK_FOR_NEXT_CHUNK;
555
556
  }
@@ -638,18 +639,17 @@ class Config {
638
639
  let cron;
639
640
 
640
641
  // NOTE: logic is as follows:
641
- // - if event.utc is true --> always use UTC
642
+ // - if event.utc is true --> always use UTC (default is false)
642
643
  // - if event.useCronTimezone is false OR event.cronTimezone is not defined --> use UTC as well
643
644
  // - if event.utc is not true AND event.cronTimezone is set AND event.useCronTimezone is NOT set to false use event.cronTimezone
644
645
  event.utc = event.utc ?? UTC_DEFAULT;
645
-
646
- if (!event.cronTimezone) {
646
+ if (!this.cronTimezone) {
647
647
  event.useCronTimezone = false;
648
648
  } else {
649
649
  event.useCronTimezone = event.useCronTimezone ?? USE_CRON_TZ_DEFAULT;
650
650
  }
651
651
 
652
- event.tz = event.utc || !event.useCronTimezone ? "UTC" : event.cronTimezone;
652
+ event.tz = event.utc || !event.useCronTimezone ? "UTC" : this.cronTimezone;
653
653
 
654
654
  try {
655
655
  cron = CronExpressionParser.parse(event.cron);
@@ -972,6 +972,18 @@ class Config {
972
972
  return this.#eventMap;
973
973
  }
974
974
 
975
+ get developmentMode() {
976
+ return cds.env.profiles.find((profile) => profile === "development");
977
+ }
978
+
979
+ get enableAdminService() {
980
+ return this.#enableAdminService;
981
+ }
982
+
983
+ set enableAdminService(value) {
984
+ this.#enableAdminService = value;
985
+ }
986
+
975
987
  /**
976
988
  @return { Config }
977
989
  **/
package/src/index.d.ts CHANGED
@@ -257,6 +257,8 @@ declare class Config {
257
257
  get insertEventsBeforeCommit(): any;
258
258
  set enableTelemetry(value: any);
259
259
  get enableTelemetry(): any;
260
+ set enableAdminService(value: any);
261
+ get enableAdminService(): any;
260
262
  get isMultiTenancy(): boolean;
261
263
  }
262
264
 
package/src/initialize.js CHANGED
@@ -46,6 +46,7 @@ const CONFIG_VARS = [
46
46
  ["redisNamespace", null],
47
47
  ["publishEventBlockList", true],
48
48
  ["crashOnRedisUnavailable", false],
49
+ ["enableAdminService", false],
49
50
  ];
50
51
 
51
52
  /**
@@ -87,7 +88,7 @@ const initialize = async (options = {}) => {
87
88
  "Event queue initialization skipped: no configFilePath provided, and event queue is not configured as a CAP outbox."
88
89
  );
89
90
  }
90
-
91
+ _disableAdminService();
91
92
  const redisEnabled = config.checkRedisEnabled();
92
93
  let resolveFn;
93
94
  let initFinished = new Promise((resolve) => (resolveFn = resolve));
@@ -182,6 +183,14 @@ const monkeyPatchCAPOutbox = () => {
182
183
  get: () => eventQueueAsOutbox.unboxed,
183
184
  configurable: true,
184
185
  });
186
+ Object.defineProperty(cds, "queued", {
187
+ get: () => eventQueueAsOutbox.outboxed,
188
+ configurable: true,
189
+ });
190
+ Object.defineProperty(cds, "unqueued", {
191
+ get: () => eventQueueAsOutbox.unboxed,
192
+ configurable: true,
193
+ });
185
194
  }
186
195
  };
187
196
 
@@ -193,34 +202,31 @@ const mixConfigVarsWithEnv = (options) => {
193
202
  };
194
203
 
195
204
  const registerCdsShutdown = () => {
196
- const isTestProfile = cds.env.profiles.find((profile) => profile.includes("test"));
197
- if (isTestProfile || !config.redisEnabled) {
198
- return;
199
- }
200
205
  cds.on("shutdown", async () => {
201
206
  return await new Promise((resolve) => {
202
- const timeoutRef = setTimeout(() => {
207
+ let timeoutRef;
208
+ timeoutRef = setTimeout(() => {
203
209
  clearTimeout(timeoutRef);
204
210
  cds.log(COMPONENT).info("shutdown timeout reached - some locks might not have been released!");
205
211
  resolve();
206
212
  }, TIMEOUT_SHUTDOWN);
207
- distributedLock.shutdownHandler().then(() =>
208
- Promise.allSettled([redis.closeMainClient(), redis.closeSubscribeClient()]).then((result) => {
213
+ distributedLock.shutdownHandler().then(() => {
214
+ Promise.allSettled(
215
+ config.redisEnabled ? [redis.closeMainClient(), redis.closeSubscribeClient()] : [Promise.resolve()]
216
+ ).then((result) => {
209
217
  clearTimeout(timeoutRef);
210
218
  resolve(result);
211
- })
212
- );
219
+ });
220
+ });
213
221
  });
214
222
  });
215
223
  };
216
224
 
217
225
  const registerCleanupForDevDb = async () => {
218
- const profile = cds.env.profiles.find((profile) => profile === "development");
219
- if (!profile || process.env.NODE_ENV === "production") {
226
+ if (!config.developmentMode) {
220
227
  return;
221
228
  }
222
-
223
- const tenantIds = await getAllTenantIds();
229
+ const tenantIds = config.isMultiTenancy ? await getAllTenantIds() : [null];
224
230
  for (const tenantId of tenantIds) {
225
231
  await cds.tx({ tenant: tenantId }, async (tx) => {
226
232
  await tx.run(DELETE.from(config.tableNameEventLock));
@@ -252,6 +258,18 @@ const _registerUnsubscribe = () => {
252
258
  });
253
259
  };
254
260
 
261
+ const _disableAdminService = () => {
262
+ if (config.enableAdminService) {
263
+ return;
264
+ }
265
+ cds.on("loaded", (model) => {
266
+ const srvDefinition = model.definitions["EventQueueAdminService"];
267
+ if (srvDefinition) {
268
+ srvDefinition["@protocol"] = "none";
269
+ }
270
+ });
271
+ };
272
+
255
273
  module.exports = {
256
274
  initialize,
257
275
  };
@@ -339,7 +339,7 @@ const _processEvent = async (eventTypeInstance, processContext, key, queueEntrie
339
339
  return eventTypeInstance.handleErrorDuringProcessing(err, queueEntries);
340
340
  }
341
341
  },
342
- { traceContext }
342
+ { traceContext, attributes: { eventIds: queueEntries.map(({ ID }) => ID) } }
343
343
  );
344
344
  };
345
345
 
@@ -22,7 +22,8 @@ const EVENT_QUEUE_RUN_ID = "RUN_ID";
22
22
  const EVENT_QUEUE_RUN_TS = "RUN_TS";
23
23
  const EVENT_QUEUE_RUN_REDIS_CHECK = "RUN_REDIS_CHECK";
24
24
  const EVENT_QUEUE_UPDATE_PERIODIC_EVENTS = "UPDATE_PERIODIC_EVENTS";
25
- let OFFSET_FIRST_RUN = 10 * 1000;
25
+
26
+ let OFFSET_FIRST_RUN;
26
27
 
27
28
  let tenantIdHash;
28
29
  let singleRunDone;
@@ -399,7 +400,7 @@ const _acquireRunId = async (context) => {
399
400
  };
400
401
 
401
402
  const _calculateOffsetForFirstRun = async () => {
402
- let offsetDependingOnLastRun = OFFSET_FIRST_RUN;
403
+ let offsetDependingOnLastRun = OFFSET_FIRST_RUN ?? (config.developmentMode ? 500 : 10 * 1000);
403
404
  const now = Date.now();
404
405
  // NOTE: this is only supported with Redis, because this is a tenant agnostic information
405
406
  // currently there is no proper place to store this information beside t0 schema
@@ -116,21 +116,29 @@ async function executeInNewTransaction(context = {}, transactionTag, fn, args, {
116
116
  }
117
117
  }
118
118
 
119
- const getAllTenantIds = async () => {
119
+ const _getAllTenantBase = async () => {
120
120
  if (!config.isMultiTenancy) {
121
121
  return null;
122
122
  }
123
123
 
124
124
  // NOTE: tmp workaround until cds-mtxs fixes the connect.to service
125
125
  for (let i = 0; i < 10; i++) {
126
- if (cds.services["saas-registry"]) {
126
+ if (cds.services["cds.xt.SaasProvisioningService"] || cds.services["saas-registry"]) {
127
127
  break;
128
128
  }
129
129
  await new Promise((resolve) => setTimeout(resolve, 1000));
130
130
  }
131
131
 
132
132
  const ssp = await cds.connect.to("cds.xt.SaasProvisioningService");
133
- const response = await ssp.get("/tenant");
133
+ return await ssp.get("/tenant");
134
+ };
135
+
136
+ const getAllTenantIds = async () => {
137
+ const response = await _getAllTenantBase();
138
+ if (!response) {
139
+ return null;
140
+ }
141
+
134
142
  return response
135
143
  .map((tenant) => tenant.subscribedTenantId ?? tenant.tenant)
136
144
  .reduce(async (result, tenantId) => {
@@ -142,7 +150,27 @@ const getAllTenantIds = async () => {
142
150
  }, []);
143
151
  };
144
152
 
153
+ const getAllTenantWithSubdomain = async () => {
154
+ const response = await _getAllTenantBase();
155
+ if (!response) {
156
+ return null;
157
+ }
158
+
159
+ return response.reduce(async (result, row) => {
160
+ const tenantId = row.subscribedTenantId ?? row.tenant;
161
+ result = await result;
162
+ if (await common.isTenantIdValidCb(TenantIdCheckTypes.eventProcessing, tenantId)) {
163
+ result.push({
164
+ ID: tenantId,
165
+ subdomain: row.subscribedSubdomain,
166
+ });
167
+ }
168
+ return result;
169
+ }, []);
170
+ };
171
+
145
172
  module.exports = {
146
173
  executeInNewTransaction,
147
174
  getAllTenantIds,
175
+ getAllTenantWithSubdomain,
148
176
  };
@@ -17,7 +17,7 @@ const acquireLock = async (
17
17
  if (config.redisEnabled) {
18
18
  return await _acquireLockRedis(context, fullKey, expiryTime, { keepTrackOfLock });
19
19
  } else {
20
- return await _acquireLockDB(context, fullKey, expiryTime);
20
+ return await _acquireLockDB(context, fullKey, expiryTime, { keepTrackOfLock });
21
21
  }
22
22
  };
23
23
 
@@ -73,7 +73,7 @@ const _acquireLockRedis = async (
73
73
  context,
74
74
  fullKey,
75
75
  expiryTime,
76
- { value = "true", overrideValue = false, keepTrackOfLock } = {}
76
+ { value = Date.now(), overrideValue = false, keepTrackOfLock } = {}
77
77
  ) => {
78
78
  const client = await redis.createMainClientAndConnect(config.redisOptions);
79
79
  const result = await client.set(fullKey, value, {
@@ -82,7 +82,7 @@ const _acquireLockRedis = async (
82
82
  });
83
83
  const isOk = result === REDIS_COMMAND_OK;
84
84
  if (isOk && keepTrackOfLock) {
85
- existingLocks[fullKey] = 1;
85
+ existingLocks[fullKey] = context.tenant;
86
86
  }
87
87
  return isOk;
88
88
  };
@@ -129,9 +129,15 @@ const _releaseLockDb = async (context, fullKey) => {
129
129
  await cdsHelper.executeInNewTransaction(context, "distributedLock-release", async (tx) => {
130
130
  await tx.run(DELETE.from(config.tableNameEventLock).where("code =", fullKey));
131
131
  });
132
+ delete existingLocks[fullKey];
132
133
  };
133
134
 
134
- const _acquireLockDB = async (context, fullKey, expiryTime, { value = "true", overrideValue = false } = {}) => {
135
+ const _acquireLockDB = async (
136
+ context,
137
+ fullKey,
138
+ expiryTime,
139
+ { value = "true", overrideValue = false, keepTrackOfLock } = {}
140
+ ) => {
135
141
  let result;
136
142
  await cdsHelper.executeInNewTransaction(context, "distributedLock-acquire", async (tx) => {
137
143
  try {
@@ -171,6 +177,9 @@ const _acquireLockDB = async (context, fullKey, expiryTime, { value = "true", ov
171
177
  }
172
178
  }
173
179
  });
180
+ if (result && keepTrackOfLock) {
181
+ existingLocks[fullKey] = context.tenant;
182
+ }
174
183
  return result;
175
184
  };
176
185
 
@@ -181,14 +190,58 @@ const _generateKey = (context, tenantScoped, key) => {
181
190
  return `${keyParts.join("##")}`;
182
191
  };
183
192
 
193
+ const getAllLocksRedis = async () => {
194
+ const client = await redis.createMainClientAndConnect(config.redisOptions);
195
+ const keys = await client.keys(`${config.redisOptions.redisNamespace}*`);
196
+ const relevantKeys = {};
197
+ for (const key of keys) {
198
+ const [, tenant, guidOrType, subType] = key.split("##");
199
+ if (subType) {
200
+ relevantKeys[key] = { tenant, guidOrType, subType };
201
+ }
202
+ }
203
+
204
+ if (!Object.keys(relevantKeys)) {
205
+ return [];
206
+ }
207
+
208
+ const pipeline = client.multi();
209
+ for (const key in relevantKeys) {
210
+ pipeline.ttl(key);
211
+ pipeline.get(key);
212
+ }
213
+
214
+ const replies = await pipeline.exec();
215
+
216
+ let counter = 0;
217
+ const result = [];
218
+ for (const value of Object.values(relevantKeys)) {
219
+ const ttl = replies[counter];
220
+ const createdAt = replies[counter + 1];
221
+ result.push({
222
+ tenant: value.tenant,
223
+ type: value.guidOrType,
224
+ subType: value.subType,
225
+ ttl,
226
+ createdAt,
227
+ });
228
+ counter = counter + 2;
229
+ }
230
+ return result;
231
+ };
232
+
184
233
  const shutdownHandler = async () => {
185
234
  const logger = cds.log(COMPONENT_NAME);
186
235
  logger.info("received shutdown event, trying to release all locks", {
187
236
  numberOfLocks: Object.keys(existingLocks).length,
188
237
  });
189
238
  const result = await Promise.allSettled(
190
- Object.keys(existingLocks).map(async (key) => {
191
- await _releaseLockRedis(null, key);
239
+ Object.entries(existingLocks).map(async ([key, tenant]) => {
240
+ if (config.redisEnabled) {
241
+ await _releaseLockRedis({ tenant }, key);
242
+ } else {
243
+ await _releaseLockDb({ tenant }, key);
244
+ }
192
245
  logger.info("lock released", { key });
193
246
  })
194
247
  );
@@ -206,4 +259,5 @@ module.exports = {
206
259
  setValueWithExpire,
207
260
  shutdownHandler,
208
261
  renewLock,
262
+ getAllLocksRedis,
209
263
  };
@@ -85,11 +85,22 @@ const _startOtelTrace = async (ctxWithSpan, traceContext, span, fn) => {
85
85
  const _setAttributes = (context, span, attributes) => {
86
86
  span.setAttribute("sap.tenancy.tenant_id", context.tenant);
87
87
  span.setAttribute("sap.correlation_id", context.id);
88
+ _sanitizeAttributes(attributes);
88
89
  for (const attributeKey in attributes) {
89
90
  span.setAttribute(attributeKey, attributes[attributeKey]);
90
91
  }
91
92
  };
92
93
 
94
+ const _sanitizeAttributes = (attributes = {}) => {
95
+ for (const attributeKey in attributes) {
96
+ attributes[attributeKey] =
97
+ typeof attributes[attributeKey] !== "string"
98
+ ? JSON.stringify(attributes[attributeKey])
99
+ : attributes[attributeKey];
100
+ }
101
+ return attributes;
102
+ };
103
+
93
104
  const getCurrentTraceContext = () => {
94
105
  if (!otel) {
95
106
  return null;
package/srv/index.cds ADDED
@@ -0,0 +1 @@
1
+ using from './service';
@@ -0,0 +1,42 @@
1
+ using sap.eventqueue as db from '../../db';
2
+
3
+ @path: 'event-queue/admin'
4
+ @impl: './admin-service.js'
5
+ @requires: 'internal-user'
6
+ service EventQueueAdminService {
7
+
8
+ @readonly
9
+ @cds.persistence.skip
10
+ entity Event as projection on db.Event {
11
+ null as tenant: String,
12
+ null as landscape: String,
13
+ null as space: String,
14
+ *
15
+ } actions {
16
+ action setStatusAndAttempts(
17
+ // TODO: remove tenant as soon as CAP issue is fixed https://github.tools.sap/cap/issues/issues/18445
18
+ @mandatory
19
+ tenant: String,
20
+ status: db.Status,
21
+ @assert.range: [0,100]
22
+ attempts: Integer) returns Event;
23
+ }
24
+
25
+ @cds.persistence.exists
26
+ entity Lock {
27
+ key tenant: String;
28
+ key type: String;
29
+ key subType: String;
30
+ landscape: String;
31
+ space: String;
32
+ ttl: Integer;
33
+ createdAt: Integer;
34
+ }
35
+
36
+ @readonly
37
+ @cds.persistence.skip
38
+ entity Tenant {
39
+ Key ID: String;
40
+ subdomain: String;
41
+ }
42
+ }
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+
3
+ const cds = require("@sap/cds");
4
+ const cdsHelper = require("../../src/shared/cdsHelper");
5
+ const { EventProcessingStatus } = require("../../src");
6
+ const config = require("../../src/config");
7
+ const distributedLock = require("../../src/shared/distributedLock");
8
+
9
+ module.exports = class AdminService extends cds.ApplicationService {
10
+ async init() {
11
+ const { Event: EventService, Tenant, Lock: LockService } = this.entities();
12
+ const { Event: EventDb } = cds.db.entities("sap.eventqueue");
13
+ const { landscape, space } = this.getLandscapeAndSpace();
14
+
15
+ this.before("*", (req) => {
16
+ if (!config.enableAdminService) {
17
+ req.reject(403, "Admin service is disabled by configuration");
18
+ }
19
+
20
+ if (req.target.name === Tenant.name) {
21
+ return;
22
+ }
23
+ const headers = Object.assign({}, req.headers, req.req?.headers);
24
+ const tenant = headers["z-id"] ?? req.data.tenant;
25
+
26
+ if (config.isMultiTenancy && tenant == null) {
27
+ req.reject(400, "Missing tenant ID in request header (z-id)");
28
+ }
29
+ req.headers ??= {};
30
+ req.headers["z-id"] = tenant;
31
+ });
32
+
33
+ this.on("READ", EventService, async (req) => {
34
+ const tenant = req.headers["z-id"];
35
+ return await cds.tx({ tenant: tenant }, async (tx) => {
36
+ if (req.query.SELECT.from.ref[0].id) {
37
+ req.query.SELECT.from.ref[0].id = EventDb.name;
38
+ } else {
39
+ req.query.SELECT.from.ref[0] = EventDb.name;
40
+ }
41
+ const events = await tx.run(req.query);
42
+ return events?.map((event) => {
43
+ event.landscape = landscape;
44
+ event.space = space;
45
+ event.tenant = tenant;
46
+ return event;
47
+ });
48
+ });
49
+ });
50
+
51
+ this.on("READ", LockService, async () => {
52
+ if (!config.redisEnabled) {
53
+ return [];
54
+ }
55
+ const locks = await distributedLock.getAllLocksRedis();
56
+ return locks.map((lock) => ({
57
+ ...lock,
58
+ landscape: landscape,
59
+ space: space,
60
+ }));
61
+ });
62
+
63
+ this.on("READ", Tenant, async () => {
64
+ const tenants = await cdsHelper.getAllTenantWithSubdomain();
65
+ return tenants ?? [];
66
+ });
67
+
68
+ this.on("setStatusAndAttempts", async (req) => {
69
+ const tenant = req.headers["z-id"];
70
+ cds.log("eventQueue").info("Restarting processing for event queue");
71
+ const updateData = {};
72
+
73
+ if (Number.isInteger(req.data.attempts)) {
74
+ updateData.attempts = req.data.attempts;
75
+ }
76
+
77
+ if (Object.values(EventProcessingStatus).includes(req.data.status)) {
78
+ updateData.status = req.data.status;
79
+ }
80
+
81
+ if (!Object.keys(updateData).length) {
82
+ return req.reject(400, "No status or attempts provided");
83
+ }
84
+
85
+ await cds.tx({ tenant, headers: { "z-id": tenant } }, async () => {
86
+ await UPDATE.entity(EventDb)
87
+ .set(updateData)
88
+ .where({ ID: req.params[0].ID ?? req.params[0] });
89
+ });
90
+ return await this.send(new cds.Request({ query: req.query, headers: req.headers }));
91
+ });
92
+
93
+ await super.init();
94
+ }
95
+
96
+ getLandscapeAndSpace() {
97
+ const url = cds.requires.db.credentials.url;
98
+ if (!url) {
99
+ return { landscape: "eu10-canary", space: "local-dev" };
100
+ }
101
+ const match = url.match(/https?:\/\/[^.]+\.authentication\.([^.]+)\.hana\.ondemand\.com/);
102
+ const landscape = (match?.[1] ?? "sap") === "sap" ? "eu10-canary" : match?.[1];
103
+ let space = "local-dev";
104
+ try {
105
+ space = JSON.parse(process.env.VCAP_APPLICATION)?.space_name;
106
+ } catch {
107
+ /* empty */
108
+ }
109
+ return { landscape, space };
110
+ }
111
+ };
@@ -0,0 +1 @@
1
+ using from './admin-service';