From f9948b57636876d6ec008f1d16de60db39a03bed Mon Sep 17 00:00:00 2001 From: Max Gruenfelder Date: Wed, 10 Sep 2025 13:48:07 +0200 Subject: [PATCH 1/4] add authInfo to align with CDS --- CHANGELOG.md | 6 ++ src/config.js | 10 ++-- src/constants.js | 2 +- src/index.d.ts | 6 +- src/outbox/EventQueueGenericOutboxHandler.js | 4 +- src/redis/redisPub.js | 2 +- src/redis/redisSub.js | 3 +- src/runner/runner.js | 16 ++++-- src/shared/cdsHelper.js | 3 +- src/shared/common.js | 55 ++++++++++--------- test/__snapshots__/common.test.js.snap | 8 +-- .../srv/service/serviceCustomHooks.js | 4 +- test/common.test.js | 42 +++++++------- test/eventQueueOutbox.test.js | 8 +-- 14 files changed, 93 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66ce893b..120bbb1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v1.11.0 - 2025-07-09 + +### Added + +- Added `authInfo` to cds.User as CDS 9.3 deprecated `tokenInfo`. + ## v1.10.10 - 2025-07-09 ### Fixed diff --git a/src/config.js b/src/config.js index e24bfe5f..6e43ac45 100644 --- a/src/config.js +++ b/src/config.js @@ -114,7 +114,7 @@ class Config { #redisNamespace; #publishEventBlockList; #crashOnRedisUnavailable; - #tenantIdFilterTokenInfoCb; + #tenantIdFilterAuthContextCb; #tenantIdFilterEventProcessingCb; #configEvents; #configPeriodicEvents; @@ -770,12 +770,12 @@ class Config { this.#crashOnRedisUnavailable = value; } - get tenantIdFilterTokenInfo() { - return this.#tenantIdFilterTokenInfoCb; + get tenantIdFilterAuthContext() { + return this.#tenantIdFilterAuthContextCb; } - set tenantIdFilterTokenInfo(value) { - this.#tenantIdFilterTokenInfoCb = value; + set tenantIdFilterAuthContext(value) { + this.#tenantIdFilterAuthContextCb = value; } get tenantIdFilterEventProcessing() { diff --git a/src/constants.js b/src/constants.js index 8a112bb6..bea3c775 100644 --- a/src/constants.js +++ b/src/constants.js @@ -22,6 +22,6 @@ module.exports = { }, TenantIdCheckTypes: { eventProcessing: "eventProcessing", - getTokenInfo: "getTokenInfo", + getAuthContext: "getAuthContext", }, }; diff --git a/src/index.d.ts b/src/index.d.ts index 55d77770..b37735b6 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -14,7 +14,7 @@ export declare type EventProcessingStatusType = (typeof EventProcessingStatus)[E export declare const TenantIdCheckTypes: { eventProcessing: "eventProcessing"; - getTokenInfo: "getTokenInfo"; + getAuthContext: "getAuthContext"; }; export declare const TransactionMode: { @@ -215,8 +215,8 @@ declare class Config { get publishEventBlockList(): any; set crashOnRedisUnavailable(value: any); get crashOnRedisUnavailable(): any; - set tenantIdFilterTokenInfo(value: any); - get tenantIdFilterTokenInfo(): any; + set tenantIdFilterAuthContext(value: any); + get tenantIdFilterAuthContext(): any; set tenantIdFilterEventProcessing(value: any); get tenantIdFilterEventProcessing(): any; set runInterval(value: any); diff --git a/src/outbox/EventQueueGenericOutboxHandler.js b/src/outbox/EventQueueGenericOutboxHandler.js index 765c1e29..60d10509 100644 --- a/src/outbox/EventQueueGenericOutboxHandler.js +++ b/src/outbox/EventQueueGenericOutboxHandler.js @@ -327,9 +327,11 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass { } async #setContextUser(context, userId, reg) { + const authInfo = await common.getAuthContext(this.baseContext.tenant); context.user = new cds.User.Privileged({ id: userId, - tokenInfo: await common.getTokenInfo(this.baseContext.tenant), + authInfo, + tokenInfo: authInfo?.token, }); if (reg) { reg.user = context.user; diff --git a/src/redis/redisPub.js b/src/redis/redisPub.js index 508dc6bb..6380bbf8 100644 --- a/src/redis/redisPub.js +++ b/src/redis/redisPub.js @@ -119,7 +119,7 @@ const _processLocalWithoutRedis = async (tenantId, events) => { let context = {}; if (tenantId) { const user = await cds.tx({ tenant: tenantId }, async () => { - return new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(tenantId) }); + return new cds.User.Privileged({ id: config.userId, authContext: await common.getAuthContext(tenantId) }); }); context = { tenant: tenantId, diff --git a/src/redis/redisSub.js b/src/redis/redisSub.js index 52fec84a..857e394e 100644 --- a/src/redis/redisSub.js +++ b/src/redis/redisSub.js @@ -78,7 +78,8 @@ const _messageHandlerProcessEvents = async (messageData) => { } const user = await cds.tx({ tenant: tenantId }, async () => { - return new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(tenantId) }); + const authInfo = await common.getAuthContext(this.baseContext.tenant); + return new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo?.token }); }); const tenantContext = { tenant: tenantId, diff --git a/src/runner/runner.js b/src/runner/runner.js index 882f775e..bc981516 100644 --- a/src/runner/runner.js +++ b/src/runner/runner.js @@ -149,9 +149,11 @@ const _executeEventsAllTenantsRedis = async (tenantIds) => { tx.context, "get-openEvents-and-publish", async () => { + const authInfo = await common.getAuthContext(this.baseContext.tenant); tx.context.user = new cds.User.Privileged({ id: config.userId, - tokenInfo: await common.getTokenInfo(tenantId), + authInfo, + tokenInfo: authInfo?.token, }); const entries = await openEvents.getOpenQueueEntries(tx, false); logger.info("broadcasting events for run", { @@ -187,10 +189,11 @@ const _executeEventsAllTenants = async (tenantIds, runId) => { try { events = await trace( { id, tenant: tenantId }, - "fetch-openEvents-and-tokenInfo", + "fetch-openEvents-and-authInfo", async () => { const user = await cds.tx({ tenant: tenantId }, async () => { - return new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(tenantId) }); + const authInfo = await common.getAuthContext(this.baseContext.tenant); + return new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo?.token }); }); tenantContext = { tenant: tenantId, @@ -258,7 +261,8 @@ const _executePeriodicEventsAllTenants = async (tenantIds) => { for (const tenantId of tenantIds) { try { const user = await cds.tx({ tenant: tenantId }, async () => { - return new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(tenantId) }); + const authInfo = await common.getAuthContext(this.baseContext.tenant); + return new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo?.token }); }); const tenantContext = { tenant: tenantId, @@ -289,7 +293,7 @@ const _singleTenantDb = async () => { const id = cds.utils.uuid(); const events = await trace( { id }, - "fetch-openEvents-and-tokenInfo", + "fetch-openEvents-and-authInfo", async () => { return await cds.tx({}, async (tx) => { return await openEvents.getOpenQueueEntries(tx); @@ -530,7 +534,7 @@ const _checkPeriodicEventsSingleTenant = async (context) => { try { logger.info("executing updating periodic events", { tenantId: context.tenant, - subdomain: context.user?.tokenInfo?.extAttributes?.zdn, + subdomain: context.user?.authInfo?.getSubdomain?.(), }); await periodicEvents.checkAndInsertPeriodicEvents(context); } catch (err) { diff --git a/src/shared/cdsHelper.js b/src/shared/cdsHelper.js index f4908560..01c65a22 100644 --- a/src/shared/cdsHelper.js +++ b/src/shared/cdsHelper.js @@ -25,7 +25,8 @@ async function executeInNewTransaction(context = {}, transactionTag, fn, args, { const logger = cds.log(COMPONENT_NAME); let transactionRollbackPromise = Promise.resolve(false); try { - const user = new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(context.tenant) }); + const authInfo = await common.getAuthContext(this.baseContext.tenant); + const user = new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo?.token }); if (cds.db.kind === "hana") { await cds.tx( { diff --git a/src/shared/common.js b/src/shared/common.js index 200d28f9..ff27e254 100644 --- a/src/shared/common.js +++ b/src/shared/common.js @@ -87,21 +87,22 @@ const processChunkedSync = (inputs, chunkSize, chunkHandler) => { const hashStringTo32Bit = (value) => crypto.createHash("sha256").update(String(value)).digest("base64").slice(0, 32); -const _getNewTokenInfo = async (tenantId) => { - const tokenInfoCache = getTokenInfo._tokenInfoCache; - tokenInfoCache[tenantId] = tokenInfoCache[tenantId] ?? {}; +const _getNewAuthContext = async (tenantId) => { + const authContextCache = getAuthContext._authContextCache; + authContextCache[tenantId] = authContextCache[tenantId] ?? {}; try { - if (!_getNewTokenInfo._xsuaaService) { - _getNewTokenInfo._xsuaaService = new xssec.XsuaaService(cds.requires.auth.credentials); + if (!_getNewAuthContext._xsuaaService) { + _getNewAuthContext._xsuaaService = new xssec.XsuaaService(cds.requires.auth.credentials); } - const authService = _getNewTokenInfo._xsuaaService; + const authService = _getNewAuthContext._xsuaaService; const token = await authService.fetchClientCredentialsToken({ zid: tenantId }); const tokenInfo = new xssec.XsuaaToken(token.access_token); - tokenInfoCache[tenantId].expireTs = tokenInfo.getExpirationDate().getTime() - MARGIN_AUTH_INFO_EXPIRY; - return tokenInfo; + const authInfo = new xssec.XsuaaSecurityContext(authService, tokenInfo); + authContextCache[tenantId].expireTs = tokenInfo.getExpirationDate().getTime() - MARGIN_AUTH_INFO_EXPIRY; + return authInfo; } catch (err) { - tokenInfoCache[tenantId] = null; - cds.log(COMPONENT_NAME).warn("failed to request tokenInfo", { + authContextCache[tenantId] = null; + cds.log(COMPONENT_NAME).warn("failed to request authContext", { err: err.message, responseCode: err.responseCode, responseText: err.responseText, @@ -109,42 +110,44 @@ const _getNewTokenInfo = async (tenantId) => { } }; -const getTokenInfo = async (tenantId) => { - if (!(await isTenantIdValidCb(TenantIdCheckTypes.getTokenInfo, tenantId))) { +const getAuthContext = async (tenantId) => { + if (!(await isTenantIdValidCb(TenantIdCheckTypes.getAuthContext, tenantId))) { return null; } if (!cds.requires?.auth?.credentials) { - return null; // no credentials not tokenInfo + return null; // no credentials not authContext } if (!config.isMultiTenancy) { return null; // does only make sense for multi tenancy } - if (!cds.requires?.auth.kind.match(/jwt|xsuaa/i)) { + if (!cds.requires?.auth.kind.match(/jwt|xsuaa/i) && !cds.requires?.xsuaa) { return null; } - getTokenInfo._tokenInfoCache = getTokenInfo._tokenInfoCache ?? {}; - const tokenInfoCache = getTokenInfo._tokenInfoCache; + getAuthContext._authContextCache = getAuthContext._authContextCache ?? {}; + const authContextCache = getAuthContext._authContextCache; // not existing or existing but expired if ( - !tokenInfoCache[tenantId] || - (tokenInfoCache[tenantId] && tokenInfoCache[tenantId].expireTs && Date.now() > tokenInfoCache[tenantId].expireTs) + !authContextCache[tenantId] || + (authContextCache[tenantId] && + authContextCache[tenantId].expireTs && + Date.now() > authContextCache[tenantId].expireTs) ) { - tokenInfoCache[tenantId] ??= {}; - tokenInfoCache[tenantId].value = _getNewTokenInfo(tenantId); - tokenInfoCache[tenantId].expireTs = null; + authContextCache[tenantId] ??= {}; + authContextCache[tenantId].value = _getNewAuthContext(tenantId); + authContextCache[tenantId].expireTs = null; } - return await tokenInfoCache[tenantId].value; + return await authContextCache[tenantId].value; }; const isTenantIdValidCb = async (checkType, tenantId) => { let cb; switch (checkType) { - case TenantIdCheckTypes.getTokenInfo: - cb = config.tenantIdFilterTokenInfo; + case TenantIdCheckTypes.getAuthContext: + cb = config.tenantIdFilterAuthContext; break; case TenantIdCheckTypes.eventProcessing: cb = config.tenantIdFilterEventProcessing; @@ -167,10 +170,10 @@ module.exports = { isValidDate, processChunkedSync, hashStringTo32Bit, - getTokenInfo, + getAuthContext, isTenantIdValidCb, promiseAllDone, __: { - clearTokenInfoCache: () => (getTokenInfo._tokenInfoCache = {}), + clearAuthContextCache: () => (getAuthContext._authContextCache = {}), }, }; diff --git a/test/__snapshots__/common.test.js.snap b/test/__snapshots__/common.test.js.snap index f528dc93..66267a9c 100644 --- a/test/__snapshots__/common.test.js.snap +++ b/test/__snapshots__/common.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`getTokenInfo should clear cache for error case 1`] = ` +exports[`getAuthContext should clear cache for error case 1`] = ` [ [ - "failed to request tokenInfo", + "failed to request authContext", { "err": "", "responseCode": undefined, @@ -13,10 +13,10 @@ exports[`getTokenInfo should clear cache for error case 1`] = ` ] `; -exports[`getTokenInfo should handle error 1`] = ` +exports[`getAuthContext should handle error 1`] = ` [ [ - "failed to request tokenInfo", + "failed to request authContext", { "err": "", "responseCode": undefined, diff --git a/test/asset/outboxProject/srv/service/serviceCustomHooks.js b/test/asset/outboxProject/srv/service/serviceCustomHooks.js index 8d682407..89773ae1 100644 --- a/test/asset/outboxProject/srv/service/serviceCustomHooks.js +++ b/test/asset/outboxProject/srv/service/serviceCustomHooks.js @@ -30,10 +30,10 @@ class OutboxCustomHooks extends cds.Service { }); } - this.on("tokenInfo", (req) => { + this.on("authInfo", (req) => { cds.log(this.name).info(req.event, { data: req.data, - tokenInfo: req.user.tokenInfo, + authInfo: req.user.authInfo, user: req.user.id, subType: req.eventQueue.processor.eventSubType, }); diff --git a/test/common.test.js b/test/common.test.js index bb72f375..dd6e45fb 100644 --- a/test/common.test.js +++ b/test/common.test.js @@ -8,9 +8,9 @@ jest.mock("@sap/cds", () => ({ jest.mock("@sap/xssec"); const { - getTokenInfo, + getAuthContext, isTenantIdValidCb, - __: { clearTokenInfoCache }, + __: { clearAuthContextCache }, } = require("../src/shared/common"); const xssec = require("@sap/xssec"); const cds = require("@sap/cds"); @@ -18,9 +18,9 @@ const cds = require("@sap/cds"); const tenantId1 = "cc0edebc-58df-44ab-ab1f-1cee383b423e"; const tenantId2 = "61d0f6f5-449d-49c2-980f-cc6b45310b5d"; -describe("getTokenInfo", () => { +describe("getAuthContext", () => { beforeEach(() => { - clearTokenInfoCache(); + clearAuthContextCache(); xssec.XsuaaService.mockRestore(); jest.clearAllMocks(); cds.requires.auth = { @@ -32,19 +32,19 @@ describe("getTokenInfo", () => { it("should return null when no credentials provided", async () => { cds.requires.auth.credentials = null; - const result = await getTokenInfo(tenantId1); + const result = await getAuthContext(tenantId1); expect(result).toBeNull(); expect(cds.log().warn.mock.calls).toHaveLength(0); }); - it("should set and return new TokenInfo", async () => { + it("should set and return new AuthInfo", async () => { jest .spyOn(xssec.XsuaaService.prototype, "fetchClientCredentialsToken") .mockResolvedValueOnce({ access_token: "token" }); jest.spyOn(xssec.XsuaaToken.prototype, "constructor").mockReturnValueOnce({ getExpirationDate: () => new Date(), }); - const result = await getTokenInfo(tenantId1); + const result = await getAuthContext(tenantId1); expect(result).toBeDefined(); expect(cds.log().warn.mock.calls).toHaveLength(0); }); @@ -56,7 +56,7 @@ describe("getTokenInfo", () => { jest.spyOn(xssec.XsuaaToken.prototype, "constructor").mockReturnValueOnce({ getExpirationDate: () => new Date(), }); - const result = await getTokenInfo(tenantId1); + const result = await getAuthContext(tenantId1); expect(result).toBeDefined(); expect(fetchClientCredentialsTokenSpy).toHaveBeenCalledWith({ zid: tenantId1 }); expect(cds.log().warn.mock.calls).toHaveLength(0); @@ -71,10 +71,10 @@ describe("getTokenInfo", () => { getExpirationDate: () => new Date(Date.now() + 61 * 1000), }); - const result = await getTokenInfo(tenantId1); + const result = await getAuthContext(tenantId1); expect(result).toBeDefined(); - const result2 = await getTokenInfo(tenantId1); + const result2 = await getAuthContext(tenantId1); expect(result).toEqual(result2); expect(xssec.XsuaaService.prototype.fetchClientCredentialsToken).toHaveBeenCalledTimes(1); @@ -90,8 +90,8 @@ describe("getTokenInfo", () => { getExpirationDate: () => new Date(Date.now() + 65 * 1000), }); - const resultPromise = getTokenInfo(tenantId1); - const resultPromise2 = getTokenInfo(tenantId1); + const resultPromise = getAuthContext(tenantId1); + const resultPromise2 = getAuthContext(tenantId1); const [result1, result2] = await Promise.all([resultPromise, resultPromise2]); @@ -108,10 +108,10 @@ describe("getTokenInfo", () => { jest.spyOn(xssec.XsuaaToken.prototype, "constructor").mockImplementation(() => ({ getExpirationDate: () => new Date(Date.now() + 59 * 1000), })); - const result = await getTokenInfo(tenantId1); + const result = await getAuthContext(tenantId1); expect(result).toBeDefined(); - const result2 = await getTokenInfo(tenantId1); + const result2 = await getAuthContext(tenantId1); expect(result).not.toEqual(result2); expect(xssec.XsuaaService.prototype.fetchClientCredentialsToken).toHaveBeenCalledTimes(2); @@ -126,10 +126,10 @@ describe("getTokenInfo", () => { jest.spyOn(xssec.XsuaaToken.prototype, "constructor").mockImplementation(() => ({ getExpirationDate: () => new Date(Date.now() + 59 * 1000), })); - const result = await getTokenInfo(tenantId1); + const result = await getAuthContext(tenantId1); expect(result).toBeDefined(); - const result2 = await getTokenInfo(tenantId2); + const result2 = await getAuthContext(tenantId2); expect(result).not.toEqual(result2); expect(xssec.XsuaaService.prototype.fetchClientCredentialsToken).toHaveBeenCalledTimes(2); @@ -139,14 +139,14 @@ describe("getTokenInfo", () => { it("should handle error", async () => { jest.spyOn(xssec.XsuaaService.prototype, "fetchClientCredentialsToken").mockRejectedValueOnce(new Error()); - const result = await getTokenInfo(tenantId1); + const result = await getAuthContext(tenantId1); expect(result).toBeUndefined(); expect(cds.log().warn.mock.calls).toMatchSnapshot(); }); it("should clear cache for error case", async () => { jest.spyOn(xssec.XsuaaService.prototype, "fetchClientCredentialsToken").mockRejectedValueOnce(new Error()); - const result = await getTokenInfo(tenantId1); + const result = await getAuthContext(tenantId1); expect(result).toBeUndefined(); expect(cds.log().warn.mock.calls).toMatchSnapshot(); @@ -156,14 +156,14 @@ describe("getTokenInfo", () => { jest.spyOn(xssec.XsuaaToken.prototype, "constructor").mockReturnValueOnce({ getExpirationDate: () => new Date(Date.now() + 120 * 1000), }); - const result2 = await getTokenInfo(tenantId1); + const result2 = await getAuthContext(tenantId1); expect(result2).toBeDefined(); }); it("two parallel requests should get the same error", async () => { jest.spyOn(xssec.XsuaaService.prototype, "fetchClientCredentialsToken").mockRejectedValueOnce(new Error()); - const resultPromise = getTokenInfo(tenantId1); - const resultPromise2 = getTokenInfo(tenantId1); + const resultPromise = getAuthContext(tenantId1); + const resultPromise2 = getAuthContext(tenantId1); const [result1, result2] = await Promise.all([resultPromise, resultPromise2]); diff --git a/test/eventQueueOutbox.test.js b/test/eventQueueOutbox.test.js index 61b62f9e..d5893ed3 100644 --- a/test/eventQueueOutbox.test.js +++ b/test/eventQueueOutbox.test.js @@ -630,16 +630,16 @@ describe("event-queue outbox", () => { expect(loggerMock.callsLengths().error).toEqual(0); }); - it("check that tokenInfo is correctly exposed on the user", async () => { - jest.spyOn(common, "getTokenInfo").mockResolvedValue({ tokenInfo: 123 }); + it("check that authInfo is correctly exposed on the user", async () => { + jest.spyOn(common, "getAuthContext").mockResolvedValue({ authInfo: 123 }); const service = (await cds.connect.to("OutboxCustomHooks")).tx(context); const data = { to: "to", subject: "subject", body: "body" }; - await service.send("tokenInfo", data); + await service.send("authInfo", data); await commitAndOpenNew(); await testHelper.selectEventQueueAndExpectOpen(tx, { expectedLength: 1 }); await processEventQueue(tx.context, "CAP_OUTBOX", service.name); await commitAndOpenNew(); - expect(loggerMock).actionCalled("tokenInfo", { tokenInfo: { tokenInfo: 123 } }); + expect(loggerMock).actionCalled("authInfo", { authInfo: { authInfo: 123 } }); await testHelper.selectEventQueueAndExpectDone(tx, { expectedLength: 1 }); expect(loggerMock.callsLengths().error).toEqual(0); }); From dc863516bfadda34ea438daeee7de32f1cb12ecf Mon Sep 17 00:00:00 2001 From: Max Gruenfelder Date: Wed, 10 Sep 2025 15:17:16 +0200 Subject: [PATCH 2/4] upgrade cds --- package-lock.json | 315 +++++++++++++++++++++++----------------------- package.json | 8 +- 2 files changed, 160 insertions(+), 163 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7ff3f063..5e533170 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@sap/xssec": "^4.6.0", - "cron-parser": "^5.2.0", + "cron-parser": "^5.3.1", "redis": "^4.7.0", "verror": "^1.10.1", "yaml": "^2.7.1" @@ -18,11 +18,11 @@ "devDependencies": { "@actions/core": "^1.11.1", "@cap-js/cds-test": "^0.4.0", - "@cap-js/hana": "^2.1.0", + "@cap-js/hana": "^2.2.0", "@cap-js/sqlite": "^2.0.1", "@opentelemetry/api": "^1.9.0", - "@sap/cds": "^9.1.0", - "@sap/cds-dk": "^9.1.0", + "@sap/cds": "^9.3.1", + "@sap/cds-dk": "^9.3.1", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^28.6.0", @@ -76,20 +76,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -106,9 +92,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", "dev": true, "license": "MIT", "engines": { @@ -116,22 +102,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -157,14 +143,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -225,15 +211,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -283,27 +269,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", - "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -567,18 +553,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/types": "^7.28.4", "debug": "^4.3.1" }, "engines": { @@ -586,9 +572,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -630,9 +616,9 @@ } }, "node_modules/@cap-js/db-service": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@cap-js/db-service/-/db-service-2.3.0.tgz", - "integrity": "sha512-KkYrnI05bx0PvwP5jxEUos8UQ+XYPucUMQt3i0muQXWJw3rEkWvEArvOwCYjTNnjzHqtVQfpPXsy1+ehkHUrWg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@cap-js/db-service/-/db-service-2.4.0.tgz", + "integrity": "sha512-tWZUkgAPgZQIVu3xownD9cA9joXPI84KDstx3ZezOxXJuRsapgM2QEs0TXwQK5XLG43FUAsGO7wVJPoZ29ZH2Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -643,9 +629,9 @@ } }, "node_modules/@cap-js/hana": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@cap-js/hana/-/hana-2.1.2.tgz", - "integrity": "sha512-QlcBoQt7ChoJUyBEoGWrIeGsBzX5B4lCx0HS4YAnS++14qb14I2xyKwWkdb+D+p42Rh5pIK1cE6xisUhdyr7nQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cap-js/hana/-/hana-2.2.0.tgz", + "integrity": "sha512-UKAnj/KMyZZpYlbqna02E2KXJfz8pKsKLmhjDbExsGk/1Fh34VojxzD0wAYqV/CrTkx4Su25Ru3VarEurMP3kg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -690,9 +676,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -1228,6 +1214,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1398,13 +1395,13 @@ } }, "node_modules/@sap/cds": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-9.2.0.tgz", - "integrity": "sha512-u/R+Bzz8RsiL4JINwwmbPFmSD8Y10PGpXetGHYvUyefUG/V/S8OBAWy/elu4FmTr1X8J4+kuynCgoETaWRveKw==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-9.3.1.tgz", + "integrity": "sha512-xA9EN29X8HKaz83Aod1e3Bs6LRvqcCVoaTvCtyOup5rMYDQumcoZ7UOKH6NIUTeO0zOtafUiMHucMG+dBnq5XQ==", "dev": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@sap/cds-compiler": "^6", + "@sap/cds-compiler": "^6.1", "@sap/cds-fiori": "^2", "js-yaml": "^4.1.0" }, @@ -1430,9 +1427,9 @@ } }, "node_modules/@sap/cds-compiler": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-6.2.2.tgz", - "integrity": "sha512-LqRNfBsWnEJpXVJ6/v52VqAdelWoETLbECMPcy7BfoHE63DGBpKA/ZIgoH11t7vUm7GYQDcSfR7ryy02Q/fP7A==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-6.3.0.tgz", + "integrity": "sha512-4HhZuiKa8SztqtjfM9Iey7yb2U6xQGzUbjib3JXcSSP6AJ6Bz0gViliTwf8dASPTLRtv7HU82277m4uqxU2qjw==", "dev": true, "license": "SEE LICENSE IN LICENSE", "bin": { @@ -1445,9 +1442,9 @@ } }, "node_modules/@sap/cds-dk": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@sap/cds-dk/-/cds-dk-9.2.0.tgz", - "integrity": "sha512-LKGTp5DITk7xJ9nc6OrJZdWGr/p3O2z14ReymbTi8C01AyNm/j0dTJeTLhz52MIaHuWziEFI81ZpfIdeH/Tgdw==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@sap/cds-dk/-/cds-dk-9.3.1.tgz", + "integrity": "sha512-Afa7hg+YwPgSq+tGY7CX2ZOo0decIp0qEyLaDdeXViAVPYZGiUjWp9Zyb8PoNElC5UNkRMDRJ4ZGOKjzf+mM+A==", "dev": true, "hasShrinkwrap": true, "license": "SEE LICENSE IN LICENSE", @@ -1486,8 +1483,8 @@ } }, "node_modules/@sap/cds-dk/node_modules/@cap-js/db-service": { - "version": "2.3.0", - "integrity": "sha512-KkYrnI05bx0PvwP5jxEUos8UQ+XYPucUMQt3i0muQXWJw3rEkWvEArvOwCYjTNnjzHqtVQfpPXsy1+ehkHUrWg==", + "version": "2.4.0", + "integrity": "sha512-tWZUkgAPgZQIVu3xownD9cA9joXPI84KDstx3ZezOxXJuRsapgM2QEs0TXwQK5XLG43FUAsGO7wVJPoZ29ZH2Q==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -1525,8 +1522,8 @@ } }, "node_modules/@sap/cds-dk/node_modules/@eslint/js": { - "version": "9.32.0", - "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "version": "9.35.0", + "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", "extraneous": true, "license": "MIT", "engines": { @@ -1537,12 +1534,12 @@ } }, "node_modules/@sap/cds-dk/node_modules/@sap/cds": { - "version": "9.2.0", - "integrity": "sha512-u/R+Bzz8RsiL4JINwwmbPFmSD8Y10PGpXetGHYvUyefUG/V/S8OBAWy/elu4FmTr1X8J4+kuynCgoETaWRveKw==", + "version": "9.3.1", + "integrity": "sha512-xA9EN29X8HKaz83Aod1e3Bs6LRvqcCVoaTvCtyOup5rMYDQumcoZ7UOKH6NIUTeO0zOtafUiMHucMG+dBnq5XQ==", "dev": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@sap/cds-compiler": "^6", + "@sap/cds-compiler": "^6.1", "@sap/cds-fiori": "^2", "js-yaml": "^4.1.0" }, @@ -1568,8 +1565,8 @@ } }, "node_modules/@sap/cds-dk/node_modules/@sap/cds-compiler": { - "version": "6.2.2", - "integrity": "sha512-LqRNfBsWnEJpXVJ6/v52VqAdelWoETLbECMPcy7BfoHE63DGBpKA/ZIgoH11t7vUm7GYQDcSfR7ryy02Q/fP7A==", + "version": "6.3.0", + "integrity": "sha512-4HhZuiKa8SztqtjfM9Iey7yb2U6xQGzUbjib3JXcSSP6AJ6Bz0gViliTwf8dASPTLRtv7HU82277m4uqxU2qjw==", "dev": true, "license": "SEE LICENSE IN LICENSE", "bin": { @@ -1592,8 +1589,8 @@ } }, "node_modules/@sap/cds-dk/node_modules/@sap/cds-mtxs": { - "version": "3.2.0", - "integrity": "sha512-8X+fK0rsnOaKZpettVAXuzCJT9MRebpuzN1JjNEWPiYVafvIjXIQc2diYgq8rtzhfdFWk6QNlZUjMM7jLgNfsw==", + "version": "3.3.1", + "integrity": "sha512-66yWxkVlqu5FsOHxDF8w2JJSlcbgtT2PTegBSECEq9+fjMOaqlxnBHpXXTODCUgK762Ad3kyCUZqW///gmpuMA==", "dev": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { @@ -2298,8 +2295,8 @@ "license": "MIT" }, "node_modules/@sap/cds-dk/node_modules/follow-redirects": { - "version": "1.15.10", - "integrity": "sha512-V7O/fFKM539IC2bweloFWuoiJ9OtI3W2uIqJPWM8IT5xxNyt73QtvVqmSpcDmk07ivmmlKB+rRY0vpQjIYNtKw==", + "version": "1.15.11", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "dev": true, "funding": [ { @@ -2764,8 +2761,8 @@ "license": "MIT" }, "node_modules/@sap/cds-dk/node_modules/node-abi": { - "version": "3.75.0", - "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "version": "3.77.0", + "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==", "dev": true, "license": "MIT", "optional": true, @@ -3438,8 +3435,8 @@ } }, "node_modules/@sap/cds-dk/node_modules/yaml": { - "version": "2.8.0", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "version": "2.8.1", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", "bin": { @@ -3604,9 +3601,9 @@ } }, "node_modules/@types/node": { - "version": "24.2.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", - "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", + "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", "dev": true, "license": "MIT", "dependencies": { @@ -3638,14 +3635,14 @@ "license": "MIT" }, "node_modules/@typescript-eslint/project-service": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz", - "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.43.0.tgz", + "integrity": "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.39.1", - "@typescript-eslint/types": "^8.39.1", + "@typescript-eslint/tsconfig-utils": "^8.43.0", + "@typescript-eslint/types": "^8.43.0", "debug": "^4.3.4" }, "engines": { @@ -3660,14 +3657,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz", - "integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.43.0.tgz", + "integrity": "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1" + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/visitor-keys": "8.43.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3678,9 +3675,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz", - "integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.43.0.tgz", + "integrity": "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==", "dev": true, "license": "MIT", "engines": { @@ -3695,9 +3692,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz", - "integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.43.0.tgz", + "integrity": "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==", "dev": true, "license": "MIT", "engines": { @@ -3709,16 +3706,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz", - "integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.43.0.tgz", + "integrity": "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.39.1", - "@typescript-eslint/tsconfig-utils": "8.39.1", - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1", + "@typescript-eslint/project-service": "8.43.0", + "@typescript-eslint/tsconfig-utils": "8.43.0", + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/visitor-keys": "8.43.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -3764,16 +3761,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz", - "integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.43.0.tgz", + "integrity": "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.39.1", - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/typescript-estree": "8.39.1" + "@typescript-eslint/scope-manager": "8.43.0", + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/typescript-estree": "8.43.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3788,13 +3785,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz", - "integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.43.0.tgz", + "integrity": "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/types": "8.43.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -4333,9 +4330,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", - "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", "dev": true, "funding": [ { @@ -4353,8 +4350,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001733", - "electron-to-chromium": "^1.5.199", + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -4522,9 +4519,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001735", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", - "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", + "version": "1.0.30001741", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", + "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", "dev": true, "funding": [ { @@ -4846,12 +4843,12 @@ } }, "node_modules/cron-parser": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-5.3.0.tgz", - "integrity": "sha512-IS4mnFu6n3CFgEmXjr+B2zzGHsjJmHEdN+BViKvYSiEn3KWss9ICRDETDX/VZldiW82B94OyAZm4LIT4vcKK0g==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-5.3.1.tgz", + "integrity": "sha512-Mu5Jk1b4cUfY8u34+thI9TZxvQiuhaMBS2Ag84rOSoHlU33xtIPkXwr6lWuw3XPmxSxq317B+hl0o4J+LdhwNg==", "license": "MIT", "dependencies": { - "luxon": "^3.6.1" + "luxon": "^3.7.1" }, "engines": { "node": ">=18" @@ -4906,9 +4903,9 @@ } }, "node_modules/dedent": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -5065,9 +5062,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.200", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.200.tgz", - "integrity": "sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==", + "version": "1.5.215", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.215.tgz", + "integrity": "sha512-TIvGp57UpeNetj/wV/xpFNpWGb0b/ROw372lHPx5Aafx02gjTBtWnEEcaSX3W2dLM3OSdGGyHX/cHl01JQsLaQ==", "dev": true, "license": "ISC" }, @@ -6651,9 +6648,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -7425,9 +7422,9 @@ } }, "node_modules/luxon": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz", - "integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", "license": "MIT", "engines": { "node": ">=12" @@ -7830,9 +7827,9 @@ } }, "node_modules/node-abi": { - "version": "3.75.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", - "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "version": "3.77.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz", + "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7883,9 +7880,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.20.tgz", + "integrity": "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index a6cf31ec..6a96da38 100644 --- a/package.json +++ b/package.json @@ -48,17 +48,17 @@ }, "dependencies": { "@sap/xssec": "^4.6.0", - "cron-parser": "^5.2.0", + "cron-parser": "^5.3.1", "redis": "^4.7.0", "verror": "^1.10.1", "yaml": "^2.7.1" }, "devDependencies": { "@cap-js/cds-test": "^0.4.0", - "@cap-js/hana": "^2.1.0", + "@cap-js/hana": "^2.2.0", "@cap-js/sqlite": "^2.0.1", - "@sap/cds": "^9.1.0", - "@sap/cds-dk": "^9.1.0", + "@sap/cds": "^9.3.1", + "@sap/cds-dk": "^9.3.1", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^28.6.0", From c838af822e1a2f68be0d1a3cce6bac50e4dd99ad Mon Sep 17 00:00:00 2001 From: Max Gruenfelder Date: Wed, 10 Sep 2025 15:21:45 +0200 Subject: [PATCH 3/4] wip --- src/outbox/EventQueueGenericOutboxHandler.js | 2 +- src/redis/redisPub.js | 3 ++- src/redis/redisSub.js | 2 +- src/runner/runner.js | 6 +++--- src/shared/cdsHelper.js | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/outbox/EventQueueGenericOutboxHandler.js b/src/outbox/EventQueueGenericOutboxHandler.js index 60d10509..09e7eda0 100644 --- a/src/outbox/EventQueueGenericOutboxHandler.js +++ b/src/outbox/EventQueueGenericOutboxHandler.js @@ -327,7 +327,7 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass { } async #setContextUser(context, userId, reg) { - const authInfo = await common.getAuthContext(this.baseContext.tenant); + const authInfo = await common.getAuthContext(context.tenant); context.user = new cds.User.Privileged({ id: userId, authInfo, diff --git a/src/redis/redisPub.js b/src/redis/redisPub.js index 6380bbf8..fd935d59 100644 --- a/src/redis/redisPub.js +++ b/src/redis/redisPub.js @@ -119,7 +119,8 @@ const _processLocalWithoutRedis = async (tenantId, events) => { let context = {}; if (tenantId) { const user = await cds.tx({ tenant: tenantId }, async () => { - return new cds.User.Privileged({ id: config.userId, authContext: await common.getAuthContext(tenantId) }); + const authInfo = await common.getAuthContext(tenantId); + return new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo.token }); }); context = { tenant: tenantId, diff --git a/src/redis/redisSub.js b/src/redis/redisSub.js index 857e394e..7bf316df 100644 --- a/src/redis/redisSub.js +++ b/src/redis/redisSub.js @@ -78,7 +78,7 @@ const _messageHandlerProcessEvents = async (messageData) => { } const user = await cds.tx({ tenant: tenantId }, async () => { - const authInfo = await common.getAuthContext(this.baseContext.tenant); + const authInfo = await common.getAuthContext(tenantId); return new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo?.token }); }); const tenantContext = { diff --git a/src/runner/runner.js b/src/runner/runner.js index bc981516..c59839a6 100644 --- a/src/runner/runner.js +++ b/src/runner/runner.js @@ -149,7 +149,7 @@ const _executeEventsAllTenantsRedis = async (tenantIds) => { tx.context, "get-openEvents-and-publish", async () => { - const authInfo = await common.getAuthContext(this.baseContext.tenant); + const authInfo = await common.getAuthContext(tenantId); tx.context.user = new cds.User.Privileged({ id: config.userId, authInfo, @@ -192,7 +192,7 @@ const _executeEventsAllTenants = async (tenantIds, runId) => { "fetch-openEvents-and-authInfo", async () => { const user = await cds.tx({ tenant: tenantId }, async () => { - const authInfo = await common.getAuthContext(this.baseContext.tenant); + const authInfo = await common.getAuthContext(tenantId); return new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo?.token }); }); tenantContext = { @@ -261,7 +261,7 @@ const _executePeriodicEventsAllTenants = async (tenantIds) => { for (const tenantId of tenantIds) { try { const user = await cds.tx({ tenant: tenantId }, async () => { - const authInfo = await common.getAuthContext(this.baseContext.tenant); + const authInfo = await common.getAuthContext(tenantId); return new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo?.token }); }); const tenantContext = { diff --git a/src/shared/cdsHelper.js b/src/shared/cdsHelper.js index 01c65a22..4475342a 100644 --- a/src/shared/cdsHelper.js +++ b/src/shared/cdsHelper.js @@ -25,7 +25,7 @@ async function executeInNewTransaction(context = {}, transactionTag, fn, args, { const logger = cds.log(COMPONENT_NAME); let transactionRollbackPromise = Promise.resolve(false); try { - const authInfo = await common.getAuthContext(this.baseContext.tenant); + const authInfo = await common.getAuthContext(context.tenant); const user = new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo?.token }); if (cds.db.kind === "hana") { await cds.tx( From 5e7164aebcdd00596e9709c630d12708d09353a8 Mon Sep 17 00:00:00 2001 From: Max Gruenfelder Date: Wed, 10 Sep 2025 16:44:21 +0200 Subject: [PATCH 4/4] wip --- src/shared/common.js | 26 ++----- src/shared/lazyCache.js | 148 ++++++++++++++++++++++++++++++++++++++++ test/common.test.js | 4 +- 3 files changed, 156 insertions(+), 22 deletions(-) create mode 100644 src/shared/lazyCache.js diff --git a/src/shared/common.js b/src/shared/common.js index ff27e254..6922ac08 100644 --- a/src/shared/common.js +++ b/src/shared/common.js @@ -7,6 +7,7 @@ const xssec = require("@sap/xssec"); const VError = require("verror"); const config = require("../config"); +const { ExpiringLazyCache } = require("./lazyCache"); const { TenantIdCheckTypes } = require("../constants"); const MARGIN_AUTH_INFO_EXPIRY = 60 * 1000; @@ -88,8 +89,6 @@ const processChunkedSync = (inputs, chunkSize, chunkHandler) => { const hashStringTo32Bit = (value) => crypto.createHash("sha256").update(String(value)).digest("base64").slice(0, 32); const _getNewAuthContext = async (tenantId) => { - const authContextCache = getAuthContext._authContextCache; - authContextCache[tenantId] = authContextCache[tenantId] ?? {}; try { if (!_getNewAuthContext._xsuaaService) { _getNewAuthContext._xsuaaService = new xssec.XsuaaService(cds.requires.auth.credentials); @@ -98,15 +97,14 @@ const _getNewAuthContext = async (tenantId) => { const token = await authService.fetchClientCredentialsToken({ zid: tenantId }); const tokenInfo = new xssec.XsuaaToken(token.access_token); const authInfo = new xssec.XsuaaSecurityContext(authService, tokenInfo); - authContextCache[tenantId].expireTs = tokenInfo.getExpirationDate().getTime() - MARGIN_AUTH_INFO_EXPIRY; - return authInfo; + return [tokenInfo.getExpirationDate().getTime() - Date.now(), authInfo]; } catch (err) { - authContextCache[tenantId] = null; cds.log(COMPONENT_NAME).warn("failed to request authContext", { err: err.message, responseCode: err.responseCode, responseText: err.responseText, }); + return [0, null]; } }; @@ -127,20 +125,8 @@ const getAuthContext = async (tenantId) => { return null; } - getAuthContext._authContextCache = getAuthContext._authContextCache ?? {}; - const authContextCache = getAuthContext._authContextCache; - // not existing or existing but expired - if ( - !authContextCache[tenantId] || - (authContextCache[tenantId] && - authContextCache[tenantId].expireTs && - Date.now() > authContextCache[tenantId].expireTs) - ) { - authContextCache[tenantId] ??= {}; - authContextCache[tenantId].value = _getNewAuthContext(tenantId); - authContextCache[tenantId].expireTs = null; - } - return await authContextCache[tenantId].value; + getAuthContext._cache = getAuthContext._cache ?? new ExpiringLazyCache({ expirationGap: MARGIN_AUTH_INFO_EXPIRY }); + return await getAuthContext._cache.getSetCb(tenantId, async () => _getNewAuthContext(tenantId)); }; const isTenantIdValidCb = async (checkType, tenantId) => { @@ -174,6 +160,6 @@ module.exports = { isTenantIdValidCb, promiseAllDone, __: { - clearAuthContextCache: () => (getAuthContext._authContextCache = {}), + clearAuthContextCache: () => getAuthContext._cache?.clear(), }, }; diff --git a/src/shared/lazyCache.js b/src/shared/lazyCache.js new file mode 100644 index 00000000..1ec8d7a8 --- /dev/null +++ b/src/shared/lazyCache.js @@ -0,0 +1,148 @@ +"use strict"; + +const DEFAULT_SEPARATOR = "##"; +const DEFAULT_EXPIRATION_GAP = 5000; // 5 seconds + +class LazyCache { + constructor({ separator = DEFAULT_SEPARATOR } = {}) { + this.__data = Object.create(null); + this.__separator = separator; + } + _separator() { + return this.__separator; + } + _data() { + return this.__data; + } + async _dataSettled() { + return await Object.entries(this.__data).reduce(async (result, [key, value]) => { + (await result)[key] = await value; + return result; + }, Promise.resolve({})); + } + _key(keyOrKeys) { + return Array.isArray(keyOrKeys) ? keyOrKeys.join(this.__separator) : keyOrKeys; + } + has(keyOrKeys) { + return Object.prototype.hasOwnProperty.call(this.__data, this._key(keyOrKeys)); + } + get(keyOrKeys) { + return this.__data[this._key(keyOrKeys)]; + } + set(keyOrKeys, value) { + this.__data[this._key(keyOrKeys)] = value; + return this; + } + setCb(keyOrKeys, callback) { + const resultOrPromise = callback(); + return this.set( + keyOrKeys, + resultOrPromise instanceof Promise + ? resultOrPromise.catch((err) => { + this.delete(keyOrKeys); + return Promise.reject(err); + }) + : resultOrPromise + ); + } + getSetCb(keyOrKeys, callback) { + const key = this._key(keyOrKeys); + if (!this.has(key)) { + this.setCb(key, callback); + } + return this.get(key); + } + count() { + return Object.keys(this.__data).length; + } + delete(keyOrKeys) { + Reflect.deleteProperty(this.__data, this._key(keyOrKeys)); + return this; + } + clear() { + this.__data = Object.create(null); + } +} + +class ExpiringLazyCache extends LazyCache { + constructor({ separator = DEFAULT_SEPARATOR, expirationGap = DEFAULT_EXPIRATION_GAP } = {}) { + super({ separator }); + this.__expirationGap = expirationGap; + } + _expiringGap() { + return this.__expirationGap; + } + _isValid(expirationTime, currentTime = Date.now()) { + return expirationTime && currentTime <= expirationTime; + } + has(keyOrKeys, currentTime = Date.now()) { + if (!super.has(keyOrKeys)) { + return false; + } + const [expirationTime] = super.get(keyOrKeys) ?? []; + return this._isValid(expirationTime, currentTime); + } + get(keyOrKeys, currentTime = Date.now()) { + const [expirationTime, value] = super.get(keyOrKeys) ?? []; + return this._isValid(expirationTime, currentTime) ? value : undefined; + } + // NOTE the expiration gap is substracted here, because we want to expire a + // little earlier than necessary. + // NOTE if the expiration is _less_ than the gap, the value is never valid, + // we still need to call set, because we want getSetCb to always return + // when the callback is used. + set(keyOrKeys, expiration, value, currentTime = Date.now()) { + return super.set(keyOrKeys, [currentTime + expiration - this.__expirationGap, value]); + } + + static _extract(result, extractor) { + if (!extractor) { + return result; + } + const { expiration, expiry, value, result: extractedResult } = extractor(result); + return [expiration ?? expiry, value ?? extractedResult]; + } + + // NOTE callback can either return a pair [expiration, value] or use an extractor to extract the right values from + // the callback's result + setCb(keyOrKeys, callback, { currentTime = Date.now(), extractor } = {}) { + const resultOrPromise = callback(); + if (!(resultOrPromise instanceof Promise)) { + const [expiration, value] = ExpiringLazyCache._extract(resultOrPromise, extractor); + return this.set(keyOrKeys, expiration, value, currentTime); + } + return this.set( + keyOrKeys, + Infinity, + resultOrPromise + .catch((err) => { + this.delete(keyOrKeys); + return Promise.reject(err); + }) + .then((result) => { + const [expiration, value] = ExpiringLazyCache._extract(result, extractor); + this.set(keyOrKeys, expiration, value, currentTime); + return value; + }) + ); + } + + // NOTE callback can either return a pair [expiration, value] or use an extractor to extract the right values from + // the callback's result + getSetCb(keyOrKeys, callback, { currentTime = Date.now(), extractor } = {}) { + const key = this._key(keyOrKeys); + if (!this.has(key, currentTime) || !super.has(key)) { + this.setCb(key, callback, { currentTime, extractor }); + const [, value] = super.get(key); + return value; + } + return this.get(key, currentTime); + } +} + +module.exports = { + DEFAULT_EXPIRATION_GAP, + DEFAULT_SEPARATOR, + LazyCache, + ExpiringLazyCache, +}; diff --git a/test/common.test.js b/test/common.test.js index dd6e45fb..769c51ae 100644 --- a/test/common.test.js +++ b/test/common.test.js @@ -140,14 +140,14 @@ describe("getAuthContext", () => { it("should handle error", async () => { jest.spyOn(xssec.XsuaaService.prototype, "fetchClientCredentialsToken").mockRejectedValueOnce(new Error()); const result = await getAuthContext(tenantId1); - expect(result).toBeUndefined(); + expect(result).toBeNull(); expect(cds.log().warn.mock.calls).toMatchSnapshot(); }); it("should clear cache for error case", async () => { jest.spyOn(xssec.XsuaaService.prototype, "fetchClientCredentialsToken").mockRejectedValueOnce(new Error()); const result = await getAuthContext(tenantId1); - expect(result).toBeUndefined(); + expect(result).toBeNull(); expect(cds.log().warn.mock.calls).toMatchSnapshot(); jest