From 02ebbac5bda7a13dee7804cec2d600e3e8fbd690 Mon Sep 17 00:00:00 2001 From: Buse Halis Date: Wed, 27 May 2026 11:19:38 +0200 Subject: [PATCH 01/19] feat: add NotificationTemplateProviderService as remote service --- cds-feature-notifications/pom.xml | 7 +- .../NotificationServiceConfiguration.java | 24 + .../src/main/resources/application.yaml | 28 +- .../NotificationTemplateProviderService.cds | 50 + .../cds-feature-notifications/index.cds | 3 +- .../src/test/java/com/sap/cds/EmptyTest.java | 17 - .../NotificationTemplateProviderService.cds | 50 + .../NotificationTemplateProviderService.xml | 1 + package-lock.json | 2182 ++++------------- sample-app/srv/pom.xml | 2 + 10 files changed, 679 insertions(+), 1685 deletions(-) create mode 100644 cds-feature-notifications/src/main/resources/cds/com.sap.cds/cds-feature-notifications/NotificationTemplateProviderService.cds delete mode 100644 cds-feature-notifications/src/test/java/com/sap/cds/EmptyTest.java create mode 100644 cds-feature-notifications/srv/external/NotificationTemplateProviderService.cds create mode 100644 cds-feature-notifications/srv/external/NotificationTemplateProviderService.xml diff --git a/cds-feature-notifications/pom.xml b/cds-feature-notifications/pom.xml index 89ae9eb..6040ea5 100644 --- a/cds-feature-notifications/pom.xml +++ b/cds-feature-notifications/pom.xml @@ -1,5 +1,4 @@ - - + 4.0.0 @@ -151,7 +150,7 @@ generate-sources - compile ${project.basedir}/srv/external/*.cds ${project.basedir}/src/main/resources/cds/com.sap.cds/cds-feature-notifications/types.cds --to csn --dest ${project.build.directory}/cds-output/all.csn + compile ${project.basedir}/srv/external/*.cds --to csn --dest ${project.build.directory}/cds-output/all.csn @@ -194,4 +193,4 @@ - + \ No newline at end of file diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/NotificationServiceConfiguration.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/NotificationServiceConfiguration.java index d63c86f..35e1b34 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/NotificationServiceConfiguration.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/NotificationServiceConfiguration.java @@ -5,6 +5,8 @@ import cds.gen.notificationproviderservice.NotificationProviderService; import cds.gen.notificationproviderservice.NotificationProviderService_; +import cds.gen.notificationtemplateproviderservice.NotificationTemplateProviderService; +import cds.gen.notificationtemplateproviderservice.NotificationTemplateProviderService_; import cds.gen.notificationtypeproviderservice.NotificationTypeProviderService; import cds.gen.notificationtypeproviderservice.NotificationTypeProviderService_; import com.sap.cds.notifications.handlers.EntityNotificationHandler; @@ -186,5 +188,27 @@ public void environment(CdsRuntimeConfigurer configurer) { .getRemote() .getServices() .put("NotificationTypeProviderService", notificationTypeConfig); + + // Define the remote service for notification templates in CDS runtime that uses the + // SAP_Notifications destination + RemoteServiceConfig notificationTemplateConfig = new RemoteServiceConfig(); + + notificationTemplateConfig.setType("odata-v2"); + + notificationTemplateConfig.getDestination().setName("SAP_Notifications"); + + notificationTemplateConfig.getHttp().setSuffix("/odatav2"); + notificationTemplateConfig.getHttp().setService("NotificationTemplate.svc"); + notificationTemplateConfig.getHttp().getCsrf().setEnabled(true); + + // Register the remote service in CDS runtime environment under the name + // "NotificationTemplateProviderService" + configurer + .getCdsRuntime() + .getEnvironment() + .getCdsProperties() + .getRemote() + .getServices() + .put("NotificationTemplateProviderService", notificationTemplateConfig); } } diff --git a/cds-feature-notifications/src/main/resources/application.yaml b/cds-feature-notifications/src/main/resources/application.yaml index 7ed0f30..5616d3f 100644 --- a/cds-feature-notifications/src/main/resources/application.yaml +++ b/cds-feature-notifications/src/main/resources/application.yaml @@ -4,14 +4,20 @@ spring: config: activate: on-profile: default -#cds: -# remote: -# services: -# '[NotificationProviderService]': -# destination: -# name: NotificationProviderService -# type: odata-v2 -# '[NotificationTypeProviderService]': -# destination: -# name: NotificationTypeProviderService -# type: odata-v2 + +cds: + remote: + services: + '[NotificationProviderService]': + destination: + name: NotificationProviderService + type: odata-v2 + '[NotificationTypeProviderService]': + destination: + name: NotificationTypeProviderService + type: odata-v2 + '[NotificationTemplateProviderService]': + destination: + name: NotificationTemplateProviderService + type: odata-v2 + diff --git a/cds-feature-notifications/src/main/resources/cds/com.sap.cds/cds-feature-notifications/NotificationTemplateProviderService.cds b/cds-feature-notifications/src/main/resources/cds/com.sap.cds/cds-feature-notifications/NotificationTemplateProviderService.cds new file mode 100644 index 0000000..28e6b14 --- /dev/null +++ b/cds-feature-notifications/src/main/resources/cds/com.sap.cds/cds-feature-notifications/NotificationTemplateProviderService.cds @@ -0,0 +1,50 @@ +/* checksum : cac3625183c69a17811b517c0646c808 */ +@cds.external : true +@m.IsDefaultEntityContainer : 'true' +service NotificationTemplateProviderService { + @cds.external : true + @cds.persistence.skip : true + entity NotificationTemplates { + key ![Key] : String not null; + CreatedAt : Integer64; + UpdatedAt : Integer64; + PropertiesSchema : String; + OwnerId : String; + Visibility : String; + BasedOn : String; + SanitizeEmails : Boolean; + Translations : Association to many Translations { }; + Tags : Association to many Tags { }; + }; + + @cds.external : true + @cds.persistence.skip : true + entity Translations { + key Language : String not null; + Syntax : String; + Title : String; + Body : String; + Preview : String; + Email : Email; + Description : String; + DisplayName : String; + Source : String; + Event : String; + }; + + @cds.external : true + @cds.persistence.skip : true + entity Tags { + key ![Key] : String not null; + Value : String; + }; + + @cds.external : true + type Email { + Subject : String; + BodyText : String; + BodyHtml : String; + SenderName : String; + }; +}; + diff --git a/cds-feature-notifications/src/main/resources/cds/com.sap.cds/cds-feature-notifications/index.cds b/cds-feature-notifications/src/main/resources/cds/com.sap.cds/cds-feature-notifications/index.cds index 458f1c4..9c3a8cf 100644 --- a/cds-feature-notifications/src/main/resources/cds/com.sap.cds/cds-feature-notifications/index.cds +++ b/cds-feature-notifications/src/main/resources/cds/com.sap.cds/cds-feature-notifications/index.cds @@ -1,2 +1,3 @@ using from './NotificationProviderService.cds'; -using from './NotificationTypeProviderService.cds'; \ No newline at end of file +using from './NotificationTypeProviderService.cds'; +using from './NotificationTemplateProviderService.cds'; \ No newline at end of file diff --git a/cds-feature-notifications/src/test/java/com/sap/cds/EmptyTest.java b/cds-feature-notifications/src/test/java/com/sap/cds/EmptyTest.java deleted file mode 100644 index ee0f85c..0000000 --- a/cds-feature-notifications/src/test/java/com/sap/cds/EmptyTest.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * © 2026 SAP SE or an SAP affiliate company and cds-feature-notifications contributors. - */ -package com.sap.cds; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -/** Unit test for App. */ -class EmptyTest { - - @Test - void testAppMain() { - assertTrue(true); - } -} diff --git a/cds-feature-notifications/srv/external/NotificationTemplateProviderService.cds b/cds-feature-notifications/srv/external/NotificationTemplateProviderService.cds new file mode 100644 index 0000000..28e6b14 --- /dev/null +++ b/cds-feature-notifications/srv/external/NotificationTemplateProviderService.cds @@ -0,0 +1,50 @@ +/* checksum : cac3625183c69a17811b517c0646c808 */ +@cds.external : true +@m.IsDefaultEntityContainer : 'true' +service NotificationTemplateProviderService { + @cds.external : true + @cds.persistence.skip : true + entity NotificationTemplates { + key ![Key] : String not null; + CreatedAt : Integer64; + UpdatedAt : Integer64; + PropertiesSchema : String; + OwnerId : String; + Visibility : String; + BasedOn : String; + SanitizeEmails : Boolean; + Translations : Association to many Translations { }; + Tags : Association to many Tags { }; + }; + + @cds.external : true + @cds.persistence.skip : true + entity Translations { + key Language : String not null; + Syntax : String; + Title : String; + Body : String; + Preview : String; + Email : Email; + Description : String; + DisplayName : String; + Source : String; + Event : String; + }; + + @cds.external : true + @cds.persistence.skip : true + entity Tags { + key ![Key] : String not null; + Value : String; + }; + + @cds.external : true + type Email { + Subject : String; + BodyText : String; + BodyHtml : String; + SenderName : String; + }; +}; + diff --git a/cds-feature-notifications/srv/external/NotificationTemplateProviderService.xml b/cds-feature-notifications/srv/external/NotificationTemplateProviderService.xml new file mode 100644 index 0000000..d1a14fc --- /dev/null +++ b/cds-feature-notifications/srv/external/NotificationTemplateProviderService.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d7ff780..662cafe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "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==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -33,6 +33,19 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", @@ -44,15 +57,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -71,7 +84,7 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/core": { + "node_modules/@eslint/config-helpers/node_modules/@eslint/core": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", @@ -84,21 +97,34 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -109,9 +135,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -132,43 +158,57 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0", + "@eslint/core": "^1.2.1", "levn": "^0.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -197,74 +237,23 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@sap/cds": { - "version": "9.4.4", - "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-9.4.4.tgz", - "integrity": "sha512-JJCHeEJF4xzFyZSf2ToocvVE9dyHfNLTRXOauOxlmpfyaLg97G7Qp+L4bD132eB0onBG9bQj3eH8DzBm0hVvIw==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "peer": true, - "dependencies": { - "@sap/cds-compiler": "^6.3", - "@sap/cds-fiori": "^2", - "js-yaml": "^4.1.0" - }, - "bin": { - "cds-deploy": "bin/deploy.js", - "cds-serve": "bin/serve.js" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@eslint/js": "^9", - "express": "^4", - "tar": "^7" - }, - "peerDependenciesMeta": { - "express": { - "optional": true - }, - "tar": { - "optional": true - } - } - }, - "node_modules/@sap/cds-compiler": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-6.4.6.tgz", - "integrity": "sha512-auAjRh9t0KKj4LiGAr/fxikZRIngx9YXVHTJWf0LeaGv0ZpYOi6iWbSnU1XRB2e6hsf+Ou1w5oTOHooC5sZfog==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "peer": true, - "bin": { - "cdsc": "bin/cdsc.js", - "cdshi": "bin/cdshi.js", - "cdsse": "bin/cdsse.js" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/@sap/cds-dk": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/@sap/cds-dk/-/cds-dk-9.4.2.tgz", - "integrity": "sha512-umNz4D30a/Oh3cFQU1wK8rxoIOBQrBiUs/k6OfG2ibQaF/rGOXz8N8lGzkY7pC535RJbtJ31OwNjKAcLGrchVQ==", + "version": "9.9.1", + "resolved": "https://registry.npmjs.org/@sap/cds-dk/-/cds-dk-9.9.1.tgz", + "integrity": "sha512-cZoHI/ZhEVffmLo2k9Y/HMR5X+aGCpk60PwJJcZgoat8Kwk6dDl3mUDERhZORQUhp9FwOiyWmNujmNCV8YWWCg==", "dev": true, "hasShrinkwrap": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { "@cap-js/asyncapi": "^1.0.0", "@cap-js/openapi": "^1.0.0", - "@sap/cds": ">=8.3", - "@sap/cds-mtxs": ">=2", + "@sap/cds": "^8.3 || ^9", + "@sap/cds-mtxs": "^2 || ^3", "@sap/hdi-deploy": "^5", - "axios": "^1", - "express": "^4.17.3", - "hdb": "^0", + "express": "^4.22.1 || ^5", + "hdb": "^2.0.0", "livereload-js": "^4.0.1", "mustache": "^4.0.1", - "node-watch": ">=0.7", "ws": "^8.4.2", "xml-js": "^1.6.11", "yaml": "^2" @@ -288,8 +277,8 @@ } }, "node_modules/@sap/cds-dk/node_modules/@cap-js/db-service": { - "version": "2.5.1", - "integrity": "sha512-dpz9lvOepcXOgE8uTs9abCotLcCt1odpwlrUT4AW2pnSrMB0EVMRbHKVIlufmrm8Kh/Y5c2d6ODald8eahsEDw==", + "version": "2.11.0", + "integrity": "sha512-sl33LcxZYAJgMCQZDw4lMGe4kWYq6685Xc6ze4qcoM+rd6aqiyVsSC6C7XH5yerXs7cVHhRC+Dgo8AsaapFzlQ==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -297,12 +286,12 @@ "generic-pool": "^3.9.0" }, "peerDependencies": { - "@sap/cds": ">=9" + "@sap/cds": ">=9.8" } }, "node_modules/@sap/cds-dk/node_modules/@cap-js/openapi": { - "version": "1.2.3", - "integrity": "sha512-UnEUBrBIjMvYYJTtAmSrnWLKIjnaK9KcCS6pPoVBRgZrMaL0bl/aB3KMH4xzc6LWjtbxzlyI71XC7No4+SKerg==", + "version": "1.4.0", + "integrity": "sha512-/LRSwn4SDxAi3qKwl09zoOhEVGaPGlYOPz/0S3UBnaMJVvaLyPiKbbaOtOnrrgulUX5OXt+ujPIQznOsbTzuAw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -313,41 +302,55 @@ } }, "node_modules/@sap/cds-dk/node_modules/@cap-js/sqlite": { - "version": "2.0.3", - "integrity": "sha512-Zm2JLpPuddFkeqM47TX7V7IC6x3WOfn5lTRbaiVOMvRQoshCNXTkjv083ssfiwF/jF/46/PSVox+fTElozg6XA==", + "version": "2.4.0", + "integrity": "sha512-Ao+AzIN6BWHNpLbGxAzF79OezFNHzDG2srwiBABs0FYxIxEGkc2hg6ETo79pTTt66gcWtx7pWh/N9xk2M6SFBQ==", "dev": true, "license": "Apache-2.0", "optional": true, "dependencies": { - "@cap-js/db-service": "^2", + "@cap-js/db-service": "^2.11.0", "better-sqlite3": "^12.0.0" }, "peerDependencies": { - "@sap/cds": ">=9" + "@sap/cds": ">=9.8", + "sql.js": "^1.13.0" + }, + "peerDependenciesMeta": { + "sql.js": { + "optional": true + } } }, "node_modules/@sap/cds-dk/node_modules/@eslint/js": { - "version": "9.38.0", - "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", - "dev": true, + "version": "10.0.1", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "extraneous": true, "license": "MIT", - "peer": true, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, "node_modules/@sap/cds-dk/node_modules/@sap/cds": { - "version": "9.4.4", - "integrity": "sha512-JJCHeEJF4xzFyZSf2ToocvVE9dyHfNLTRXOauOxlmpfyaLg97G7Qp+L4bD132eB0onBG9bQj3eH8DzBm0hVvIw==", + "version": "9.9.1", + "integrity": "sha512-GqdsBsRkZThhpOyzj8ihf/jDmf/2zprZFgaun6ZymUw4/ahzjK/bbdd6eQ8txDuv88pnUl2HPFjvUVq3O/6hCA==", "dev": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@sap/cds-compiler": "^6.3", + "@sap/cds-compiler": "^6.4", "@sap/cds-fiori": "^2", - "js-yaml": "^4.1.0" + "express": "^4.22.1 || ^5", + "yaml": "^2" }, "bin": { "cds-deploy": "bin/deploy.js", @@ -357,22 +360,18 @@ "node": ">=20" }, "peerDependencies": { - "@eslint/js": "^9", - "express": "^4", - "tar": "^7" + "@eslint/js": "^9 || ^10", + "tar": "^7.5.6" }, "peerDependenciesMeta": { - "express": { - "optional": true - }, "tar": { "optional": true } } }, "node_modules/@sap/cds-dk/node_modules/@sap/cds-compiler": { - "version": "6.4.2", - "integrity": "sha512-kjhq8GxzCLIfsM4aln9saxkifPC26MYuAqT82QB7RGZDVFM5Pw09Dp6TcKCSp65hut0YgHkvHlVGWMZWWBJNdA==", + "version": "6.9.1", + "integrity": "sha512-j5C61t1mPhMW3vpD3LIRVn40DMiIF2XahOPeJIPjRpUiGMbQPdVreqAhiRHg39GYhSK6etlr5/MIx3a2ljtqHg==", "dev": true, "license": "SEE LICENSE IN LICENSE", "bin": { @@ -385,18 +384,17 @@ } }, "node_modules/@sap/cds-dk/node_modules/@sap/cds-fiori": { - "version": "2.1.0", - "integrity": "sha512-EoNtPT5aVurxwsqHQfW2FgnXA3Ysu9GvihR0jTQNUZ4pVlHHFkfFehH6G2DIHKyfTROWaahegJsMpiwpE8clkg==", + "version": "2.3.0", + "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", "dev": true, "license": "SEE LICENSE IN LICENSE", "peerDependencies": { - "@sap/cds": ">=8", - "express": "^4" + "@sap/cds": ">=8" } }, "node_modules/@sap/cds-dk/node_modules/@sap/cds-mtxs": { - "version": "3.4.2", - "integrity": "sha512-bpKHKbehDSZpe9IoY1Vn2LQ5kgQns6RdlMsTOE9rw9zP//Yq5hFtl7SKFEM0A0/+WL3E+Rp/7Y2tc76erKYqdA==", + "version": "3.9.0", + "integrity": "sha512-U9H9NXQxlxSNwSD/6U59+Egn9LIE2SRdu8i5bZqEG2GB4xEU6csduy0kY4EWvi8XXD8onbFSgw4AA9SB4pN0Yg==", "dev": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { @@ -407,7 +405,7 @@ "cds-mtx-migrate": "bin/cds-mtx-migrate.js" }, "peerDependencies": { - "@sap/cds": "^9" + "@sap/cds": ">=9" } }, "node_modules/@sap/cds-dk/node_modules/@sap/hdi": { @@ -435,13 +433,13 @@ } }, "node_modules/@sap/cds-dk/node_modules/@sap/hdi-deploy": { - "version": "5.5.1", - "integrity": "sha512-5r9SIkXX7cO+MwRFF32O566sMx6LP1mLin0eT9F+Adqy+0SrdwkWv4JslQzYetiWLuNsfqQljcao62alaxts8A==", + "version": "5.6.1", + "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", "dev": true, "license": "See LICENSE file", "dependencies": { "@sap/hdi": "^4.8.0", - "@sap/xsenv": "^5.2.0", + "@sap/xsenv": "^6.0.0", "async": "^3.2.6", "dotenv": "^16.4.5", "handlebars": "^4.7.8", @@ -464,44 +462,32 @@ } }, "node_modules/@sap/cds-dk/node_modules/@sap/xsenv": { - "version": "5.6.1", - "integrity": "sha512-4pDpsYLNJsLUBWtTSG+TJ8ul5iY0dWDyJgTy2H/WZGZww9CSPLP/39x+syDDTjkggsmZAlo9t7y9TiXMmtAunw==", + "version": "6.2.0", + "integrity": "sha512-8jrsX1OAM3YUqGU+4deggqvkxrBrHAPYEllBX0YJfWNffgxSZKHG75bRd/RV6hxPwulPL0DeHfd2eYJMeY5gdw==", "dev": true, "license": "SEE LICENSE IN LICENSE file", "dependencies": { - "debug": "4.4.0", + "debug": "4.4.3", "node-cache": "^5.1.2", "verror": "1.10.1" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || ^22.0.0" + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" } }, "node_modules/@sap/cds-dk/node_modules/accepts": { - "version": "1.3.8", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "version": "2.0.0", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "dev": true, "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { "node": ">= 0.6" } }, - "node_modules/@sap/cds-dk/node_modules/argparse": { - "version": "2.0.1", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/@sap/cds-dk/node_modules/array-flatten": { - "version": "1.1.1", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true, - "license": "MIT" - }, "node_modules/@sap/cds-dk/node_modules/assert-plus": { "version": "1.0.0", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", @@ -517,23 +503,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@sap/cds-dk/node_modules/asynckit": { - "version": "0.4.0", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/axios": { - "version": "1.12.2", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/@sap/cds-dk/node_modules/base64-js": { "version": "1.5.1", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", @@ -556,8 +525,8 @@ "optional": true }, "node_modules/@sap/cds-dk/node_modules/better-sqlite3": { - "version": "12.4.1", - "integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==", + "version": "12.9.0", + "integrity": "sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -567,7 +536,7 @@ "prebuild-install": "^7.1.1" }, "engines": { - "node": "20.x || 22.x || 23.x || 24.x" + "node": "20.x || 22.x || 23.x || 24.x || 25.x" } }, "node_modules/@sap/cds-dk/node_modules/bindings": { @@ -593,44 +562,29 @@ } }, "node_modules/@sap/cds-dk/node_modules/body-parser": { - "version": "1.20.3", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "2.2.2", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/@sap/cds-dk/node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@sap/cds-dk/node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/@sap/cds-dk/node_modules/braces": { "version": "3.0.3", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", @@ -722,28 +676,17 @@ "node": ">=0.8" } }, - "node_modules/@sap/cds-dk/node_modules/combined-stream": { - "version": "1.0.8", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/@sap/cds-dk/node_modules/content-disposition": { - "version": "0.5.4", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "version": "1.1.0", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "dev": true, "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/@sap/cds-dk/node_modules/content-type": { @@ -756,8 +699,8 @@ } }, "node_modules/@sap/cds-dk/node_modules/cookie": { - "version": "0.7.1", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", "engines": { @@ -765,10 +708,13 @@ } }, "node_modules/@sap/cds-dk/node_modules/cookie-signature": { - "version": "1.0.6", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "version": "1.2.2", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } }, "node_modules/@sap/cds-dk/node_modules/core-util-is": { "version": "1.0.2", @@ -777,8 +723,8 @@ "license": "MIT" }, "node_modules/@sap/cds-dk/node_modules/debug": { - "version": "4.4.0", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -819,15 +765,6 @@ "node": ">=4.0.0" } }, - "node_modules/@sap/cds-dk/node_modules/delayed-stream": { - "version": "1.0.0", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/@sap/cds-dk/node_modules/depd": { "version": "2.0.0", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", @@ -837,16 +774,6 @@ "node": ">= 0.8" } }, - "node_modules/@sap/cds-dk/node_modules/destroy": { - "version": "1.2.0", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/@sap/cds-dk/node_modules/detect-libc": { "version": "2.1.2", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", @@ -938,21 +865,6 @@ "node": ">= 0.4" } }, - "node_modules/@sap/cds-dk/node_modules/es-set-tostringtag": { - "version": "2.1.0", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/@sap/cds-dk/node_modules/escape-html": { "version": "1.0.3", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", @@ -979,66 +891,48 @@ } }, "node_modules/@sap/cds-dk/node_modules/express": { - "version": "4.21.2", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" + "version": "5.2.1", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" } }, - "node_modules/@sap/cds-dk/node_modules/express/node_modules/debug": { - "version": "2.6.9", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/express/node_modules/ms": { - "version": "2.0.0", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, "node_modules/@sap/cds-dk/node_modules/extsprintf": { "version": "1.4.1", "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", @@ -1068,72 +962,24 @@ } }, "node_modules/@sap/cds-dk/node_modules/finalhandler": { - "version": "1.3.1", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "version": "2.1.1", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "dev": true, "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/follow-redirects": { - "version": "1.15.11", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/form-data": { - "version": "4.0.4", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "node": ">= 18.0.0" }, - "engines": { - "node": ">= 6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/@sap/cds-dk/node_modules/forwarded": { @@ -1146,12 +992,12 @@ } }, "node_modules/@sap/cds-dk/node_modules/fresh": { - "version": "0.5.2", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "version": "2.0.0", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/@sap/cds-dk/node_modules/fs-constants": { @@ -1237,8 +1083,8 @@ } }, "node_modules/@sap/cds-dk/node_modules/handlebars": { - "version": "4.7.8", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1269,24 +1115,9 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@sap/cds-dk/node_modules/has-tostringtag": { - "version": "1.0.2", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/@sap/cds-dk/node_modules/hasown": { - "version": "2.0.2", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "dev": true, "license": "MIT", "dependencies": { @@ -1297,43 +1128,70 @@ } }, "node_modules/@sap/cds-dk/node_modules/hdb": { - "version": "0.19.12", - "integrity": "sha512-vv+cjmvr6fNH/s0Q2zOZc4sEjMpSC0KuacFn8dp3L38qM3RA2LLeX70wWhZLESpwvwUf1pQkRfUhZeooFSmv3A==", + "version": "2.27.1", + "integrity": "sha512-xYL/W+fq2TyGHyzm8muolQnw8tdh4+2NQ8mQP2FpLSuhfJ8l0jQNSUZoAXic7NfMEan1Jvf8V1L4blwkgTc6+A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "iconv-lite": "^0.4.18" + "iconv-lite": "0.7.0" }, "engines": { - "node": ">= 0.12" - } - }, - "node_modules/@sap/cds-dk/node_modules/http-errors": { - "version": "2.0.0", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "node": ">= 18" }, - "engines": { - "node": ">= 0.8" + "optionalDependencies": { + "lz4-wasm-nodejs": "0.9.2" } }, - "node_modules/@sap/cds-dk/node_modules/iconv-lite": { - "version": "0.4.24", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "node_modules/@sap/cds-dk/node_modules/hdb/node_modules/iconv-lite": { + "version": "0.7.0", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/http-errors": { + "version": "2.0.1", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/iconv-lite": { + "version": "0.7.2", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/@sap/cds-dk/node_modules/ieee754": { @@ -1388,17 +1246,11 @@ "node": ">=0.12.0" } }, - "node_modules/@sap/cds-dk/node_modules/js-yaml": { - "version": "4.1.0", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "node_modules/@sap/cds-dk/node_modules/is-promise": { + "version": "4.0.0", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } + "license": "MIT" }, "node_modules/@sap/cds-dk/node_modules/livereload-js": { "version": "4.0.2", @@ -1406,6 +1258,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@sap/cds-dk/node_modules/lz4-wasm-nodejs": { + "version": "0.9.2", + "integrity": "sha512-hSwgJPS98q/Oe/89Y1OxzeA/UdnASG8GvldRyKa7aZyoAFCC8VPRtViBSava7wWC66WocjUwBpWau2rEmyFPsw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/@sap/cds-dk/node_modules/math-intrinsics": { "version": "1.1.0", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", @@ -1416,32 +1275,26 @@ } }, "node_modules/@sap/cds-dk/node_modules/media-typer": { - "version": "0.3.0", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "version": "1.1.0", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/@sap/cds-dk/node_modules/merge-descriptors": { - "version": "1.0.3", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "version": "2.0.0", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "dev": true, "license": "MIT", + "engines": { + "node": ">=18" + }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@sap/cds-dk/node_modules/methods": { - "version": "1.1.2", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/@sap/cds-dk/node_modules/micromatch": { "version": "4.0.8", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", @@ -1456,8 +1309,8 @@ } }, "node_modules/@sap/cds-dk/node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -1467,21 +1320,9 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@sap/cds-dk/node_modules/mime": { - "version": "1.6.0", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@sap/cds-dk/node_modules/mime-db": { - "version": "1.52.0", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, "license": "MIT", "engines": { @@ -1489,15 +1330,19 @@ } }, "node_modules/@sap/cds-dk/node_modules/mime-types": { - "version": "2.1.35", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "version": "3.0.2", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/@sap/cds-dk/node_modules/mimic-response": { @@ -1552,8 +1397,8 @@ "optional": true }, "node_modules/@sap/cds-dk/node_modules/negotiator": { - "version": "0.6.3", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "1.0.0", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "dev": true, "license": "MIT", "engines": { @@ -1567,8 +1412,8 @@ "license": "MIT" }, "node_modules/@sap/cds-dk/node_modules/node-abi": { - "version": "3.78.0", - "integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==", + "version": "3.92.0", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", "dev": true, "license": "MIT", "optional": true, @@ -1591,15 +1436,6 @@ "node": ">= 8.0.0" } }, - "node_modules/@sap/cds-dk/node_modules/node-watch": { - "version": "0.7.4", - "integrity": "sha512-RinNxoz4W1cep1b928fuFhvAQ5ag/+1UlMDV7rbyGthBIgsiEouS4kvRayvvboxii4m8eolKOIBo3OjDqbc+uQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/@sap/cds-dk/node_modules/object-inspect": { "version": "1.13.4", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", @@ -1629,7 +1465,6 @@ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", - "optional": true, "dependencies": { "wrappy": "1" } @@ -1644,10 +1479,14 @@ } }, "node_modules/@sap/cds-dk/node_modules/path-to-regexp": { - "version": "0.1.12", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "8.4.2", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, "node_modules/@sap/cds-dk/node_modules/pluralize": { "version": "8.0.0", @@ -1661,6 +1500,7 @@ "node_modules/@sap/cds-dk/node_modules/prebuild-install": { "version": "7.1.3", "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", "dev": true, "license": "MIT", "optional": true, @@ -1698,15 +1538,9 @@ "node": ">= 0.10" } }, - "node_modules/@sap/cds-dk/node_modules/proxy-from-env": { - "version": "1.1.0", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true, - "license": "MIT" - }, "node_modules/@sap/cds-dk/node_modules/pump": { - "version": "3.0.3", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "version": "3.0.4", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "dev": true, "license": "MIT", "optional": true, @@ -1716,12 +1550,12 @@ } }, "node_modules/@sap/cds-dk/node_modules/qs": { - "version": "6.13.0", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.15.1", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -1740,18 +1574,18 @@ } }, "node_modules/@sap/cds-dk/node_modules/raw-body": { - "version": "2.5.2", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "3.0.2", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, "node_modules/@sap/cds-dk/node_modules/rc": { @@ -1785,6 +1619,22 @@ "node": ">= 6" } }, + "node_modules/@sap/cds-dk/node_modules/router": { + "version": "2.2.0", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/@sap/cds-dk/node_modules/safe-buffer": { "version": "5.2.1", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", @@ -1803,7 +1653,8 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/@sap/cds-dk/node_modules/safer-buffer": { "version": "2.1.2", @@ -1812,14 +1663,17 @@ "license": "MIT" }, "node_modules/@sap/cds-dk/node_modules/sax": { - "version": "1.4.1", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "version": "1.6.0", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } }, "node_modules/@sap/cds-dk/node_modules/semver": { - "version": "7.7.3", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "optional": true, @@ -1831,66 +1685,48 @@ } }, "node_modules/@sap/cds-dk/node_modules/send": { - "version": "0.19.0", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "version": "1.2.1", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" }, "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/send/node_modules/debug": { - "version": "2.6.9", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/@sap/cds-dk/node_modules/serve-static": { - "version": "1.16.2", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "2.2.1", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "dev": true, "license": "MIT", "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/@sap/cds-dk/node_modules/setprototypeof": { @@ -1919,13 +1755,13 @@ } }, "node_modules/@sap/cds-dk/node_modules/side-channel-list": { - "version": "1.0.0", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -2028,8 +1864,8 @@ } }, "node_modules/@sap/cds-dk/node_modules/statuses": { - "version": "2.0.1", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, "license": "MIT", "engines": { @@ -2121,13 +1957,14 @@ } }, "node_modules/@sap/cds-dk/node_modules/type-is": { - "version": "1.6.18", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "version": "2.0.1", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "dev": true, "license": "MIT", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, "engines": { "node": ">= 0.6" @@ -2162,15 +1999,6 @@ "license": "MIT", "optional": true }, - "node_modules/@sap/cds-dk/node_modules/utils-merge": { - "version": "1.0.1", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/@sap/cds-dk/node_modules/vary": { "version": "1.1.2", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", @@ -2204,12 +2032,11 @@ "version": "1.0.2", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/@sap/cds-dk/node_modules/ws": { - "version": "8.18.3", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.20.0", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "dev": true, "license": "MIT", "engines": { @@ -2241,8 +2068,8 @@ } }, "node_modules/@sap/cds-dk/node_modules/yaml": { - "version": "2.8.1", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "version": "2.8.4", + "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", "dev": true, "license": "ISC", "bin": { @@ -2250,41 +2077,37 @@ }, "engines": { "node": ">= 14.6" - } - }, - "node_modules/@sap/cds-fiori": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@sap/cds-fiori/-/cds-fiori-2.1.1.tgz", - "integrity": "sha512-X+4v4LBAT8HIt0zr28/kJNS15nlNlcM97vAMW+agLrmK134nyBiMwUMcp8BMhxlG9B2PykrnAKH56D9O3tfoBg==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "peer": true, - "peerDependencies": { - "@sap/cds": ">=8", - "express": "^4" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/@sap/eslint-plugin-cds": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@sap/eslint-plugin-cds/-/eslint-plugin-cds-4.1.0.tgz", - "integrity": "sha512-NuvDmF0devaiioU2/JsXYJ5eZZUEl9PZktT7McBoLVzykQmn31IFuDoy5DrfGwndFmZZvHCIex4BVmq+qymoSw==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@sap/eslint-plugin-cds/-/eslint-plugin-cds-4.2.3.tgz", + "integrity": "sha512-I9+PzDY4ODNPCIpS55IdlVgp4rVjEQDMS6xKN3xxPBcy5Y4GYgA1/6yBW5W/MZrwT01OiA9w+G+MN06IPzCufA==", "dev": true, "license": "See LICENSE file", "dependencies": { + "@eslint/plugin-kit": "^0.7.0", "semver": "^7.7.1" }, "engines": { "node": ">=20" }, + "optionalDependencies": { + "tree-sitter": "^0.21.1", + "tree-sitter-java": "^0.23.5" + }, "peerDependencies": { "@sap/cds": ">=9", - "eslint": "^9" + "eslint": "^9 || ^10" } }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -2295,25 +2118,10 @@ "dev": true, "license": "MIT" }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -2334,9 +2142,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -2373,14 +2181,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2388,55 +2188,10 @@ "dev": true, "license": "MIT" }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -2444,50 +2199,6 @@ "concat-map": "0.0.1" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2542,74 +2253,30 @@ "dev": true, "license": "MIT" }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "safe-buffer": "5.2.1" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">= 0.6" + "node": ">= 8" } }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, + "dependencies": { + "ms": "^2.1.3" + }, "engines": { "node": ">=6.0" }, @@ -2626,108 +2293,6 @@ "dev": true, "license": "MIT" }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2742,25 +2307,25 @@ } }, "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -2779,7 +2344,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -2819,29 +2384,43 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "node_modules/eslint/node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/eslint/node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/espree": { @@ -2862,23 +2441,10 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2921,84 +2487,6 @@ "node": ">=0.10.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3033,45 +2521,6 @@ "node": ">=16.0.0" } }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3104,86 +2553,12 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3210,90 +2585,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3331,25 +2632,6 @@ "node": ">=0.8.19" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC", - "peer": true - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3381,9 +2663,9 @@ "license": "ISC" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -3461,93 +2743,10 @@ "dev": true, "license": "MIT" }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "dev": true, - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3571,43 +2770,28 @@ "dev": true, "license": "MIT" }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "node_modules/node-addon-api": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.8.0.tgz", + "integrity": "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA==", "dev": true, "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "peer": true, + "optional": true, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18 || ^20 || >= 21" } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", "dev": true, "license": "MIT", - "peer": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" } }, "node_modules/optionator": { @@ -3673,17 +2857,6 @@ "node": ">=6" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3704,14 +2877,6 @@ "node": ">=8" } }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3722,21 +2887,6 @@ "node": ">= 0.8.0" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3747,51 +2897,6 @@ "node": ">=6" } }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3802,40 +2907,10 @@ "node": ">=4" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "peer": true - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", "dev": true, "license": "ISC", "bin": { @@ -3845,87 +2920,6 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC", - "peer": true - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3949,97 +2943,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -4066,54 +2969,51 @@ "node": ">=8" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "node_modules/tree-sitter": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz", + "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.6" + "optional": true, + "dependencies": { + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.0" } }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "node_modules/tree-sitter-java": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/tree-sitter-java/-/tree-sitter-java-0.23.5.tgz", + "integrity": "sha512-Yju7oQ0Xx7GcUT01mUglPP+bYfvqjNCGdxqigTnew9nLGoII42PNVP3bHrYeMxswiCRM0yubWmN5qk+zsg0zMA==", "dev": true, + "hasInstallScript": true, "license": "MIT", + "optional": true, "dependencies": { - "prelude-ls": "^1.2.1" + "node-addon-api": "^8.2.2", + "node-gyp-build": "^4.8.2" }, - "engines": { - "node": ">= 0.8.0" + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } } }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "prelude-ls": "^1.2.1" }, "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" + "node": ">= 0.8.0" } }, "node_modules/uri-js": { @@ -4126,28 +3026,6 @@ "punycode": "^2.1.0" } }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/sample-app/srv/pom.xml b/sample-app/srv/pom.xml index 2666792..0f8a5e7 100644 --- a/sample-app/srv/pom.xml +++ b/sample-app/srv/pom.xml @@ -174,6 +174,8 @@ NotificationProviderService NotificationTypeProviderService.** NotificationTypeProviderService + NotificationTemplateProviderService.** + NotificationTemplateProviderService From a9c03c9b04617845e87e76b946d8a364541f14f0 Mon Sep 17 00:00:00 2001 From: Buse Halis Date: Thu, 28 May 2026 11:20:57 +0200 Subject: [PATCH 02/19] feat: add standalone NotificationTemplate provisioning and extract I18nHelper --- README.md | 20 ++ .../NotificationServiceConfiguration.java | 21 +- ...icationTemplateAutoProvisionerHandler.java | 117 +++++++ .../handlers/NotificationBuilder.java | 1 + ...icationTemplateAutoProvisionerHandler.java | 166 ++++++++++ .../handlers/NotificationTemplateBuilder.java | 302 ++++++++++++++++++ .../handlers/NotificationTypeBuilder.java | 162 +--------- .../cds/notifications/helpers/I18nHelper.java | 144 +++++++++ 8 files changed, 786 insertions(+), 147 deletions(-) create mode 100644 cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTemplateAutoProvisionerHandler.java create mode 100644 cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateAutoProvisionerHandler.java create mode 100644 cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateBuilder.java create mode 100644 cds-feature-notifications/src/main/java/com/sap/cds/notifications/helpers/I18nHelper.java diff --git a/README.md b/README.md index 1c24fef..88a2d3c 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ In **local mode**, notifications are logged to the console with no ANS binding r - [Identity Authentication Destination (Language Resolution)](#identity-authentication-destination-language-resolution) - [Step 1: Create a Technical User in Identity Authentication](#step-1-create-a-technical-user-in-identity-authentication) - [Step 2: Create the BTP Destination](#step-2-create-the-btp-destination) + - [Template Customization](#template-customization) - [Outbox](#outbox) - [Minimum Versions](#minimum-versions) - [Monitoring & Logging](#monitoring--logging) @@ -144,6 +145,7 @@ The `template` section defines the visible content of the notification: titles, | `@notification.template.groupedTitle` | **Yes** | Title shown when multiple notifications of the same type are collapsed into a single group (e.g. "Low stock alerts"). | | `@notification.template.email.subject` | No | Email subject line. | | `@notification.template.email.html` | No | Inline HTML or classpath path to HTML template file. See [Step 3](#step-3-add-email-html-template-optional) for details. | +| `@notification.template.visibility` | No | Controls whether customer administrators can see and customize this template. `'PUBLIC'` or `'PRIVATE'` (default). See [Template Customization](#template-customization). | | `@notification.deliveryChannels` | No | How the notification is delivered: Web and/or Email. If omitted, notifications are delivered via Web only (no email). Each entry has: `channel` (`#Mail` or `#Web`), `enabled` (Boolean), and `defaultPreference` (Boolean, whether the channel is enabled by default for users). Note: setting `#Mail` here is not enough on its own. The ANS instance must be configured with an email infrastructure (see [ANS Service Binding](#option-1-ans-service-binding)). | | `@notification.priority` | No | You can set a static notification priority: Priority enum (`#LOW`, `#NEUTRAL`, `#MEDIUM`, `#HIGH`). Can also be a CDS expression (see [Dynamic Priority](#dynamic-priority)). | | `@Common.SemanticObject` | No | Maps to `NavigationTargetObject` in ANS. Used for SAP Fiori launchpad navigation. Allows users to navigate directly to the relevant Fiori application when clicking the notification in SAP Build Work Zone. | @@ -151,6 +153,8 @@ The `template` section defines the visible content of the notification: titles, | `recipients` | **Yes** | Who receives the notification. Supports 4 formats (see [Recipient Formats](#recipient-formats)). | | Event fields | No | Define `{{variableName}}` (Mustache syntax) placeholders in your templates and matching fields in the CDS event. When you emit a notification, set these fields with the actual values. The plugin passes them to ANS, which replaces the placeholders at delivery time. | +> **Important:** Event names must be **unique across all services** in your application. The event name is used as the key for both NotificationType and NotificationTemplate in ANS. If two services define an event with the same name (e.g. `LowStockAlert`), the last one provisioned will silently overwrite the other. + #### Step 2: Add i18n Translations (Optional) Create i18n property files under `srv/_i18n/` to define the `{i18n>KEY}` placeholders used in your annotation values. Each entry follows the `KEY=value` format, where the key matches the placeholder name in the annotation value. @@ -715,6 +719,22 @@ Once configured, ANS can resolve each recipient's language preference from IAS a For the full setup guide, see [Identity Directory Connectivity](https://help.sap.com/docs/task-center/sap-task-center/identity-directory-connectivity?q=destination) in the SAP Task Center documentation. +### Template Customization + +The plugin automatically provisions notification templates to ANS during application startup. These templates enable customer administrators to create customized copies of your notification content for their organization. For details on how template customization works from the admin perspective, see the [Template Customization documentation](https://github.wdf.sap.corp/pages/sl-hybrid/ans/notifications-scenario/template-customization/). + +By default, templates are `PRIVATE` (not visible to customer admins). To make a template visible and customizable by admins, set it to `PUBLIC`: + +```cds +event BookOrdered : { + @notification.template.visibility: 'PUBLIC' + @notification.template.title: '{i18n>TEMPLATE_SENSITIVE}' + ... +} +``` + +> **Important:** Once a template is made `PUBLIC`, it **cannot be reverted to `PRIVATE`**. This is enforced by ANS. + ### Outbox In production mode, the plugin uses the CAP persistent ordered outbox to send notifications. This ensures: diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/NotificationServiceConfiguration.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/NotificationServiceConfiguration.java index 35e1b34..74c06d7 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/NotificationServiceConfiguration.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/NotificationServiceConfiguration.java @@ -11,7 +11,9 @@ import cds.gen.notificationtypeproviderservice.NotificationTypeProviderService_; import com.sap.cds.notifications.handlers.EntityNotificationHandler; import com.sap.cds.notifications.handlers.LocalHandler; +import com.sap.cds.notifications.handlers.LocalNotificationTemplateAutoProvisionerHandler; import com.sap.cds.notifications.handlers.LocalNotificationTypeAutoProvisionerHandler; +import com.sap.cds.notifications.handlers.NotificationTemplateAutoProvisionerHandler; import com.sap.cds.notifications.handlers.NotificationTypeAutoProvisionerHandler; import com.sap.cds.notifications.handlers.ProductionHandler; import com.sap.cds.services.environment.CdsProperties; @@ -74,6 +76,14 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { .getService( NotificationTypeProviderService.class, NotificationTypeProviderService_.CDS_NAME); + NotificationTemplateProviderService templateProviderSvc = + configurer + .getCdsRuntime() + .getServiceCatalog() + .getService( + NotificationTemplateProviderService.class, + NotificationTemplateProviderService_.CDS_NAME); + NotificationProviderService outboxedSvc; if (outbox != null) { outboxedSvc = outbox.outboxed(providerSvc); @@ -89,13 +99,20 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { if (environment.getProduction().isEnabled()) { logger.info("Production mode enabled - using ProductionHandler"); configurer.eventHandler(new ProductionHandler(outboxedSvc, configurer.getCdsRuntime())); - // Register handler for auto-provisioning on application prepared event + // Register handler for auto-provisioning standalone templates on application prepared event + configurer.eventHandler( + new NotificationTemplateAutoProvisionerHandler( + configurer.getCdsRuntime(), templateProviderSvc)); + // Register handler for auto-provisioning notification types on application prepared event configurer.eventHandler( new NotificationTypeAutoProvisionerHandler(configurer.getCdsRuntime(), typeProviderSvc)); } else { logger.info("Local mode enabled - using LocalHandler (notifications will be logged only)"); configurer.eventHandler(new LocalHandler(configurer.getCdsRuntime())); - // Register local handler for auto-provisioning (logging only, not sending to ANS) + // Register local handler for auto-provisioning standalone templates (logging only) + configurer.eventHandler( + new LocalNotificationTemplateAutoProvisionerHandler(configurer.getCdsRuntime())); + // Register local handler for auto-provisioning notification types (logging only) configurer.eventHandler( new LocalNotificationTypeAutoProvisionerHandler(configurer.getCdsRuntime())); } diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTemplateAutoProvisionerHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTemplateAutoProvisionerHandler.java new file mode 100644 index 0000000..093d88c --- /dev/null +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTemplateAutoProvisionerHandler.java @@ -0,0 +1,117 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-notifications contributors. + */ +package com.sap.cds.notifications.handlers; + +import cds.gen.notificationtemplateproviderservice.NotificationTemplates; +import cds.gen.notificationtemplateproviderservice.Translations; +import com.sap.cds.services.application.ApplicationLifecycleService; +import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.handler.annotations.On; +import com.sap.cds.services.handler.annotations.ServiceName; +import com.sap.cds.services.runtime.CdsRuntime; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Local-mode handler for standalone NotificationTemplate provisioning. Logs template details to + * console instead of sending to ANS. + */ +@ServiceName(ApplicationLifecycleService.DEFAULT_NAME) +public class LocalNotificationTemplateAutoProvisionerHandler implements EventHandler { + + private static final Logger logger = + LoggerFactory.getLogger(LocalNotificationTemplateAutoProvisionerHandler.class); + + private final NotificationTemplateBuilder notificationTemplateBuilder; + + public LocalNotificationTemplateAutoProvisionerHandler(CdsRuntime runtime) { + this.notificationTemplateBuilder = new NotificationTemplateBuilder(runtime); + } + + @On(event = ApplicationLifecycleService.EVENT_APPLICATION_PREPARED) + public void onApplicationPrepared() { + logger.info( + "Auto-provisioning standalone NotificationTemplates from CDS annotations" + + " (LOCAL MODE - Logging Only)..."); + try { + provisionNotificationTemplates(); + logger.info("Standalone NotificationTemplate auto-provisioning completed (LOCAL MODE)"); + } catch (Exception e) { + logger.error("Standalone NotificationTemplate auto-provisioning failed", e); + } + } + + private void provisionNotificationTemplates() { + List templates = + notificationTemplateBuilder.buildAllNotificationTemplates(); + + if (templates.isEmpty()) { + logger.info("No standalone NotificationTemplates found in CDS model (LOCAL MODE)"); + return; + } + + for (NotificationTemplates template : templates) { + logTemplate(template); + } + } + + private void logTemplate(NotificationTemplates template) { + System.out.println("\n==============================================================="); + System.out.println("Standalone NotificationTemplate (Local Mode - Not Sent to ANS)"); + System.out.println(" Key: " + template.getKey()); + System.out.println(" Visibility: " + template.getVisibility()); + System.out.println( + " Translations (" + + (template.getTranslations() != null ? template.getTranslations().size() : 0) + + "):"); + + if (template.getTranslations() != null) { + for (Translations translation : template.getTranslations()) { + System.out.println( + " - Language: " + + translation.getLanguage() + + "\n" + + " Syntax: " + + translation.getSyntax() + + "\n" + + " Title: " + + translation.getTitle() + + "\n" + + " Preview: " + + translation.getPreview() + + "\n" + + " Body: " + + translation.getBody() + + "\n" + + " Description: " + + translation.getDescription()); + + if (translation.getEmail() != null) { + System.out.println( + " Email Subject: " + + translation.getEmail().getSubject() + + "\n" + + " Email BodyHtml: " + + (translation.getEmail().getBodyHtml() != null + ? translation + .getEmail() + .getBodyHtml() + .substring( + 0, Math.min(100, translation.getEmail().getBodyHtml().length())) + + "..." + : "null") + + "\n" + + " Email BodyText: " + + translation.getEmail().getBodyText()); + } + } + } + + System.out.println("===============================================================\n"); + + logger.info( + "Standalone template '{}' logged (LOCAL MODE - not sent to ANS)", template.getKey()); + } +} diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationBuilder.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationBuilder.java index 888ce19..f76e00d 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationBuilder.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationBuilder.java @@ -155,6 +155,7 @@ private NotificationBuildResult buildSingleNotification( Notifications notification = Struct.create(Notifications.class); notification.setNotificationTypeKey(notificationTypeKey); notification.setNotificationTypeVersion("1"); + notification.setNotificationTemplateKey(notificationTypeKey); notification.setPriority(priority); notification.setRecipients(recipientsList); diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateAutoProvisionerHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateAutoProvisionerHandler.java new file mode 100644 index 0000000..052d550 --- /dev/null +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateAutoProvisionerHandler.java @@ -0,0 +1,166 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-notifications contributors. + */ +package com.sap.cds.notifications.handlers; + +import cds.gen.notificationtemplateproviderservice.NotificationTemplateProviderService; +import cds.gen.notificationtemplateproviderservice.NotificationTemplates; +import cds.gen.notificationtemplateproviderservice.NotificationTemplates_; +import com.sap.cds.ql.Insert; +import com.sap.cds.ql.Select; +import com.sap.cds.ql.Update; +import com.sap.cds.services.application.ApplicationLifecycleService; +import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.handler.annotations.On; +import com.sap.cds.services.handler.annotations.ServiceName; +import com.sap.cds.services.runtime.CdsRuntime; +import java.util.*; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provisions standalone NotificationTemplates to ANS during application startup. Runs during the + * APPLICATION_PREPARED lifecycle event. Standalone templates are referenced by Notifications (via + * {@code NotificationTemplateKey}). + */ +@ServiceName(ApplicationLifecycleService.DEFAULT_NAME) +public class NotificationTemplateAutoProvisionerHandler implements EventHandler { + + private static final Logger logger = + LoggerFactory.getLogger(NotificationTemplateAutoProvisionerHandler.class); + + private final NotificationTemplateBuilder notificationTemplateBuilder; + private final NotificationTemplateProviderService notificationTemplateProviderService; + + public NotificationTemplateAutoProvisionerHandler( + CdsRuntime runtime, + NotificationTemplateProviderService notificationTemplateProviderService) { + this.notificationTemplateBuilder = new NotificationTemplateBuilder(runtime); + this.notificationTemplateProviderService = notificationTemplateProviderService; + } + + @On(event = ApplicationLifecycleService.EVENT_APPLICATION_PREPARED) + public void onApplicationPrepared() { + logger.info("Auto-provisioning standalone NotificationTemplates from CDS annotations..."); + try { + provisionNotificationTemplates(); + logger.info("Standalone NotificationTemplate auto-provisioning completed"); + } catch (IllegalStateException e) { + // Developer mistake (e.g. malformed template) — fail hard + throw e; + } catch (Exception e) { + // Transient error (network, ANS down) — log and continue + logger.error("Standalone NotificationTemplate auto-provisioning failed", e); + } + } + + private void provisionNotificationTemplates() { + List templates = + notificationTemplateBuilder.buildAllNotificationTemplates(); + + if (templates.isEmpty()) { + logger.info("No standalone NotificationTemplates found in CDS model"); + return; + } + + // Fetch all existing templates from ANS to determine create vs update + Set existingTemplateKeys = fetchExistingTemplateKeys(); + logger.info("Found {} existing standalone templates in ANS", existingTemplateKeys.size()); + + for (NotificationTemplates template : templates) { + String key = template.getKey(); + + if (existingTemplateKeys.contains(key)) { + updateTemplate(template); + } else { + createTemplate(template); + } + } + } + + /** Fetch all existing template keys from ANS. */ + private Set fetchExistingTemplateKeys() { + try { + return notificationTemplateProviderService + .run(Select.from(NotificationTemplates_.class).columns(nt -> nt.Key())) + .streamOf(NotificationTemplates.class) + .map(NotificationTemplates::getKey) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } catch (Exception e) { + logger.warn( + "Could not fetch existing standalone templates from ANS: {}", e.getMessage()); + return Collections.emptySet(); + } + } + + private void createTemplate(NotificationTemplates template) { + try { + notificationTemplateProviderService.run( + Insert.into(NotificationTemplates_.CDS_NAME).entry(template)); + + logger.info( + "Standalone NotificationTemplate '{}' created in ANS successfully", template.getKey()); + } catch (Exception e) { + String errorMsg = e.getMessage() != null ? e.getMessage() : ""; + + if (errorMsg.contains("409")) { + // Race condition: template was created between our GET and INSERT + logger.info( + "Standalone template '{}' was created concurrently (409). Attempting update...", + template.getKey()); + updateTemplate(template); + return; + } + + if (errorMsg.contains("400")) { + logger.error( + "ANS rejected standalone template '{}' with 400 Bad Request. " + + "Check that all required fields (Title in Translation) are set. Error: {}", + template.getKey(), + errorMsg); + throw new IllegalStateException( + String.format( + "ANS rejected standalone template '%s' with 400 Bad Request. " + + "Ensure @notification.template annotations are properly configured. Error: %s", + template.getKey(), errorMsg), + e); + } + + logger.error( + "Failed to create standalone template '{}' in ANS", template.getKey(), e); + throw e; + } + } + + private void updateTemplate(NotificationTemplates template) { + logger.info("Updating standalone template '{}'", template.getKey()); + + try { + notificationTemplateProviderService.run( + Update.entity(NotificationTemplates_.CDS_NAME).data(template)); + + logger.info( + "Standalone NotificationTemplate '{}' updated in ANS successfully", template.getKey()); + } catch (Exception e) { + String errorMsg = e.getMessage() != null ? e.getMessage() : ""; + + if (errorMsg.contains("400")) { + logger.error( + "ANS rejected standalone template update '{}' with 400 Bad Request. Error: {}", + template.getKey(), + errorMsg); + throw new IllegalStateException( + String.format( + "ANS rejected standalone template update '%s' with 400 Bad Request. Error: %s", + template.getKey(), errorMsg), + e); + } + + logger.error( + "Failed to update standalone template '{}' in ANS", template.getKey(), e); + throw e; + } + } +} diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateBuilder.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateBuilder.java new file mode 100644 index 0000000..40ecd20 --- /dev/null +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateBuilder.java @@ -0,0 +1,302 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-notifications contributors. + */ +package com.sap.cds.notifications.handlers; + +import cds.gen.notificationtemplateproviderservice.Email; +import cds.gen.notificationtemplateproviderservice.NotificationTemplates; +import cds.gen.notificationtemplateproviderservice.Tags; +import cds.gen.notificationtemplateproviderservice.Translations; +import com.sap.cds.Struct; +import com.sap.cds.notifications.helpers.I18nHelper; +import com.sap.cds.reflect.CdsBaseType; +import com.sap.cds.reflect.CdsEvent; +import com.sap.cds.reflect.CdsModel; +import com.sap.cds.reflect.CdsSimpleType; +import com.sap.cds.services.runtime.CdsRuntime; +import java.util.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Helper class to build standalone NotificationTemplate objects from CDS event annotations. + * + *

Annotation mapping to standalone template fields: + * + *

    + *
  • {@code @notification.template.title} → Translation.Title + *
  • {@code @notification.template.subtitle} → Translation.Body + *
  • {@code @notification.template.publicTitle} → Translation.Preview + *
  • {@code @notification.template.email.subject} → Translation.Email.Subject + *
  • {@code @notification.template.email.html} → Translation.Email.BodyHtml + *
  • {@code @notification.template.email.text} → Translation.Email.BodyText + *
  • {@code @notification.template.description} → Translation.Description + *
+ */ +public class NotificationTemplateBuilder { + + private static final Logger logger = LoggerFactory.getLogger(NotificationTemplateBuilder.class); + + private static final String DEFAULT_SYNTAX = "MUSTACHE"; + + private final CdsRuntime runtime; + private final I18nHelper i18nHelper; + + public NotificationTemplateBuilder(CdsRuntime runtime) { + this.runtime = runtime; + this.i18nHelper = new I18nHelper(runtime); + } + + /** Build all standalone notification templates from CDS model event annotations. */ + public List buildAllNotificationTemplates() { + List templates = new ArrayList<>(); + CdsModel model = runtime.getCdsModel(); + + model.events() + .filter(event -> event.findAnnotation("notification.template.title").isPresent()) + .forEach(event -> extractTemplateFromEvent(event).ifPresent(templates::add)); + + return templates; + } + + /** Extract a standalone NotificationTemplate from a CDS event with annotations. */ + private Optional extractTemplateFromEvent(CdsEvent event) { + String key = event.getName(); + String qualifiedName = event.getQualifiedName(); + String source = extractSource(qualifiedName); + String eventName = key; + + NotificationTemplates template = Struct.create(NotificationTemplates.class); + template.setKey(key); + + // Visibility - from @notification.template.visibility annotation (ANS defaults to PRIVATE) + String visibility = extractVisibility(event); + if (visibility != null) { + template.setVisibility(visibility); + } + + // PropertiesSchema - auto-generated from event elements + String propertiesSchema = buildPropertiesSchema(event); + if (propertiesSchema != null) { + template.setPropertiesSchema(propertiesSchema); + } + + // Tags - source and event for filtering in admin UI + List tags = buildTags(source, eventName); + template.setTags(tags); + + Set locales = i18nHelper.getAvailableLocales(); + logger.debug("Creating translations for {} discovered i18n locales", locales.size()); + + List translations = new ArrayList<>(); + + for (Locale locale : locales) { + String lang = locale.toLanguageTag(); + Map i18nTexts = i18nHelper.getI18nTexts(locale); + Translations translation = createTranslation(event, lang, i18nTexts, source, eventName); + translations.add(translation); + logger.debug( + "Created translation for language: {} with {} i18n texts", lang, i18nTexts.size()); + } + + template.setTranslations(translations); + + logger.info("Extracted standalone NotificationTemplate: Key={}, Source={}", key, source); + return Optional.of(template); + } + + private Translations createTranslation( + CdsEvent event, String lang, Map i18nTexts, String source, String eventName) { + Translations translation = Struct.create(Translations.class); + translation.setLanguage(lang); + translation.setSyntax(DEFAULT_SYNTAX); + + // Source, Event, DisplayName - for admin UI filtering and display + translation.setSource(source); + translation.setEvent(eventName); + translation.setDisplayName(eventName); + + // Title (required) - from @notification.template.title + String title = i18nHelper.resolveAnnotationValue(event, "notification.template.title", i18nTexts); + if (title == null || title.trim().isEmpty()) { + throw new IllegalStateException( + String.format( + "Missing required annotation: @notification.template.title for event '%s'. " + + "The standalone template API requires a Title to be set.", + event.getName())); + } + translation.setTitle(title); + + // Preview - from @notification.template.publicTitle + String preview = i18nHelper.resolveAnnotationValue(event, "notification.template.publicTitle", i18nTexts); + if (preview != null && !preview.trim().isEmpty()) { + translation.setPreview(preview); + } + + // Body - from @notification.template.subtitle + String body = i18nHelper.resolveAnnotationValue(event, "notification.template.subtitle", i18nTexts); + if (body != null && !body.trim().isEmpty()) { + translation.setBody(body); + } + + // Description - from @notification.template.description or @description + String description = i18nHelper.resolveAnnotationValue(event, "notification.template.description", i18nTexts); + if (description == null) { + description = i18nHelper.resolveAnnotationValue(event, "description", i18nTexts); + } + if (description != null && !description.trim().isEmpty()) { + translation.setDescription(description); + } + + // Email + Email email = buildEmail(event, i18nTexts); + if (email != null) { + translation.setEmail(email); + } + + return translation; + } + + private Email buildEmail(CdsEvent event, Map i18nTexts) { + String subject = + i18nHelper.resolveAnnotationValue(event, "notification.template.email.subject", i18nTexts); + String htmlValue = + i18nHelper.resolveAnnotationValue(event, "notification.template.email.html", i18nTexts); + String textValue = + i18nHelper.resolveAnnotationValue(event, "notification.template.email.text", i18nTexts); + + if (subject == null && htmlValue == null && textValue == null) { + return null; + } + + Email email = Struct.create(Email.class); + + if (subject != null) { + email.setSubject(subject); + } + + // Resolve email HTML - if it's a file path, load the file content + if (htmlValue != null) { + if (htmlValue.endsWith(".html")) { + String htmlContent = i18nHelper.loadHtmlFromClasspath(htmlValue, i18nTexts); + email.setBodyHtml(htmlContent); + logger.info("Loaded HTML template from file: {}", htmlValue); + } else { + email.setBodyHtml(htmlValue); + } + } + + if (textValue != null) { + email.setBodyText(textValue); + } + + return email; + } + + /** + * Extract visibility from @notification.template.visibility annotation. Returns null if not + * specified (ANS defaults to PRIVATE). Only PUBLIC and PRIVATE are valid values. + */ + private String extractVisibility(CdsEvent event) { + return event + .findAnnotation("notification.template.visibility") + .map(a -> { + Object val = a.getValue(); + if (val instanceof Map enumMap) { + // Enum: {#=PUBLIC} + return enumMap.values().stream().findFirst().map(Object::toString).orElse(null).toUpperCase(); + } + return val.toString().toUpperCase(); + }) + .filter(v -> "PUBLIC".equals(v) || "PRIVATE".equals(v)) + .orElse(null); + } + + /** + * Extract source (service name) from the event's qualified name. E.g. "CatalogService.BookOrdered" + * → "CatalogService". + */ + private String extractSource(String qualifiedName) { + int lastDot = qualifiedName.lastIndexOf('.'); + if (lastDot > 0) { + return qualifiedName.substring(0, lastDot); + } + return qualifiedName; + } + + /** Build Tags list for source and event filtering in admin UI. */ + private List buildTags(String source, String eventName) { + List tags = new ArrayList<>(); + + Tags sourceTag = Struct.create(Tags.class); + sourceTag.setKey("source"); + sourceTag.setValue(source); + tags.add(sourceTag); + + Tags eventTag = Struct.create(Tags.class); + eventTag.setKey("event"); + eventTag.setValue(eventName); + tags.add(eventTag); + + return tags; + } + + /** + * Build JSON Schema from event elements for the PropertiesSchema field. Maps CDS types to JSON + * Schema types. Excludes the 'recipients' field as it's not a template variable. + */ + private String buildPropertiesSchema(CdsEvent event) { + StringBuilder sb = new StringBuilder(); + sb.append("{\"type\":\"object\",\"properties\":{"); + + List propertyEntries = new ArrayList<>(); + List requiredFields = new ArrayList<>(); + + event + .elements() + .forEach( + element -> { + String fieldName = element.getName(); + // Skip recipients - it's not a template variable + if (!"recipients".equals(fieldName)) { + String jsonType = cdsTypeToJsonSchemaType(element); + propertyEntries.add( + String.format( + "\"%s\":{\"type\":\"%s\",\"title\":\"%s\"}", + fieldName, jsonType, fieldName)); + requiredFields.add("\"" + fieldName + "\""); + } + }); + + sb.append(String.join(",", propertyEntries)); + sb.append("},\"required\":["); + sb.append(String.join(",", requiredFields)); + sb.append("]}"); + + String schema = sb.toString(); + logger.debug("Generated PropertiesSchema: {}", schema); + return schema; + } + + /** Map CDS element type to JSON Schema type string. */ + private String cdsTypeToJsonSchemaType(com.sap.cds.reflect.CdsElementDefinition element) { + try { + com.sap.cds.reflect.CdsType cdsType = element.getType(); + if (!cdsType.isSimple()) { + return "string"; + } + CdsSimpleType simpleType = (CdsSimpleType) cdsType; + CdsBaseType baseType = simpleType.getType(); + return switch (baseType) { + case INT16, INT32, INT64, INTEGER, INTEGER64, UINT8 -> "integer"; + case DECIMAL, DOUBLE -> "number"; + case BOOLEAN -> "boolean"; + default -> "string"; + }; + } catch (Exception e) { + // If type cannot be resolved, default to string + return "string"; + } + } + +} diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeBuilder.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeBuilder.java index 87ac357..16f396b 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeBuilder.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeBuilder.java @@ -7,12 +7,10 @@ import cds.gen.notificationtypeproviderservice.NotificationTypes; import cds.gen.notificationtypeproviderservice.Templates; import com.sap.cds.Struct; -import com.sap.cds.adapter.edmx.EdmxI18nProvider; +import com.sap.cds.notifications.helpers.I18nHelper; import com.sap.cds.reflect.CdsEvent; import com.sap.cds.reflect.CdsModel; import com.sap.cds.services.runtime.CdsRuntime; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.util.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,10 +21,11 @@ public class NotificationTypeBuilder { private static final Logger logger = LoggerFactory.getLogger(NotificationTypeBuilder.class); private final CdsRuntime runtime; - private EdmxI18nProvider i18nProvider; + private final I18nHelper i18nHelper; public NotificationTypeBuilder(CdsRuntime runtime) { this.runtime = runtime; + this.i18nHelper = new I18nHelper(runtime); } /** Build all notification types from CDS model event annotations. */ @@ -34,25 +33,9 @@ public List buildAllNotificationTypes() { List notificationTypes = new ArrayList<>(); CdsModel model = runtime.getCdsModel(); - model - .events() - .forEach( - event -> { - logger.info("=== Checking event: {} ===", event.getQualifiedName()); - - // Check for notification annotation - boolean hasNotificationAnnotation = - event.findAnnotation("notification.template.title").isPresent(); - - if (hasNotificationAnnotation) { - logger.info( - "Found @notification annotation on event: {}", event.getQualifiedName()); - extractNotificationTypeFromEvent(event).ifPresent(notificationTypes::add); - } else { - logger.debug( - "Event {} has no @notification annotation, skipping", event.getQualifiedName()); - } - }); + model.events() + .filter(event -> event.findAnnotation("notification.template.title").isPresent()) + .forEach(event -> extractNotificationTypeFromEvent(event).ifPresent(notificationTypes::add)); return notificationTypes; } @@ -65,7 +48,7 @@ private Optional extractNotificationTypeFromEvent(CdsEvent ev nt.setNotificationTypeKey(key); nt.setNotificationTypeVersion("1"); - Set locales = getAvailableLocales(); + Set locales = i18nHelper.getAvailableLocales(); logger.debug("Creating templates for {} discovered i18n locales", locales.size()); List templates = new ArrayList<>(); @@ -73,7 +56,7 @@ private Optional extractNotificationTypeFromEvent(CdsEvent ev // Create template for each discovered locale using EdmxI18nProvider for (Locale locale : locales) { String lang = locale.toLanguageTag(); - Map i18nTexts = getI18nTexts(locale); + Map i18nTexts = i18nHelper.getI18nTexts(locale); Templates template = createTemplate(event, lang, i18nTexts); templates.add(template); logger.debug("Created template for language: {} with {} i18n texts", lang, i18nTexts.size()); @@ -97,7 +80,7 @@ private Templates createTemplate(CdsEvent event, String lang, Map extractDeliveryChannels(CdsEvent event) { return deliveryChannels; } - - /** - * Discover available locales from the EdmxI18nProvider. This dynamically detects which languages - * have i18n translations defined in the application's _i18n/ properties files, rather than - * relying on a hardcoded list. Always includes English as a fallback. - * - * @since cds-services 4.9.0 - */ - private Set getAvailableLocales() { - if (i18nProvider == null) { - i18nProvider = runtime.getProvider(EdmxI18nProvider.class); - } - if (i18nProvider != null) { - Set locales = i18nProvider.getLocales(); - if (!locales.isEmpty()) { - return locales; - } - } - logger.warn("EdmxI18nProvider.getLocales() returned no locales, falling back to English only"); - return Set.of(Locale.ENGLISH); - } - - /** - * Get i18n texts for a given locale from the EdmxI18nProvider. The provider reads from - * edmx/_i18n/i18n.json which is generated by the CDS compiler from _i18n/i18n*.properties files - * located next to the .cds source files. - */ - private Map getI18nTexts(Locale locale) { - if (i18nProvider != null) { - return i18nProvider.getTexts(locale); - } - logger.warn("EdmxI18nProvider not available, i18n resolution will not work"); - return Collections.emptyMap(); - } - - /** Extract i18n key from annotation value like {i18n>KEY} and resolve using EdmxI18nProvider. */ - private String resolveAnnotationValue( - CdsEvent event, String annotationPath, Map i18nTexts) { - String annotationValue = - event.findAnnotation(annotationPath).map(a -> (String) a.getValue()).orElse(null); - - if (annotationValue == null) { - return null; - } - - return resolveI18n(annotationValue, i18nTexts); - } - - /** - * Resolve all {i18n>KEY} patterns in a string using the i18n texts map. Supports both - * single-placeholder values (e.g. "{i18n>TITLE}") and multi-placeholder values (e.g. - * "{i18n>GREETING}, {i18n>BODY}"). - */ - private String resolveI18n(String value, Map i18nTexts) { - if (value == null || !value.contains("{i18n>")) { - return value; - } - - for (Map.Entry entry : i18nTexts.entrySet()) { - value = value.replace("{i18n>" + entry.getKey() + "}", entry.getValue()); - } - return value; - } - - /** Cache for raw HTML content loaded from classpath to avoid repeated disk reads */ - private final Map htmlCache = new HashMap<>(); - - /** - * Load HTML content from classpath resource and resolve {i18n>KEY} placeholders. HTML content is - * cached after first load since only i18n values change per language. - */ - private String loadHtmlFromClasspath(String filePath, Map i18nTexts) { - String content = - htmlCache.computeIfAbsent( - filePath, - path -> { - try (InputStream is = getClass().getClassLoader().getResourceAsStream(path)) { - if (is != null) { - return new String(is.readAllBytes(), StandardCharsets.UTF_8); - } else { - logger.error("HTML file not found: {}", path); - return null; - } - } catch (Exception e) { - logger.error("Failed to load HTML file: {}", path, e); - return null; - } - }); - - if (content == null) { - return null; - } - - // Resolve {i18n>KEY} placeholders in the HTML content - String resolvedHtml = resolveI18n(content, i18nTexts); - - // Minify HTML: remove comments and unnecessary whitespace - String compactHtml = - resolvedHtml - .replaceAll("(?s)", "") - .replaceAll(">\\s+<", "><") - .replaceAll("\\s{2,}", " ") - .trim(); - - logger.debug( - "Loaded HTML file: {} (original: {} bytes, resolved+compacted: {} bytes)", - filePath, - content.length(), - compactHtml.length()); - return compactHtml; - } } diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/helpers/I18nHelper.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/helpers/I18nHelper.java new file mode 100644 index 0000000..ab707a7 --- /dev/null +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/helpers/I18nHelper.java @@ -0,0 +1,144 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-notifications contributors. + */ +package com.sap.cds.notifications.helpers; + +import com.sap.cds.adapter.edmx.EdmxI18nProvider; +import com.sap.cds.reflect.CdsEvent; +import com.sap.cds.services.runtime.CdsRuntime; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Shared helper for i18n resolution, locale discovery, and HTML template loading. Used by both + * NotificationTypeBuilder and NotificationTemplateBuilder to avoid code duplication. + */ +public class I18nHelper { + + private static final Logger logger = LoggerFactory.getLogger(I18nHelper.class); + + private final CdsRuntime runtime; + private EdmxI18nProvider i18nProvider; + private final Map htmlCache = new HashMap<>(); + + public I18nHelper(CdsRuntime runtime) { + this.runtime = runtime; + } + + private EdmxI18nProvider getProvider() { + if (i18nProvider == null) { + i18nProvider = runtime.getProvider(EdmxI18nProvider.class); + } + return i18nProvider; + } + + /** + * Discover available locales from the EdmxI18nProvider. This dynamically detects which languages + * have i18n translations defined in the application's _i18n/ properties files, rather than + * relying on a hardcoded list. Always includes English as a fallback. + * + * @since cds-services 4.9.0 + */ + public Set getAvailableLocales() { + EdmxI18nProvider provider = getProvider(); + if (provider != null) { + Set locales = provider.getLocales(); + if (!locales.isEmpty()) { + return locales; + } + } + logger.warn("EdmxI18nProvider.getLocales() returned no locales, falling back to English only"); + return Set.of(Locale.ENGLISH); + } + + /** + * Get i18n texts for a given locale from the EdmxI18nProvider. The provider reads from + * edmx/_i18n/i18n.json which is generated by the CDS compiler from _i18n/i18n*.properties files + * located next to the .cds source files. + */ + public Map getI18nTexts(Locale locale) { + EdmxI18nProvider provider = getProvider(); + if (provider != null) { + return provider.getTexts(locale); + } + logger.warn("EdmxI18nProvider not available, i18n resolution will not work"); + return Collections.emptyMap(); + } + + /** Extract i18n key from annotation value like {i18n>KEY} and resolve using EdmxI18nProvider. */ + public String resolveAnnotationValue( + CdsEvent event, String annotationPath, Map i18nTexts) { + String annotationValue = + event.findAnnotation(annotationPath).map(a -> (String) a.getValue()).orElse(null); + + if (annotationValue == null) { + return null; + } + + return resolveI18n(annotationValue, i18nTexts); + } + + /** + * Resolve all {i18n>KEY} patterns in a string using the i18n texts map. Supports both + * single-placeholder values (e.g. "{i18n>TITLE}") and multi-placeholder values (e.g. + * "{i18n>GREETING}, {i18n>BODY}"). + */ + public String resolveI18n(String value, Map i18nTexts) { + if (value == null || !value.contains("{i18n>")) { + return value; + } + + for (Map.Entry entry : i18nTexts.entrySet()) { + value = value.replace("{i18n>" + entry.getKey() + "}", entry.getValue()); + } + return value; + } + + /** + * Load HTML content from classpath resource and resolve {i18n>KEY} placeholders. HTML content is + * cached after first load since only i18n values change per language. + */ + public String loadHtmlFromClasspath(String filePath, Map i18nTexts) { + String content = + htmlCache.computeIfAbsent( + filePath, + path -> { + try (InputStream is = getClass().getClassLoader().getResourceAsStream(path)) { + if (is != null) { + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } else { + logger.error("HTML file not found: {}", path); + return null; + } + } catch (Exception e) { + logger.error("Failed to load HTML file: {}", path, e); + return null; + } + }); + + if (content == null) { + return null; + } + + // Resolve {i18n>KEY} placeholders in the HTML content + String resolvedHtml = resolveI18n(content, i18nTexts); + + // Minify HTML: remove comments and unnecessary whitespace + String compactHtml = + resolvedHtml + .replaceAll("(?s)", "") + .replaceAll(">\\s+<", "><") + .replaceAll("\\s{2,}", " ") + .trim(); + + logger.debug( + "Loaded HTML file: {} (original: {} bytes, resolved+compacted: {} bytes)", + filePath, + content.length(), + compactHtml.length()); + return compactHtml; + } +} From 3039c6eeabe8dc3120574be038890e1ee60e5f9b Mon Sep 17 00:00:00 2001 From: Buse Halis Date: Tue, 2 Jun 2026 10:52:44 +0300 Subject: [PATCH 03/19] refactor: use @UI.AdaptationHidden annotation for template visibility control --- README.md | 17 +++++++++++------ .../handlers/NotificationTemplateBuilder.java | 19 ++++++++++--------- sample-app/srv/notificationtypes-data.cds | 1 + 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 88a2d3c..bdc0b38 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ The `template` section defines the visible content of the notification: titles, | `@notification.template.groupedTitle` | **Yes** | Title shown when multiple notifications of the same type are collapsed into a single group (e.g. "Low stock alerts"). | | `@notification.template.email.subject` | No | Email subject line. | | `@notification.template.email.html` | No | Inline HTML or classpath path to HTML template file. See [Step 3](#step-3-add-email-html-template-optional) for details. | -| `@notification.template.visibility` | No | Controls whether customer administrators can see and customize this template. `'PUBLIC'` or `'PRIVATE'` (default). See [Template Customization](#template-customization). | +| `@UI.AdaptationHidden` | No | Controls whether customer administrators can see and customize this template. `false` = PUBLIC (visible for customization), absent or `true` = PRIVATE (default). See [Template Customization](#template-customization). | | `@notification.deliveryChannels` | No | How the notification is delivered: Web and/or Email. If omitted, notifications are delivered via Web only (no email). Each entry has: `channel` (`#Mail` or `#Web`), `enabled` (Boolean), and `defaultPreference` (Boolean, whether the channel is enabled by default for users). Note: setting `#Mail` here is not enough on its own. The ANS instance must be configured with an email infrastructure (see [ANS Service Binding](#option-1-ans-service-binding)). | | `@notification.priority` | No | You can set a static notification priority: Priority enum (`#LOW`, `#NEUTRAL`, `#MEDIUM`, `#HIGH`). Can also be a CDS expression (see [Dynamic Priority](#dynamic-priority)). | | `@Common.SemanticObject` | No | Maps to `NavigationTargetObject` in ANS. Used for SAP Fiori launchpad navigation. Allows users to navigate directly to the relevant Fiori application when clicking the notification in SAP Build Work Zone. | @@ -723,17 +723,22 @@ For the full setup guide, see [Identity Directory Connectivity](https://help.sap The plugin automatically provisions notification templates to ANS during application startup. These templates enable customer administrators to create customized copies of your notification content for their organization. For details on how template customization works from the admin perspective, see the [Template Customization documentation](https://github.wdf.sap.corp/pages/sl-hybrid/ans/notifications-scenario/template-customization/). -By default, templates are `PRIVATE` (not visible to customer admins). To make a template visible and customizable by admins, set it to `PUBLIC`: +By default, templates are `PRIVATE` (not visible to customer admins). To make a template visible and customizable by admins, add `@UI.AdaptationHidden: false`: ```cds -event BookOrdered : { - @notification.template.visibility: 'PUBLIC' - @notification.template.title: '{i18n>TEMPLATE_SENSITIVE}' +@UI.AdaptationHidden: false +@notification : { + template: { + title : '{i18n>TEMPLATE_SENSITIVE}', + ... + } +} +event BookOrdered { ... } ``` -> **Important:** Once a template is made `PUBLIC`, it **cannot be reverted to `PRIVATE`**. This is enforced by ANS. +> **Important:** Once a template is made `PUBLIC`, it **cannot be reverted to `PRIVATE`**. This is enforced by ANS. Making this a deliberate opt-in ensures that only templates intended for customization are exposed to customer administrators. ### Outbox diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateBuilder.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateBuilder.java index 40ecd20..d439309 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateBuilder.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateBuilder.java @@ -69,7 +69,8 @@ private Optional extractTemplateFromEvent(CdsEvent event) NotificationTemplates template = Struct.create(NotificationTemplates.class); template.setKey(key); - // Visibility - from @notification.template.visibility annotation (ANS defaults to PRIVATE) + // Visibility - from @UI.AdaptationHidden annotation (ANS defaults to PRIVATE) + // @UI.AdaptationHidden: false → PUBLIC, absent or true → PRIVATE (default) String visibility = extractVisibility(event); if (visibility != null) { template.setVisibility(visibility); @@ -194,21 +195,21 @@ private Email buildEmail(CdsEvent event, Map i18nTexts) { } /** - * Extract visibility from @notification.template.visibility annotation. Returns null if not - * specified (ANS defaults to PRIVATE). Only PUBLIC and PRIVATE are valid values. + * Extract visibility from @UI.AdaptationHidden annotation. + * Returns "PUBLIC" if @UI.AdaptationHidden: false, null otherwise (ANS defaults to PRIVATE). */ private String extractVisibility(CdsEvent event) { return event - .findAnnotation("notification.template.visibility") + .findAnnotation("UI.AdaptationHidden") .map(a -> { Object val = a.getValue(); - if (val instanceof Map enumMap) { - // Enum: {#=PUBLIC} - return enumMap.values().stream().findFirst().map(Object::toString).orElse(null).toUpperCase(); + // @UI.AdaptationHidden: false → template is PUBLIC (visible for customization) + if (Boolean.FALSE.equals(val) || "false".equalsIgnoreCase(String.valueOf(val))) { + return "PUBLIC"; } - return val.toString().toUpperCase(); + // @UI.AdaptationHidden: true or any other value → PRIVATE (default) + return null; }) - .filter(v -> "PUBLIC".equals(v) || "PRIVATE".equals(v)) .orElse(null); } diff --git a/sample-app/srv/notificationtypes-data.cds b/sample-app/srv/notificationtypes-data.cds index c5bb3f0..b7c8dc5 100644 --- a/sample-app/srv/notificationtypes-data.cds +++ b/sample-app/srv/notificationtypes-data.cds @@ -138,6 +138,7 @@ type DeliveryChannelType : String enum { service NotificationService { @description : '{i18n>DESCRIPTION}' + @UI.AdaptationHidden: false @notification : { template: { title : '{i18n>TEMPLATE_SENSITIVE}', From 57599b2701fb7664522de09fed0bacd07d5cdcfc Mon Sep 17 00:00:00 2001 From: Buse Halis Date: Tue, 2 Jun 2026 14:21:07 +0300 Subject: [PATCH 04/19] refactor: use @notification.customizable annotation for template visibility instead of @UI.AdaptationHidden --- README.md | 6 +++--- .../handlers/NotificationTemplateBuilder.java | 16 ++++++++-------- sample-app/srv/notificationtypes-data.cds | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index bdc0b38..8e15216 100644 --- a/README.md +++ b/README.md @@ -139,13 +139,13 @@ The `template` section defines the visible content of the notification: titles, | Annotation | Required | Description | |---|---|---| | `@description` | No | Description label of a notification type. Shown to administrators and end-users in the SAP Build Work Zone notification type management UI. | +| `@notification.customizable` | No | Controls whether customer administrators can see and customize this template. `true` = PUBLIC (visible for customization), absent or `false` = PRIVATE (default). See [Template Customization](#template-customization). | | `@notification.template.title` | **Yes** | Detailed notification title, shown to the recipient and authorized users. May contain sensitive information (e.g. "Low stock: Wuthering Heights by Emily Brontë"). Mapped to `TemplateSensitive` in ANS. | | `@notification.template.publicTitle` | **Yes** | Short, non-sensitive title shown when the viewer is not authorized to see the full details (e.g. "Low Stock Alert"). Mapped to `TemplatePublic` in ANS. | | `@notification.template.subtitle` | **Yes** | Subtitle for the notification. | | `@notification.template.groupedTitle` | **Yes** | Title shown when multiple notifications of the same type are collapsed into a single group (e.g. "Low stock alerts"). | | `@notification.template.email.subject` | No | Email subject line. | | `@notification.template.email.html` | No | Inline HTML or classpath path to HTML template file. See [Step 3](#step-3-add-email-html-template-optional) for details. | -| `@UI.AdaptationHidden` | No | Controls whether customer administrators can see and customize this template. `false` = PUBLIC (visible for customization), absent or `true` = PRIVATE (default). See [Template Customization](#template-customization). | | `@notification.deliveryChannels` | No | How the notification is delivered: Web and/or Email. If omitted, notifications are delivered via Web only (no email). Each entry has: `channel` (`#Mail` or `#Web`), `enabled` (Boolean), and `defaultPreference` (Boolean, whether the channel is enabled by default for users). Note: setting `#Mail` here is not enough on its own. The ANS instance must be configured with an email infrastructure (see [ANS Service Binding](#option-1-ans-service-binding)). | | `@notification.priority` | No | You can set a static notification priority: Priority enum (`#LOW`, `#NEUTRAL`, `#MEDIUM`, `#HIGH`). Can also be a CDS expression (see [Dynamic Priority](#dynamic-priority)). | | `@Common.SemanticObject` | No | Maps to `NavigationTargetObject` in ANS. Used for SAP Fiori launchpad navigation. Allows users to navigate directly to the relevant Fiori application when clicking the notification in SAP Build Work Zone. | @@ -723,11 +723,11 @@ For the full setup guide, see [Identity Directory Connectivity](https://help.sap The plugin automatically provisions notification templates to ANS during application startup. These templates enable customer administrators to create customized copies of your notification content for their organization. For details on how template customization works from the admin perspective, see the [Template Customization documentation](https://github.wdf.sap.corp/pages/sl-hybrid/ans/notifications-scenario/template-customization/). -By default, templates are `PRIVATE` (not visible to customer admins). To make a template visible and customizable by admins, add `@UI.AdaptationHidden: false`: +By default, templates are `PRIVATE` (not visible to customer admins). To make a template visible and customizable by admins, add `@notification.customizable: true`: ```cds -@UI.AdaptationHidden: false @notification : { + customizable: true, template: { title : '{i18n>TEMPLATE_SENSITIVE}', ... diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateBuilder.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateBuilder.java index d439309..47648ae 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateBuilder.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateBuilder.java @@ -69,8 +69,8 @@ private Optional extractTemplateFromEvent(CdsEvent event) NotificationTemplates template = Struct.create(NotificationTemplates.class); template.setKey(key); - // Visibility - from @UI.AdaptationHidden annotation (ANS defaults to PRIVATE) - // @UI.AdaptationHidden: false → PUBLIC, absent or true → PRIVATE (default) + // Visibility - from @notification.customizable annotation (ANS defaults to PRIVATE) + // @notification.customizable: true → PUBLIC, absent or false → PRIVATE (default) String visibility = extractVisibility(event); if (visibility != null) { template.setVisibility(visibility); @@ -195,19 +195,19 @@ private Email buildEmail(CdsEvent event, Map i18nTexts) { } /** - * Extract visibility from @UI.AdaptationHidden annotation. - * Returns "PUBLIC" if @UI.AdaptationHidden: false, null otherwise (ANS defaults to PRIVATE). + * Extract visibility from @notification.customizable annotation. + * Returns "PUBLIC" if @notification.customizable: true, null otherwise (ANS defaults to PRIVATE). */ private String extractVisibility(CdsEvent event) { return event - .findAnnotation("UI.AdaptationHidden") + .findAnnotation("notification.customizable") .map(a -> { Object val = a.getValue(); - // @UI.AdaptationHidden: false → template is PUBLIC (visible for customization) - if (Boolean.FALSE.equals(val) || "false".equalsIgnoreCase(String.valueOf(val))) { + // @notification.customizable: true → template is PUBLIC (visible for customization) + if (Boolean.TRUE.equals(val) || "true".equalsIgnoreCase(String.valueOf(val))) { return "PUBLIC"; } - // @UI.AdaptationHidden: true or any other value → PRIVATE (default) + // @notification.customizable: false or any other value → PRIVATE (default) return null; }) .orElse(null); diff --git a/sample-app/srv/notificationtypes-data.cds b/sample-app/srv/notificationtypes-data.cds index b7c8dc5..09eeb88 100644 --- a/sample-app/srv/notificationtypes-data.cds +++ b/sample-app/srv/notificationtypes-data.cds @@ -138,8 +138,8 @@ type DeliveryChannelType : String enum { service NotificationService { @description : '{i18n>DESCRIPTION}' - @UI.AdaptationHidden: false @notification : { + customizable: true, template: { title : '{i18n>TEMPLATE_SENSITIVE}', publicTitle : '{i18n>TEMPLATE_PUBLIC}', From 24960d14469b9108adfbad8eb17c489036ef3551 Mon Sep 17 00:00:00 2001 From: Buse Halis Date: Tue, 2 Jun 2026 14:21:51 +0300 Subject: [PATCH 05/19] test: add integration tests for standalone template provisioning --- ...ionTemplateProviderServiceMockHandler.java | 183 +++++++ .../NotificationTemplateProvisioningTest.java | 502 ++++++++++++++++++ 2 files changed, 685 insertions(+) create mode 100644 sample-app/srv/src/test/java/customer/sample_app/handlers/mock/NotificationTemplateProviderServiceMockHandler.java create mode 100644 sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTemplateProvisioningTest.java diff --git a/sample-app/srv/src/test/java/customer/sample_app/handlers/mock/NotificationTemplateProviderServiceMockHandler.java b/sample-app/srv/src/test/java/customer/sample_app/handlers/mock/NotificationTemplateProviderServiceMockHandler.java new file mode 100644 index 0000000..065e768 --- /dev/null +++ b/sample-app/srv/src/test/java/customer/sample_app/handlers/mock/NotificationTemplateProviderServiceMockHandler.java @@ -0,0 +1,183 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-notifications contributors. + */ +package customer.sample_app.handlers.mock; + +import cds.gen.notificationtemplateproviderservice.NotificationTemplates; +import cds.gen.notificationtemplateproviderservice.NotificationTemplates_; +import com.sap.cds.services.cds.CdsCreateEventContext; +import com.sap.cds.services.cds.CdsReadEventContext; +import com.sap.cds.services.cds.CdsUpdateEventContext; +import com.sap.cds.services.cds.CqnService; +import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.handler.annotations.On; +import com.sap.cds.services.handler.annotations.ServiceName; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +/** + * Mock handler for NotificationTemplateProviderService. Intercepts CREATE, READ and UPDATE + * operations to prevent actual remote HTTP calls during integration tests. + */ +@Component +@Profile("test") +@ServiceName("NotificationTemplateProviderService") +public class NotificationTemplateProviderServiceMockHandler implements EventHandler { + + private static final Logger logger = + LoggerFactory.getLogger(NotificationTemplateProviderServiceMockHandler.class); + + // In-memory storage for notification templates + private static final Map templateStore = + new ConcurrentHashMap<>(); + + // Tracks how many times each template key has been updated + private static final Map updateCountByKey = new ConcurrentHashMap<>(); + + @On(event = CqnService.EVENT_CREATE, entity = NotificationTemplates_.CDS_NAME) + public void interceptCreate(CdsCreateEventContext context) { + logger.info( + "MockHandler intercepting NotificationTemplates CREATE - {} entries", + context.getCqn().entries().size()); + + List> resultEntries = new ArrayList<>(); + + context + .getCqn() + .entries() + .forEach( + entry -> { + logger.info("NotificationTemplate entry data: {}", entry); + + NotificationTemplates template = NotificationTemplates.create(); + entry.forEach(template::put); + + String key = template.getKey(); + if (key == null || key.isEmpty()) { + throw new IllegalArgumentException("NotificationTemplate Key is required"); + } + + // Store template by key + templateStore.put(key, template); + + logger.info( + "Mock NotificationTemplateProviderService: Stored template with Key: {}, Visibility: {}", + key, + template.getVisibility()); + + resultEntries.add(entry); + }); + + context.setResult(resultEntries); + context.setCompleted(); + } + + @On(event = CqnService.EVENT_READ, entity = NotificationTemplates_.CDS_NAME) + public void interceptRead(CdsReadEventContext context) { + logger.info("MockHandler intercepting NotificationTemplates READ"); + + List> results = new ArrayList<>(); + for (NotificationTemplates template : templateStore.values()) { + Map row = new LinkedHashMap<>(); + template.forEach(row::put); + results.add(row); + } + + logger.info("MockHandler returning {} notification templates for READ", results.size()); + context.setResult(results); + context.setCompleted(); + } + + @On(event = CqnService.EVENT_UPDATE, entity = NotificationTemplates_.CDS_NAME) + public void interceptUpdate(CdsUpdateEventContext context) { + logger.info("MockHandler intercepting NotificationTemplates UPDATE"); + + context + .getCqn() + .entries() + .forEach( + entry -> { + NotificationTemplates updated = NotificationTemplates.create(); + entry.forEach(updated::put); + + String key = updated.getKey(); + if (key != null && templateStore.containsKey(key)) { + templateStore.put(key, updated); + updateCountByKey + .computeIfAbsent(key, k -> new AtomicInteger(0)) + .incrementAndGet(); + logger.info("MockHandler updated notification template: Key={}", key); + } else { + logger.warn("MockHandler UPDATE: no existing template with Key={}", key); + } + }); + + context.setResult(Collections.emptyList()); + context.setCompleted(); + } + + // ────────────────────────────────────────────────────────────── + // Static helpers for test assertions + // ────────────────────────────────────────────────────────────── + + /** + * Retrieves a template by key. + * + * @param key The template key + * @return The template or null if not found + */ + public static NotificationTemplates getTemplateByKey(String key) { + return templateStore.get(key); + } + + /** + * Retrieves all stored templates. + * + * @return All templates + */ + public static List getAllTemplates() { + return new ArrayList<>(templateStore.values()); + } + + /** Clears all stored templates. Useful for test cleanup. */ + public static void clearAllTemplates() { + templateStore.clear(); + updateCountByKey.clear(); + logger.info("Mock NotificationTemplateProviderService: Cleared all templates"); + } + + /** + * Gets the count of stored templates. + * + * @return The number of templates + */ + public static int getTemplateCount() { + return templateStore.size(); + } + + /** + * Checks if a template exists by key. + * + * @param key The template key + * @return true if the template exists + */ + public static boolean hasTemplate(String key) { + return templateStore.containsKey(key); + } + + /** + * Gets the number of times a template was updated. + * + * @param key The template key + * @return The number of updates, 0 if never updated + */ + public static int getUpdateCount(String key) { + AtomicInteger count = updateCountByKey.get(key); + return count != null ? count.get() : 0; + } +} diff --git a/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTemplateProvisioningTest.java b/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTemplateProvisioningTest.java new file mode 100644 index 0000000..f39a0c5 --- /dev/null +++ b/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTemplateProvisioningTest.java @@ -0,0 +1,502 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-notifications contributors. + */ +package customer.sample_app.integration; + +import static org.junit.jupiter.api.Assertions.*; + +import cds.gen.notificationtemplateproviderservice.NotificationTemplates; +import cds.gen.notificationtemplateproviderservice.Translations; +import customer.sample_app.handlers.mock.NotificationTemplateProviderServiceMockHandler; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +/** + * Integration tests for standalone NotificationTemplate provisioning. Verifies that + * NotificationTemplateAutoProvisionerHandler correctly creates templates from CDS event annotations + * at application startup. + */ +@SpringBootTest +@ActiveProfiles("test") +public class NotificationTemplateProvisioningTest { + + private static final Logger LOG = + LoggerFactory.getLogger(NotificationTemplateProvisioningTest.class); + + /** Pattern to detect unresolved i18n placeholders like {i18n>KEY} */ + private static final Pattern UNRESOLVED_I18N = Pattern.compile("\\{i18n>[^}]+\\}"); + + // ────────────────────────────────────────────────────────────── + // Test 1: Templates are provisioned at startup + // ────────────────────────────────────────────────────────────── + + @Test + void testTemplatesProvisionedAtStartup() { + LOG.info("=========================================="); + LOG.info("Test: Templates should be auto-provisioned at startup"); + LOG.info("=========================================="); + + List allTemplates = + NotificationTemplateProviderServiceMockHandler.getAllTemplates(); + assertFalse(allTemplates.isEmpty(), "At least one NotificationTemplate should be provisioned"); + + LOG.info("Total templates provisioned: {}", allTemplates.size()); + + // The sample-app has 6 events with @notification.template.title → 6 templates + assertTrue( + allTemplates.size() >= 6, + "Expected at least 6 templates (one per annotated event), got: " + allTemplates.size()); + } + + // ────────────────────────────────────────────────────────────── + // Test 2: CertificateExpiration template has correct structure + // ────────────────────────────────────────────────────────────── + + @Test + void testCertificateExpirationTemplateStructure() { + LOG.info("=========================================="); + LOG.info("Test: CertificateExpiration template structure"); + LOG.info("=========================================="); + + NotificationTemplates template = + NotificationTemplateProviderServiceMockHandler.getTemplateByKey("CertificateExpiration"); + assertNotNull(template, "CertificateExpiration template should be provisioned"); + + // Key + assertEquals("CertificateExpiration", template.getKey()); + + // Visibility - @notification.customizable: true → PUBLIC + assertEquals("PUBLIC", template.getVisibility(), "Template with @notification.customizable: true should be PUBLIC"); + + // PropertiesSchema - should include all event elements EXCEPT recipients + String schema = template.getPropertiesSchema(); + assertNotNull(schema, "PropertiesSchema should be set"); + assertFalse(schema.contains("recipients"), "Schema should NOT contain 'recipients' (it's not a template variable)"); + assertTrue(schema.contains("certificateName"), "Schema should contain 'certificateName' property"); + assertTrue(schema.contains("expirationDate"), "Schema should contain 'expirationDate' property"); + assertTrue(schema.contains("name"), "Schema should contain 'name' property"); + + // Translations + List translations = template.getTranslations(); + assertNotNull(translations, "Translations should not be null"); + assertFalse(translations.isEmpty(), "Translations should not be empty"); + + LOG.info("CertificateExpiration template: Key={}, Visibility={}, Translations={}", + template.getKey(), template.getVisibility(), translations.size()); + } + + // ────────────────────────────────────────────────────────────── + // Test 3: Visibility defaults to PRIVATE (not set) when no annotation + // ────────────────────────────────────────────────────────────── + + @Test + void testVisibilityDefaultsToPrivate() { + LOG.info("=========================================="); + LOG.info("Test: Templates without @notification.customizable should have null visibility (PRIVATE)"); + LOG.info("=========================================="); + + // SystemMaintenance has no @notification.customizable → visibility should be null (ANS defaults PRIVATE) + NotificationTemplates template = + NotificationTemplateProviderServiceMockHandler.getTemplateByKey("SystemMaintenance"); + assertNotNull(template, "SystemMaintenance template should be provisioned"); + assertNull(template.getVisibility(), + "Template without @notification.customizable should have null visibility (ANS defaults to PRIVATE)"); + + LOG.info("SystemMaintenance visibility: {} (null = ANS default PRIVATE)", template.getVisibility()); + } + + // ────────────────────────────────────────────────────────────── + // Test 4: i18n resolved in translations + // ────────────────────────────────────────────────────────────── + + @Test + void testI18nResolvedInTranslations() { + LOG.info("=========================================="); + LOG.info("Test: i18n placeholders should be resolved in template translations"); + LOG.info("=========================================="); + + NotificationTemplates template = + NotificationTemplateProviderServiceMockHandler.getTemplateByKey("CertificateExpiration"); + assertNotNull(template, "CertificateExpiration template should be provisioned"); + + List translations = template.getTranslations(); + assertFalse(translations.isEmpty(), "Should have at least one translation"); + + for (Translations t : translations) { + String lang = t.getLanguage(); + + // Title must be set (required field) + assertNotNull(t.getTitle(), "Title should not be null for lang: " + lang); + assertFalse(t.getTitle().isEmpty(), "Title should not be empty for lang: " + lang); + + // No unresolved i18n placeholders + assertNoUnresolvedI18n(t.getTitle(), "Title", lang); + assertNoUnresolvedI18n(t.getBody(), "Body", lang); + assertNoUnresolvedI18n(t.getPreview(), "Preview", lang); + assertNoUnresolvedI18n(t.getDescription(), "Description", lang); + + if (t.getEmail() != null) { + assertNoUnresolvedI18n(t.getEmail().getSubject(), "Email.Subject", lang); + assertNoUnresolvedI18n(t.getEmail().getBodyHtml(), "Email.BodyHtml", lang); + } + + LOG.info("[{}] Title={}, Body={}, Preview={}", lang, t.getTitle(), t.getBody(), t.getPreview()); + } + } + + // ────────────────────────────────────────────────────────────── + // Test 5: Multi-language translations for i18n-based template + // ────────────────────────────────────────────────────────────── + + @Test + void testMultiLanguageTranslations() { + LOG.info("=========================================="); + LOG.info("Test: CertificateExpiration should have translations for all i18n languages"); + LOG.info("=========================================="); + + NotificationTemplates template = + NotificationTemplateProviderServiceMockHandler.getTemplateByKey("CertificateExpiration"); + assertNotNull(template); + + List translations = template.getTranslations(); + + // The sample-app has i18n files for: en, de, tr, es + assertTrue(translations.size() >= 4, + "Expected translations for at least 4 languages, got: " + translations.size()); + + // Verify English + Translations en = findTranslation(translations, "en"); + assertNotNull(en, "English translation should exist"); + assertTrue(en.getTitle().contains("{{certificateName}}"), + "English title should contain Mustache variable {{certificateName}}"); + + // Verify German + Translations de = findTranslation(translations, "de"); + assertNotNull(de, "German translation should exist"); + + // Verify Turkish + Translations tr = findTranslation(translations, "tr"); + assertNotNull(tr, "Turkish translation should exist"); + + // Verify Spanish + Translations es = findTranslation(translations, "es"); + assertNotNull(es, "Spanish translation should exist"); + + // Titles should differ per language (i18n resolved) + assertNotEquals(en.getTitle(), de.getTitle(), "EN and DE titles should differ"); + + LOG.info("Translations verified for {} languages", translations.size()); + } + + // ────────────────────────────────────────────────────────────── + // Test 6: Static template (no i18n) has identical translations for all locales + // ────────────────────────────────────────────────────────────── + + @Test + void testStaticTemplateTranslation() { + LOG.info("=========================================="); + LOG.info("Test: Templates with static strings should have same value across all locale translations"); + LOG.info("=========================================="); + + // SystemMaintenance uses static strings (no {i18n>...} placeholders). + // Since i18n files exist in the project for other events, translations are created + // for all discovered locales — but all with the same static value. + NotificationTemplates template = + NotificationTemplateProviderServiceMockHandler.getTemplateByKey("SystemMaintenance"); + assertNotNull(template, "SystemMaintenance template should be provisioned"); + + List translations = template.getTranslations(); + assertNotNull(translations, "Translations should not be null"); + assertFalse(translations.isEmpty(), "Should have at least one translation"); + + // Verify title contains Mustache variable + Translations first = translations.get(0); + assertNotNull(first.getTitle()); + assertTrue(first.getTitle().contains("{{systemName}}"), + "Title should contain Mustache variable: " + first.getTitle()); + + // All translations should have the same title (static string, no i18n differentiation) + String expectedTitle = first.getTitle(); + for (Translations t : translations) { + assertEquals(expectedTitle, t.getTitle(), + "Static template should have identical title across all locales, but lang '" + + t.getLanguage() + "' differs"); + } + + LOG.info("Static template: {} translations, all with title: {}", translations.size(), expectedTitle); + } + + // ────────────────────────────────────────────────────────────── + // Test 7: Email HTML loaded from file + // ────────────────────────────────────────────────────────────── + + @Test + void testEmailHtmlLoadedFromFile() { + LOG.info("=========================================="); + LOG.info("Test: CertificateExpiration email HTML should be loaded from file"); + LOG.info("=========================================="); + + NotificationTemplates template = + NotificationTemplateProviderServiceMockHandler.getTemplateByKey("CertificateExpiration"); + assertNotNull(template); + + List translations = template.getTranslations(); + Translations en = findTranslation(translations, "en"); + assertNotNull(en, "English translation should exist"); + assertNotNull(en.getEmail(), "Email should be set for CertificateExpiration"); + assertNotNull(en.getEmail().getBodyHtml(), "Email HTML body should be loaded"); + + // Verify it's actual HTML content, not a file path + String html = en.getEmail().getBodyHtml(); + assertFalse(html.endsWith(".html"), "Email body should be HTML content, not a file path"); + assertTrue(html.contains("<"), "Email body should contain HTML tags"); + + LOG.info("Email HTML loaded successfully ({} chars)", html.length()); + } + + // ────────────────────────────────────────────────────────────── + // Test 8: Tags contain source and event + // ────────────────────────────────────────────────────────────── + + @Test + void testTagsContainSourceAndEvent() { + LOG.info("=========================================="); + LOG.info("Test: Template tags should contain source and event information"); + LOG.info("=========================================="); + + NotificationTemplates template = + NotificationTemplateProviderServiceMockHandler.getTemplateByKey("CertificateExpiration"); + assertNotNull(template); + + var tags = template.getTags(); + assertNotNull(tags, "Tags should not be null"); + assertFalse(tags.isEmpty(), "Tags should not be empty"); + + LOG.info("Tags: {}", tags); + } + + // ────────────────────────────────────────────────────────────── + // Test 9: Translation metadata (source, event, displayName) + // ────────────────────────────────────────────────────────────── + + @Test + void testTranslationMetadata() { + LOG.info("=========================================="); + LOG.info("Test: Translations should have source, event, and displayName set"); + LOG.info("=========================================="); + + NotificationTemplates template = + NotificationTemplateProviderServiceMockHandler.getTemplateByKey("CertificateExpiration"); + assertNotNull(template); + + Translations en = findTranslation(template.getTranslations(), "en"); + assertNotNull(en); + + // Source = service name (e.g. "NotificationService") + assertNotNull(en.getSource(), "Source should be set"); + assertFalse(en.getSource().isEmpty(), "Source should not be empty"); + + // Event = event name + assertEquals("CertificateExpiration", en.getEvent(), "Event should be the event name"); + + // DisplayName = event name + assertEquals("CertificateExpiration", en.getDisplayName(), "DisplayName should be the event name"); + + LOG.info("Source={}, Event={}, DisplayName={}", en.getSource(), en.getEvent(), en.getDisplayName()); + } + + // ────────────────────────────────────────────────────────────── + // Test 10: PropertiesSchema reflects event elements + // ────────────────────────────────────────────────────────────── + + @Test + void testPropertiesSchemaReflectsEventElements() { + LOG.info("=========================================="); + LOG.info("Test: PropertiesSchema should be a JSON schema of event elements"); + LOG.info("=========================================="); + + NotificationTemplates template = + NotificationTemplateProviderServiceMockHandler.getTemplateByKey("SystemMaintenance"); + assertNotNull(template); + + String schema = template.getPropertiesSchema(); + assertNotNull(schema, "PropertiesSchema should be set"); + + // Should be valid JSON-like structure + assertTrue(schema.contains("properties") || schema.contains("type"), + "Schema should contain JSON Schema keywords: " + schema); + + LOG.info("PropertiesSchema: {}", schema); + } + + // ────────────────────────────────────────────────────────────── + // Test 11: i18n placeholders resolved for all languages (exact values) + // ────────────────────────────────────────────────────────────── + + @Test + void testI18nPlaceholdersResolvedForAllLanguages() { + LOG.info("=========================================="); + LOG.info("Test: i18n placeholders should be resolved for EN, DE, TR, ES"); + LOG.info("=========================================="); + + NotificationTemplates template = + NotificationTemplateProviderServiceMockHandler.getTemplateByKey("CertificateExpiration"); + assertNotNull(template, "CertificateExpiration template should be provisioned"); + + List translations = template.getTranslations(); + + // Verify Title (maps to @notification.template.title) + assertEquals("Certificate: {{certificateName}}", findTranslation(translations, "en").getTitle()); + assertEquals("Zertifikat: {{certificateName}}", findTranslation(translations, "de").getTitle()); + assertEquals("Sertifika: {{certificateName}}", findTranslation(translations, "tr").getTitle()); + assertEquals("Certificado: {{certificateName}}", findTranslation(translations, "es").getTitle()); + + // Verify Body (maps to @notification.template.subtitle) + assertEquals("Certificate Expiration", findTranslation(translations, "en").getBody()); + assertEquals("Zertifikatablauf", findTranslation(translations, "de").getBody()); + assertEquals("Sertifika Sona Ermesi", findTranslation(translations, "tr").getBody()); + assertEquals("Expiración de Certificado", findTranslation(translations, "es").getBody()); + + // Verify Preview (maps to @notification.template.publicTitle) + assertEquals("Certificate Expiry", findTranslation(translations, "en").getPreview()); + assertEquals("Zertifikatablauf", findTranslation(translations, "de").getPreview()); + assertEquals("Sertifika Sona Ermesi", findTranslation(translations, "tr").getPreview()); + assertEquals("Expiración de Certificado", findTranslation(translations, "es").getPreview()); + + // Verify Email Subject + assertEquals("Certificate Expiration Alert", findTranslation(translations, "en").getEmail().getSubject()); + assertEquals("Zertifikatablauf-Warnung", findTranslation(translations, "de").getEmail().getSubject()); + assertEquals("Sertifika Sona Erme Uyarısı", findTranslation(translations, "tr").getEmail().getSubject()); + assertEquals("Alerta de Expiración de Certificado", findTranslation(translations, "es").getEmail().getSubject()); + + // Verify Description + assertEquals("Certificate Expiration Alert", findTranslation(translations, "en").getDescription()); + assertEquals("Zertifikatablauf-Warnung", findTranslation(translations, "de").getDescription()); + assertEquals("Sertifika Sona Erme Uyarısı", findTranslation(translations, "tr").getDescription()); + assertEquals("Alerta de Expiración de Certificado", findTranslation(translations, "es").getDescription()); + + LOG.info("All 4 languages verified with exact i18n values"); + } + + // ────────────────────────────────────────────────────────────── + // Test 12: Email HTML contains Mustache variables (runtime) + // ────────────────────────────────────────────────────────────── + + @Test + void testEmailHtmlContainsMustacheVariables() { + LOG.info("=========================================="); + LOG.info("Test: Email HTML should contain Mustache variables for ANS runtime"); + LOG.info("=========================================="); + + NotificationTemplates template = + NotificationTemplateProviderServiceMockHandler.getTemplateByKey("CertificateExpiration"); + assertNotNull(template); + + Translations en = findTranslation(template.getTranslations(), "en"); + assertNotNull(en, "English translation should exist"); + assertNotNull(en.getEmail(), "Email should be set"); + + String emailHtml = en.getEmail().getBodyHtml(); + assertNotNull(emailHtml, "Email HTML should not be null"); + + // These Mustache variables must be present — ANS resolves them at runtime + List expectedVariables = + List.of( + "{{certificateName}}", + "{{expirationDate}}", + "{{renewLink}}", + "{{name}}", + "{{year}}", + "{{companyName}}"); + + for (String variable : expectedVariables) { + assertTrue( + emailHtml.contains(variable), "Email HTML should contain Mustache variable: " + variable); + } + + LOG.info("All {} Mustache variables present in email HTML", expectedVariables.size()); + } + + // ────────────────────────────────────────────────────────────── + // Test 13: Email HTML i18n resolved per language + // ────────────────────────────────────────────────────────────── + + @Test + void testEmailHtmlI18nResolvedPerLanguage() { + LOG.info("=========================================="); + LOG.info("Test: Email HTML i18n values should differ per language"); + LOG.info("=========================================="); + + NotificationTemplates template = + NotificationTemplateProviderServiceMockHandler.getTemplateByKey("CertificateExpiration"); + assertNotNull(template); + + List translations = template.getTranslations(); + + // Button text per language + Map expectedButtonTexts = + Map.of( + "en", "Renew Now", + "de", "Jetzt erneuern", + "tr", "Şimdi Yenile", + "es", "Renovar Ahora"); + + // Greeting per language (contains Mustache variable {{name}}) + Map expectedGreetings = + Map.of( + "en", "Dear {{name}}", + "de", "Sehr geehrte(r) {{name}}", + "tr", "Sayın {{name}}", + "es", "Estimado(a) {{name}}"); + + for (String lang : List.of("en", "de", "tr", "es")) { + Translations t = findTranslation(translations, lang); + assertNotNull(t, "Translation should exist for lang: " + lang); + assertNotNull(t.getEmail(), "Email should exist for lang: " + lang); + + String emailHtml = t.getEmail().getBodyHtml(); + assertNotNull(emailHtml, "Email HTML should not be null for lang: " + lang); + + // Verify button text + String expectedButton = expectedButtonTexts.get(lang); + assertTrue( + emailHtml.contains(expectedButton), + "[" + lang + "] Email HTML should contain button text: '" + expectedButton + "'"); + + // Verify greeting text + String expectedGreeting = expectedGreetings.get(lang); + assertTrue( + emailHtml.contains(expectedGreeting), + "[" + lang + "] Email HTML should contain greeting: '" + expectedGreeting + "'"); + + LOG.info("[{}] Button: '{}', Greeting: '{}'", lang, expectedButton, expectedGreeting); + } + + LOG.info("Email HTML i18n verified for all 4 languages"); + } + + // ────────────────────────────────────────────────────────────── + // Helpers + // ────────────────────────────────────────────────────────────── + + private Translations findTranslation(List translations, String lang) { + return translations.stream() + .filter(t -> lang.equals(t.getLanguage())) + .findFirst() + .orElse(null); + } + + private void assertNoUnresolvedI18n(String value, String fieldName, String lang) { + if (value != null) { + assertFalse( + UNRESOLVED_I18N.matcher(value).find(), + "Unresolved {i18n>...} placeholder in " + fieldName + " [" + lang + "]: " + value); + } + } +} From 216d2b111839dc4498ff322e233eda73ee680bb4 Mon Sep 17 00:00:00 2001 From: Buse Halis Date: Tue, 2 Jun 2026 14:45:35 +0300 Subject: [PATCH 06/19] refactor: reduce log verbosity, per-item INFO to DEBUG --- .../handlers/EntityNotificationHandler.java | 4 +- .../handlers/NotificationBuilder.java | 6 +- ...icationTemplateAutoProvisionerHandler.java | 10 +- .../handlers/NotificationTemplateBuilder.java | 4 +- ...otificationTypeAutoProvisionerHandler.java | 10 +- .../handlers/NotificationTypeBuilder.java | 6 +- .../handlers/ProductionHandler.java | 4 +- ...otificationProviderServiceMockHandler.java | 8 +- ...ionTemplateProviderServiceMockHandler.java | 16 +- ...icationTypeProviderServiceMockHandler.java | 16 +- .../NotificationIntegrationTest.java | 268 +++++++++--------- .../NotificationTemplateProvisioningTest.java | 106 +++---- .../NotificationTypeProvisioningTest.java | 42 +-- .../NotificationTypeTemplateTest.java | 38 +-- 14 files changed, 269 insertions(+), 269 deletions(-) diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/EntityNotificationHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/EntityNotificationHandler.java index ebab548..b464d71 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/EntityNotificationHandler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/EntityNotificationHandler.java @@ -172,7 +172,7 @@ private void processNotificationEntry( // Optional: explicit parameter mapping (entity field → notification property) Map properties = notification.get("parameters") instanceof Map m ? m : null; - logger.info( + logger.debug( "Entity notification triggered: type='{}', event='{}', entity='{}'", notificationType, currentEvent, @@ -307,7 +307,7 @@ private void emitBatchNotification( EventContext notificationCtx = EventContext.create(notificationType, null); notificationCtx.put("data", batchData); service.emit(notificationCtx); - logger.info( + logger.debug( "Emitted {} notification(s) for event '{}' on service '{}'", batchData.size(), notificationType, diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationBuilder.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationBuilder.java index f76e00d..b7e0abd 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationBuilder.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationBuilder.java @@ -110,7 +110,7 @@ public List buildNotifications(EventContext context) { "Batch notification emit for event '{}' with empty data list, skipping", eventName); return List.of(); } - logger.info("Batch notification emit for event '{}': {} entries", eventName, dataList.size()); + logger.debug("Batch notification emit for event '{}': {} entries", eventName, dataList.size()); List results = new ArrayList<>(); for (Object item : dataList) { if (!(item instanceof CdsData cdsData)) { @@ -166,7 +166,7 @@ private NotificationBuildResult buildSingleNotification( List properties = extractProperties(event, eventData); notification.setProperties(properties); - logger.info("Built notification with {} properties", properties.size()); + logger.debug("Built notification with {} properties", properties.size()); return new NotificationBuildResult(eventName, notification, event); } @@ -251,7 +251,7 @@ public CqnPredicate containment( .map(row -> row.get("result")) .map(v -> v.toString().toUpperCase()) .orElse("NEUTRAL"); - logger.info("Dynamic priority evaluated via DB to: {}", priority); + logger.debug("Dynamic priority evaluated via DB to: {}", priority); return validatePriority(priority); } catch (Exception e) { logger.error( diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateAutoProvisionerHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateAutoProvisionerHandler.java index 052d550..f265ff3 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateAutoProvisionerHandler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateAutoProvisionerHandler.java @@ -66,7 +66,7 @@ private void provisionNotificationTemplates() { // Fetch all existing templates from ANS to determine create vs update Set existingTemplateKeys = fetchExistingTemplateKeys(); - logger.info("Found {} existing standalone templates in ANS", existingTemplateKeys.size()); + logger.debug("Found {} existing standalone templates in ANS", existingTemplateKeys.size()); for (NotificationTemplates template : templates) { String key = template.getKey(); @@ -100,14 +100,14 @@ private void createTemplate(NotificationTemplates template) { notificationTemplateProviderService.run( Insert.into(NotificationTemplates_.CDS_NAME).entry(template)); - logger.info( + logger.debug( "Standalone NotificationTemplate '{}' created in ANS successfully", template.getKey()); } catch (Exception e) { String errorMsg = e.getMessage() != null ? e.getMessage() : ""; if (errorMsg.contains("409")) { // Race condition: template was created between our GET and INSERT - logger.info( + logger.debug( "Standalone template '{}' was created concurrently (409). Attempting update...", template.getKey()); updateTemplate(template); @@ -135,13 +135,13 @@ private void createTemplate(NotificationTemplates template) { } private void updateTemplate(NotificationTemplates template) { - logger.info("Updating standalone template '{}'", template.getKey()); + logger.debug("Updating standalone template '{}'", template.getKey()); try { notificationTemplateProviderService.run( Update.entity(NotificationTemplates_.CDS_NAME).data(template)); - logger.info( + logger.debug( "Standalone NotificationTemplate '{}' updated in ANS successfully", template.getKey()); } catch (Exception e) { String errorMsg = e.getMessage() != null ? e.getMessage() : ""; diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateBuilder.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateBuilder.java index 47648ae..4ece271 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateBuilder.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateBuilder.java @@ -102,7 +102,7 @@ private Optional extractTemplateFromEvent(CdsEvent event) template.setTranslations(translations); - logger.info("Extracted standalone NotificationTemplate: Key={}, Source={}", key, source); + logger.debug("Extracted standalone NotificationTemplate: Key={}, Source={}", key, source); return Optional.of(template); } @@ -181,7 +181,7 @@ private Email buildEmail(CdsEvent event, Map i18nTexts) { if (htmlValue.endsWith(".html")) { String htmlContent = i18nHelper.loadHtmlFromClasspath(htmlValue, i18nTexts); email.setBodyHtml(htmlContent); - logger.info("Loaded HTML template from file: {}", htmlValue); + logger.debug("Loaded HTML template from file: {}", htmlValue); } else { email.setBodyHtml(htmlValue); } diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeAutoProvisionerHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeAutoProvisionerHandler.java index f672748..b28c754 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeAutoProvisionerHandler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeAutoProvisionerHandler.java @@ -55,7 +55,7 @@ private void provisionNotificationTypes() { // Fetch ALL existing notification types from ANS in a single call // to build a reliable Key → Id mapping. Map existingTypeIds = fetchExistingNotificationTypeIds(); - logger.info("Found {} existing notification types in ANS", existingTypeIds.size()); + logger.debug("Found {} existing notification types in ANS", existingTypeIds.size()); for (NotificationTypes notificationType : notificationTypes) { String key = notificationType.getNotificationTypeKey(); @@ -99,7 +99,7 @@ private void createNotificationType(NotificationTypes notificationType) { notificationTypeProviderService.run( Insert.into(NotificationTypes_.CDS_NAME).entry(notificationType)); - logger.info( + logger.debug( "NotificationType '{}' created in ANS successfully", notificationType.getNotificationTypeKey()); } catch (Exception e) { @@ -108,7 +108,7 @@ private void createNotificationType(NotificationTypes notificationType) { if (errorMsg.contains("409")) { // Race condition: type was created between our GET-all and this INSERT. // Re-fetch this specific type's ID and update. - logger.info( + logger.debug( "NotificationType '{}' was created concurrently (409). Attempting update...", notificationType.getNotificationTypeKey()); String id = @@ -160,7 +160,7 @@ private void createNotificationType(NotificationTypes notificationType) { private void updateNotificationType( NotificationTypes notificationType, String notificationTypeId) { notificationType.setNotificationTypeId(notificationTypeId); - logger.info( + logger.debug( "Updating NotificationType '{}' (id={})", notificationType.getNotificationTypeKey(), notificationTypeId); @@ -169,7 +169,7 @@ private void updateNotificationType( notificationTypeProviderService.run( Update.entity(NotificationTypes_.CDS_NAME).data(notificationType)); - logger.info( + logger.debug( "NotificationType '{}' updated in ANS successfully", notificationType.getNotificationTypeKey()); } catch (Exception e) { diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeBuilder.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeBuilder.java index 16f396b..fca07c8 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeBuilder.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeBuilder.java @@ -70,7 +70,7 @@ private Optional extractNotificationTypeFromEvent(CdsEvent ev nt.setDeliveryChannels(deliveryChannels); } - logger.info("Extracted NotificationType: {}", key); + logger.debug("Extracted NotificationType: {}", key); return Optional.of(nt); } @@ -140,7 +140,7 @@ private Templates createTemplate(CdsEvent event, String lang, Map extractDeliveryChannels(CdsEvent event) { deliveryChannels.add(deliveryChannel); - logger.info( + logger.debug( "Parsed delivery channel: Type={}, Enabled={}, DefaultPreference={}", deliveryChannel.getType(), deliveryChannel.getEnabled(), diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/ProductionHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/ProductionHandler.java index e75c6e3..e9f9df2 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/ProductionHandler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/ProductionHandler.java @@ -39,7 +39,7 @@ public void postNotifications(EventContext context) { } String eventName = results.get(0).eventName(); - logger.info("=== Processing {} notification(s) for event: {} ===", results.size(), eventName); + logger.debug("=== Processing {} notification(s) for event: {} ===", results.size(), eventName); int successCount = 0; Exception firstError = null; @@ -66,7 +66,7 @@ public void postNotifications(EventContext context) { } } - logger.info( + logger.debug( "Sent {}/{} notification(s) for event: {}", successCount, results.size(), eventName); if (firstError != null) { diff --git a/sample-app/srv/src/test/java/customer/sample_app/handlers/mock/NotificationProviderServiceMockHandler.java b/sample-app/srv/src/test/java/customer/sample_app/handlers/mock/NotificationProviderServiceMockHandler.java index 91ded2c..d95db31 100644 --- a/sample-app/srv/src/test/java/customer/sample_app/handlers/mock/NotificationProviderServiceMockHandler.java +++ b/sample-app/srv/src/test/java/customer/sample_app/handlers/mock/NotificationProviderServiceMockHandler.java @@ -35,7 +35,7 @@ public class NotificationProviderServiceMockHandler implements EventHandler { @On(event = CqnService.EVENT_CREATE, entity = Notifications_.CDS_NAME) public void interceptCreate(CdsCreateEventContext context) { - logger.info("MockHandler intercepting CREATE - {} entries", context.getCqn().entries().size()); + logger.debug("MockHandler intercepting CREATE - {} entries", context.getCqn().entries().size()); List> resultEntries = new ArrayList<>(); @@ -45,7 +45,7 @@ public void interceptCreate(CdsCreateEventContext context) { .entries() .forEach( entry -> { - logger.info("Entry data: {}", entry); + logger.debug("Entry data: {}", entry); Notifications notification = Notifications.create(); entry.forEach(notification::put); @@ -82,7 +82,7 @@ public void interceptCreate(CdsCreateEventContext context) { // Store notification notificationStore.put(notification.getId(), notification); - logger.info( + logger.debug( "Mock NotificationProviderService: Stored notification with ID: {}, TypeKey: {}", notification.getId(), notification.getNotificationTypeKey()); @@ -130,7 +130,7 @@ public static List getAllNotifications() { /** Clears all stored notifications. Useful for test cleanup. */ public static void clearAllNotifications() { notificationStore.clear(); - logger.info("Mock NotificationProviderService: Cleared all notifications"); + logger.debug("Mock NotificationProviderService: Cleared all notifications"); } /** diff --git a/sample-app/srv/src/test/java/customer/sample_app/handlers/mock/NotificationTemplateProviderServiceMockHandler.java b/sample-app/srv/src/test/java/customer/sample_app/handlers/mock/NotificationTemplateProviderServiceMockHandler.java index 065e768..15e640c 100644 --- a/sample-app/srv/src/test/java/customer/sample_app/handlers/mock/NotificationTemplateProviderServiceMockHandler.java +++ b/sample-app/srv/src/test/java/customer/sample_app/handlers/mock/NotificationTemplateProviderServiceMockHandler.java @@ -41,7 +41,7 @@ public class NotificationTemplateProviderServiceMockHandler implements EventHand @On(event = CqnService.EVENT_CREATE, entity = NotificationTemplates_.CDS_NAME) public void interceptCreate(CdsCreateEventContext context) { - logger.info( + logger.debug( "MockHandler intercepting NotificationTemplates CREATE - {} entries", context.getCqn().entries().size()); @@ -52,7 +52,7 @@ public void interceptCreate(CdsCreateEventContext context) { .entries() .forEach( entry -> { - logger.info("NotificationTemplate entry data: {}", entry); + logger.debug("NotificationTemplate entry data: {}", entry); NotificationTemplates template = NotificationTemplates.create(); entry.forEach(template::put); @@ -65,7 +65,7 @@ public void interceptCreate(CdsCreateEventContext context) { // Store template by key templateStore.put(key, template); - logger.info( + logger.debug( "Mock NotificationTemplateProviderService: Stored template with Key: {}, Visibility: {}", key, template.getVisibility()); @@ -79,7 +79,7 @@ public void interceptCreate(CdsCreateEventContext context) { @On(event = CqnService.EVENT_READ, entity = NotificationTemplates_.CDS_NAME) public void interceptRead(CdsReadEventContext context) { - logger.info("MockHandler intercepting NotificationTemplates READ"); + logger.debug("MockHandler intercepting NotificationTemplates READ"); List> results = new ArrayList<>(); for (NotificationTemplates template : templateStore.values()) { @@ -88,14 +88,14 @@ public void interceptRead(CdsReadEventContext context) { results.add(row); } - logger.info("MockHandler returning {} notification templates for READ", results.size()); + logger.debug("MockHandler returning {} notification templates for READ", results.size()); context.setResult(results); context.setCompleted(); } @On(event = CqnService.EVENT_UPDATE, entity = NotificationTemplates_.CDS_NAME) public void interceptUpdate(CdsUpdateEventContext context) { - logger.info("MockHandler intercepting NotificationTemplates UPDATE"); + logger.debug("MockHandler intercepting NotificationTemplates UPDATE"); context .getCqn() @@ -111,7 +111,7 @@ public void interceptUpdate(CdsUpdateEventContext context) { updateCountByKey .computeIfAbsent(key, k -> new AtomicInteger(0)) .incrementAndGet(); - logger.info("MockHandler updated notification template: Key={}", key); + logger.debug("MockHandler updated notification template: Key={}", key); } else { logger.warn("MockHandler UPDATE: no existing template with Key={}", key); } @@ -148,7 +148,7 @@ public static List getAllTemplates() { public static void clearAllTemplates() { templateStore.clear(); updateCountByKey.clear(); - logger.info("Mock NotificationTemplateProviderService: Cleared all templates"); + logger.debug("Mock NotificationTemplateProviderService: Cleared all templates"); } /** diff --git a/sample-app/srv/src/test/java/customer/sample_app/handlers/mock/NotificationTypeProviderServiceMockHandler.java b/sample-app/srv/src/test/java/customer/sample_app/handlers/mock/NotificationTypeProviderServiceMockHandler.java index 65a7aba..c6a9cda 100644 --- a/sample-app/srv/src/test/java/customer/sample_app/handlers/mock/NotificationTypeProviderServiceMockHandler.java +++ b/sample-app/srv/src/test/java/customer/sample_app/handlers/mock/NotificationTypeProviderServiceMockHandler.java @@ -45,7 +45,7 @@ public class NotificationTypeProviderServiceMockHandler implements EventHandler @On(event = CqnService.EVENT_CREATE, entity = NotificationTypes_.CDS_NAME) public void interceptCreate(CdsCreateEventContext context) { - logger.info( + logger.debug( "MockHandler intercepting NotificationTypes CREATE - {} entries", context.getCqn().entries().size()); @@ -57,7 +57,7 @@ public void interceptCreate(CdsCreateEventContext context) { .entries() .forEach( entry -> { - logger.info("NotificationType entry data: {}", entry); + logger.debug("NotificationType entry data: {}", entry); NotificationTypes notificationType = NotificationTypes.create(); entry.forEach(notificationType::put); @@ -78,7 +78,7 @@ public void interceptCreate(CdsCreateEventContext context) { + notificationType.getNotificationTypeVersion(); notificationTypeByKeyVersion.put(keyVersion, notificationType); - logger.info( + logger.debug( "Mock NotificationTypeProviderService: Stored notification type with ID: {}, Key: {}, Version: {}", notificationType.getNotificationTypeId(), notificationType.getNotificationTypeKey(), @@ -95,7 +95,7 @@ public void interceptCreate(CdsCreateEventContext context) { @On(event = CqnService.EVENT_READ, entity = NotificationTypes_.CDS_NAME) public void interceptRead(CdsReadEventContext context) { - logger.info("MockHandler intercepting NotificationTypes READ"); + logger.debug("MockHandler intercepting NotificationTypes READ"); List> results = new ArrayList<>(); for (NotificationTypes nt : notificationTypeStore.values()) { @@ -104,14 +104,14 @@ public void interceptRead(CdsReadEventContext context) { results.add(row); } - logger.info("MockHandler returning {} notification types for READ", results.size()); + logger.debug("MockHandler returning {} notification types for READ", results.size()); context.setResult(results); context.setCompleted(); } @On(event = CqnService.EVENT_UPDATE, entity = NotificationTypes_.CDS_NAME) public void interceptUpdate(CdsUpdateEventContext context) { - logger.info("MockHandler intercepting NotificationTypes UPDATE"); + logger.debug("MockHandler intercepting NotificationTypes UPDATE"); context .getCqn() @@ -133,7 +133,7 @@ public void interceptUpdate(CdsUpdateEventContext context) { String key = updated.getNotificationTypeKey(); updateCountByKey.computeIfAbsent(key, k -> new AtomicInteger(0)).incrementAndGet(); - logger.info("MockHandler updated notification type: Key={}, ID={}", key, id); + logger.debug("MockHandler updated notification type: Key={}, ID={}", key, id); } else { logger.warn("MockHandler UPDATE: no existing type with ID={}", id); } @@ -180,7 +180,7 @@ public static void clearAllNotificationTypes() { notificationTypeStore.clear(); notificationTypeByKeyVersion.clear(); updateCountByKey.clear(); - logger.info("Mock NotificationTypeProviderService: Cleared all notification types"); + logger.debug("Mock NotificationTypeProviderService: Cleared all notification types"); } /** diff --git a/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationIntegrationTest.java b/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationIntegrationTest.java index a06c4bc..7ca2640 100644 --- a/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationIntegrationTest.java +++ b/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationIntegrationTest.java @@ -60,10 +60,10 @@ public class NotificationIntegrationTest { @BeforeEach void setup() { - LOG.info("========================================"); - LOG.info("Active profiles: {}", String.join(", ", environment.getActiveProfiles())); - LOG.info("Setting up test - clearing notifications"); - LOG.info("========================================"); + LOG.debug("========================================"); + LOG.debug("Active profiles: {}", String.join(", ", environment.getActiveProfiles())); + LOG.debug("Setting up test - clearing notifications"); + LOG.debug("========================================"); // Clear notifications before each test // Note: NotificationTypes are NOT cleared as they are provisioned at startup @@ -72,9 +72,9 @@ void setup() { @Test void testNotificationIsStoredInMockHandler() { - LOG.info("=========================================="); - LOG.info("Test: Notification should be stored in mock handler"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Notification should be stored in mock handler"); + LOG.debug("=========================================="); // Given: Create certificate expiration event using test data CertificateExpiration certificateExpiration = @@ -84,7 +84,7 @@ void testNotificationIsStoredInMockHandler() { eventContext.setData(certificateExpiration); // When: Emit notification - LOG.info("Emitting notification event"); + LOG.debug("Emitting notification event"); notificationService.emit(eventContext); // Wait for async event processing (CAP uses ordered-collector thread pool) @@ -96,7 +96,7 @@ void testNotificationIsStoredInMockHandler() { // Then: Verify notification was stored in mock handler List allNotifications = NotificationProviderServiceMockHandler.getAllNotifications(); - LOG.info("Total notifications stored: {}", allNotifications.size()); + LOG.debug("Total notifications stored: {}", allNotifications.size()); assertFalse(allNotifications.isEmpty(), "At least one notification should be stored"); @@ -106,15 +106,15 @@ void testNotificationIsStoredInMockHandler() { assertNotNull( storedNotification.getNotificationTypeKey(), "Notification type key should not be null"); - LOG.info("Notification stored successfully with ID: {}", storedNotification.getId()); - LOG.info("Notification type key: {}", storedNotification.getNotificationTypeKey()); + LOG.debug("Notification stored successfully with ID: {}", storedNotification.getId()); + LOG.debug("Notification type key: {}", storedNotification.getNotificationTypeKey()); } @Test void testNotificationTypeIsAutoProvisioned() { - LOG.info("=========================================="); - LOG.info("Test: All notification types should be auto-provisioned at startup"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: All notification types should be auto-provisioned at startup"); + LOG.debug("=========================================="); // Note: NotificationTypes are auto-provisioned during application startup // by NotificationTypeAutoProvisionerHandler, not when emitting notifications. @@ -122,7 +122,7 @@ void testNotificationTypeIsAutoProvisioned() { // Then: Verify all 4 notification types were auto-provisioned during startup List allNotificationTypes = NotificationTypeProviderServiceMockHandler.getAllNotificationTypes(); - LOG.info("Total notification types stored: {}", allNotificationTypes.size()); + LOG.debug("Total notification types stored: {}", allNotificationTypes.size()); Set expectedKeys = Set.of( @@ -148,7 +148,7 @@ void testNotificationTypeIsAutoProvisioned() { nt.getNotificationTypeVersion(), "Notification type version should not be null"); actualKeys.add(nt.getNotificationTypeKey()); - LOG.info( + LOG.debug( "Auto-provisioned: key={}, version={}, id={}", nt.getNotificationTypeKey(), nt.getNotificationTypeVersion(), @@ -163,17 +163,17 @@ void testNotificationTypeIsAutoProvisioned() { @Test void testMultipleNotificationsAreStored() { - LOG.info("=========================================="); - LOG.info("Test: Multiple notifications should be stored"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Multiple notifications should be stored"); + LOG.debug("=========================================="); // Check initial count int initialCount = NotificationProviderServiceMockHandler.getNotificationCount(); - LOG.info("BEFORE LOOP - Notification count: {}", initialCount); + LOG.debug("BEFORE LOOP - Notification count: {}", initialCount); // Given: Create multiple certificate expiration events using test data builder for (int i = 1; i <= 3; i++) { - LOG.info(">>> LOOP ITERATION {} STARTING <<<", i); + LOG.debug(">>> LOOP ITERATION {} STARTING <<<", i); CertificateExpiration certificateExpiration = CertificateExpirationTestData.builder() @@ -186,15 +186,15 @@ void testMultipleNotificationsAreStored() { eventContext.setData(certificateExpiration); // When: Emit notification - LOG.info(">>> Emitting notification #{}", i); + LOG.debug(">>> Emitting notification #{}", i); notificationService.emit(eventContext); // Check count after each emit int currentCount = NotificationProviderServiceMockHandler.getNotificationCount(); - LOG.info(">>> AFTER EMIT #{} - Notification count: {}", i, currentCount); + LOG.debug(">>> AFTER EMIT #{} - Notification count: {}", i, currentCount); } - LOG.info( + LOG.debug( "AFTER ALL EMITS - Final notification count: {}", NotificationProviderServiceMockHandler.getNotificationCount()); @@ -206,7 +206,7 @@ void testMultipleNotificationsAreStored() { // Then: Verify all notifications were stored int notificationCount = NotificationProviderServiceMockHandler.getNotificationCount(); - LOG.info("Total notifications stored: {}", notificationCount); + LOG.debug("Total notifications stored: {}", notificationCount); assertEquals(3, notificationCount, "Exactly 3 notifications should be stored"); @@ -214,15 +214,15 @@ void testMultipleNotificationsAreStored() { NotificationProviderServiceMockHandler.getAllNotifications(); for (Notifications notification : allNotifications) { assertNotNull(notification.getId(), "Each notification should have an ID"); - LOG.info("Notification stored with ID: {}", notification.getId()); + LOG.debug("Notification stored with ID: {}", notification.getId()); } } @Test void testRetrieveNotificationByTypeKey() { - LOG.info("=========================================="); - LOG.info("Test: Retrieve notification by type key"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Retrieve notification by type key"); + LOG.debug("=========================================="); // Given: Create and emit notification using test data CertificateExpiration certificateExpiration = @@ -244,7 +244,7 @@ void testRetrieveNotificationByTypeKey() { assertFalse(allNotifications.isEmpty(), "Notification should be stored"); String notificationTypeKey = allNotifications.get(0).getNotificationTypeKey(); - LOG.info("Searching for notifications with type key: {}", notificationTypeKey); + LOG.debug("Searching for notifications with type key: {}", notificationTypeKey); // Then: Retrieve notifications by type key List notificationsByTypeKey = @@ -256,7 +256,7 @@ void testRetrieveNotificationByTypeKey() { notificationsByTypeKey.get(0).getNotificationTypeKey(), "Retrieved notification should have the correct type key"); - LOG.info( + LOG.debug( "Successfully retrieved {} notification(s) with type key: {}", notificationsByTypeKey.size(), notificationTypeKey); @@ -264,9 +264,9 @@ void testRetrieveNotificationByTypeKey() { @Test void testRetrieveNotificationTypeByKeyAndVersion() { - LOG.info("=========================================="); - LOG.info("Test: Retrieve notification type by key and version"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Retrieve notification type by key and version"); + LOG.debug("=========================================="); // Note: NotificationTypes are auto-provisioned at startup, not when emitting notifications. // This test verifies the retrieval functionality using the startup-provisioned types. @@ -281,7 +281,7 @@ void testRetrieveNotificationTypeByKeyAndVersion() { String key = notificationType.getNotificationTypeKey(); String version = notificationType.getNotificationTypeVersion(); - LOG.info("Searching for notification type with key: {}, version: {}", key, version); + LOG.debug("Searching for notification type with key: {}, version: {}", key, version); // Then: Retrieve notification type by key and version NotificationTypes retrievedType = @@ -295,14 +295,14 @@ void testRetrieveNotificationTypeByKeyAndVersion() { retrievedType.getNotificationTypeVersion(), "Retrieved type should have correct version"); - LOG.info("Successfully retrieved notification type with key: {}, version: {}", key, version); + LOG.debug("Successfully retrieved notification type with key: {}, version: {}", key, version); } @Test void testValidationRejectsEmptyRecipients() { - LOG.info("=========================================="); - LOG.info("Test: Validation should reject notification without recipients"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Validation should reject notification without recipients"); + LOG.debug("=========================================="); // Given: Create certificate expiration event WITHOUT recipients using test data CertificateExpiration certificateExpiration = @@ -313,7 +313,7 @@ void testValidationRejectsEmptyRecipients() { eventContext.setData(certificateExpiration); // When & Then: Emit notification - should throw ServiceException - LOG.info("Emitting notification event without recipients: {}", eventContext.getEvent()); + LOG.debug("Emitting notification event without recipients: {}", eventContext.getEvent()); ServiceException exception = assertThrows( @@ -322,15 +322,15 @@ void testValidationRejectsEmptyRecipients() { notificationService.emit(eventContext); }); - LOG.info("Test passed - validation correctly rejected notification without recipients"); - LOG.info("Exception message: {}", exception.getMessage()); + LOG.debug("Test passed - validation correctly rejected notification without recipients"); + LOG.debug("Exception message: {}", exception.getMessage()); } @Test void testMultipleRecipientsSupport() { - LOG.info("=========================================="); - LOG.info("Test: Notification should support multiple recipients (array of String)"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Notification should support multiple recipients (array of String)"); + LOG.debug("=========================================="); // Given: SystemMaintenance event with array of String recipients SystemMaintenance data = SystemMaintenanceTestData.createValid(); @@ -354,7 +354,7 @@ void testMultipleRecipientsSupport() { assertNotNull(recipients, "Recipients list should not be null"); assertEquals(3, recipients.size(), "Should have 3 recipients"); - LOG.info("Multiple recipients test passed — {} recipients", recipients.size()); + LOG.debug("Multiple recipients test passed — {} recipients", recipients.size()); } // ────────────────────────────────────────────────────────────── @@ -363,9 +363,9 @@ void testMultipleRecipientsSupport() { @Test void testRecipientCase1_String() { - LOG.info("=========================================="); - LOG.info("Test: Case 1 — CertificateExpiration with String recipient"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Case 1 — CertificateExpiration with String recipient"); + LOG.debug("=========================================="); // Given: CertificateExpiration — recipients: String CertificateExpiration data = CertificateExpirationTestData.createValidCertificateExpiration(); @@ -391,14 +391,14 @@ void testRecipientCase1_String() { recipients.get(0).getRecipientId(), "RecipientId should match the String value"); - LOG.info("Case 1 passed — RecipientId: {}", recipients.get(0).getRecipientId()); + LOG.debug("Case 1 passed — RecipientId: {}", recipients.get(0).getRecipientId()); } @Test void testRecipientCase2_ArrayOfString() { - LOG.info("=========================================="); - LOG.info("Test: Case 2 — SystemMaintenance with array of String recipients"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Case 2 — SystemMaintenance with array of String recipients"); + LOG.debug("=========================================="); // Given: SystemMaintenance — recipients: array of String SystemMaintenance data = SystemMaintenanceTestData.createValid(); @@ -423,14 +423,14 @@ void testRecipientCase2_ArrayOfString() { assertEquals("admin2@example.com", recipients.get(1).getRecipientId()); assertEquals("admin3@example.com", recipients.get(2).getRecipientId()); - LOG.info("Case 2 passed — {} recipients resolved from array of String", recipients.size()); + LOG.debug("Case 2 passed — {} recipients resolved from array of String", recipients.size()); } @Test void testRecipientAutoDetection_UUIDMappedToGlobalUserId() { - LOG.info("=========================================="); - LOG.info("Test: UUID string auto-detected and mapped to GlobalUserId"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: UUID string auto-detected and mapped to GlobalUserId"); + LOG.debug("=========================================="); // Given: CertificateExpiration with UUID recipient CertificateExpiration data = CertificateExpirationTestData.createWithUUIDRecipient(); @@ -457,14 +457,14 @@ void testRecipientAutoDetection_UUIDMappedToGlobalUserId() { recipients.get(0).getGlobalUserId(), "UUID should be mapped to GlobalUserId"); - LOG.info("UUID auto-detection passed — GlobalUserId: {}", recipients.get(0).getGlobalUserId()); + LOG.debug("UUID auto-detection passed — GlobalUserId: {}", recipients.get(0).getGlobalUserId()); } @Test void testRecipientAutoDetection_MixedEmailAndUUIDArray() { - LOG.info("=========================================="); - LOG.info("Test: Mixed email/UUID array — each auto-detected correctly"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Mixed email/UUID array — each auto-detected correctly"); + LOG.debug("=========================================="); // Given: SystemMaintenance with mixed recipients (emails + UUID) SystemMaintenance data = SystemMaintenanceTestData.createWithMixedRecipients(); @@ -507,7 +507,7 @@ void testRecipientAutoDetection_MixedEmailAndUUIDArray() { "Third recipient email should map to RecipientId"); assertNull(recipients.get(2).getGlobalUserId(), "Third recipient should not have GlobalUserId"); - LOG.info("Mixed auto-detection passed — {} recipients resolved", recipients.size()); + LOG.debug("Mixed auto-detection passed — {} recipients resolved", recipients.size()); } // ────────────────────────────────────────────────────────────── @@ -516,9 +516,9 @@ void testRecipientAutoDetection_MixedEmailAndUUIDArray() { @Test void testNavigationTargetFromSemanticObjectAnnotation() { - LOG.info("=========================================="); - LOG.info("Test: @Common.SemanticObject should map to NavigationTargetObject/Action"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: @Common.SemanticObject should map to NavigationTargetObject/Action"); + LOG.debug("=========================================="); // Given: CertificateExpiration has @Common.SemanticObject:'project1' and // @Common.SemanticObjectAction:'display' @@ -546,7 +546,7 @@ void testNavigationTargetFromSemanticObjectAnnotation() { stored.getNavigationTargetAction(), "NavigationTargetAction should be mapped from @Common.SemanticObjectAction"); - LOG.info( + LOG.debug( "Navigation target: object={}, action={}", stored.getNavigationTargetObject(), stored.getNavigationTargetAction()); @@ -554,10 +554,10 @@ void testNavigationTargetFromSemanticObjectAnnotation() { @Test void testNoNavigationTargetWhenAnnotationMissing() { - LOG.info("=========================================="); - LOG.info( + LOG.debug("=========================================="); + LOG.debug( "Test: Notification without @Common.SemanticObject should have null navigation target"); - LOG.info("=========================================="); + LOG.debug("=========================================="); // Given: SystemMaintenance has NO @Common.SemanticObject annotation SystemMaintenance data = SystemMaintenanceTestData.createValid(); @@ -582,7 +582,7 @@ void testNoNavigationTargetWhenAnnotationMissing() { stored.getNavigationTargetAction(), "NavigationTargetAction should be null when @Common.SemanticObjectAction is missing"); - LOG.info( + LOG.debug( "No navigation target — correct behavior for events without semantic object annotation"); } @@ -592,9 +592,9 @@ void testNoNavigationTargetWhenAnnotationMissing() { @Test void testDynamicPriority_HighWhenYearAbove2025() { - LOG.info("=========================================="); - LOG.info("Test: Dynamic priority should be HIGH when year > 2025"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Dynamic priority should be HIGH when year > 2025"); + LOG.debug("=========================================="); // Given: CertificateExpiration with year = 2026 // CDS annotation: priority : (year > 2025 ? 'HIGH' : 'LOW') @@ -613,7 +613,7 @@ void testDynamicPriority_HighWhenYearAbove2025() { // Then: Priority should be HIGH (year > 2025) Notifications stored = NotificationProviderServiceMockHandler.getAllNotifications().get(0); - LOG.info("Dynamic priority result: {}", stored.getPriority()); + LOG.debug("Dynamic priority result: {}", stored.getPriority()); assertNotNull(stored.getPriority(), "Priority should not be null"); assertEquals("HIGH", stored.getPriority(), "Priority should be HIGH when year > 2025"); @@ -621,9 +621,9 @@ void testDynamicPriority_HighWhenYearAbove2025() { @Test void testDynamicPriority_LowWhenYearNotAbove2025() { - LOG.info("=========================================="); - LOG.info("Test: Dynamic priority should be LOW when year <= 2025"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Dynamic priority should be LOW when year <= 2025"); + LOG.debug("=========================================="); // Given: CertificateExpiration with year = 2024 // CDS annotation: priority : (year > 2025 ? 'HIGH' : 'LOW') @@ -642,7 +642,7 @@ void testDynamicPriority_LowWhenYearNotAbove2025() { // Then: Priority should be LOW (year <= 2025) Notifications stored = NotificationProviderServiceMockHandler.getAllNotifications().get(0); - LOG.info("Dynamic priority result: {}", stored.getPriority()); + LOG.debug("Dynamic priority result: {}", stored.getPriority()); assertNotNull(stored.getPriority(), "Priority should not be null"); assertEquals("LOW", stored.getPriority(), "Priority should be LOW when year <= 2025"); @@ -654,9 +654,9 @@ void testDynamicPriority_LowWhenYearNotAbove2025() { @Test void testBatchNotificationEmit() { - LOG.info("=========================================="); - LOG.info("Test: Batch emit — multiple notifications in a single emit call"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Batch emit — multiple notifications in a single emit call"); + LOG.debug("=========================================="); // Given: Create 3 certificate expiration payloads with different recipients List batch = CertificateExpirationTestData.createBatchOfThree(); @@ -686,16 +686,16 @@ void testBatchNotificationEmit() { assertNotNull(notification.getId(), "Each notification should have an ID"); } - LOG.info( + LOG.debug( "Batch emit test passed — {} notifications created in single emit", allNotifications.size()); } @Test void testBatchNotificationEmitPreservesIndividualData() { - LOG.info("=========================================="); - LOG.info("Test: Batch emit preserves individual notification data"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Batch emit preserves individual notification data"); + LOG.debug("=========================================="); // Given: 2 certificate expirations with different data List batch = CertificateExpirationTestData.createAliceAndBob(); @@ -726,7 +726,7 @@ void testBatchNotificationEmitPreservesIndividualData() { assertTrue(recipientIds.contains("alice@example.com"), "Should contain Alice's email"); assertTrue(recipientIds.contains("bob@example.com"), "Should contain Bob's email"); - LOG.info("Batch data preservation test passed — individual data correctly isolated"); + LOG.debug("Batch data preservation test passed — individual data correctly isolated"); } // ────────────────────────────────────────────────────────────── @@ -735,9 +735,9 @@ void testBatchNotificationEmitPreservesIndividualData() { @Test void testDynamicPriority_HighWhenDeadlineWithin30Days() { - LOG.info("=========================================="); - LOG.info("Test: days_between — HIGH priority when deadline < 30 days away"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: days_between — HIGH priority when deadline < 30 days away"); + LOG.debug("=========================================="); // Given: ContractDeadline with deadline 10 days from now // CDS annotation: priority : (days_between($now, deadlineDate) < 30 ? 'HIGH' : 'LOW') @@ -756,7 +756,7 @@ void testDynamicPriority_HighWhenDeadlineWithin30Days() { // Then: Priority should be HIGH (deadline within 30 days) Notifications stored = NotificationProviderServiceMockHandler.getAllNotifications().get(0); - LOG.info("days_between priority result: {}", stored.getPriority()); + LOG.debug("days_between priority result: {}", stored.getPriority()); assertNotNull(stored.getPriority(), "Priority should not be null"); assertEquals("HIGH", stored.getPriority(), "Priority should be HIGH when deadline < 30 days"); @@ -764,9 +764,9 @@ void testDynamicPriority_HighWhenDeadlineWithin30Days() { @Test void testDynamicPriority_LowWhenDeadlineFarAway() { - LOG.info("=========================================="); - LOG.info("Test: days_between — LOW priority when deadline >= 30 days away"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: days_between — LOW priority when deadline >= 30 days away"); + LOG.debug("=========================================="); // Given: ContractDeadline with deadline 90 days from now // CDS annotation: priority : (days_between($now, deadlineDate) < 30 ? 'HIGH' : 'LOW') @@ -785,7 +785,7 @@ void testDynamicPriority_LowWhenDeadlineFarAway() { // Then: Priority should be LOW (deadline far away) Notifications stored = NotificationProviderServiceMockHandler.getAllNotifications().get(0); - LOG.info("days_between priority result: {}", stored.getPriority()); + LOG.debug("days_between priority result: {}", stored.getPriority()); assertNotNull(stored.getPriority(), "Priority should not be null"); assertEquals("LOW", stored.getPriority(), "Priority should be LOW when deadline >= 30 days"); @@ -797,9 +797,9 @@ void testDynamicPriority_LowWhenDeadlineFarAway() { @Test void testDynamicPriority_HighWhenImpactContainsCritical() { - LOG.info("=========================================="); - LOG.info("Test: contains — HIGH priority when impact contains 'critical'"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: contains — HIGH priority when impact contains 'critical'"); + LOG.debug("=========================================="); // Given: SystemMaintenance with impact containing 'critical' // CDS annotation: priority : (contains(impact, 'critical') ? 'HIGH' : 'MEDIUM') @@ -817,7 +817,7 @@ void testDynamicPriority_HighWhenImpactContainsCritical() { // Then Notifications stored = NotificationProviderServiceMockHandler.getAllNotifications().get(0); - LOG.info("contains priority result: {}", stored.getPriority()); + LOG.debug("contains priority result: {}", stored.getPriority()); assertNotNull(stored.getPriority(), "Priority should not be null"); assertEquals( @@ -826,9 +826,9 @@ void testDynamicPriority_HighWhenImpactContainsCritical() { @Test void testDynamicPriority_MediumWhenImpactDoesNotContainCritical() { - LOG.info("=========================================="); - LOG.info("Test: contains — MEDIUM priority when impact does not contain 'critical'"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: contains — MEDIUM priority when impact does not contain 'critical'"); + LOG.debug("=========================================="); // Given: SystemMaintenance with impact NOT containing 'critical' SystemMaintenance data = SystemMaintenanceTestData.createValid(); @@ -845,7 +845,7 @@ void testDynamicPriority_MediumWhenImpactDoesNotContainCritical() { // Then Notifications stored = NotificationProviderServiceMockHandler.getAllNotifications().get(0); - LOG.info("contains priority result: {}", stored.getPriority()); + LOG.debug("contains priority result: {}", stored.getPriority()); assertNotNull(stored.getPriority(), "Priority should not be null"); assertEquals( @@ -860,9 +860,9 @@ void testDynamicPriority_MediumWhenImpactDoesNotContainCritical() { @Test void testDynamicPriority_HighWhenSeverityStartsWithCrit() { - LOG.info("=========================================="); - LOG.info("Test: startsWith — HIGH priority when severity starts with 'CRIT'"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: startsWith — HIGH priority when severity starts with 'CRIT'"); + LOG.debug("=========================================="); // Given: SecurityAlert with severity starting with 'CRIT' // CDS annotation: priority : (startsWith(severity, 'CRIT') ? 'HIGH' : 'LOW') @@ -880,7 +880,7 @@ void testDynamicPriority_HighWhenSeverityStartsWithCrit() { // Then Notifications stored = NotificationProviderServiceMockHandler.getAllNotifications().get(0); - LOG.info("startsWith priority result: {}", stored.getPriority()); + LOG.debug("startsWith priority result: {}", stored.getPriority()); assertNotNull(stored.getPriority(), "Priority should not be null"); assertEquals( @@ -889,9 +889,9 @@ void testDynamicPriority_HighWhenSeverityStartsWithCrit() { @Test void testDynamicPriority_LowWhenSeverityDoesNotStartWithCrit() { - LOG.info("=========================================="); - LOG.info("Test: startsWith — LOW priority when severity does not start with 'CRIT'"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: startsWith — LOW priority when severity does not start with 'CRIT'"); + LOG.debug("=========================================="); // Given: SecurityAlert with severity NOT starting with 'CRIT' SecurityAlert data = SecurityAlertTestData.createWithLowSeverity(); @@ -908,7 +908,7 @@ void testDynamicPriority_LowWhenSeverityDoesNotStartWithCrit() { // Then Notifications stored = NotificationProviderServiceMockHandler.getAllNotifications().get(0); - LOG.info("startsWith priority result: {}", stored.getPriority()); + LOG.debug("startsWith priority result: {}", stored.getPriority()); assertNotNull(stored.getPriority(), "Priority should not be null"); assertEquals( @@ -923,9 +923,9 @@ void testDynamicPriority_LowWhenSeverityDoesNotStartWithCrit() { @Test void testDynamicPriority_HighWhenServerNameEndsWithProd() { - LOG.info("=========================================="); - LOG.info("Test: endsWith — HIGH priority when serverName ends with '-prod'"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: endsWith — HIGH priority when serverName ends with '-prod'"); + LOG.debug("=========================================="); // Given: ServerIncident with server name ending in '-prod' // CDS annotation: priority : (endsWith(serverName, '-prod') ? 'HIGH' : 'LOW') @@ -943,7 +943,7 @@ void testDynamicPriority_HighWhenServerNameEndsWithProd() { // Then Notifications stored = NotificationProviderServiceMockHandler.getAllNotifications().get(0); - LOG.info("endsWith priority result: {}", stored.getPriority()); + LOG.debug("endsWith priority result: {}", stored.getPriority()); assertNotNull(stored.getPriority(), "Priority should not be null"); assertEquals( @@ -952,9 +952,9 @@ void testDynamicPriority_HighWhenServerNameEndsWithProd() { @Test void testDynamicPriority_LowWhenServerNameDoesNotEndWithProd() { - LOG.info("=========================================="); - LOG.info("Test: endsWith — LOW priority when serverName does not end with '-prod'"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: endsWith — LOW priority when serverName does not end with '-prod'"); + LOG.debug("=========================================="); // Given: ServerIncident with server name NOT ending in '-prod' ServerIncident data = ServerIncidentTestData.createWithDevServer(); @@ -971,7 +971,7 @@ void testDynamicPriority_LowWhenServerNameDoesNotEndWithProd() { // Then Notifications stored = NotificationProviderServiceMockHandler.getAllNotifications().get(0); - LOG.info("endsWith priority result: {}", stored.getPriority()); + LOG.debug("endsWith priority result: {}", stored.getPriority()); assertNotNull(stored.getPriority(), "Priority should not be null"); assertEquals( @@ -986,9 +986,9 @@ void testDynamicPriority_LowWhenServerNameDoesNotEndWithProd() { @Test void testDynamicPriority_NeutralWhenRequiredFieldMissing() { - LOG.info("=========================================="); - LOG.info("Test: NEUTRAL fallback when priority expression field is missing"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: NEUTRAL fallback when priority expression field is missing"); + LOG.debug("=========================================="); // Given: SystemMaintenance WITHOUT impact field // CDS annotation: priority : (contains(impact, 'critical') ? 'HIGH' : 'MEDIUM') @@ -1007,7 +1007,7 @@ void testDynamicPriority_NeutralWhenRequiredFieldMissing() { // Then: Priority should fall back to NEUTRAL Notifications stored = NotificationProviderServiceMockHandler.getAllNotifications().get(0); - LOG.info("Fallback priority result: {}", stored.getPriority()); + LOG.debug("Fallback priority result: {}", stored.getPriority()); assertNotNull(stored.getPriority(), "Priority should not be null"); assertEquals( @@ -1022,9 +1022,9 @@ void testDynamicPriority_NeutralWhenRequiredFieldMissing() { @Test void testDynamicPriority_LowWhenDeadlineExactly30DaysAway() { - LOG.info("=========================================="); - LOG.info("Test: days_between boundary — LOW when deadline is exactly 30 days away"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: days_between boundary — LOW when deadline is exactly 30 days away"); + LOG.debug("=========================================="); // Given: ContractDeadline with deadline exactly 30 days from now // CDS annotation: priority : (days_between($now, deadlineDate) < 30 ? 'HIGH' : 'LOW') @@ -1043,7 +1043,7 @@ void testDynamicPriority_LowWhenDeadlineExactly30DaysAway() { // Then: Priority should be LOW (30 < 30 is false) Notifications stored = NotificationProviderServiceMockHandler.getAllNotifications().get(0); - LOG.info("Boundary priority result: {}", stored.getPriority()); + LOG.debug("Boundary priority result: {}", stored.getPriority()); assertNotNull(stored.getPriority(), "Priority should not be null"); assertEquals( @@ -1058,9 +1058,9 @@ void testDynamicPriority_LowWhenDeadlineExactly30DaysAway() { @Test void testDynamicPriority_HighWhenConcatContainsProdCritical() { - LOG.info("=========================================="); - LOG.info("Test: contains(concat()) — HIGH when result contains 'PROD-critical'"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: contains(concat()) — HIGH when result contains 'PROD-critical'"); + LOG.debug("=========================================="); // Given: environment="PROD", appName="critical-service" // concat → "PROD-critical-service" which contains "PROD-critical" → HIGH @@ -1078,7 +1078,7 @@ void testDynamicPriority_HighWhenConcatContainsProdCritical() { // Then Notifications stored = NotificationProviderServiceMockHandler.getAllNotifications().get(0); - LOG.info("contains(concat()) priority result: {}", stored.getPriority()); + LOG.debug("contains(concat()) priority result: {}", stored.getPriority()); assertNotNull(stored.getPriority(), "Priority should not be null"); assertEquals( @@ -1089,9 +1089,9 @@ void testDynamicPriority_HighWhenConcatContainsProdCritical() { @Test void testDynamicPriority_LowWhenConcatDoesNotContainProdCritical() { - LOG.info("=========================================="); - LOG.info("Test: contains(concat()) — LOW when result does not contain 'PROD-critical'"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: contains(concat()) — LOW when result does not contain 'PROD-critical'"); + LOG.debug("=========================================="); // Given: environment="DEV", appName="my-app" // concat → "DEV-my-app" which does NOT contain "PROD-critical" → LOW @@ -1109,7 +1109,7 @@ void testDynamicPriority_LowWhenConcatDoesNotContainProdCritical() { // Then Notifications stored = NotificationProviderServiceMockHandler.getAllNotifications().get(0); - LOG.info("contains(concat()) priority result: {}", stored.getPriority()); + LOG.debug("contains(concat()) priority result: {}", stored.getPriority()); assertNotNull(stored.getPriority(), "Priority should not be null"); assertEquals( diff --git a/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTemplateProvisioningTest.java b/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTemplateProvisioningTest.java index f39a0c5..05c852a 100644 --- a/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTemplateProvisioningTest.java +++ b/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTemplateProvisioningTest.java @@ -38,15 +38,15 @@ public class NotificationTemplateProvisioningTest { @Test void testTemplatesProvisionedAtStartup() { - LOG.info("=========================================="); - LOG.info("Test: Templates should be auto-provisioned at startup"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Templates should be auto-provisioned at startup"); + LOG.debug("=========================================="); List allTemplates = NotificationTemplateProviderServiceMockHandler.getAllTemplates(); assertFalse(allTemplates.isEmpty(), "At least one NotificationTemplate should be provisioned"); - LOG.info("Total templates provisioned: {}", allTemplates.size()); + LOG.debug("Total templates provisioned: {}", allTemplates.size()); // The sample-app has 6 events with @notification.template.title → 6 templates assertTrue( @@ -60,9 +60,9 @@ void testTemplatesProvisionedAtStartup() { @Test void testCertificateExpirationTemplateStructure() { - LOG.info("=========================================="); - LOG.info("Test: CertificateExpiration template structure"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: CertificateExpiration template structure"); + LOG.debug("=========================================="); NotificationTemplates template = NotificationTemplateProviderServiceMockHandler.getTemplateByKey("CertificateExpiration"); @@ -87,7 +87,7 @@ void testCertificateExpirationTemplateStructure() { assertNotNull(translations, "Translations should not be null"); assertFalse(translations.isEmpty(), "Translations should not be empty"); - LOG.info("CertificateExpiration template: Key={}, Visibility={}, Translations={}", + LOG.debug("CertificateExpiration template: Key={}, Visibility={}, Translations={}", template.getKey(), template.getVisibility(), translations.size()); } @@ -97,9 +97,9 @@ void testCertificateExpirationTemplateStructure() { @Test void testVisibilityDefaultsToPrivate() { - LOG.info("=========================================="); - LOG.info("Test: Templates without @notification.customizable should have null visibility (PRIVATE)"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Templates without @notification.customizable should have null visibility (PRIVATE)"); + LOG.debug("=========================================="); // SystemMaintenance has no @notification.customizable → visibility should be null (ANS defaults PRIVATE) NotificationTemplates template = @@ -108,7 +108,7 @@ void testVisibilityDefaultsToPrivate() { assertNull(template.getVisibility(), "Template without @notification.customizable should have null visibility (ANS defaults to PRIVATE)"); - LOG.info("SystemMaintenance visibility: {} (null = ANS default PRIVATE)", template.getVisibility()); + LOG.debug("SystemMaintenance visibility: {} (null = ANS default PRIVATE)", template.getVisibility()); } // ────────────────────────────────────────────────────────────── @@ -117,9 +117,9 @@ void testVisibilityDefaultsToPrivate() { @Test void testI18nResolvedInTranslations() { - LOG.info("=========================================="); - LOG.info("Test: i18n placeholders should be resolved in template translations"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: i18n placeholders should be resolved in template translations"); + LOG.debug("=========================================="); NotificationTemplates template = NotificationTemplateProviderServiceMockHandler.getTemplateByKey("CertificateExpiration"); @@ -146,7 +146,7 @@ void testI18nResolvedInTranslations() { assertNoUnresolvedI18n(t.getEmail().getBodyHtml(), "Email.BodyHtml", lang); } - LOG.info("[{}] Title={}, Body={}, Preview={}", lang, t.getTitle(), t.getBody(), t.getPreview()); + LOG.debug("[{}] Title={}, Body={}, Preview={}", lang, t.getTitle(), t.getBody(), t.getPreview()); } } @@ -156,9 +156,9 @@ void testI18nResolvedInTranslations() { @Test void testMultiLanguageTranslations() { - LOG.info("=========================================="); - LOG.info("Test: CertificateExpiration should have translations for all i18n languages"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: CertificateExpiration should have translations for all i18n languages"); + LOG.debug("=========================================="); NotificationTemplates template = NotificationTemplateProviderServiceMockHandler.getTemplateByKey("CertificateExpiration"); @@ -191,7 +191,7 @@ void testMultiLanguageTranslations() { // Titles should differ per language (i18n resolved) assertNotEquals(en.getTitle(), de.getTitle(), "EN and DE titles should differ"); - LOG.info("Translations verified for {} languages", translations.size()); + LOG.debug("Translations verified for {} languages", translations.size()); } // ────────────────────────────────────────────────────────────── @@ -200,9 +200,9 @@ void testMultiLanguageTranslations() { @Test void testStaticTemplateTranslation() { - LOG.info("=========================================="); - LOG.info("Test: Templates with static strings should have same value across all locale translations"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Templates with static strings should have same value across all locale translations"); + LOG.debug("=========================================="); // SystemMaintenance uses static strings (no {i18n>...} placeholders). // Since i18n files exist in the project for other events, translations are created @@ -229,7 +229,7 @@ void testStaticTemplateTranslation() { + t.getLanguage() + "' differs"); } - LOG.info("Static template: {} translations, all with title: {}", translations.size(), expectedTitle); + LOG.debug("Static template: {} translations, all with title: {}", translations.size(), expectedTitle); } // ────────────────────────────────────────────────────────────── @@ -238,9 +238,9 @@ void testStaticTemplateTranslation() { @Test void testEmailHtmlLoadedFromFile() { - LOG.info("=========================================="); - LOG.info("Test: CertificateExpiration email HTML should be loaded from file"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: CertificateExpiration email HTML should be loaded from file"); + LOG.debug("=========================================="); NotificationTemplates template = NotificationTemplateProviderServiceMockHandler.getTemplateByKey("CertificateExpiration"); @@ -257,7 +257,7 @@ void testEmailHtmlLoadedFromFile() { assertFalse(html.endsWith(".html"), "Email body should be HTML content, not a file path"); assertTrue(html.contains("<"), "Email body should contain HTML tags"); - LOG.info("Email HTML loaded successfully ({} chars)", html.length()); + LOG.debug("Email HTML loaded successfully ({} chars)", html.length()); } // ────────────────────────────────────────────────────────────── @@ -266,9 +266,9 @@ void testEmailHtmlLoadedFromFile() { @Test void testTagsContainSourceAndEvent() { - LOG.info("=========================================="); - LOG.info("Test: Template tags should contain source and event information"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Template tags should contain source and event information"); + LOG.debug("=========================================="); NotificationTemplates template = NotificationTemplateProviderServiceMockHandler.getTemplateByKey("CertificateExpiration"); @@ -278,7 +278,7 @@ void testTagsContainSourceAndEvent() { assertNotNull(tags, "Tags should not be null"); assertFalse(tags.isEmpty(), "Tags should not be empty"); - LOG.info("Tags: {}", tags); + LOG.debug("Tags: {}", tags); } // ────────────────────────────────────────────────────────────── @@ -287,9 +287,9 @@ void testTagsContainSourceAndEvent() { @Test void testTranslationMetadata() { - LOG.info("=========================================="); - LOG.info("Test: Translations should have source, event, and displayName set"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Translations should have source, event, and displayName set"); + LOG.debug("=========================================="); NotificationTemplates template = NotificationTemplateProviderServiceMockHandler.getTemplateByKey("CertificateExpiration"); @@ -308,7 +308,7 @@ void testTranslationMetadata() { // DisplayName = event name assertEquals("CertificateExpiration", en.getDisplayName(), "DisplayName should be the event name"); - LOG.info("Source={}, Event={}, DisplayName={}", en.getSource(), en.getEvent(), en.getDisplayName()); + LOG.debug("Source={}, Event={}, DisplayName={}", en.getSource(), en.getEvent(), en.getDisplayName()); } // ────────────────────────────────────────────────────────────── @@ -317,9 +317,9 @@ void testTranslationMetadata() { @Test void testPropertiesSchemaReflectsEventElements() { - LOG.info("=========================================="); - LOG.info("Test: PropertiesSchema should be a JSON schema of event elements"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: PropertiesSchema should be a JSON schema of event elements"); + LOG.debug("=========================================="); NotificationTemplates template = NotificationTemplateProviderServiceMockHandler.getTemplateByKey("SystemMaintenance"); @@ -332,7 +332,7 @@ void testPropertiesSchemaReflectsEventElements() { assertTrue(schema.contains("properties") || schema.contains("type"), "Schema should contain JSON Schema keywords: " + schema); - LOG.info("PropertiesSchema: {}", schema); + LOG.debug("PropertiesSchema: {}", schema); } // ────────────────────────────────────────────────────────────── @@ -341,9 +341,9 @@ void testPropertiesSchemaReflectsEventElements() { @Test void testI18nPlaceholdersResolvedForAllLanguages() { - LOG.info("=========================================="); - LOG.info("Test: i18n placeholders should be resolved for EN, DE, TR, ES"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: i18n placeholders should be resolved for EN, DE, TR, ES"); + LOG.debug("=========================================="); NotificationTemplates template = NotificationTemplateProviderServiceMockHandler.getTemplateByKey("CertificateExpiration"); @@ -381,7 +381,7 @@ void testI18nPlaceholdersResolvedForAllLanguages() { assertEquals("Sertifika Sona Erme Uyarısı", findTranslation(translations, "tr").getDescription()); assertEquals("Alerta de Expiración de Certificado", findTranslation(translations, "es").getDescription()); - LOG.info("All 4 languages verified with exact i18n values"); + LOG.debug("All 4 languages verified with exact i18n values"); } // ────────────────────────────────────────────────────────────── @@ -390,9 +390,9 @@ void testI18nPlaceholdersResolvedForAllLanguages() { @Test void testEmailHtmlContainsMustacheVariables() { - LOG.info("=========================================="); - LOG.info("Test: Email HTML should contain Mustache variables for ANS runtime"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Email HTML should contain Mustache variables for ANS runtime"); + LOG.debug("=========================================="); NotificationTemplates template = NotificationTemplateProviderServiceMockHandler.getTemplateByKey("CertificateExpiration"); @@ -420,7 +420,7 @@ void testEmailHtmlContainsMustacheVariables() { emailHtml.contains(variable), "Email HTML should contain Mustache variable: " + variable); } - LOG.info("All {} Mustache variables present in email HTML", expectedVariables.size()); + LOG.debug("All {} Mustache variables present in email HTML", expectedVariables.size()); } // ────────────────────────────────────────────────────────────── @@ -429,9 +429,9 @@ void testEmailHtmlContainsMustacheVariables() { @Test void testEmailHtmlI18nResolvedPerLanguage() { - LOG.info("=========================================="); - LOG.info("Test: Email HTML i18n values should differ per language"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Email HTML i18n values should differ per language"); + LOG.debug("=========================================="); NotificationTemplates template = NotificationTemplateProviderServiceMockHandler.getTemplateByKey("CertificateExpiration"); @@ -475,10 +475,10 @@ void testEmailHtmlI18nResolvedPerLanguage() { emailHtml.contains(expectedGreeting), "[" + lang + "] Email HTML should contain greeting: '" + expectedGreeting + "'"); - LOG.info("[{}] Button: '{}', Greeting: '{}'", lang, expectedButton, expectedGreeting); + LOG.debug("[{}] Button: '{}', Greeting: '{}'", lang, expectedButton, expectedGreeting); } - LOG.info("Email HTML i18n verified for all 4 languages"); + LOG.debug("Email HTML i18n verified for all 4 languages"); } // ────────────────────────────────────────────────────────────── diff --git a/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTypeProvisioningTest.java b/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTypeProvisioningTest.java index 26dbb0e..ce6babf 100644 --- a/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTypeProvisioningTest.java +++ b/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTypeProvisioningTest.java @@ -60,9 +60,9 @@ private NotificationTypeAutoProvisionerHandler createProvisioner() { @Test void testEachNotificationTypeHasUniqueId() { - LOG.info("=========================================="); - LOG.info("Test: Each notification type should have a unique NotificationTypeId"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Each notification type should have a unique NotificationTypeId"); + LOG.debug("=========================================="); List allTypes = NotificationTypeProviderServiceMockHandler.getAllNotificationTypes(); @@ -81,7 +81,7 @@ void testEachNotificationTypeHasUniqueId() { assertFalse(id.isEmpty(), "NotificationTypeId should not be empty for: " + key); keyToId.put(key, id); - LOG.info("Type: {} → ID: {}", key, id); + LOG.debug("Type: {} → ID: {}", key, id); } // Verify all IDs are unique @@ -91,7 +91,7 @@ void testEachNotificationTypeHasUniqueId() { uniqueIds.size(), "Each notification type must have a UNIQUE NotificationTypeId. " + "Found IDs: " + keyToId); - LOG.info("All {} notification types have unique IDs", uniqueIds.size()); + LOG.debug("All {} notification types have unique IDs", uniqueIds.size()); } // ────────────────────────────────────────────────────────────── @@ -100,9 +100,9 @@ void testEachNotificationTypeHasUniqueId() { @Test void testReProvisioningUpdatesEachTypeCorrectly() { - LOG.info("=========================================="); - LOG.info("Test: Re-provisioning should update each type with its own correct data"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Re-provisioning should update each type with its own correct data"); + LOG.debug("=========================================="); // Record state BEFORE re-provisioning Map idsBefore = new HashMap<>(); @@ -114,7 +114,7 @@ void testReProvisioningUpdatesEachTypeCorrectly() { EXPECTED_KEYS.size(), idsBefore.size(), "All types should exist before re-provisioning"); // Trigger re-provisioning (simulates app restart) - LOG.info("Triggering re-provisioning..."); + LOG.debug("Triggering re-provisioning..."); createProvisioner().onApplicationPrepared(); // Verify IDs remain the same (UPDATE, not new INSERT) @@ -137,7 +137,7 @@ void testReProvisioningUpdatesEachTypeCorrectly() { "Maintenance Notice", "System maintenance scheduled for {{systemName}}"); - LOG.info("Re-provisioning verified — all types retain correct data"); + LOG.debug("Re-provisioning verified — all types retain correct data"); } // ────────────────────────────────────────────────────────────── @@ -146,9 +146,9 @@ void testReProvisioningUpdatesEachTypeCorrectly() { @Test void testReProvisioningUpdatesAllTypes() { - LOG.info("=========================================="); - LOG.info("Test: Re-provisioning should trigger UPDATE for each existing type"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Re-provisioning should trigger UPDATE for each existing type"); + LOG.debug("=========================================="); // Record update counts before Map countsBefore = new HashMap<>(); @@ -175,10 +175,10 @@ void testReProvisioningUpdatesAllTypes() { + ", After: " + after); - LOG.info("Type '{}': update count {} → {}", key, before, after); + LOG.debug("Type '{}': update count {} → {}", key, before, after); } - LOG.info("All {} types were updated during re-provisioning", EXPECTED_KEYS.size()); + LOG.debug("All {} types were updated during re-provisioning", EXPECTED_KEYS.size()); } // ────────────────────────────────────────────────────────────── @@ -187,9 +187,9 @@ void testReProvisioningUpdatesAllTypes() { @Test void testNoDataCrossContaminationBetweenTypes() { - LOG.info("=========================================="); - LOG.info("Test: Each type's English template must contain its own specific content"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Each type's English template must contain its own specific content"); + LOG.debug("=========================================="); Map typeToExpectedSensitive = Map.of( @@ -227,7 +227,7 @@ void testNoDataCrossContaminationBetweenTypes() { // Also verify publicTitle is unique per type String publicTitle = enTemplate.getTemplatePublic(); - LOG.info("[{}] publicTitle='{}', sensitive='{}'", typeKey, publicTitle, sensitive); + LOG.debug("[{}] publicTitle='{}', sensitive='{}'", typeKey, publicTitle, sensitive); } // Verify all publicTitles are distinct @@ -251,7 +251,7 @@ void testNoDataCrossContaminationBetweenTypes() { publicTitles.size(), "Each notification type must have a unique publicTitle. Found: " + publicTitles); - LOG.info("No cross-contamination detected — all types have unique, correct data"); + LOG.debug("No cross-contamination detected — all types have unique, correct data"); } // ────────────────────────────────────────────────────────────── @@ -283,7 +283,7 @@ private void assertTypeDataIsCorrect( + typeKey + "' — data may have been overwritten by another type"); - LOG.info( + LOG.debug( "[{}] ✓ publicTitle='{}', sensitive='{}'", typeKey, expectedPublicTitle, expectedSensitive); } } diff --git a/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTypeTemplateTest.java b/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTypeTemplateTest.java index 2643c51..59c5934 100644 --- a/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTypeTemplateTest.java +++ b/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTypeTemplateTest.java @@ -74,9 +74,9 @@ private void assertNoUnresolvedI18n(String value, String fieldName, String lang) @Test void testI18nPlaceholdersResolvedForAllLanguages() { - LOG.info("=========================================="); - LOG.info("Test: i18n placeholders should be resolved for EN, DE, TR, ES"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: i18n placeholders should be resolved for EN, DE, TR, ES"); + LOG.debug("=========================================="); NotificationTypes nt = getNotificationType("CertificateExpiration"); @@ -99,7 +99,7 @@ void testI18nPlaceholdersResolvedForAllLanguages() { template.getTemplateSensitive(), "TemplateSensitive should be resolved for language: " + lang); - LOG.info("[{}] TemplateSensitive: {}", lang, template.getTemplateSensitive()); + LOG.debug("[{}] TemplateSensitive: {}", lang, template.getTemplateSensitive()); } // Also verify subtitle and other fields for specific languages @@ -127,7 +127,7 @@ void testI18nPlaceholdersResolvedForAllLanguages() { assertEquals("Certificados por expirar", esTemplate.getTemplateGrouped()); assertEquals("Alerta de Expiración de Certificado", esTemplate.getEmailSubject()); - LOG.info("All 4 languages verified successfully"); + LOG.debug("All 4 languages verified successfully"); } // ────────────────────────────────────────────────────────────── @@ -136,9 +136,9 @@ void testI18nPlaceholdersResolvedForAllLanguages() { @Test void testNoUnresolvedI18nPlaceholdersInTemplates() { - LOG.info("=========================================="); - LOG.info("Test: No unresolved {i18n>...} placeholders in any template"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: No unresolved {i18n>...} placeholders in any template"); + LOG.debug("=========================================="); List allTypes = NotificationTypeProviderServiceMockHandler.getAllNotificationTypes(); @@ -168,7 +168,7 @@ void testNoUnresolvedI18nPlaceholdersInTemplates() { } } - LOG.info( + LOG.debug( "Checked {} templates across {} notification types — no unresolved i18n placeholders", totalTemplatesChecked, allTypes.size()); @@ -180,9 +180,9 @@ void testNoUnresolvedI18nPlaceholdersInTemplates() { @Test void testEmailHtmlContainsMustacheVariables() { - LOG.info("=========================================="); - LOG.info("Test: Email HTML should contain Mustache variables for ANS runtime"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Email HTML should contain Mustache variables for ANS runtime"); + LOG.debug("=========================================="); NotificationTypes nt = getNotificationType("CertificateExpiration"); Templates enTemplate = getTemplateForLanguage(nt, "en"); @@ -203,10 +203,10 @@ void testEmailHtmlContainsMustacheVariables() { for (String variable : expectedVariables) { assertTrue( emailHtml.contains(variable), "Email HTML should contain Mustache variable: " + variable); - LOG.info("Found Mustache variable: {}", variable); + LOG.debug("Found Mustache variable: {}", variable); } - LOG.info("All {} Mustache variables present in email HTML", expectedVariables.size()); + LOG.debug("All {} Mustache variables present in email HTML", expectedVariables.size()); } // ────────────────────────────────────────────────────────────── @@ -215,9 +215,9 @@ void testEmailHtmlContainsMustacheVariables() { @Test void testEmailHtmlI18nResolvedPerLanguage() { - LOG.info("=========================================="); - LOG.info("Test: Email HTML i18n values should differ per language"); - LOG.info("=========================================="); + LOG.debug("=========================================="); + LOG.debug("Test: Email HTML i18n values should differ per language"); + LOG.debug("=========================================="); NotificationTypes nt = getNotificationType("CertificateExpiration"); @@ -254,9 +254,9 @@ void testEmailHtmlI18nResolvedPerLanguage() { emailHtml.contains(expectedGreeting), "[" + lang + "] Email HTML should contain greeting: '" + expectedGreeting + "'"); - LOG.info("[{}] Button: '{}', Greeting: '{}'", lang, expectedButton, expectedGreeting); + LOG.debug("[{}] Button: '{}', Greeting: '{}'", lang, expectedButton, expectedGreeting); } - LOG.info("Email HTML i18n verified for all 4 languages"); + LOG.debug("Email HTML i18n verified for all 4 languages"); } } From cd0c284ff052eb4ae8ea2bbfa3fc292b7a75d4cd Mon Sep 17 00:00:00 2001 From: Buse Halis Date: Tue, 16 Jun 2026 10:12:27 +0200 Subject: [PATCH 07/19] chore: update CHANGELOG with standalone notification template provisioning --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2ab1e6..afa6232 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). - Navigation target support via `@Common.SemanticObject` and `@Common.SemanticObjectAction` annotations for SAP Fiori Launchpad integration - Recipient language resolution via Identity Authentication Service (IAS) destination `Identity_Authentication_Connectivity_IDS` - Persistent outbox integration for guaranteed in-order delivery with automatic retry on failure -- Automatic registration of `NotificationProviderService` and `NotificationTypeProviderService` as OData v2 remote services +- Automatic registration of `NotificationProviderService`, `NotificationTypeProviderService`, and `NotificationTemplateProviderService` as OData v2 remote services +- Standalone notification template provisioning via `@notification.customizable` annotation for user-customizable templates - Support for SAP ANS standalone service binding (`business-notifications` plan) and SAP Work Zone credentials - Sample application demonstrating programmatic and declarative notification patterns with integration tests for Java 17 and 21 From 412310e64178e6225e8e564ff31c43ef9fbfd397 Mon Sep 17 00:00:00 2001 From: Buse Halis Date: Tue, 16 Jun 2026 10:57:25 +0200 Subject: [PATCH 08/19] test: add unit tests for I18nHelper --- .../notifications/helpers/I18nHelperTest.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 cds-feature-notifications/src/test/java/com/sap/cds/notifications/helpers/I18nHelperTest.java diff --git a/cds-feature-notifications/src/test/java/com/sap/cds/notifications/helpers/I18nHelperTest.java b/cds-feature-notifications/src/test/java/com/sap/cds/notifications/helpers/I18nHelperTest.java new file mode 100644 index 0000000..39d0c7e --- /dev/null +++ b/cds-feature-notifications/src/test/java/com/sap/cds/notifications/helpers/I18nHelperTest.java @@ -0,0 +1,75 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-notifications contributors. + */ +package com.sap.cds.notifications.helpers; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; + +import com.sap.cds.services.runtime.CdsRuntime; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class I18nHelperTest { + + private I18nHelper i18nHelper; + + @BeforeEach + void setUp() { + i18nHelper = new I18nHelper(mock(CdsRuntime.class)); + } + + @Nested + @DisplayName("resolveI18n") + class ResolveI18nTests { + + @Test + void nullValue_returnsNull() { + assertNull(i18nHelper.resolveI18n(null, Map.of())); + } + + @Test + void noPlaceholder_returnsOriginal() { + String input = "Book Order"; + assertEquals(input, i18nHelper.resolveI18n(input, Map.of("SUBTITLE", "Book Order"))); + } + + @Test + void singlePlaceholder_resolved() { + assertEquals( + "Book Order", + i18nHelper.resolveI18n("{i18n>SUBTITLE}", Map.of("SUBTITLE", "Book Order"))); + } + + @Test + void unknownKey_notReplaced() { + String input = "{i18n>UNKNOWN_KEY}"; + assertEquals(input, i18nHelper.resolveI18n(input, Map.of("SUBTITLE", "Book Order"))); + } + + @Test + void multiplePlaceholders_allResolved() { + String input = "{i18n>EMAIL_SUBJECT}: {i18n>SUBTITLE}"; + assertEquals( + "Book Order: Book has been ordered", + i18nHelper.resolveI18n( + input, + Map.of( + "EMAIL_SUBJECT", "Book Order", + "SUBTITLE", "Book has been ordered"))); + } + } + + @Nested + @DisplayName("loadHtmlFromClasspath") + class LoadHtmlFromClasspathTests { + + @Test + void fileNotFound_returnsNull() { + assertNull(i18nHelper.loadHtmlFromClasspath("non/existent/file.html", Map.of())); + } + } +} From dae8c70a4fbd2b2d6c6c414651a4b9ec51a65b41 Mon Sep 17 00:00:00 2001 From: Buse Halis Date: Tue, 16 Jun 2026 11:21:41 +0200 Subject: [PATCH 09/19] test: verify re-provisioning updates existing notification templates --- .../NotificationTemplateProvisioningTest.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTemplateProvisioningTest.java b/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTemplateProvisioningTest.java index 05c852a..e806030 100644 --- a/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTemplateProvisioningTest.java +++ b/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTemplateProvisioningTest.java @@ -5,8 +5,11 @@ import static org.junit.jupiter.api.Assertions.*; +import cds.gen.notificationtemplateproviderservice.NotificationTemplateProviderService; import cds.gen.notificationtemplateproviderservice.NotificationTemplates; import cds.gen.notificationtemplateproviderservice.Translations; +import com.sap.cds.notifications.handlers.NotificationTemplateAutoProvisionerHandler; +import com.sap.cds.services.runtime.CdsRuntime; import customer.sample_app.handlers.mock.NotificationTemplateProviderServiceMockHandler; import java.util.List; import java.util.Map; @@ -14,6 +17,7 @@ import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; @@ -32,6 +36,15 @@ public class NotificationTemplateProvisioningTest { /** Pattern to detect unresolved i18n placeholders like {i18n>KEY} */ private static final Pattern UNRESOLVED_I18N = Pattern.compile("\\{i18n>[^}]+\\}"); + @Autowired private CdsRuntime cdsRuntime; + + @Autowired private NotificationTemplateProviderService notificationTemplateProviderService; + + private NotificationTemplateAutoProvisionerHandler createProvisioner() { + return new NotificationTemplateAutoProvisionerHandler( + cdsRuntime, notificationTemplateProviderService); + } + // ────────────────────────────────────────────────────────────── // Test 1: Templates are provisioned at startup // ────────────────────────────────────────────────────────────── @@ -499,4 +512,37 @@ private void assertNoUnresolvedI18n(String value, String fieldName, String lang) "Unresolved {i18n>...} placeholder in " + fieldName + " [" + lang + "]: " + value); } } + + // ────────────────────────────────────────────────────────────── + // Test: Re-provisioning updates existing templates + // ────────────────────────────────────────────────────────────── + + @Test + void testReProvisioningUpdatesExistingTemplates() { + LOG.debug("=========================================="); + LOG.debug("Test: Re-provisioning should UPDATE existing templates"); + LOG.debug("=========================================="); + + int countBefore = NotificationTemplateProviderServiceMockHandler.getTemplateCount(); + assertTrue(countBefore > 0, "Templates should already be provisioned at startup"); + + int updatesBefore = + NotificationTemplateProviderServiceMockHandler.getUpdateCount("CertificateExpiration"); + + createProvisioner().onApplicationPrepared(); + + int updatesAfter = + NotificationTemplateProviderServiceMockHandler.getUpdateCount("CertificateExpiration"); + assertEquals( + updatesBefore + 1, + updatesAfter, + "CertificateExpiration template should have been updated once during re-provisioning"); + + assertEquals( + countBefore, + NotificationTemplateProviderServiceMockHandler.getTemplateCount(), + "Template count should remain the same after re-provisioning"); + + LOG.debug("Re-provisioning triggered UPDATE for existing templates"); + } } From 7ee6a79359c244832cfc6714b3d2962f38f9eb07 Mon Sep 17 00:00:00 2001 From: Buse Halis Date: Tue, 16 Jun 2026 12:03:17 +0200 Subject: [PATCH 10/19] test: add local mode integration tests for LocalHandler and local provisioner handlers --- .../integration/LocalModeIntegrationTest.java | 56 +++++++++++++++++++ .../src/test/resources/application-local.yaml | 10 ++++ 2 files changed, 66 insertions(+) create mode 100644 sample-app/srv/src/test/java/customer/sample_app/integration/LocalModeIntegrationTest.java create mode 100644 sample-app/srv/src/test/resources/application-local.yaml diff --git a/sample-app/srv/src/test/java/customer/sample_app/integration/LocalModeIntegrationTest.java b/sample-app/srv/src/test/java/customer/sample_app/integration/LocalModeIntegrationTest.java new file mode 100644 index 0000000..dcd0293 --- /dev/null +++ b/sample-app/srv/src/test/java/customer/sample_app/integration/LocalModeIntegrationTest.java @@ -0,0 +1,56 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-notifications contributors. + */ +package customer.sample_app.integration; + +import static org.junit.jupiter.api.Assertions.*; + +import cds.gen.my.notifications.notificationservice.CertificateExpirationContext; +import cds.gen.my.notifications.notificationservice.NotificationService; +import com.sap.cds.notifications.handlers.LocalNotificationTemplateAutoProvisionerHandler; +import com.sap.cds.notifications.handlers.LocalNotificationTypeAutoProvisionerHandler; +import com.sap.cds.services.runtime.CdsRuntime; +import customer.sample_app.testdata.CertificateExpirationTestData; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +/** + * Integration tests for local mode (production.enabled: false). Verifies that LocalHandler, + * LocalNotificationTypeAutoProvisionerHandler, and LocalNotificationTemplateAutoProvisionerHandler + * start up and handle events without errors. + */ +@SpringBootTest +@ActiveProfiles("local") +public class LocalModeIntegrationTest { + + @Autowired private NotificationService.Application notificationService; + + @Autowired private CdsRuntime cdsRuntime; + + @Test + void testLocalHandlerLogsNotification() { + CertificateExpirationContext context = CertificateExpirationContext.create(); + context.setData(CertificateExpirationTestData.createValidCertificateExpiration()); + + assertDoesNotThrow( + () -> notificationService.emit(context), + "LocalHandler should handle notification without errors in local mode"); + } + + @Test + void testLocalNotificationTypeAutoProvisionerHandlerRunsOnStartup() { + assertDoesNotThrow( + () -> new LocalNotificationTypeAutoProvisionerHandler(cdsRuntime).onApplicationPrepared(), + "LocalNotificationTypeAutoProvisionerHandler should run without errors in local mode"); + } + + @Test + void testLocalNotificationTemplateAutoProvisionerHandlerRunsOnStartup() { + assertDoesNotThrow( + () -> + new LocalNotificationTemplateAutoProvisionerHandler(cdsRuntime).onApplicationPrepared(), + "LocalNotificationTemplateAutoProvisionerHandler should run without errors in local mode"); + } +} diff --git a/sample-app/srv/src/test/resources/application-local.yaml b/sample-app/srv/src/test/resources/application-local.yaml new file mode 100644 index 0000000..c8c2281 --- /dev/null +++ b/sample-app/srv/src/test/resources/application-local.yaml @@ -0,0 +1,10 @@ +--- +spring: + config: + activate: + on-profile: local + sql.init.platform: h2 +cds: + environment: + production: + enabled: false From e9f74c4c209ef90652d2f006fb7f5b44bec9a5bd Mon Sep 17 00:00:00 2001 From: Buse Halis Date: Tue, 16 Jun 2026 14:00:18 +0200 Subject: [PATCH 11/19] test: add unit tests for I18nHelper getAvailableLocales and getI18nTexts fallback behavior --- .../notifications/helpers/I18nHelperTest.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/cds-feature-notifications/src/test/java/com/sap/cds/notifications/helpers/I18nHelperTest.java b/cds-feature-notifications/src/test/java/com/sap/cds/notifications/helpers/I18nHelperTest.java index 39d0c7e..89bac1e 100644 --- a/cds-feature-notifications/src/test/java/com/sap/cds/notifications/helpers/I18nHelperTest.java +++ b/cds-feature-notifications/src/test/java/com/sap/cds/notifications/helpers/I18nHelperTest.java @@ -5,9 +5,14 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import com.sap.cds.adapter.edmx.EdmxI18nProvider; import com.sap.cds.services.runtime.CdsRuntime; +import java.util.Collections; +import java.util.Locale; import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -72,4 +77,45 @@ void fileNotFound_returnsNull() { assertNull(i18nHelper.loadHtmlFromClasspath("non/existent/file.html", Map.of())); } } + + @Nested + @DisplayName("getAvailableLocales") + class GetAvailableLocalesTests { + + @Test + void nullProvider_fallsBackToEnglish() { + CdsRuntime runtime = mock(CdsRuntime.class); + when(runtime.getProvider(EdmxI18nProvider.class)).thenReturn(null); + I18nHelper helper = new I18nHelper(runtime); + + Set locales = helper.getAvailableLocales(); + assertEquals(Set.of(Locale.ENGLISH), locales); + } + + @Test + void emptyLocalesFromProvider_fallsBackToEnglish() { + EdmxI18nProvider provider = mock(EdmxI18nProvider.class); + when(provider.getLocales()).thenReturn(Collections.emptySet()); + CdsRuntime runtime = mock(CdsRuntime.class); + when(runtime.getProvider(EdmxI18nProvider.class)).thenReturn(provider); + I18nHelper helper = new I18nHelper(runtime); + + Set locales = helper.getAvailableLocales(); + assertEquals(Set.of(Locale.ENGLISH), locales); + } + } + + @Nested + @DisplayName("getI18nTexts") + class GetI18nTextsTests { + + @Test + void nullProvider_returnsEmptyMap() { + CdsRuntime runtime = mock(CdsRuntime.class); + when(runtime.getProvider(EdmxI18nProvider.class)).thenReturn(null); + I18nHelper helper = new I18nHelper(runtime); + + assertTrue(helper.getI18nTexts(Locale.ENGLISH).isEmpty()); + } + } } From 4a57dc2ef42b52f79de2e3255f1040160dd9115d Mon Sep 17 00:00:00 2001 From: Buse Halis Date: Tue, 16 Jun 2026 15:07:49 +0200 Subject: [PATCH 12/19] refactor: move builder classes from handlers to builders package --- .../{handlers => builders}/NotificationBuilder.java | 12 ++++++------ .../NotificationTemplateBuilder.java | 2 +- .../NotificationTypeBuilder.java | 2 +- .../handlers/EntityNotificationHandler.java | 1 + .../sap/cds/notifications/handlers/LocalHandler.java | 2 ++ ...alNotificationTemplateAutoProvisionerHandler.java | 2 ++ .../LocalNotificationTypeAutoProvisionerHandler.java | 2 ++ .../NotificationTemplateAutoProvisionerHandler.java | 2 ++ .../NotificationTypeAutoProvisionerHandler.java | 2 ++ .../notifications/handlers/ProductionHandler.java | 2 ++ .../NotificationBuilderTest.java | 4 +++- 11 files changed, 24 insertions(+), 9 deletions(-) rename cds-feature-notifications/src/main/java/com/sap/cds/notifications/{handlers => builders}/NotificationBuilder.java (98%) rename cds-feature-notifications/src/main/java/com/sap/cds/notifications/{handlers => builders}/NotificationTemplateBuilder.java (99%) rename cds-feature-notifications/src/main/java/com/sap/cds/notifications/{handlers => builders}/NotificationTypeBuilder.java (99%) rename cds-feature-notifications/src/test/java/com/sap/cds/notifications/{handlers => builders}/NotificationBuilderTest.java (99%) diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationBuilder.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationBuilder.java similarity index 98% rename from cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationBuilder.java rename to cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationBuilder.java index b7e0abd..e16e400 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationBuilder.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationBuilder.java @@ -1,7 +1,7 @@ /* * © 2026 SAP SE or an SAP affiliate company and cds-feature-notifications contributors. */ -package com.sap.cds.notifications.handlers; +package com.sap.cds.notifications.builders; import cds.gen.notificationproviderservice.NotificationProperties; import cds.gen.notificationproviderservice.Notifications; @@ -274,7 +274,7 @@ public CqnPredicate containment( * @param serviceCatalog service catalog to obtain the persistence service * @return query result containing a single row with column "result" */ - static Result executeDummySelect(CqnValue resolvedExpression, ServiceCatalog serviceCatalog) { + public static Result executeDummySelect(CqnValue resolvedExpression, ServiceCatalog serviceCatalog) { Value expr = ExpressionBuilder.create(resolvedExpression).value(); PersistenceService ps = serviceCatalog.getService(PersistenceService.class, PersistenceService.DEFAULT_NAME); @@ -298,7 +298,7 @@ static Result executeDummySelect(CqnValue resolvedExpression, ServiceCatalog ser * be null if both {@code value} and {@code term} are guaranteed to be literals * @return a tautology ({@code 1=1}) or contradiction ({@code 1=0}) predicate */ - static CqnPredicate evaluateContainment( + public static CqnPredicate evaluateContainment( CqnContainmentTest.Position position, CqnValue value, CqnValue term, @@ -431,7 +431,7 @@ private List resolveRecipients(String eventName, CdsData eventData) // Auto-detects UUID vs email and maps to GlobalUserId or RecipientId accordingly @VisibleForTesting - static Recipients createRecipientFromId(String recipientId) { + public static Recipients createRecipientFromId(String recipientId) { Recipients recipient = Struct.create(Recipients.class); if (isUUID(recipientId)) { recipient.setGlobalUserId(recipientId); @@ -449,7 +449,7 @@ static Recipients createRecipientFromId(String recipientId) { } @VisibleForTesting - static boolean isUUID(String value) { + public static boolean isUUID(String value) { try { UUID.fromString(value); return true; @@ -464,7 +464,7 @@ static boolean isUUID(String value) { Pattern.compile("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"); @VisibleForTesting - static boolean isEmail(String value) { + public static boolean isEmail(String value) { return EMAIL_PATTERN.matcher(value).matches(); } diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateBuilder.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationTemplateBuilder.java similarity index 99% rename from cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateBuilder.java rename to cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationTemplateBuilder.java index 4ece271..27200ff 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateBuilder.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationTemplateBuilder.java @@ -1,7 +1,7 @@ /* * © 2026 SAP SE or an SAP affiliate company and cds-feature-notifications contributors. */ -package com.sap.cds.notifications.handlers; +package com.sap.cds.notifications.builders; import cds.gen.notificationtemplateproviderservice.Email; import cds.gen.notificationtemplateproviderservice.NotificationTemplates; diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeBuilder.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationTypeBuilder.java similarity index 99% rename from cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeBuilder.java rename to cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationTypeBuilder.java index fca07c8..74170bf 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeBuilder.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationTypeBuilder.java @@ -1,7 +1,7 @@ /* * © 2026 SAP SE or an SAP affiliate company and cds-feature-notifications contributors. */ -package com.sap.cds.notifications.handlers; +package com.sap.cds.notifications.builders; import cds.gen.notificationtypeproviderservice.DeliveryChannels; import cds.gen.notificationtypeproviderservice.NotificationTypes; diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/EntityNotificationHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/EntityNotificationHandler.java index b464d71..654cc71 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/EntityNotificationHandler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/EntityNotificationHandler.java @@ -6,6 +6,7 @@ import com.sap.cds.CdsData; import com.sap.cds.Result; import com.sap.cds.Struct; +import com.sap.cds.notifications.builders.NotificationBuilder; import com.sap.cds.ql.CQL; import com.sap.cds.ql.cqn.CqnContainmentTest; import com.sap.cds.ql.cqn.CqnElementRef; diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalHandler.java index 47a8f2f..5686d91 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalHandler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalHandler.java @@ -6,6 +6,8 @@ import cds.gen.notificationproviderservice.NotificationProperties; import cds.gen.notificationproviderservice.Notifications; import cds.gen.notificationproviderservice.Recipients; + +import com.sap.cds.notifications.builders.NotificationBuilder; import com.sap.cds.services.EventContext; import com.sap.cds.services.cds.ApplicationService; import com.sap.cds.services.handler.EventHandler; diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTemplateAutoProvisionerHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTemplateAutoProvisionerHandler.java index 093d88c..9a5f6a9 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTemplateAutoProvisionerHandler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTemplateAutoProvisionerHandler.java @@ -5,6 +5,8 @@ import cds.gen.notificationtemplateproviderservice.NotificationTemplates; import cds.gen.notificationtemplateproviderservice.Translations; + +import com.sap.cds.notifications.builders.NotificationTemplateBuilder; import com.sap.cds.services.application.ApplicationLifecycleService; import com.sap.cds.services.handler.EventHandler; import com.sap.cds.services.handler.annotations.On; diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTypeAutoProvisionerHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTypeAutoProvisionerHandler.java index b22d4ae..ce3375d 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTypeAutoProvisionerHandler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTypeAutoProvisionerHandler.java @@ -6,6 +6,8 @@ import cds.gen.notificationtypeproviderservice.DeliveryChannels; import cds.gen.notificationtypeproviderservice.NotificationTypes; import cds.gen.notificationtypeproviderservice.Templates; + +import com.sap.cds.notifications.builders.NotificationTypeBuilder; import com.sap.cds.services.application.ApplicationLifecycleService; import com.sap.cds.services.handler.EventHandler; import com.sap.cds.services.handler.annotations.On; diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateAutoProvisionerHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateAutoProvisionerHandler.java index f265ff3..d319bae 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateAutoProvisionerHandler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateAutoProvisionerHandler.java @@ -6,6 +6,8 @@ import cds.gen.notificationtemplateproviderservice.NotificationTemplateProviderService; import cds.gen.notificationtemplateproviderservice.NotificationTemplates; import cds.gen.notificationtemplateproviderservice.NotificationTemplates_; + +import com.sap.cds.notifications.builders.NotificationTemplateBuilder; import com.sap.cds.ql.Insert; import com.sap.cds.ql.Select; import com.sap.cds.ql.Update; diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeAutoProvisionerHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeAutoProvisionerHandler.java index b28c754..ab663b5 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeAutoProvisionerHandler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeAutoProvisionerHandler.java @@ -6,6 +6,8 @@ import cds.gen.notificationtypeproviderservice.NotificationTypeProviderService; import cds.gen.notificationtypeproviderservice.NotificationTypes; import cds.gen.notificationtypeproviderservice.NotificationTypes_; + +import com.sap.cds.notifications.builders.NotificationTypeBuilder; import com.sap.cds.ql.Insert; import com.sap.cds.ql.Select; import com.sap.cds.ql.Update; diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/ProductionHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/ProductionHandler.java index e9f9df2..913f598 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/ProductionHandler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/ProductionHandler.java @@ -6,6 +6,8 @@ import cds.gen.notificationproviderservice.NotificationProviderService; import cds.gen.notificationproviderservice.Notifications; import cds.gen.notificationproviderservice.Notifications_; + +import com.sap.cds.notifications.builders.NotificationBuilder; import com.sap.cds.ql.Insert; import com.sap.cds.services.EventContext; import com.sap.cds.services.cds.ApplicationService; diff --git a/cds-feature-notifications/src/test/java/com/sap/cds/notifications/handlers/NotificationBuilderTest.java b/cds-feature-notifications/src/test/java/com/sap/cds/notifications/builders/NotificationBuilderTest.java similarity index 99% rename from cds-feature-notifications/src/test/java/com/sap/cds/notifications/handlers/NotificationBuilderTest.java rename to cds-feature-notifications/src/test/java/com/sap/cds/notifications/builders/NotificationBuilderTest.java index 8165765..0e7633c 100644 --- a/cds-feature-notifications/src/test/java/com/sap/cds/notifications/handlers/NotificationBuilderTest.java +++ b/cds-feature-notifications/src/test/java/com/sap/cds/notifications/builders/NotificationBuilderTest.java @@ -1,11 +1,13 @@ /* * © 2026 SAP SE or an SAP affiliate company and cds-feature-notifications contributors. */ -package com.sap.cds.notifications.handlers; +package com.sap.cds.notifications.builders; import static org.junit.jupiter.api.Assertions.*; import cds.gen.notificationproviderservice.Recipients; + +import com.sap.cds.notifications.builders.NotificationBuilder; import com.sap.cds.ql.CQL; import com.sap.cds.ql.cqn.CqnComparisonPredicate; import com.sap.cds.ql.cqn.CqnContainmentTest; From 14165c704da29f4f8e91fb7c93e5f5f4227113d5 Mon Sep 17 00:00:00 2001 From: Buse Halis Date: Tue, 16 Jun 2026 15:16:55 +0200 Subject: [PATCH 13/19] refactor: replace System.out.println with logger and use text blocks in LocalNotificationTemplateAutoProvisionerHandler --- ...icationTemplateAutoProvisionerHandler.java | 86 +++++++++---------- 1 file changed, 40 insertions(+), 46 deletions(-) diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTemplateAutoProvisionerHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTemplateAutoProvisionerHandler.java index 9a5f6a9..06564dd 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTemplateAutoProvisionerHandler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTemplateAutoProvisionerHandler.java @@ -60,60 +60,54 @@ private void provisionNotificationTemplates() { } private void logTemplate(NotificationTemplates template) { - System.out.println("\n==============================================================="); - System.out.println("Standalone NotificationTemplate (Local Mode - Not Sent to ANS)"); - System.out.println(" Key: " + template.getKey()); - System.out.println(" Visibility: " + template.getVisibility()); - System.out.println( - " Translations (" - + (template.getTranslations() != null ? template.getTranslations().size() : 0) - + "):"); + logger.info("==============================================================="); + logger.info("Standalone NotificationTemplate (Local Mode - Not Sent to ANS)"); + logger.info( + """ + Key: {} + Visibility: {} + Translations: {}""", + template.getKey(), + template.getVisibility(), + template.getTranslations() != null ? template.getTranslations().size() : 0); if (template.getTranslations() != null) { for (Translations translation : template.getTranslations()) { - System.out.println( - " - Language: " - + translation.getLanguage() - + "\n" - + " Syntax: " - + translation.getSyntax() - + "\n" - + " Title: " - + translation.getTitle() - + "\n" - + " Preview: " - + translation.getPreview() - + "\n" - + " Body: " - + translation.getBody() - + "\n" - + " Description: " - + translation.getDescription()); + logger.info( + """ + - Language: {} + Syntax: {} + Title: {} + Preview: {} + Body: {} + Description: {}""", + translation.getLanguage(), + translation.getSyntax(), + translation.getTitle(), + translation.getPreview(), + translation.getBody(), + translation.getDescription()); if (translation.getEmail() != null) { - System.out.println( - " Email Subject: " - + translation.getEmail().getSubject() - + "\n" - + " Email BodyHtml: " - + (translation.getEmail().getBodyHtml() != null - ? translation - .getEmail() - .getBodyHtml() - .substring( - 0, Math.min(100, translation.getEmail().getBodyHtml().length())) - + "..." - : "null") - + "\n" - + " Email BodyText: " - + translation.getEmail().getBodyText()); + logger.info( + """ + Email Subject: {} + Email BodyHtml: {} + Email BodyText: {}""", + translation.getEmail().getSubject(), + translation.getEmail().getBodyHtml() != null + ? translation + .getEmail() + .getBodyHtml() + .substring(0, Math.min(100, translation.getEmail().getBodyHtml().length())) + + "..." + : "null", + translation.getEmail().getBodyText()); } } } - System.out.println("===============================================================\n"); - - logger.info( - "Standalone template '{}' logged (LOCAL MODE - not sent to ANS)", template.getKey()); + logger.info("==============================================================="); + logger.info("Standalone template '{}' logged (LOCAL MODE - not sent to ANS)", template.getKey()); } } From 651b4edd3022f34e92e074a39b68259b3c6e076c Mon Sep 17 00:00:00 2001 From: Buse Halis Date: Tue, 16 Jun 2026 15:17:07 +0200 Subject: [PATCH 14/19] chore: remove debug System.out.println statements from NotificationServiceConfiguration --- .../cds/notifications/NotificationServiceConfiguration.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/NotificationServiceConfiguration.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/NotificationServiceConfiguration.java index 74c06d7..8219c35 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/NotificationServiceConfiguration.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/NotificationServiceConfiguration.java @@ -148,10 +148,6 @@ public void environment(CdsRuntimeConfigurer configurer) { .onBehalfOf(OnBehalfOf.TECHNICAL_USER_CURRENT_TENANT) .build()); - System.out.println("http destination is " + httpDestination); - System.out.println("http destination url is " + httpDestination.getUri()); - System.out.println("http destination name value is" + httpDestination.get("name").get()); - // Add the destination to CDS runtime so RemoteServiceConfig can use it DestinationAccessor.prependDestinationLoader( new DefaultDestinationLoader() From 42b83fbbfeb313e3eac5580de5a4208e45515556 Mon Sep 17 00:00:00 2001 From: Buse Halis Date: Tue, 16 Jun 2026 15:21:16 +0200 Subject: [PATCH 15/19] refactor: use Jackson ObjectMapper instead of manual string building for PropertiesSchema JSON --- .../builders/NotificationTemplateBuilder.java | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationTemplateBuilder.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationTemplateBuilder.java index 27200ff..0f9b073 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationTemplateBuilder.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationTemplateBuilder.java @@ -14,6 +14,7 @@ import com.sap.cds.reflect.CdsModel; import com.sap.cds.reflect.CdsSimpleType; import com.sap.cds.services.runtime.CdsRuntime; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -247,10 +248,10 @@ private List buildTags(String source, String eventName) { * Schema types. Excludes the 'recipients' field as it's not a template variable. */ private String buildPropertiesSchema(CdsEvent event) { - StringBuilder sb = new StringBuilder(); - sb.append("{\"type\":\"object\",\"properties\":{"); + Map schema = new LinkedHashMap<>(); + schema.put("type", "object"); - List propertyEntries = new ArrayList<>(); + Map properties = new LinkedHashMap<>(); List requiredFields = new ArrayList<>(); event @@ -258,25 +259,24 @@ private String buildPropertiesSchema(CdsEvent event) { .forEach( element -> { String fieldName = element.getName(); - // Skip recipients - it's not a template variable if (!"recipients".equals(fieldName)) { String jsonType = cdsTypeToJsonSchemaType(element); - propertyEntries.add( - String.format( - "\"%s\":{\"type\":\"%s\",\"title\":\"%s\"}", - fieldName, jsonType, fieldName)); - requiredFields.add("\"" + fieldName + "\""); + properties.put(fieldName, Map.of("type", jsonType, "title", fieldName)); + requiredFields.add(fieldName); } }); - sb.append(String.join(",", propertyEntries)); - sb.append("},\"required\":["); - sb.append(String.join(",", requiredFields)); - sb.append("]}"); + schema.put("properties", properties); + schema.put("required", requiredFields); - String schema = sb.toString(); - logger.debug("Generated PropertiesSchema: {}", schema); - return schema; + try { + String schemaJson = new ObjectMapper().writeValueAsString(schema); + logger.debug("Generated PropertiesSchema: {}", schemaJson); + return schemaJson; + } catch (Exception e) { + logger.error("Failed to serialize PropertiesSchema", e); + return "{}"; + } } /** Map CDS element type to JSON Schema type string. */ From 99e8ab29cda019bf905d97c17c8b669e9c5fdb07 Mon Sep 17 00:00:00 2001 From: Buse Halis Date: Tue, 16 Jun 2026 15:37:00 +0200 Subject: [PATCH 16/19] refactor: rename builder classes to assemblers and move to assemblers package --- .../NotificationAssembler.java} | 10 ++-- .../NotificationTemplateAssembler.java} | 8 +-- .../NotificationTypeAssembler.java} | 8 +-- .../handlers/EntityNotificationHandler.java | 8 +-- .../notifications/handlers/LocalHandler.java | 8 +-- ...icationTemplateAutoProvisionerHandler.java | 6 +- ...otificationTypeAutoProvisionerHandler.java | 6 +- ...icationTemplateAutoProvisionerHandler.java | 6 +- ...otificationTypeAutoProvisionerHandler.java | 6 +- .../handlers/ProductionHandler.java | 8 +-- .../builders/NotificationBuilderTest.java | 60 +++++++++---------- 11 files changed, 67 insertions(+), 67 deletions(-) rename cds-feature-notifications/src/main/java/com/sap/cds/notifications/{builders/NotificationBuilder.java => assemblers/NotificationAssembler.java} (98%) rename cds-feature-notifications/src/main/java/com/sap/cds/notifications/{builders/NotificationTemplateBuilder.java => assemblers/NotificationTemplateAssembler.java} (98%) rename cds-feature-notifications/src/main/java/com/sap/cds/notifications/{builders/NotificationTypeBuilder.java => assemblers/NotificationTypeAssembler.java} (97%) diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationBuilder.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationAssembler.java similarity index 98% rename from cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationBuilder.java rename to cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationAssembler.java index e16e400..fd3a9a2 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationBuilder.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationAssembler.java @@ -1,7 +1,7 @@ /* * © 2026 SAP SE or an SAP affiliate company and cds-feature-notifications contributors. */ -package com.sap.cds.notifications.builders; +package com.sap.cds.notifications.assemblers; import cds.gen.notificationproviderservice.NotificationProperties; import cds.gen.notificationproviderservice.Notifications; @@ -45,16 +45,16 @@ * Helper class to build notification objects from event context. Reduces code duplication between * ProductionHandler and LocalHandler. */ -public class NotificationBuilder { +public class NotificationAssembler { - private static final Logger logger = LoggerFactory.getLogger(NotificationBuilder.class); + private static final Logger logger = LoggerFactory.getLogger(NotificationAssembler.class); /** Valid priority values as defined by the SAP Alert Notification service. */ private static final Set VALID_PRIORITIES = Set.of("LOW", "NEUTRAL", "MEDIUM", "HIGH"); private final CdsRuntime runtime; - public NotificationBuilder(CdsRuntime runtime) { + public NotificationAssembler(CdsRuntime runtime) { this.runtime = runtime; } @@ -400,7 +400,7 @@ private List resolveRecipients(String eventName, CdsData eventData) recipientsList.addAll( list.stream() .map(String.class::cast) - .map(NotificationBuilder::createRecipientFromId) + .map(NotificationAssembler::createRecipientFromId) .toList()); } else if (recipientsObj instanceof String) { String recipientsStr = ((String) recipientsObj).trim(); diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationTemplateBuilder.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTemplateAssembler.java similarity index 98% rename from cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationTemplateBuilder.java rename to cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTemplateAssembler.java index 0f9b073..3eac299 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationTemplateBuilder.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTemplateAssembler.java @@ -1,7 +1,7 @@ /* * © 2026 SAP SE or an SAP affiliate company and cds-feature-notifications contributors. */ -package com.sap.cds.notifications.builders; +package com.sap.cds.notifications.assemblers; import cds.gen.notificationtemplateproviderservice.Email; import cds.gen.notificationtemplateproviderservice.NotificationTemplates; @@ -34,16 +34,16 @@ *
  • {@code @notification.template.description} → Translation.Description * */ -public class NotificationTemplateBuilder { +public class NotificationTemplateAssembler { - private static final Logger logger = LoggerFactory.getLogger(NotificationTemplateBuilder.class); + private static final Logger logger = LoggerFactory.getLogger(NotificationTemplateAssembler.class); private static final String DEFAULT_SYNTAX = "MUSTACHE"; private final CdsRuntime runtime; private final I18nHelper i18nHelper; - public NotificationTemplateBuilder(CdsRuntime runtime) { + public NotificationTemplateAssembler(CdsRuntime runtime) { this.runtime = runtime; this.i18nHelper = new I18nHelper(runtime); } diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationTypeBuilder.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTypeAssembler.java similarity index 97% rename from cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationTypeBuilder.java rename to cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTypeAssembler.java index 74170bf..818f782 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/builders/NotificationTypeBuilder.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTypeAssembler.java @@ -1,7 +1,7 @@ /* * © 2026 SAP SE or an SAP affiliate company and cds-feature-notifications contributors. */ -package com.sap.cds.notifications.builders; +package com.sap.cds.notifications.assemblers; import cds.gen.notificationtypeproviderservice.DeliveryChannels; import cds.gen.notificationtypeproviderservice.NotificationTypes; @@ -16,14 +16,14 @@ import org.slf4j.LoggerFactory; /** Helper class to build NotificationType objects from CDS event annotations. */ -public class NotificationTypeBuilder { +public class NotificationTypeAssembler { - private static final Logger logger = LoggerFactory.getLogger(NotificationTypeBuilder.class); + private static final Logger logger = LoggerFactory.getLogger(NotificationTypeAssembler.class); private final CdsRuntime runtime; private final I18nHelper i18nHelper; - public NotificationTypeBuilder(CdsRuntime runtime) { + public NotificationTypeAssembler(CdsRuntime runtime) { this.runtime = runtime; this.i18nHelper = new I18nHelper(runtime); } diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/EntityNotificationHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/EntityNotificationHandler.java index 654cc71..1035844 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/EntityNotificationHandler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/EntityNotificationHandler.java @@ -6,7 +6,7 @@ import com.sap.cds.CdsData; import com.sap.cds.Result; import com.sap.cds.Struct; -import com.sap.cds.notifications.builders.NotificationBuilder; +import com.sap.cds.notifications.assemblers.NotificationAssembler; import com.sap.cds.ql.CQL; import com.sap.cds.ql.cqn.CqnContainmentTest; import com.sap.cds.ql.cqn.CqnElementRef; @@ -280,7 +280,7 @@ private CdsData buildEventData( * *

    The event is looked up in the CDS model to determine its owning service, then emitted on * that service. Downstream {@code @On} handlers ({@link ProductionHandler} or {@link - * LocalHandler}) pick up the event and delegate to {@link NotificationBuilder}, which iterates + * LocalHandler}) pick up the event and delegate to {@link NotificationAssembler}, which iterates * the list and builds one ANS notification per entry. */ private void emitBatchNotification( @@ -401,12 +401,12 @@ public CqnPredicate containment( CqnValue value, CqnValue term, boolean caseInsensitive) { - return NotificationBuilder.evaluateContainment( + return NotificationAssembler.evaluateContainment( position, value, term, caseInsensitive, context.getServiceCatalog()); } }); - Result result = NotificationBuilder.executeDummySelect(resolved, context.getServiceCatalog()); + Result result = NotificationAssembler.executeDummySelect(resolved, context.getServiceCatalog()); boolean conditionMet = result diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalHandler.java index 5686d91..f0bd228 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalHandler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalHandler.java @@ -7,7 +7,7 @@ import cds.gen.notificationproviderservice.Notifications; import cds.gen.notificationproviderservice.Recipients; -import com.sap.cds.notifications.builders.NotificationBuilder; +import com.sap.cds.notifications.assemblers.NotificationAssembler; import com.sap.cds.services.EventContext; import com.sap.cds.services.cds.ApplicationService; import com.sap.cds.services.handler.EventHandler; @@ -22,15 +22,15 @@ public class LocalHandler implements EventHandler { private static final Logger logger = LoggerFactory.getLogger(LocalHandler.class); - private final NotificationBuilder notificationBuilder; + private final NotificationAssembler notificationBuilder; public LocalHandler(CdsRuntime runtime) { - this.notificationBuilder = new NotificationBuilder(runtime); + this.notificationBuilder = new NotificationAssembler(runtime); } @On(event = "*") public void postNotifications(EventContext context) { - List results = + List results = notificationBuilder.buildNotifications(context); if (results.isEmpty()) { return; diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTemplateAutoProvisionerHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTemplateAutoProvisionerHandler.java index 06564dd..58f656e 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTemplateAutoProvisionerHandler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTemplateAutoProvisionerHandler.java @@ -6,7 +6,7 @@ import cds.gen.notificationtemplateproviderservice.NotificationTemplates; import cds.gen.notificationtemplateproviderservice.Translations; -import com.sap.cds.notifications.builders.NotificationTemplateBuilder; +import com.sap.cds.notifications.assemblers.NotificationTemplateAssembler; import com.sap.cds.services.application.ApplicationLifecycleService; import com.sap.cds.services.handler.EventHandler; import com.sap.cds.services.handler.annotations.On; @@ -26,10 +26,10 @@ public class LocalNotificationTemplateAutoProvisionerHandler implements EventHan private static final Logger logger = LoggerFactory.getLogger(LocalNotificationTemplateAutoProvisionerHandler.class); - private final NotificationTemplateBuilder notificationTemplateBuilder; + private final NotificationTemplateAssembler notificationTemplateBuilder; public LocalNotificationTemplateAutoProvisionerHandler(CdsRuntime runtime) { - this.notificationTemplateBuilder = new NotificationTemplateBuilder(runtime); + this.notificationTemplateBuilder = new NotificationTemplateAssembler(runtime); } @On(event = ApplicationLifecycleService.EVENT_APPLICATION_PREPARED) diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTypeAutoProvisionerHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTypeAutoProvisionerHandler.java index ce3375d..3ab91f8 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTypeAutoProvisionerHandler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTypeAutoProvisionerHandler.java @@ -7,7 +7,7 @@ import cds.gen.notificationtypeproviderservice.NotificationTypes; import cds.gen.notificationtypeproviderservice.Templates; -import com.sap.cds.notifications.builders.NotificationTypeBuilder; +import com.sap.cds.notifications.assemblers.NotificationTypeAssembler; import com.sap.cds.services.application.ApplicationLifecycleService; import com.sap.cds.services.handler.EventHandler; import com.sap.cds.services.handler.annotations.On; @@ -23,10 +23,10 @@ public class LocalNotificationTypeAutoProvisionerHandler implements EventHandler private static final Logger logger = LoggerFactory.getLogger(LocalNotificationTypeAutoProvisionerHandler.class); - private final NotificationTypeBuilder notificationTypeBuilder; + private final NotificationTypeAssembler notificationTypeBuilder; public LocalNotificationTypeAutoProvisionerHandler(CdsRuntime runtime) { - this.notificationTypeBuilder = new NotificationTypeBuilder(runtime); + this.notificationTypeBuilder = new NotificationTypeAssembler(runtime); } @On(event = ApplicationLifecycleService.EVENT_APPLICATION_PREPARED) diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateAutoProvisionerHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateAutoProvisionerHandler.java index d319bae..4ddc142 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateAutoProvisionerHandler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateAutoProvisionerHandler.java @@ -7,7 +7,7 @@ import cds.gen.notificationtemplateproviderservice.NotificationTemplates; import cds.gen.notificationtemplateproviderservice.NotificationTemplates_; -import com.sap.cds.notifications.builders.NotificationTemplateBuilder; +import com.sap.cds.notifications.assemblers.NotificationTemplateAssembler; import com.sap.cds.ql.Insert; import com.sap.cds.ql.Select; import com.sap.cds.ql.Update; @@ -32,13 +32,13 @@ public class NotificationTemplateAutoProvisionerHandler implements EventHandler private static final Logger logger = LoggerFactory.getLogger(NotificationTemplateAutoProvisionerHandler.class); - private final NotificationTemplateBuilder notificationTemplateBuilder; + private final NotificationTemplateAssembler notificationTemplateBuilder; private final NotificationTemplateProviderService notificationTemplateProviderService; public NotificationTemplateAutoProvisionerHandler( CdsRuntime runtime, NotificationTemplateProviderService notificationTemplateProviderService) { - this.notificationTemplateBuilder = new NotificationTemplateBuilder(runtime); + this.notificationTemplateBuilder = new NotificationTemplateAssembler(runtime); this.notificationTemplateProviderService = notificationTemplateProviderService; } diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeAutoProvisionerHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeAutoProvisionerHandler.java index ab663b5..84c82ed 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeAutoProvisionerHandler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeAutoProvisionerHandler.java @@ -7,7 +7,7 @@ import cds.gen.notificationtypeproviderservice.NotificationTypes; import cds.gen.notificationtypeproviderservice.NotificationTypes_; -import com.sap.cds.notifications.builders.NotificationTypeBuilder; +import com.sap.cds.notifications.assemblers.NotificationTypeAssembler; import com.sap.cds.ql.Insert; import com.sap.cds.ql.Select; import com.sap.cds.ql.Update; @@ -27,12 +27,12 @@ public class NotificationTypeAutoProvisionerHandler implements EventHandler { private static final Logger logger = LoggerFactory.getLogger(NotificationTypeAutoProvisionerHandler.class); - private final NotificationTypeBuilder notificationTypeBuilder; + private final NotificationTypeAssembler notificationTypeBuilder; private final NotificationTypeProviderService notificationTypeProviderService; public NotificationTypeAutoProvisionerHandler( CdsRuntime runtime, NotificationTypeProviderService notificationTypeProviderService) { - this.notificationTypeBuilder = new NotificationTypeBuilder(runtime); + this.notificationTypeBuilder = new NotificationTypeAssembler(runtime); this.notificationTypeProviderService = notificationTypeProviderService; } diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/ProductionHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/ProductionHandler.java index 913f598..10519bb 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/ProductionHandler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/ProductionHandler.java @@ -7,7 +7,7 @@ import cds.gen.notificationproviderservice.Notifications; import cds.gen.notificationproviderservice.Notifications_; -import com.sap.cds.notifications.builders.NotificationBuilder; +import com.sap.cds.notifications.assemblers.NotificationAssembler; import com.sap.cds.ql.Insert; import com.sap.cds.services.EventContext; import com.sap.cds.services.cds.ApplicationService; @@ -24,17 +24,17 @@ public class ProductionHandler implements EventHandler { private static final Logger logger = LoggerFactory.getLogger(ProductionHandler.class); private final NotificationProviderService notificationProviderService; - private final NotificationBuilder notificationBuilder; + private final NotificationAssembler notificationBuilder; public ProductionHandler( NotificationProviderService notificationProviderService, CdsRuntime runtime) { this.notificationProviderService = notificationProviderService; - this.notificationBuilder = new NotificationBuilder(runtime); + this.notificationBuilder = new NotificationAssembler(runtime); } @On(event = "*") public void postNotifications(EventContext context) { - List results = + List results = notificationBuilder.buildNotifications(context); if (results.isEmpty()) { return; diff --git a/cds-feature-notifications/src/test/java/com/sap/cds/notifications/builders/NotificationBuilderTest.java b/cds-feature-notifications/src/test/java/com/sap/cds/notifications/builders/NotificationBuilderTest.java index 0e7633c..6faa89d 100644 --- a/cds-feature-notifications/src/test/java/com/sap/cds/notifications/builders/NotificationBuilderTest.java +++ b/cds-feature-notifications/src/test/java/com/sap/cds/notifications/builders/NotificationBuilderTest.java @@ -7,7 +7,7 @@ import cds.gen.notificationproviderservice.Recipients; -import com.sap.cds.notifications.builders.NotificationBuilder; +import com.sap.cds.notifications.assemblers.NotificationAssembler; import com.sap.cds.ql.CQL; import com.sap.cds.ql.cqn.CqnComparisonPredicate; import com.sap.cds.ql.cqn.CqnContainmentTest; @@ -20,7 +20,7 @@ import org.junit.jupiter.params.provider.ValueSource; /** - * Unit tests for recipient auto-detection logic in {@link NotificationBuilder}. Covers isUUID, + * Unit tests for recipient auto-detection logic in {@link NotificationAssembler}. Covers isUUID, * isEmail, and createRecipientFromId including edge cases (malformed, null, empty). */ class NotificationBuilderTest { @@ -33,17 +33,17 @@ class IsUUIDTests { @Test void validLowercaseUUID() { - assertTrue(NotificationBuilder.isUUID("550e8400-e29b-41d4-a716-446655440000")); + assertTrue(NotificationAssembler.isUUID("550e8400-e29b-41d4-a716-446655440000")); } @Test void validUppercaseUUID() { - assertTrue(NotificationBuilder.isUUID("550E8400-E29B-41D4-A716-446655440000")); + assertTrue(NotificationAssembler.isUUID("550E8400-E29B-41D4-A716-446655440000")); } @Test void validRandomUUID() { - assertTrue(NotificationBuilder.isUUID(java.util.UUID.randomUUID().toString())); + assertTrue(NotificationAssembler.isUUID(java.util.UUID.randomUUID().toString())); } @ParameterizedTest @@ -57,12 +57,12 @@ void validRandomUUID() { " " }) void invalidStringsReturnFalse(String value) { - assertFalse(NotificationBuilder.isUUID(value)); + assertFalse(NotificationAssembler.isUUID(value)); } @Test void nullThrowsNullPointerException() { - assertThrows(NullPointerException.class, () -> NotificationBuilder.isUUID(null)); + assertThrows(NullPointerException.class, () -> NotificationAssembler.isUUID(null)); } } @@ -83,7 +83,7 @@ class IsEmailTests { "user123@test-server.example.com" }) void validEmailsReturnTrue(String email) { - assertTrue(NotificationBuilder.isEmail(email), "Should accept valid email: " + email); + assertTrue(NotificationAssembler.isEmail(email), "Should accept valid email: " + email); } @ParameterizedTest @@ -100,12 +100,12 @@ void validEmailsReturnTrue(String email) { "550e8400-e29b-41d4-a716-446655440000" }) void invalidEmailsReturnFalse(String value) { - assertFalse(NotificationBuilder.isEmail(value), "Should reject invalid email: " + value); + assertFalse(NotificationAssembler.isEmail(value), "Should reject invalid email: " + value); } @Test void nullThrowsNullPointerException() { - assertThrows(NullPointerException.class, () -> NotificationBuilder.isEmail(null)); + assertThrows(NullPointerException.class, () -> NotificationAssembler.isEmail(null)); } } @@ -118,7 +118,7 @@ class CreateRecipientFromIdTests { @Test void validUUID_mapsToGlobalUserId() { String uuid = "550e8400-e29b-41d4-a716-446655440000"; - Recipients recipient = NotificationBuilder.createRecipientFromId(uuid); + Recipients recipient = NotificationAssembler.createRecipientFromId(uuid); assertEquals(uuid, recipient.getGlobalUserId(), "UUID should be mapped to GlobalUserId"); assertNull(recipient.getRecipientId(), "UUID recipient should not have RecipientId"); @@ -127,7 +127,7 @@ void validUUID_mapsToGlobalUserId() { @Test void validEmail_mapsToRecipientId() { String email = "ops-team@example.com"; - Recipients recipient = NotificationBuilder.createRecipientFromId(email); + Recipients recipient = NotificationAssembler.createRecipientFromId(email); assertEquals(email, recipient.getRecipientId(), "Email should be mapped to RecipientId"); assertNull(recipient.getGlobalUserId(), "Email recipient should not have GlobalUserId"); @@ -139,7 +139,7 @@ void unsupportedFormat_throwsIllegalArgumentException(String value) { IllegalArgumentException ex = assertThrows( IllegalArgumentException.class, - () -> NotificationBuilder.createRecipientFromId(value)); + () -> NotificationAssembler.createRecipientFromId(value)); assertTrue( ex.getMessage().contains("Unsupported recipient format"), "Exception message should mention unsupported format"); @@ -148,14 +148,14 @@ void unsupportedFormat_throwsIllegalArgumentException(String value) { @Test void emptyString_throwsIllegalArgumentException() { assertThrows( - IllegalArgumentException.class, () -> NotificationBuilder.createRecipientFromId("")); + IllegalArgumentException.class, () -> NotificationAssembler.createRecipientFromId("")); } @ParameterizedTest @NullSource void null_throwsNullPointerException(String value) { assertThrows( - NullPointerException.class, () -> NotificationBuilder.createRecipientFromId(value)); + NullPointerException.class, () -> NotificationAssembler.createRecipientFromId(value)); } } @@ -184,7 +184,7 @@ private boolean isMatch(CqnPredicate predicate) { @Test void contains_caseSensitive_match() { CqnPredicate result = - NotificationBuilder.evaluateContainment( + NotificationAssembler.evaluateContainment( CqnContainmentTest.Position.ANY, CQL.val("Hello World"), CQL.val("World"), @@ -196,7 +196,7 @@ void contains_caseSensitive_match() { @Test void contains_caseSensitive_noMatch() { CqnPredicate result = - NotificationBuilder.evaluateContainment( + NotificationAssembler.evaluateContainment( CqnContainmentTest.Position.ANY, CQL.val("Hello World"), CQL.val("world"), @@ -210,7 +210,7 @@ void contains_caseSensitive_noMatch() { @Test void contains_caseInsensitive_match() { CqnPredicate result = - NotificationBuilder.evaluateContainment( + NotificationAssembler.evaluateContainment( CqnContainmentTest.Position.ANY, CQL.val("production-server"), CQL.val("PRODUCTION"), @@ -222,7 +222,7 @@ void contains_caseInsensitive_match() { @Test void contains_caseInsensitive_noMatch() { CqnPredicate result = - NotificationBuilder.evaluateContainment( + NotificationAssembler.evaluateContainment( CqnContainmentTest.Position.ANY, CQL.val("Hello World"), CQL.val("missing"), @@ -236,7 +236,7 @@ void contains_caseInsensitive_noMatch() { @Test void startsWith_caseSensitive_match() { CqnPredicate result = - NotificationBuilder.evaluateContainment( + NotificationAssembler.evaluateContainment( CqnContainmentTest.Position.START, CQL.val("CRITICAL-alert"), CQL.val("CRITICAL"), @@ -248,7 +248,7 @@ void startsWith_caseSensitive_match() { @Test void startsWith_caseSensitive_noMatch() { CqnPredicate result = - NotificationBuilder.evaluateContainment( + NotificationAssembler.evaluateContainment( CqnContainmentTest.Position.START, CQL.val("CRITICAL-alert"), CQL.val("critical"), @@ -262,7 +262,7 @@ void startsWith_caseSensitive_noMatch() { @Test void startsWith_caseInsensitive_match() { CqnPredicate result = - NotificationBuilder.evaluateContainment( + NotificationAssembler.evaluateContainment( CqnContainmentTest.Position.START, CQL.val("CRITICAL-alert"), CQL.val("critical"), @@ -274,7 +274,7 @@ void startsWith_caseInsensitive_match() { @Test void startsWith_caseInsensitive_noMatch() { CqnPredicate result = - NotificationBuilder.evaluateContainment( + NotificationAssembler.evaluateContainment( CqnContainmentTest.Position.START, CQL.val("Hello World"), CQL.val("world"), @@ -288,7 +288,7 @@ void startsWith_caseInsensitive_noMatch() { @Test void endsWith_caseSensitive_match() { CqnPredicate result = - NotificationBuilder.evaluateContainment( + NotificationAssembler.evaluateContainment( CqnContainmentTest.Position.END, CQL.val("server-PROD"), CQL.val("-PROD"), @@ -300,7 +300,7 @@ void endsWith_caseSensitive_match() { @Test void endsWith_caseSensitive_noMatch() { CqnPredicate result = - NotificationBuilder.evaluateContainment( + NotificationAssembler.evaluateContainment( CqnContainmentTest.Position.END, CQL.val("server-PROD"), CQL.val("-prod"), @@ -314,7 +314,7 @@ void endsWith_caseSensitive_noMatch() { @Test void endsWith_caseInsensitive_match() { CqnPredicate result = - NotificationBuilder.evaluateContainment( + NotificationAssembler.evaluateContainment( CqnContainmentTest.Position.END, CQL.val("server-PROD"), CQL.val("-prod"), @@ -326,7 +326,7 @@ void endsWith_caseInsensitive_match() { @Test void endsWith_caseInsensitive_noMatch() { CqnPredicate result = - NotificationBuilder.evaluateContainment( + NotificationAssembler.evaluateContainment( CqnContainmentTest.Position.END, CQL.val("server-PROD"), CQL.val("-staging"), @@ -340,7 +340,7 @@ void endsWith_caseInsensitive_noMatch() { @Test void emptyTerm_alwaysMatches() { CqnPredicate result = - NotificationBuilder.evaluateContainment( + NotificationAssembler.evaluateContainment( CqnContainmentTest.Position.ANY, CQL.val("anything"), CQL.val(""), false, null); assertTrue(isMatch(result)); } @@ -348,7 +348,7 @@ void emptyTerm_alwaysMatches() { @Test void emptyValue_noMatch() { CqnPredicate result = - NotificationBuilder.evaluateContainment( + NotificationAssembler.evaluateContainment( CqnContainmentTest.Position.ANY, CQL.val(""), CQL.val("something"), false, null); assertFalse(isMatch(result)); } @@ -356,7 +356,7 @@ void emptyValue_noMatch() { @Test void bothEmpty_matches() { CqnPredicate result = - NotificationBuilder.evaluateContainment( + NotificationAssembler.evaluateContainment( CqnContainmentTest.Position.ANY, CQL.val(""), CQL.val(""), false, null); assertTrue(isMatch(result)); } From 5e906021deb15d7d913e10ca3ddac0ccf00ea008 Mon Sep 17 00:00:00 2001 From: Buse Halis Date: Tue, 16 Jun 2026 15:40:20 +0200 Subject: [PATCH 17/19] refactor: simplify extractVisibility using getAnnotationValue --- .../NotificationTemplateAssembler.java | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTemplateAssembler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTemplateAssembler.java index 3eac299..9d82ef8 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTemplateAssembler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTemplateAssembler.java @@ -195,23 +195,11 @@ private Email buildEmail(CdsEvent event, Map i18nTexts) { return email; } - /** - * Extract visibility from @notification.customizable annotation. - * Returns "PUBLIC" if @notification.customizable: true, null otherwise (ANS defaults to PRIVATE). - */ private String extractVisibility(CdsEvent event) { - return event - .findAnnotation("notification.customizable") - .map(a -> { - Object val = a.getValue(); - // @notification.customizable: true → template is PUBLIC (visible for customization) - if (Boolean.TRUE.equals(val) || "true".equalsIgnoreCase(String.valueOf(val))) { - return "PUBLIC"; - } - // @notification.customizable: false or any other value → PRIVATE (default) - return null; - }) - .orElse(null); + return Boolean.TRUE.equals( + event.getAnnotationValue("notification.customizable", Boolean.FALSE)) + ? "PUBLIC" + : null; } /** From a874ea2aa28a788a91698955a12cbf59923175b4 Mon Sep 17 00:00:00 2001 From: Buse Halis Date: Tue, 16 Jun 2026 15:41:49 +0200 Subject: [PATCH 18/19] chore: remove redundant remote service definitions from application.yaml --- .../src/main/resources/application.yaml | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/cds-feature-notifications/src/main/resources/application.yaml b/cds-feature-notifications/src/main/resources/application.yaml index 5616d3f..fac6cb3 100644 --- a/cds-feature-notifications/src/main/resources/application.yaml +++ b/cds-feature-notifications/src/main/resources/application.yaml @@ -4,20 +4,3 @@ spring: config: activate: on-profile: default - -cds: - remote: - services: - '[NotificationProviderService]': - destination: - name: NotificationProviderService - type: odata-v2 - '[NotificationTypeProviderService]': - destination: - name: NotificationTypeProviderService - type: odata-v2 - '[NotificationTemplateProviderService]': - destination: - name: NotificationTemplateProviderService - type: odata-v2 - From fc6cdf53340531b7281f6068f1b5bd669726b400 Mon Sep 17 00:00:00 2001 From: Buse Halis Date: Wed, 17 Jun 2026 10:04:04 +0200 Subject: [PATCH 19/19] refactor: replace fully qualified type names with imports in NotificationTemplateAssembler --- .../assemblers/NotificationTemplateAssembler.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTemplateAssembler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTemplateAssembler.java index 9d82ef8..da2ac44 100644 --- a/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTemplateAssembler.java +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTemplateAssembler.java @@ -10,9 +10,11 @@ import com.sap.cds.Struct; import com.sap.cds.notifications.helpers.I18nHelper; import com.sap.cds.reflect.CdsBaseType; +import com.sap.cds.reflect.CdsElementDefinition; import com.sap.cds.reflect.CdsEvent; import com.sap.cds.reflect.CdsModel; import com.sap.cds.reflect.CdsSimpleType; +import com.sap.cds.reflect.CdsType; import com.sap.cds.services.runtime.CdsRuntime; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.*; @@ -268,9 +270,9 @@ private String buildPropertiesSchema(CdsEvent event) { } /** Map CDS element type to JSON Schema type string. */ - private String cdsTypeToJsonSchemaType(com.sap.cds.reflect.CdsElementDefinition element) { + private String cdsTypeToJsonSchemaType(CdsElementDefinition element) { try { - com.sap.cds.reflect.CdsType cdsType = element.getType(); + CdsType cdsType = element.getType(); if (!cdsType.isSimple()) { return "string"; }