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 diff --git a/README.md b/README.md index 1c24fef..8e15216 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) @@ -138,6 +139,7 @@ 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. | @@ -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,27 @@ 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, add `@notification.customizable: true`: + +```cds +@notification : { + customizable: true, + 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. Making this a deliberate opt-in ensures that only templates intended for customization are exposed to customer administrators. + ### Outbox In production mode, the plugin uses the CAP persistent ordered outbox to send notifications. This ensures: diff --git a/cds-feature-notifications/pom.xml b/cds-feature-notifications/pom.xml index b2d7882..1856161 100644 --- a/cds-feature-notifications/pom.xml +++ b/cds-feature-notifications/pom.xml @@ -1,5 +1,4 @@ - - + 4.0.0 @@ -172,7 +171,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 @@ -215,4 +214,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..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 @@ -5,11 +5,15 @@ 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; 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; @@ -72,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); @@ -87,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())); } @@ -129,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() @@ -186,5 +201,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/java/com/sap/cds/notifications/handlers/NotificationBuilder.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationAssembler.java similarity index 95% 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/assemblers/NotificationAssembler.java index 888ce19..fd3a9a2 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/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.handlers; +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; } @@ -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)) { @@ -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); @@ -165,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); } @@ -250,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( @@ -273,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); @@ -297,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, @@ -399,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(); @@ -430,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); @@ -448,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; @@ -463,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/assemblers/NotificationTemplateAssembler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTemplateAssembler.java new file mode 100644 index 0000000..da2ac44 --- /dev/null +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/assemblers/NotificationTemplateAssembler.java @@ -0,0 +1,293 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-notifications contributors. + */ +package com.sap.cds.notifications.assemblers; + +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.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.*; +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 NotificationTemplateAssembler { + + 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 NotificationTemplateAssembler(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.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); + } + + // 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.debug("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.debug("Loaded HTML template from file: {}", htmlValue); + } else { + email.setBodyHtml(htmlValue); + } + } + + if (textValue != null) { + email.setBodyText(textValue); + } + + return email; + } + + private String extractVisibility(CdsEvent event) { + return Boolean.TRUE.equals( + event.getAnnotationValue("notification.customizable", Boolean.FALSE)) + ? "PUBLIC" + : 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) { + Map schema = new LinkedHashMap<>(); + schema.put("type", "object"); + + Map properties = new LinkedHashMap<>(); + List requiredFields = new ArrayList<>(); + + event + .elements() + .forEach( + element -> { + String fieldName = element.getName(); + if (!"recipients".equals(fieldName)) { + String jsonType = cdsTypeToJsonSchemaType(element); + properties.put(fieldName, Map.of("type", jsonType, "title", fieldName)); + requiredFields.add(fieldName); + } + }); + + schema.put("properties", properties); + schema.put("required", requiredFields); + + 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. */ + private String cdsTypeToJsonSchemaType(CdsElementDefinition element) { + try { + 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/assemblers/NotificationTypeAssembler.java similarity index 52% 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/assemblers/NotificationTypeAssembler.java index 87ac357..818f782 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/assemblers/NotificationTypeAssembler.java @@ -1,32 +1,31 @@ /* * © 2026 SAP SE or an SAP affiliate company and cds-feature-notifications contributors. */ -package com.sap.cds.notifications.handlers; +package com.sap.cds.notifications.assemblers; import cds.gen.notificationtypeproviderservice.DeliveryChannels; 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; /** 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 EdmxI18nProvider i18nProvider; + private final I18nHelper i18nHelper; - public NotificationTypeBuilder(CdsRuntime runtime) { + public NotificationTypeAssembler(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()); @@ -87,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); } @@ -97,7 +80,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(), @@ -218,115 +201,4 @@ private List 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/handlers/EntityNotificationHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/EntityNotificationHandler.java index ebab548..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,6 +6,7 @@ import com.sap.cds.CdsData; import com.sap.cds.Result; import com.sap.cds.Struct; +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; @@ -172,7 +173,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, @@ -279,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( @@ -307,7 +308,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, @@ -400,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 47a8f2f..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 @@ -6,6 +6,8 @@ import cds.gen.notificationproviderservice.NotificationProperties; import cds.gen.notificationproviderservice.Notifications; import cds.gen.notificationproviderservice.Recipients; + +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; @@ -20,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 new file mode 100644 index 0000000..58f656e --- /dev/null +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTemplateAutoProvisionerHandler.java @@ -0,0 +1,113 @@ +/* + * © 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.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; +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 NotificationTemplateAssembler notificationTemplateBuilder; + + public LocalNotificationTemplateAutoProvisionerHandler(CdsRuntime runtime) { + this.notificationTemplateBuilder = new NotificationTemplateAssembler(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) { + 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()) { + logger.info( + """ + - Language: {} + Syntax: {} + Title: {} + Preview: {} + Body: {} + Description: {}""", + translation.getLanguage(), + translation.getSyntax(), + translation.getTitle(), + translation.getPreview(), + translation.getBody(), + translation.getDescription()); + + if (translation.getEmail() != null) { + 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()); + } + } + } + + logger.info("==============================================================="); + 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/LocalNotificationTypeAutoProvisionerHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/LocalNotificationTypeAutoProvisionerHandler.java index b22d4ae..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 @@ -6,6 +6,8 @@ import cds.gen.notificationtypeproviderservice.DeliveryChannels; import cds.gen.notificationtypeproviderservice.NotificationTypes; import cds.gen.notificationtypeproviderservice.Templates; + +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; @@ -21,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 new file mode 100644 index 0000000..4ddc142 --- /dev/null +++ b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTemplateAutoProvisionerHandler.java @@ -0,0 +1,168 @@ +/* + * © 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.notifications.assemblers.NotificationTemplateAssembler; +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 NotificationTemplateAssembler notificationTemplateBuilder; + private final NotificationTemplateProviderService notificationTemplateProviderService; + + public NotificationTemplateAutoProvisionerHandler( + CdsRuntime runtime, + NotificationTemplateProviderService notificationTemplateProviderService) { + this.notificationTemplateBuilder = new NotificationTemplateAssembler(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.debug("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.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.debug( + "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.debug("Updating standalone template '{}'", template.getKey()); + + try { + notificationTemplateProviderService.run( + Update.entity(NotificationTemplates_.CDS_NAME).data(template)); + + logger.debug( + "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/NotificationTypeAutoProvisionerHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/NotificationTypeAutoProvisionerHandler.java index f672748..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 @@ -6,6 +6,8 @@ import cds.gen.notificationtypeproviderservice.NotificationTypeProviderService; import cds.gen.notificationtypeproviderservice.NotificationTypes; import cds.gen.notificationtypeproviderservice.NotificationTypes_; + +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; @@ -25,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; } @@ -55,7 +57,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 +101,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 +110,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 +162,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 +171,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/ProductionHandler.java b/cds-feature-notifications/src/main/java/com/sap/cds/notifications/handlers/ProductionHandler.java index e75c6e3..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 @@ -6,6 +6,8 @@ import cds.gen.notificationproviderservice.NotificationProviderService; import cds.gen.notificationproviderservice.Notifications; import cds.gen.notificationproviderservice.Notifications_; + +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; @@ -22,24 +24,24 @@ 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; } 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 +68,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/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; + } +} diff --git a/cds-feature-notifications/src/main/resources/application.yaml b/cds-feature-notifications/src/main/resources/application.yaml index 7ed0f30..fac6cb3 100644 --- a/cds-feature-notifications/src/main/resources/application.yaml +++ b/cds-feature-notifications/src/main/resources/application.yaml @@ -4,14 +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 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/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 84% 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..6faa89d 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.assemblers.NotificationAssembler; import com.sap.cds.ql.CQL; import com.sap.cds.ql.cqn.CqnComparisonPredicate; import com.sap.cds.ql.cqn.CqnContainmentTest; @@ -18,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 { @@ -31,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 @@ -55,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)); } } @@ -81,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 @@ -98,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)); } } @@ -116,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"); @@ -125,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"); @@ -137,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"); @@ -146,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)); } } @@ -182,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"), @@ -194,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"), @@ -208,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"), @@ -220,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"), @@ -234,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"), @@ -246,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"), @@ -260,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"), @@ -272,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"), @@ -286,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"), @@ -298,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"), @@ -312,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"), @@ -324,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"), @@ -338,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)); } @@ -346,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)); } @@ -354,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)); } 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..89bac1e --- /dev/null +++ b/cds-feature-notifications/src/test/java/com/sap/cds/notifications/helpers/I18nHelperTest.java @@ -0,0 +1,121 @@ +/* + * © 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 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; +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())); + } + } + + @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()); + } + } +} 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/notificationtypes-data.cds b/sample-app/srv/notificationtypes-data.cds index c5bb3f0..09eeb88 100644 --- a/sample-app/srv/notificationtypes-data.cds +++ b/sample-app/srv/notificationtypes-data.cds @@ -139,6 +139,7 @@ service NotificationService { @description : '{i18n>DESCRIPTION}' @notification : { + customizable: true, template: { title : '{i18n>TEMPLATE_SENSITIVE}', publicTitle : '{i18n>TEMPLATE_PUBLIC}', diff --git a/sample-app/srv/pom.xml b/sample-app/srv/pom.xml index e889460..24e3ef1 100644 --- a/sample-app/srv/pom.xml +++ b/sample-app/srv/pom.xml @@ -188,6 +188,8 @@ NotificationProviderService NotificationTypeProviderService.** NotificationTypeProviderService + NotificationTemplateProviderService.** + NotificationTemplateProviderService 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 new file mode 100644 index 0000000..15e640c --- /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.debug( + "MockHandler intercepting NotificationTemplates CREATE - {} entries", + context.getCqn().entries().size()); + + List> resultEntries = new ArrayList<>(); + + context + .getCqn() + .entries() + .forEach( + entry -> { + logger.debug("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.debug( + "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.debug("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.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.debug("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.debug("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.debug("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/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/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/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 new file mode 100644 index 0000000..e806030 --- /dev/null +++ b/sample-app/srv/src/test/java/customer/sample_app/integration/NotificationTemplateProvisioningTest.java @@ -0,0 +1,548 @@ +/* + * © 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.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; +import java.util.regex.Pattern; +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; + +/** + * 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>[^}]+\\}"); + + @Autowired private CdsRuntime cdsRuntime; + + @Autowired private NotificationTemplateProviderService notificationTemplateProviderService; + + private NotificationTemplateAutoProvisionerHandler createProvisioner() { + return new NotificationTemplateAutoProvisionerHandler( + cdsRuntime, notificationTemplateProviderService); + } + + // ────────────────────────────────────────────────────────────── + // Test 1: Templates are provisioned at startup + // ────────────────────────────────────────────────────────────── + + @Test + void testTemplatesProvisionedAtStartup() { + 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.debug("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.debug("=========================================="); + LOG.debug("Test: CertificateExpiration template structure"); + LOG.debug("=========================================="); + + 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.debug("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.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 = + 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.debug("SystemMaintenance visibility: {} (null = ANS default PRIVATE)", template.getVisibility()); + } + + // ────────────────────────────────────────────────────────────── + // Test 4: i18n resolved in translations + // ────────────────────────────────────────────────────────────── + + @Test + void testI18nResolvedInTranslations() { + LOG.debug("=========================================="); + LOG.debug("Test: i18n placeholders should be resolved in template translations"); + LOG.debug("=========================================="); + + 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.debug("[{}] Title={}, Body={}, Preview={}", lang, t.getTitle(), t.getBody(), t.getPreview()); + } + } + + // ────────────────────────────────────────────────────────────── + // Test 5: Multi-language translations for i18n-based template + // ────────────────────────────────────────────────────────────── + + @Test + void testMultiLanguageTranslations() { + LOG.debug("=========================================="); + LOG.debug("Test: CertificateExpiration should have translations for all i18n languages"); + LOG.debug("=========================================="); + + 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.debug("Translations verified for {} languages", translations.size()); + } + + // ────────────────────────────────────────────────────────────── + // Test 6: Static template (no i18n) has identical translations for all locales + // ────────────────────────────────────────────────────────────── + + @Test + void testStaticTemplateTranslation() { + 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 + // 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.debug("Static template: {} translations, all with title: {}", translations.size(), expectedTitle); + } + + // ────────────────────────────────────────────────────────────── + // Test 7: Email HTML loaded from file + // ────────────────────────────────────────────────────────────── + + @Test + void testEmailHtmlLoadedFromFile() { + LOG.debug("=========================================="); + LOG.debug("Test: CertificateExpiration email HTML should be loaded from file"); + LOG.debug("=========================================="); + + 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.debug("Email HTML loaded successfully ({} chars)", html.length()); + } + + // ────────────────────────────────────────────────────────────── + // Test 8: Tags contain source and event + // ────────────────────────────────────────────────────────────── + + @Test + void testTagsContainSourceAndEvent() { + LOG.debug("=========================================="); + LOG.debug("Test: Template tags should contain source and event information"); + LOG.debug("=========================================="); + + 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.debug("Tags: {}", tags); + } + + // ────────────────────────────────────────────────────────────── + // Test 9: Translation metadata (source, event, displayName) + // ────────────────────────────────────────────────────────────── + + @Test + void testTranslationMetadata() { + LOG.debug("=========================================="); + LOG.debug("Test: Translations should have source, event, and displayName set"); + LOG.debug("=========================================="); + + 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.debug("Source={}, Event={}, DisplayName={}", en.getSource(), en.getEvent(), en.getDisplayName()); + } + + // ────────────────────────────────────────────────────────────── + // Test 10: PropertiesSchema reflects event elements + // ────────────────────────────────────────────────────────────── + + @Test + void testPropertiesSchemaReflectsEventElements() { + LOG.debug("=========================================="); + LOG.debug("Test: PropertiesSchema should be a JSON schema of event elements"); + LOG.debug("=========================================="); + + 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.debug("PropertiesSchema: {}", schema); + } + + // ────────────────────────────────────────────────────────────── + // Test 11: i18n placeholders resolved for all languages (exact values) + // ────────────────────────────────────────────────────────────── + + @Test + void testI18nPlaceholdersResolvedForAllLanguages() { + LOG.debug("=========================================="); + LOG.debug("Test: i18n placeholders should be resolved for EN, DE, TR, ES"); + LOG.debug("=========================================="); + + 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.debug("All 4 languages verified with exact i18n values"); + } + + // ────────────────────────────────────────────────────────────── + // Test 12: Email HTML contains Mustache variables (runtime) + // ────────────────────────────────────────────────────────────── + + @Test + void testEmailHtmlContainsMustacheVariables() { + LOG.debug("=========================================="); + LOG.debug("Test: Email HTML should contain Mustache variables for ANS runtime"); + LOG.debug("=========================================="); + + 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.debug("All {} Mustache variables present in email HTML", expectedVariables.size()); + } + + // ────────────────────────────────────────────────────────────── + // Test 13: Email HTML i18n resolved per language + // ────────────────────────────────────────────────────────────── + + @Test + void testEmailHtmlI18nResolvedPerLanguage() { + LOG.debug("=========================================="); + LOG.debug("Test: Email HTML i18n values should differ per language"); + LOG.debug("=========================================="); + + 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.debug("[{}] Button: '{}', Greeting: '{}'", lang, expectedButton, expectedGreeting); + } + + LOG.debug("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); + } + } + + // ────────────────────────────────────────────────────────────── + // 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"); + } +} 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"); } } 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