From 87504332f2fdd36372c891d10b21cf9025ff6a0d Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 25 Mar 2026 16:32:27 +0100 Subject: [PATCH 01/39] add single bucket mt --- .../attachments/oss/client/AWSClient.java | 41 +++ .../attachments/oss/client/AzureClient.java | 19 + .../attachments/oss/client/GoogleClient.java | 26 ++ .../attachments/oss/client/OSClient.java | 6 + .../oss/configuration/Registration.java | 33 +- .../handler/OSSAttachmentsServiceHandler.java | 54 ++- .../oss/handler/TenantCleanupHandler.java | 39 ++ .../attachments/oss/client/AWSClientTest.java | 102 +++++- .../oss/client/AzureClientTest.java | 61 ++++ .../oss/client/GoogleClientTest.java | 98 +++++ .../attachments/oss/client/OSClientTest.java | 41 +++ .../oss/configuration/RegistrationTest.java | 102 ++++++ .../oss/handler/MultiTenantIsolationTest.java | 234 ++++++++++++ .../OSSAttachmentsServiceHandlerTest.java | 343 +++++++++++++++++- ...OSSAttachmentsServiceHandlerTestUtils.java | 3 +- .../oss/handler/TenantCleanupHandlerTest.java | 82 +++++ 16 files changed, 1268 insertions(+), 16 deletions(-) create mode 100644 storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java create mode 100644 storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java create mode 100644 storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java create mode 100644 storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java index 04084d9f3..831818ccd 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java @@ -6,6 +6,7 @@ import com.sap.cds.feature.attachments.oss.handler.ObjectStoreServiceException; import com.sap.cloud.environment.servicebinding.api.ServiceBinding; import java.io.InputStream; +import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; @@ -18,9 +19,14 @@ import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.Delete; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; +import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; +import software.amazon.awssdk.services.s3.model.ObjectIdentifier; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; @@ -129,4 +135,39 @@ public Future readContent(String completeFileName) { } }); } + + @Override + public Future deleteContentByPrefix(String prefix) { + return executor.submit( + () -> { + try { + ListObjectsV2Request listReq = + ListObjectsV2Request.builder().bucket(this.bucketName).prefix(prefix).build(); + ListObjectsV2Response listResp; + do { + listResp = s3Client.listObjectsV2(listReq); + if (!listResp.contents().isEmpty()) { + List keys = + listResp.contents().stream() + .map(obj -> ObjectIdentifier.builder().key(obj.key()).build()) + .toList(); + DeleteObjectsRequest deleteReq = + DeleteObjectsRequest.builder() + .bucket(this.bucketName) + .delete(Delete.builder().objects(keys).build()) + .build(); + s3Client.deleteObjects(deleteReq); + } + listReq = + listReq.toBuilder() + .continuationToken(listResp.nextContinuationToken()) + .build(); + } while (listResp.isTruncated()); + } catch (RuntimeException e) { + throw new ObjectStoreServiceException( + "Failed to delete objects by prefix from the AWS Object Store", e); + } + return null; + }); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java index 62c0f6fb9..b7ad983f9 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java @@ -6,6 +6,8 @@ import com.azure.storage.blob.BlobClient; import com.azure.storage.blob.BlobContainerClient; import com.azure.storage.blob.BlobContainerClientBuilder; +import com.azure.storage.blob.models.BlobItem; +import com.azure.storage.blob.models.ListBlobsOptions; import com.azure.storage.blob.specialized.BlobOutputStream; import com.azure.storage.blob.specialized.BlockBlobClient; import com.sap.cds.feature.attachments.oss.handler.ObjectStoreServiceException; @@ -86,4 +88,21 @@ public Future readContent(String completeFileName) { } }); } + + @Override + public Future deleteContentByPrefix(String prefix) { + return executor.submit( + () -> { + try { + ListBlobsOptions options = new ListBlobsOptions().setPrefix(prefix); + for (BlobItem blobItem : blobContainerClient.listBlobs(options, null)) { + blobContainerClient.getBlobClient(blobItem.getName()).delete(); + } + } catch (RuntimeException e) { + throw new ObjectStoreServiceException( + "Failed to delete objects by prefix from the Azure Object Store", e); + } + return null; + }); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java index c502898ee..ec3a0a5aa 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java @@ -134,4 +134,30 @@ public Future readContent(String completeFileName) { } }); } + + @Override + public Future deleteContentByPrefix(String prefix) { + return executor.submit( + () -> { + try { + Page blobs = + storage.list(bucketName, Storage.BlobListOption.prefix(prefix)); + for (Blob blob : blobs.iterateAll()) { + Page versions = + storage.list( + bucketName, + Storage.BlobListOption.versions(true), + Storage.BlobListOption.prefix(blob.getName())); + for (Blob version : versions.iterateAll()) { + storage.delete( + BlobId.of(bucketName, version.getName(), version.getGeneration())); + } + } + } catch (RuntimeException e) { + throw new ObjectStoreServiceException( + "Failed to delete objects by prefix from Google Object Store", e); + } + return null; + }); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClient.java index a690dd7bb..e89e5ca98 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClient.java @@ -4,6 +4,7 @@ package com.sap.cds.feature.attachments.oss.client; import java.io.InputStream; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; /** The {@link OSClient} is the connection to the object store service. */ @@ -14,4 +15,9 @@ public interface OSClient { Future deleteContent(String completeFileName); Future readContent(String completeFileName); + + default Future deleteContentByPrefix(String prefix) { + return CompletableFuture.failedFuture( + new UnsupportedOperationException("deleteContentByPrefix not supported by this client")); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java index 6fdbfcfff..5a3fff178 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java @@ -4,6 +4,7 @@ package com.sap.cds.feature.attachments.oss.configuration; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandler; +import com.sap.cds.feature.attachments.oss.handler.TenantCleanupHandler; import com.sap.cds.services.environment.CdsEnvironment; import com.sap.cds.services.runtime.CdsRuntimeConfiguration; import com.sap.cds.services.runtime.CdsRuntimeConfigurer; @@ -14,18 +15,31 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** The class registers the event handlers for the attachments feature based on filesystem. */ +/** The class registers the event handlers for the attachments feature based on object store. */ public class Registration implements CdsRuntimeConfiguration { private static final Logger logger = LoggerFactory.getLogger(Registration.class); @Override public void eventHandlers(CdsRuntimeConfigurer configurer) { - Optional bindingOpt = getOSBinding(configurer.getCdsRuntime().getEnvironment()); + CdsEnvironment env = configurer.getCdsRuntime().getEnvironment(); + Optional bindingOpt = getOSBinding(env); if (bindingOpt.isPresent()) { + boolean multitenancyEnabled = isMultitenancyEnabled(env); + String objectStoreKind = getObjectStoreKind(env); + ExecutorService executor = Executors.newCachedThreadPool(); - // Thread count could be made configurable via CdsProperties if needed in the future. - configurer.eventHandler(new OSSAttachmentsServiceHandler(bindingOpt.get(), executor)); - logger.info("Registered OSS Attachments Service Handler."); + OSSAttachmentsServiceHandler handler = + new OSSAttachmentsServiceHandler( + bindingOpt.get(), executor, multitenancyEnabled, objectStoreKind); + configurer.eventHandler(handler); + + if (multitenancyEnabled && "shared".equals(objectStoreKind)) { + configurer.eventHandler(new TenantCleanupHandler(handler.getOsClient())); + logger.info( + "Registered OSS Attachments Service Handler with shared multitenancy mode and tenant cleanup."); + } else { + logger.info("Registered OSS Attachments Service Handler."); + } } else { logger.warn( "No service binding to Object Store Service found, hence the OSS Attachments Service Handler is not connected!"); @@ -46,4 +60,13 @@ private static Optional getOSBinding(CdsEnvironment environment) .filter(b -> b.getServiceName().map(name -> name.equals("objectstore")).orElse(false)) .findFirst(); } + + private static boolean isMultitenancyEnabled(CdsEnvironment env) { + return Boolean.TRUE.equals( + env.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)); + } + + private static String getObjectStoreKind(CdsEnvironment env) { + return env.getProperty("cds.attachments.objectStore.kind", String.class, null); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java index 9a44d9c8a..09f7b8e31 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java @@ -15,6 +15,7 @@ import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; +import com.sap.cds.services.EventContext; import com.sap.cds.services.ServiceException; import com.sap.cds.services.handler.EventHandler; import com.sap.cds.services.handler.annotations.On; @@ -39,6 +40,8 @@ public class OSSAttachmentsServiceHandler implements EventHandler { private static final Logger logger = LoggerFactory.getLogger(OSSAttachmentsServiceHandler.class); private final OSClient osClient; + private final boolean multitenancyEnabled; + private final String objectStoreKind; /** * Creates a new OSSAttachmentsServiceHandler using the provided {@link ServiceBinding}. @@ -55,9 +58,18 @@ public class OSSAttachmentsServiceHandler implements EventHandler { * * * @param binding the {@link ServiceBinding} containing credentials for the object store service + * @param executor the {@link ExecutorService} for async operations + * @param multitenancyEnabled whether multitenancy is enabled + * @param objectStoreKind the object store kind (e.g. "shared") * @throws ObjectStoreServiceException if no valid object store service binding is found */ - public OSSAttachmentsServiceHandler(ServiceBinding binding, ExecutorService executor) { + public OSSAttachmentsServiceHandler( + ServiceBinding binding, + ExecutorService executor, + boolean multitenancyEnabled, + String objectStoreKind) { + this.multitenancyEnabled = multitenancyEnabled; + this.objectStoreKind = objectStoreKind; final String host = (String) binding.getCredentials().get("host"); // AWS final String containerUri = (String) binding.getCredentials().get("container_uri"); // Azure final String base64EncodedPrivateKeyData = @@ -106,9 +118,10 @@ void createAttachment(AttachmentCreateEventContext context) { String contentId = (String) context.getAttachmentIds().get(Attachments.ID); MediaData data = context.getData(); String fileName = data.getFileName(); + String objectKey = buildObjectKey(context, contentId); try { - osClient.uploadContent(data.getContent(), contentId, data.getMimeType()).get(); + osClient.uploadContent(data.getContent(), objectKey, data.getMimeType()).get(); logger.info("Uploaded file {}", fileName); context.getData().setStatus(StatusCode.SCANNING); context.setIsInternalStored(false); @@ -130,7 +143,8 @@ void markAttachmentAsDeleted(AttachmentMarkAsDeletedEventContext context) { context.getContentId()); try { - osClient.deleteContent(context.getContentId()).get(); + String objectKey = buildObjectKey(context, context.getContentId()); + osClient.deleteContent(objectKey).get(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); throw new ServiceException( @@ -159,7 +173,8 @@ void readAttachment(AttachmentReadEventContext context) { "OS Attachment Service handler called for reading attachment with document id: {}", context.getContentId()); try { - Future future = osClient.readContent(context.getContentId()); + String objectKey = buildObjectKey(context, context.getContentId()); + Future future = osClient.readContent(objectKey); InputStream inputStream = future.get(); // Wait for the content to be read if (inputStream != null) { context.getData().setContent(inputStream); @@ -179,4 +194,35 @@ void readAttachment(AttachmentReadEventContext context) { context.setCompleted(); } } + + public OSClient getOsClient() { + return osClient; + } + + private String buildObjectKey(EventContext context, String contentId) { + if (multitenancyEnabled && "shared".equals(objectStoreKind)) { + String tenant = getTenant(context); + validateTenantId(tenant); + return tenant + "/" + contentId; + } + return contentId; + } + + private String getTenant(EventContext context) { + String tenant = context.getUserInfo().getTenant(); + if (tenant == null && multitenancyEnabled) { + throw new ServiceException("Tenant ID is required for multitenant attachment operations"); + } + return tenant != null ? tenant : "default"; + } + + private static void validateTenantId(String tenantId) { + if (tenantId.isEmpty() + || tenantId.contains("/") + || tenantId.contains("\\") + || tenantId.contains("..")) { + throw new ServiceException( + "Invalid tenant ID for attachment storage: must not be empty or contain path separators"); + } + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java new file mode 100644 index 000000000..6b305cebd --- /dev/null +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java @@ -0,0 +1,39 @@ +/* + * © 2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.oss.handler; + +import com.sap.cds.feature.attachments.oss.client.OSClient; +import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.handler.annotations.After; +import com.sap.cds.services.handler.annotations.ServiceName; +import com.sap.cds.services.mt.DeploymentService; +import com.sap.cds.services.mt.UnsubscribeEventContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ServiceName(DeploymentService.DEFAULT_NAME) +public class TenantCleanupHandler implements EventHandler { + + private static final Logger logger = LoggerFactory.getLogger(TenantCleanupHandler.class); + private final OSClient osClient; + + public TenantCleanupHandler(OSClient osClient) { + this.osClient = osClient; + } + + @After(event = DeploymentService.EVENT_UNSUBSCRIBE) + void cleanupTenantData(UnsubscribeEventContext context) { + String tenantId = context.getTenant(); + String prefix = tenantId + "/"; + try { + osClient.deleteContentByPrefix(prefix).get(); + logger.info("Cleaned up all objects for tenant {} from shared object store", tenantId); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.error("Interrupted while cleaning up objects for tenant {}", tenantId, e); + } catch (Exception e) { + logger.error("Failed to clean up objects for tenant {}", tenantId, e); + } + } +} diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java index 032c1a8a2..7fc91a811 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java @@ -9,6 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandler; @@ -30,10 +31,15 @@ import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; +import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.S3Object; class AWSClientTest { ExecutorService executor = Executors.newCachedThreadPool(); @@ -42,7 +48,7 @@ class AWSClientTest { void testConstructorWithAwsBindingUsesAwsClient() throws NoSuchFieldException, IllegalAccessException { OSSAttachmentsServiceHandler handler = - new OSSAttachmentsServiceHandler(getDummyBinding(), executor); + new OSSAttachmentsServiceHandler(getDummyBinding(), executor, false, null); OSClient client = OSSAttachmentsServiceHandlerTestUtils.getOsClient(handler); assertInstanceOf(AWSClient.class, client); } @@ -225,6 +231,100 @@ void testDeleteContentThrowsOnUnsuccessfulResponse() assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } + @Test + void testDeleteContentByPrefix_DeletesAllMatchingObjects() throws Exception { + AWSClient awsClient = new AWSClient(getDummyBinding(), executor); + + S3Client mockS3Client = mock(S3Client.class); + + // Create mock list response with 2 objects matching the prefix + S3Object obj1 = S3Object.builder().key("tenantA/file1.txt").build(); + S3Object obj2 = S3Object.builder().key("tenantA/file2.txt").build(); + ListObjectsV2Response listResponse = + ListObjectsV2Response.builder() + .contents(obj1, obj2) + .isTruncated(false) + .build(); + + when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))).thenReturn(listResponse); + DeleteObjectsResponse deleteResponse = mock(DeleteObjectsResponse.class); + when(mockS3Client.deleteObjects(any(DeleteObjectsRequest.class))).thenReturn(deleteResponse); + + var field = AWSClient.class.getDeclaredField("s3Client"); + field.setAccessible(true); + field.set(awsClient, mockS3Client); + + assertDoesNotThrow(() -> awsClient.deleteContentByPrefix("tenantA/").get()); + verify(mockS3Client).listObjectsV2(any(ListObjectsV2Request.class)); + verify(mockS3Client).deleteObjects(any(DeleteObjectsRequest.class)); + } + + @Test + void testDeleteContentByPrefix_HandlesPagination() throws Exception { + AWSClient awsClient = new AWSClient(getDummyBinding(), executor); + + S3Client mockS3Client = mock(S3Client.class); + + // First page: truncated, has a continuation token + S3Object obj1 = S3Object.builder().key("tenantB/file1.txt").build(); + ListObjectsV2Response firstPage = + ListObjectsV2Response.builder() + .contents(obj1) + .isTruncated(true) + .nextContinuationToken("token-page2") + .build(); + + // Second page: not truncated, final page + S3Object obj2 = S3Object.builder().key("tenantB/file2.txt").build(); + ListObjectsV2Response secondPage = + ListObjectsV2Response.builder() + .contents(obj2) + .isTruncated(false) + .build(); + + when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))) + .thenReturn(firstPage, secondPage); + DeleteObjectsResponse deleteResponse = mock(DeleteObjectsResponse.class); + when(mockS3Client.deleteObjects(any(DeleteObjectsRequest.class))).thenReturn(deleteResponse); + + var field = AWSClient.class.getDeclaredField("s3Client"); + field.setAccessible(true); + field.set(awsClient, mockS3Client); + + assertDoesNotThrow(() -> awsClient.deleteContentByPrefix("tenantB/").get()); + // Should call listObjectsV2 twice (two pages) and deleteObjects twice + verify(mockS3Client, org.mockito.Mockito.times(2)) + .listObjectsV2(any(ListObjectsV2Request.class)); + verify(mockS3Client, org.mockito.Mockito.times(2)) + .deleteObjects(any(DeleteObjectsRequest.class)); + } + + @Test + void testDeleteContentByPrefix_EmptyPrefix_NoObjects() throws Exception { + AWSClient awsClient = new AWSClient(getDummyBinding(), executor); + + S3Client mockS3Client = mock(S3Client.class); + + // Empty listing + ListObjectsV2Response emptyResponse = + ListObjectsV2Response.builder() + .contents(java.util.Collections.emptyList()) + .isTruncated(false) + .build(); + + when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))).thenReturn(emptyResponse); + + var field = AWSClient.class.getDeclaredField("s3Client"); + field.setAccessible(true); + field.set(awsClient, mockS3Client); + + assertDoesNotThrow(() -> awsClient.deleteContentByPrefix("nonexistent/").get()); + verify(mockS3Client).listObjectsV2(any(ListObjectsV2Request.class)); + // No deleteObjects call when there are no objects + verify(mockS3Client, org.mockito.Mockito.never()) + .deleteObjects(any(DeleteObjectsRequest.class)); + } + private ServiceBinding getDummyBinding() { ServiceBinding binding = mock(ServiceBinding.class); HashMap creds = new HashMap<>(); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java index 9b7100468..59b42bfaf 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java @@ -7,16 +7,22 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import com.azure.core.http.rest.PagedIterable; import com.azure.storage.blob.BlobClient; import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.models.BlobItem; +import com.azure.storage.blob.models.ListBlobsOptions; import com.azure.storage.blob.specialized.BlobOutputStream; import com.azure.storage.blob.specialized.BlockBlobClient; import com.sap.cds.feature.attachments.oss.handler.ObjectStoreServiceException; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; +import java.util.Collections; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; class AzureClientTest { @@ -189,4 +195,59 @@ void testReadContentThrowsOnRuntimeException() assertThrows(ExecutionException.class, () -> azureClient.readContent("file.txt").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } + + @Test + @SuppressWarnings("unchecked") + void testDeleteContentByPrefix_DeletesAllMatchingBlobs() + throws NoSuchFieldException, IllegalAccessException { + AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); + + BlobContainerClient mockContainer = mock(BlobContainerClient.class); + BlobClient mockBlobClient = mock(BlobClient.class); + + var field = AzureClient.class.getDeclaredField("blobContainerClient"); + field.setAccessible(true); + field.set(azureClient, mockContainer); + var executorField = AzureClient.class.getDeclaredField("executor"); + executorField.setAccessible(true); + executorField.set(azureClient, executor); + + // Create mock blob items matching the prefix + BlobItem blob1 = mock(BlobItem.class); + when(blob1.getName()).thenReturn("tenantA/file1.txt"); + BlobItem blob2 = mock(BlobItem.class); + when(blob2.getName()).thenReturn("tenantA/file2.txt"); + + PagedIterable mockPagedIterable = mock(PagedIterable.class); + when(mockPagedIterable.stream()).thenReturn(Stream.of(blob1, blob2)); + when(mockPagedIterable.iterator()).thenReturn(Arrays.asList(blob1, blob2).iterator()); + when(mockContainer.listBlobs(any(ListBlobsOptions.class), any())).thenReturn(mockPagedIterable); + when(mockContainer.getBlobClient(anyString())).thenReturn(mockBlobClient); + + assertDoesNotThrow(() -> azureClient.deleteContentByPrefix("tenantA/").get()); + } + + @Test + @SuppressWarnings("unchecked") + void testDeleteContentByPrefix_EmptyResult() + throws NoSuchFieldException, IllegalAccessException { + AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); + + BlobContainerClient mockContainer = mock(BlobContainerClient.class); + + var field = AzureClient.class.getDeclaredField("blobContainerClient"); + field.setAccessible(true); + field.set(azureClient, mockContainer); + var executorField = AzureClient.class.getDeclaredField("executor"); + executorField.setAccessible(true); + executorField.set(azureClient, executor); + + // Empty listing + PagedIterable emptyIterable = mock(PagedIterable.class); + when(emptyIterable.stream()).thenReturn(Stream.empty()); + when(emptyIterable.iterator()).thenReturn(Collections.emptyIterator()); + when(mockContainer.listBlobs(any(ListBlobsOptions.class), any())).thenReturn(emptyIterable); + + assertDoesNotThrow(() -> azureClient.deleteContentByPrefix("nonexistent-tenant/").get()); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java index f4c2fd188..5da93a59e 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java @@ -272,4 +272,102 @@ void testReadContentThrowsOnRuntimeException() assertThrows(ExecutionException.class, () -> googleClient.readContent("file.txt").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } + + @Test + @SuppressWarnings("unchecked") + void testDeleteContentByPrefix_DeletesAllMatchingBlobs() + throws NoSuchFieldException, IllegalAccessException { + GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); + + Storage mockStorage = mock(Storage.class); + + var field = GoogleClient.class.getDeclaredField("storage"); + field.setAccessible(true); + field.set(googleClient, mockStorage); + var executorField = GoogleClient.class.getDeclaredField("executor"); + executorField.setAccessible(true); + executorField.set(googleClient, executor); + var bucketField = GoogleClient.class.getDeclaredField("bucketName"); + bucketField.setAccessible(true); + bucketField.set(googleClient, "my-bucket"); + + // Mock listing of blobs by prefix + Blob blobA = mock(Blob.class); + when(blobA.getName()).thenReturn("tenantX/file1.txt"); + when(blobA.getGeneration()).thenReturn(1L); + Blob blobB = mock(Blob.class); + when(blobB.getName()).thenReturn("tenantX/file2.txt"); + when(blobB.getGeneration()).thenReturn(2L); + + // First call: list by prefix (without versions) returns the blobs + Page prefixPage = mock(Page.class); + when(prefixPage.iterateAll()).thenReturn(() -> java.util.Arrays.asList(blobA, blobB).iterator()); + + // For each blob, list versions returns the same blob (single version each) + Page versionPageA = mock(Page.class); + when(versionPageA.iterateAll()).thenReturn(() -> Collections.singletonList(blobA).iterator()); + Page versionPageB = mock(Page.class); + when(versionPageB.iterateAll()).thenReturn(() -> Collections.singletonList(blobB).iterator()); + + // First call: prefix listing; subsequent calls: version listings + when(mockStorage.list(anyString(), any())) + .thenReturn(prefixPage); + when(mockStorage.list(anyString(), any(), any())) + .thenReturn(versionPageA, versionPageB); + + when(mockStorage.delete(any(BlobId.class))).thenReturn(true); + + assertDoesNotThrow(() -> googleClient.deleteContentByPrefix("tenantX/").get()); + } + + @Test + @SuppressWarnings("unchecked") + void testDeleteContentByPrefix_HandlesVersioning() + throws NoSuchFieldException, IllegalAccessException { + GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); + + Storage mockStorage = mock(Storage.class); + + var field = GoogleClient.class.getDeclaredField("storage"); + field.setAccessible(true); + field.set(googleClient, mockStorage); + var executorField = GoogleClient.class.getDeclaredField("executor"); + executorField.setAccessible(true); + executorField.set(googleClient, executor); + var bucketField = GoogleClient.class.getDeclaredField("bucketName"); + bucketField.setAccessible(true); + bucketField.set(googleClient, "my-bucket"); + + // Blob with 2 versions + Blob blob = mock(Blob.class); + when(blob.getName()).thenReturn("tenantY/versioned-file.txt"); + when(blob.getGeneration()).thenReturn(1L); + + Blob version1 = mock(Blob.class); + when(version1.getName()).thenReturn("tenantY/versioned-file.txt"); + when(version1.getGeneration()).thenReturn(1L); + + Blob version2 = mock(Blob.class); + when(version2.getName()).thenReturn("tenantY/versioned-file.txt"); + when(version2.getGeneration()).thenReturn(2L); + + Page prefixPage = mock(Page.class); + when(prefixPage.iterateAll()).thenReturn(() -> Collections.singletonList(blob).iterator()); + + Page versionPage = mock(Page.class); + when(versionPage.iterateAll()) + .thenReturn(() -> java.util.Arrays.asList(version1, version2).iterator()); + + when(mockStorage.list(anyString(), any())) + .thenReturn(prefixPage); + when(mockStorage.list(anyString(), any(), any())) + .thenReturn(versionPage); + when(mockStorage.delete(any(BlobId.class))).thenReturn(true); + + assertDoesNotThrow(() -> googleClient.deleteContentByPrefix("tenantY/").get()); + + // Should delete both versions + org.mockito.Mockito.verify(mockStorage, org.mockito.Mockito.times(2)) + .delete(any(BlobId.class)); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java new file mode 100644 index 000000000..f1861534b --- /dev/null +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java @@ -0,0 +1,41 @@ +/* + * © 2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.oss.client; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.Test; + +class OSClientTest { + + @Test + void testDeleteContentByPrefix_DefaultThrowsUnsupported() { + // Anonymous implementation that only has the default methods + OSClient client = + new OSClient() { + @Override + public java.util.concurrent.Future uploadContent( + java.io.InputStream content, String completeFileName, String contentType) { + return null; + } + + @Override + public java.util.concurrent.Future deleteContent(String completeFileName) { + return null; + } + + @Override + public java.util.concurrent.Future readContent( + String completeFileName) { + return null; + } + }; + + ExecutionException ex = + assertThrows(ExecutionException.class, () -> client.deleteContentByPrefix("prefix").get()); + assertInstanceOf(UnsupportedOperationException.class, ex.getCause()); + } +} diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java index a364aa7d7..5babe024d 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java @@ -7,6 +7,7 @@ import static org.mockito.Mockito.*; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandler; +import com.sap.cds.feature.attachments.oss.handler.TenantCleanupHandler; import com.sap.cds.services.environment.CdsEnvironment; import com.sap.cds.services.runtime.CdsRuntime; import com.sap.cds.services.runtime.CdsRuntimeConfigurer; @@ -48,4 +49,105 @@ void testEventHandlersRegistersOSSHandler() { // Assert: OSSAttachmentsServiceHandler should be registered verify(configurer).eventHandler(any(OSSAttachmentsServiceHandler.class)); } + + @Test + void testRegistration_SharedMode_RegistersCleanupHandler() { + Registration registration = new Registration(); + CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); + CdsRuntime cdsRuntime = mock(CdsRuntime.class); + CdsEnvironment environment = mock(CdsEnvironment.class); + ServiceBinding binding = mock(ServiceBinding.class); + + Map credentials = new HashMap<>(); + credentials.put("host", "aws.example.com"); + credentials.put("region", "us-east-1"); + credentials.put("access_key_id", "test-access-key"); + credentials.put("secret_access_key", "test-secret-key"); + credentials.put("bucket", "test-bucket"); + + when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); + when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); + when(binding.getCredentials()).thenReturn(credentials); + when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + + // Configure shared mode multitenancy + when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) + .thenReturn(Boolean.TRUE); + when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) + .thenReturn("shared"); + + registration.eventHandlers(configurer); + + // Should register both the handler AND the TenantCleanupHandler + verify(configurer).eventHandler(any(OSSAttachmentsServiceHandler.class)); + verify(configurer).eventHandler(any(TenantCleanupHandler.class)); + } + + @Test + void testRegistration_NonMTMode_NoCleanupHandler() { + Registration registration = new Registration(); + CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); + CdsRuntime cdsRuntime = mock(CdsRuntime.class); + CdsEnvironment environment = mock(CdsEnvironment.class); + ServiceBinding binding = mock(ServiceBinding.class); + + Map credentials = new HashMap<>(); + credentials.put("host", "aws.example.com"); + credentials.put("region", "us-east-1"); + credentials.put("access_key_id", "test-access-key"); + credentials.put("secret_access_key", "test-secret-key"); + credentials.put("bucket", "test-bucket"); + + when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); + when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); + when(binding.getCredentials()).thenReturn(credentials); + when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + + // No MT configuration — defaults + when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) + .thenReturn(Boolean.FALSE); + when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) + .thenReturn(null); + + registration.eventHandlers(configurer); + + // Only the handler, NOT the cleanup handler + verify(configurer).eventHandler(any(OSSAttachmentsServiceHandler.class)); + verify(configurer, never()).eventHandler(any(TenantCleanupHandler.class)); + } + + @Test + void testRegistration_PassesMTConfigToHandler() { + Registration registration = new Registration(); + CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); + CdsRuntime cdsRuntime = mock(CdsRuntime.class); + CdsEnvironment environment = mock(CdsEnvironment.class); + ServiceBinding binding = mock(ServiceBinding.class); + + Map credentials = new HashMap<>(); + credentials.put("host", "aws.example.com"); + credentials.put("region", "us-east-1"); + credentials.put("access_key_id", "test-access-key"); + credentials.put("secret_access_key", "test-secret-key"); + credentials.put("bucket", "test-bucket"); + + when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); + when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); + when(binding.getCredentials()).thenReturn(credentials); + when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + + when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) + .thenReturn(Boolean.TRUE); + when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) + .thenReturn("shared"); + + registration.eventHandlers(configurer); + + // Verify a handler was registered — we trust the implementation passes the config + // through the constructor (which is verified by OSSAttachmentsServiceHandlerTest) + verify(configurer, times(2)).eventHandler(any()); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java new file mode 100644 index 000000000..96c9cccbd --- /dev/null +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java @@ -0,0 +1,234 @@ +/* + * © 2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.oss.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; +import com.sap.cds.feature.attachments.oss.client.OSClient; +import com.sap.cds.services.ServiceException; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; +import com.sap.cds.services.request.UserInfo; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Integration-level test that verifies complete tenant isolation in shared mode. Uses a tracking + * mock OSClient to record all operations and verify that Tenant A's operations never touch Tenant + * B's key space. + */ +class MultiTenantIsolationTest { + + /** Tracks all upload, read, and delete keys passed to the OSClient. */ + private final List uploadedKeys = new ArrayList<>(); + + private final List readKeys = new ArrayList<>(); + private final List deletedKeys = new ArrayList<>(); + private final Map storage = new HashMap<>(); + + private OSClient trackingClient; + private OSSAttachmentsServiceHandler handler; + + @BeforeEach + void setUp() throws NoSuchFieldException, IllegalAccessException { + uploadedKeys.clear(); + readKeys.clear(); + deletedKeys.clear(); + storage.clear(); + + // Create a tracking OSClient that records all operations + trackingClient = mock(OSClient.class); + + when(trackingClient.uploadContent(any(InputStream.class), anyString(), anyString())) + .thenAnswer( + invocation -> { + InputStream content = invocation.getArgument(0); + String key = invocation.getArgument(1); + uploadedKeys.add(key); + storage.put(key, content.readAllBytes()); + return CompletableFuture.completedFuture(null); + }); + + when(trackingClient.readContent(anyString())) + .thenAnswer( + invocation -> { + String key = invocation.getArgument(0); + readKeys.add(key); + byte[] data = storage.get(key); + if (data != null) { + return CompletableFuture.completedFuture( + (InputStream) new ByteArrayInputStream(data)); + } + return CompletableFuture.completedFuture(null); + }); + + when(trackingClient.deleteContent(anyString())) + .thenAnswer( + invocation -> { + String key = invocation.getArgument(0); + deletedKeys.add(key); + storage.remove(key); + return CompletableFuture.completedFuture(null); + }); + + // Create handler with multitenancy enabled in shared mode + handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, trackingClient); + + var mtEnabledField = + OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtEnabledField.setAccessible(true); + mtEnabledField.set(handler, true); + + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + } + + @Test + void testTwoTenants_Upload_Read_Delete_CompleteIsolation() { + String tenantA = "tenantA"; + String tenantB = "tenantB"; + String contentIdA = "content-id-A"; + String contentIdB = "content-id-B"; + + // Tenant A uploads + simulateCreate(tenantA, contentIdA, "Data from Tenant A"); + // Tenant B uploads + simulateCreate(tenantB, contentIdB, "Data from Tenant B"); + + // Verify keys are prefixed correctly + assertEquals(2, uploadedKeys.size()); + assertTrue(uploadedKeys.contains(tenantA + "/" + contentIdA)); + assertTrue(uploadedKeys.contains(tenantB + "/" + contentIdB)); + + // Verify no cross-tenant key overlap + assertFalse(uploadedKeys.stream().anyMatch(k -> k.startsWith(tenantA) && k.contains(contentIdB))); + assertFalse(uploadedKeys.stream().anyMatch(k -> k.startsWith(tenantB) && k.contains(contentIdA))); + + // Tenant A reads its own content + simulateRead(tenantA, contentIdA); + assertEquals(tenantA + "/" + contentIdA, readKeys.get(0)); + + // Tenant B reads its own content + simulateRead(tenantB, contentIdB); + assertEquals(tenantB + "/" + contentIdB, readKeys.get(1)); + + // Tenant A deletes its content + simulateDelete(tenantA, contentIdA); + assertEquals(tenantA + "/" + contentIdA, deletedKeys.get(0)); + + // Tenant B's content is still in storage + assertTrue(storage.containsKey(tenantB + "/" + contentIdB)); + assertFalse(storage.containsKey(tenantA + "/" + contentIdA)); + + // Tenant B deletes its content + simulateDelete(tenantB, contentIdB); + assertEquals(tenantB + "/" + contentIdB, deletedKeys.get(1)); + + // Both are gone + assertTrue(storage.isEmpty()); + } + + @Test + void testTenantA_CannotAccess_TenantB_Data() { + String tenantA = "tenantA"; + String tenantB = "tenantB"; + String sharedContentId = "same-content-id"; + + // Both tenants upload with the same contentId (UUID) + simulateCreate(tenantA, sharedContentId, "Tenant A secret"); + simulateCreate(tenantB, sharedContentId, "Tenant B secret"); + + // Keys should be different due to tenant prefix + assertEquals(2, uploadedKeys.size()); + assertEquals(tenantA + "/" + sharedContentId, uploadedKeys.get(0)); + assertEquals(tenantB + "/" + sharedContentId, uploadedKeys.get(1)); + + // Storage should have 2 separate entries + assertEquals(2, storage.size()); + + // Reading as tenant A only returns tenant A's data + simulateRead(tenantA, sharedContentId); + assertEquals(tenantA + "/" + sharedContentId, readKeys.get(0)); + + // The read key never touches tenant B's namespace + assertFalse(readKeys.get(0).startsWith(tenantB)); + } + + @Test + void testNullTenant_ThrowsInSharedMTMode() { + String contentId = "content-no-tenant"; + + // In MT shared mode, null tenant must throw (H-1 security fix) + assertThrows( + ServiceException.class, () -> simulateCreate(null, contentId, "No tenant data")); + + // No upload should have occurred + assertEquals(0, uploadedKeys.size()); + } + + private void simulateCreate(String tenant, String contentId, String content) { + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(com.sap.cds.reflect.CdsEntity.class); + UserInfo userInfo = mock(UserInfo.class); + + when(userInfo.getTenant()).thenReturn(tenant); + when(context.getUserInfo()).thenReturn(userInfo); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream(content.getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + + handler.createAttachment(context); + } + + private void simulateRead(String tenant, String contentId) { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + UserInfo userInfo = mock(UserInfo.class); + + when(userInfo.getTenant()).thenReturn(tenant); + when(context.getUserInfo()).thenReturn(userInfo); + when(context.getContentId()).thenReturn(contentId); + when(context.getData()).thenReturn(mockMediaData); + + handler.readAttachment(context); + } + + private void simulateDelete(String tenant, String contentId) { + AttachmentMarkAsDeletedEventContext context = mock(AttachmentMarkAsDeletedEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(userInfo.getTenant()).thenReturn(tenant); + when(context.getUserInfo()).thenReturn(userInfo); + when(context.getContentId()).thenReturn(contentId); + + handler.markAttachmentAsDeleted(context); + } +} diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index 2d973221b..26404733d 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -11,6 +11,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; import com.sap.cds.feature.attachments.oss.client.OSClient; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; @@ -19,16 +20,19 @@ import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; import com.sap.cds.reflect.CdsEntity; import com.sap.cds.services.ServiceException; +import com.sap.cds.services.request.UserInfo; import com.sap.cloud.environment.servicebinding.api.ServiceBinding; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.Base64; import java.util.HashMap; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; class OSSAttachmentsServiceHandlerTest { ExecutorService executor = Executors.newCachedThreadPool(); @@ -45,7 +49,8 @@ void testRestoreAttachmentCallsSetCompleted() { creds.put("bucket", "test-bucket"); when(binding.getCredentials()).thenReturn(creds); - OSSAttachmentsServiceHandler handler = new OSSAttachmentsServiceHandler(binding, executor); + OSSAttachmentsServiceHandler handler = + new OSSAttachmentsServiceHandler(binding, executor, false, null); AttachmentRestoreEventContext context = mock(AttachmentRestoreEventContext.class); handler.restoreAttachment(context); verify(context).setCompleted(); @@ -179,7 +184,7 @@ void testConstructorHandlesInvalidBase64EncodedPrivateKeyData() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor)); + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); } @Test @@ -195,7 +200,7 @@ void testConstructorHandlesValidBase64ButNoGoogleOrGcp() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor)); + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); } @Test @@ -207,7 +212,7 @@ void testConstructorHandlesInValidBase64() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor)); + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); } @Test @@ -221,7 +226,7 @@ void testConstructorHandlesNoValidObjectStoreService() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor)); + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); } // Helper method to setup common mocks for createAttachment exception tests @@ -377,4 +382,332 @@ void testReadAttachmentHandlesInterruptedException() assertThrows(ServiceException.class, () -> handler.readAttachment(context)); verify(context).setCompleted(); } + + // ==================== Multi-Tenancy Tests (Phase 1 Shared Mode) ==================== + + /** + * Helper to create a handler with MT config injected via reflection. The implementation agent is + * adding multitenancyEnabled and objectStoreKind fields to the handler class. + */ + private OSSAttachmentsServiceHandler createMTHandler( + OSClient mockOsClient, boolean multitenancyEnabled, String objectStoreKind) + throws NoSuchFieldException, IllegalAccessException { + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, multitenancyEnabled); + + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, objectStoreKind); + + return handler; + } + + private static UserInfo mockUserInfo(String tenant) { + UserInfo userInfo = mock(UserInfo.class); + when(userInfo.getTenant()).thenReturn(tenant); + return userInfo; + } + + @Test + void testBuildObjectKey_SharedMode_PrefixesTenantId() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); + + String tenantId = "myTenant"; + String contentId = "content-uuid-123"; + + // Setup create context with tenant + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(CdsEntity.class); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + UserInfo userInfo = mockUserInfo(tenantId); + when(context.getUserInfo()).thenReturn(userInfo); + when(mockOsClient.uploadContent(any(), anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); + + handler.createAttachment(context); + + // Capture the object key passed to uploadContent + ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + verify(mockOsClient).uploadContent(any(), keyCaptor.capture(), anyString()); + // In shared mode, the key should be tenantId/contentId + org.junit.jupiter.api.Assertions.assertEquals( + tenantId + "/" + contentId, keyCaptor.getValue()); + } + + @Test + void testBuildObjectKey_NonMTMode_NoPrefixing() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + // multitenancy disabled + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, false, null); + + String contentId = "content-uuid-456"; + + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(CdsEntity.class); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + when(mockOsClient.uploadContent(any(), anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); + + handler.createAttachment(context); + + // Capture the object key passed to uploadContent + ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + verify(mockOsClient).uploadContent(any(), keyCaptor.capture(), anyString()); + // When MT is off, key should be plain contentId with no prefix + org.junit.jupiter.api.Assertions.assertEquals(contentId, keyCaptor.getValue()); + } + + @Test + void testBuildObjectKey_NullTenant_ThrowsInMTMode() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); + + String contentId = "content-uuid-789"; + + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(CdsEntity.class); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + // null tenant + UserInfo userInfo = mockUserInfo(null); + when(context.getUserInfo()).thenReturn(userInfo); + + // In MT mode, null tenant should throw ServiceException + assertThrows(ServiceException.class, () -> handler.createAttachment(context)); + } + + @Test + void testCreateAttachment_SharedMode_UsesObjectKey() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); + + String tenantId = "tenantX"; + String contentId = "doc-create-123"; + + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(CdsEntity.class); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("data".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("application/pdf"); + UserInfo userInfoX = mockUserInfo(tenantId); + when(context.getUserInfo()).thenReturn(userInfoX); + when(mockOsClient.uploadContent(any(), anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); + + handler.createAttachment(context); + + ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + verify(mockOsClient).uploadContent(any(), keyCaptor.capture(), anyString()); + org.junit.jupiter.api.Assertions.assertEquals( + tenantId + "/" + contentId, keyCaptor.getValue()); + // contentId stored in the context should remain unprefixed + verify(context).setContentId(contentId); + } + + @Test + void testReadAttachment_SharedMode_UsesObjectKey() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); + + String tenantId = "tenantRead"; + String contentId = "doc-read-456"; + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + when(context.getContentId()).thenReturn(contentId); + when(context.getData()).thenReturn(mockMediaData); + UserInfo userInfoRead = mockUserInfo(tenantId); + when(context.getUserInfo()).thenReturn(userInfoRead); + when(mockOsClient.readContent(anyString())) + .thenReturn( + CompletableFuture.completedFuture(new ByteArrayInputStream("test".getBytes()))); + + handler.readAttachment(context); + + // Verify the read uses the prefixed key + ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + verify(mockOsClient).readContent(keyCaptor.capture()); + org.junit.jupiter.api.Assertions.assertEquals( + tenantId + "/" + contentId, keyCaptor.getValue()); + } + + @Test + void testDeleteAttachment_SharedMode_UsesObjectKey() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); + + String tenantId = "tenantDel"; + String contentId = "doc-del-789"; + + AttachmentMarkAsDeletedEventContext context = mock(AttachmentMarkAsDeletedEventContext.class); + when(context.getContentId()).thenReturn(contentId); + UserInfo userInfoDel = mockUserInfo(tenantId); + when(context.getUserInfo()).thenReturn(userInfoDel); + when(mockOsClient.deleteContent(anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); + + handler.markAttachmentAsDeleted(context); + + // Verify the delete uses the prefixed key + ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + verify(mockOsClient).deleteContent(keyCaptor.capture()); + org.junit.jupiter.api.Assertions.assertEquals( + tenantId + "/" + contentId, keyCaptor.getValue()); + } + + @Test + void testCreateAttachment_SingleTenant_NoPrefixing() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + // Backward compatibility: MT disabled + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, false, null); + + String contentId = "doc-single-abc"; + + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(CdsEntity.class); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("data".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + when(mockOsClient.uploadContent(any(), anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); + + handler.createAttachment(context); + + ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + verify(mockOsClient).uploadContent(any(), keyCaptor.capture(), anyString()); + // No prefix, just contentId + org.junit.jupiter.api.Assertions.assertEquals(contentId, keyCaptor.getValue()); + verify(context).setContentId(contentId); + } + + // ==================== Tenant ID Validation Tests ==================== + + @Test + void testValidateTenantId_EmptyTenant_ThrowsException() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); + + String contentId = "content-id"; + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(CdsEntity.class); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + UserInfo userInfo = mockUserInfo(""); + when(context.getUserInfo()).thenReturn(userInfo); + + assertThrows(ServiceException.class, () -> handler.createAttachment(context)); + } + + @Test + void testValidateTenantId_SlashInTenant_ThrowsException() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); + + String contentId = "content-id"; + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(CdsEntity.class); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + UserInfo userInfo = mockUserInfo("tenant/evil"); + when(context.getUserInfo()).thenReturn(userInfo); + + assertThrows(ServiceException.class, () -> handler.createAttachment(context)); + } + + @Test + void testValidateTenantId_BackslashInTenant_ThrowsException() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); + + String contentId = "content-id"; + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(CdsEntity.class); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + UserInfo userInfo = mockUserInfo("tenant\\evil"); + when(context.getUserInfo()).thenReturn(userInfo); + + assertThrows(ServiceException.class, () -> handler.createAttachment(context)); + } + + @Test + void testValidateTenantId_PathTraversal_ThrowsException() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); + + String contentId = "content-id"; + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + var mockEntity = mock(CdsEntity.class); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + UserInfo userInfo = mockUserInfo("..evil"); + when(context.getUserInfo()).thenReturn(userInfo); + + assertThrows(ServiceException.class, () -> handler.createAttachment(context)); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java index 615aba16c..f96178dd3 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java @@ -39,7 +39,8 @@ public static void testCreateReadDeleteAttachmentFlow( String testFileName = "testFileName-" + System.currentTimeMillis() + ".txt"; String testFileContent = "test"; - OSSAttachmentsServiceHandler handler = new OSSAttachmentsServiceHandler(binding, executor); + OSSAttachmentsServiceHandler handler = + new OSSAttachmentsServiceHandler(binding, executor, false, null); // Create an AttachmentCreateEventContext with mocked data - to upload a test attachment MediaData createMediaData = mock(MediaData.class); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java new file mode 100644 index 000000000..7f81186ff --- /dev/null +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java @@ -0,0 +1,82 @@ +/* + * © 2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.oss.handler; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.sap.cds.feature.attachments.oss.client.OSClient; +import com.sap.cds.services.mt.UnsubscribeEventContext; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TenantCleanupHandlerTest { + + private OSClient osClient; + private UnsubscribeEventContext context; + private TenantCleanupHandler handler; + + @BeforeEach + void setUp() { + osClient = mock(OSClient.class); + context = mock(UnsubscribeEventContext.class); + handler = new TenantCleanupHandler(osClient); + } + + @Test + void testCleanupTenantData_CallsDeleteByPrefix() throws Exception { + String tenantId = "tenant-abc"; + when(context.getTenant()).thenReturn(tenantId); + when(osClient.deleteContentByPrefix(tenantId + "/")) + .thenReturn(CompletableFuture.completedFuture(null)); + + handler.cleanupTenantData(context); + + verify(osClient).deleteContentByPrefix(tenantId + "/"); + } + + @Test + void testCleanupTenantData_UsesCorrectPrefix() throws Exception { + String tenantId = "my-tenant-123"; + when(context.getTenant()).thenReturn(tenantId); + when(osClient.deleteContentByPrefix("my-tenant-123/")) + .thenReturn(CompletableFuture.completedFuture(null)); + + handler.cleanupTenantData(context); + + verify(osClient).deleteContentByPrefix("my-tenant-123/"); + } + + @Test + void testCleanupTenantData_HandlesException() throws Exception { + String tenantId = "tenant-fail"; + when(context.getTenant()).thenReturn(tenantId); + + CompletableFuture failedFuture = new CompletableFuture<>(); + failedFuture.completeExceptionally(new RuntimeException("Storage error")); + when(osClient.deleteContentByPrefix(tenantId + "/")).thenReturn(failedFuture); + + // Should NOT rethrow — the handler logs the error but does not fail the unsubscribe + handler.cleanupTenantData(context); + + verify(osClient).deleteContentByPrefix(tenantId + "/"); + } + + @Test + void testCleanupTenantData_HandlesInterruptedException() throws Exception { + String tenantId = "tenant-interrupt"; + when(context.getTenant()).thenReturn(tenantId); + + CompletableFuture failedFuture = new CompletableFuture<>(); + failedFuture.completeExceptionally(new InterruptedException("Interrupted")); + when(osClient.deleteContentByPrefix(tenantId + "/")).thenReturn(failedFuture); + + // Should NOT rethrow — the handler logs the error but does not fail the unsubscribe + handler.cleanupTenantData(context); + + verify(osClient).deleteContentByPrefix(tenantId + "/"); + } +} From 5c24888aabd3a6ba41e773c3d9afd3f5ea8db190 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Sun, 29 Mar 2026 13:23:21 +0200 Subject: [PATCH 02/39] bug fixes --- .../attachments/oss/client/AWSClient.java | 10 ++- .../attachments/oss/client/AzureClient.java | 21 ++++++- .../attachments/oss/client/GoogleClient.java | 15 ++--- .../handler/OSSAttachmentsServiceHandler.java | 12 ++++ .../oss/client/GoogleClientTest.java | 40 +++--------- .../oss/handler/MultiTenantIsolationTest.java | 61 +++++++++++++++++++ .../OSSAttachmentsServiceHandlerTest.java | 1 + 7 files changed, 118 insertions(+), 42 deletions(-) diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java index 831818ccd..137a6c9fc 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java @@ -23,7 +23,9 @@ import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.S3Error; import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; import software.amazon.awssdk.services.s3.model.ObjectIdentifier; @@ -156,7 +158,13 @@ public Future deleteContentByPrefix(String prefix) { .bucket(this.bucketName) .delete(Delete.builder().objects(keys).build()) .build(); - s3Client.deleteObjects(deleteReq); + DeleteObjectsResponse deleteResp = s3Client.deleteObjects(deleteReq); + if (deleteResp.hasErrors() && !deleteResp.errors().isEmpty()) { + logger.warn( + "Failed to delete {} objects during prefix cleanup: {}", + deleteResp.errors().size(), + deleteResp.errors().stream().map(S3Error::key).toList()); + } } listReq = listReq.toBuilder() diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java index b7ad983f9..a81258823 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java @@ -13,6 +13,8 @@ import com.sap.cds.feature.attachments.oss.handler.ObjectStoreServiceException; import com.sap.cloud.environment.servicebinding.api.ServiceBinding; import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import org.slf4j.Logger; @@ -95,12 +97,29 @@ public Future deleteContentByPrefix(String prefix) { () -> { try { ListBlobsOptions options = new ListBlobsOptions().setPrefix(prefix); + List blobNames = new ArrayList<>(); for (BlobItem blobItem : blobContainerClient.listBlobs(options, null)) { - blobContainerClient.getBlobClient(blobItem.getName()).delete(); + blobNames.add(blobItem.getName()); + } + List> deleteFutures = + blobNames.stream() + .map( + name -> + executor.submit( + () -> { + blobContainerClient.getBlobClient(name).delete(); + return (Void) null; + })) + .toList(); + for (Future f : deleteFutures) { + f.get(); } } catch (RuntimeException e) { throw new ObjectStoreServiceException( "Failed to delete objects by prefix from the Azure Object Store", e); + } catch (Exception e) { + throw new ObjectStoreServiceException( + "Failed to delete objects by prefix from the Azure Object Store", e); } return null; }); diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java index ec3a0a5aa..e82fb5a64 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java @@ -141,17 +141,12 @@ public Future deleteContentByPrefix(String prefix) { () -> { try { Page blobs = - storage.list(bucketName, Storage.BlobListOption.prefix(prefix)); + storage.list( + bucketName, + Storage.BlobListOption.prefix(prefix), + Storage.BlobListOption.versions(true)); for (Blob blob : blobs.iterateAll()) { - Page versions = - storage.list( - bucketName, - Storage.BlobListOption.versions(true), - Storage.BlobListOption.prefix(blob.getName())); - for (Blob version : versions.iterateAll()) { - storage.delete( - BlobId.of(bucketName, version.getName(), version.getGeneration())); - } + storage.delete(BlobId.of(bucketName, blob.getName(), blob.getGeneration())); } } catch (RuntimeException e) { throw new ObjectStoreServiceException( diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java index 09f7b8e31..1a41d0bb3 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java @@ -203,6 +203,7 @@ private String buildObjectKey(EventContext context, String contentId) { if (multitenancyEnabled && "shared".equals(objectStoreKind)) { String tenant = getTenant(context); validateTenantId(tenant); + validateContentId(contentId); return tenant + "/" + contentId; } return contentId; @@ -225,4 +226,15 @@ private static void validateTenantId(String tenantId) { "Invalid tenant ID for attachment storage: must not be empty or contain path separators"); } } + + private static void validateContentId(String contentId) { + if (contentId == null + || contentId.isEmpty() + || contentId.contains("/") + || contentId.contains("\\") + || contentId.contains("..")) { + throw new ServiceException( + "Invalid content ID for attachment storage: must not be empty or contain path separators"); + } + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java index 5da93a59e..f682dfaae 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java @@ -291,7 +291,7 @@ void testDeleteContentByPrefix_DeletesAllMatchingBlobs() bucketField.setAccessible(true); bucketField.set(googleClient, "my-bucket"); - // Mock listing of blobs by prefix + // Mock blobs returned by single list call with prefix + versions Blob blobA = mock(Blob.class); when(blobA.getName()).thenReturn("tenantX/file1.txt"); when(blobA.getGeneration()).thenReturn(1L); @@ -299,25 +299,15 @@ void testDeleteContentByPrefix_DeletesAllMatchingBlobs() when(blobB.getName()).thenReturn("tenantX/file2.txt"); when(blobB.getGeneration()).thenReturn(2L); - // First call: list by prefix (without versions) returns the blobs - Page prefixPage = mock(Page.class); - when(prefixPage.iterateAll()).thenReturn(() -> java.util.Arrays.asList(blobA, blobB).iterator()); - - // For each blob, list versions returns the same blob (single version each) - Page versionPageA = mock(Page.class); - when(versionPageA.iterateAll()).thenReturn(() -> Collections.singletonList(blobA).iterator()); - Page versionPageB = mock(Page.class); - when(versionPageB.iterateAll()).thenReturn(() -> Collections.singletonList(blobB).iterator()); - - // First call: prefix listing; subsequent calls: version listings - when(mockStorage.list(anyString(), any())) - .thenReturn(prefixPage); - when(mockStorage.list(anyString(), any(), any())) - .thenReturn(versionPageA, versionPageB); + Page page = mock(Page.class); + when(page.iterateAll()).thenReturn(() -> java.util.Arrays.asList(blobA, blobB).iterator()); + when(mockStorage.list(anyString(), any(), any())).thenReturn(page); when(mockStorage.delete(any(BlobId.class))).thenReturn(true); assertDoesNotThrow(() -> googleClient.deleteContentByPrefix("tenantX/").get()); + org.mockito.Mockito.verify(mockStorage, org.mockito.Mockito.times(2)) + .delete(any(BlobId.class)); } @Test @@ -338,11 +328,7 @@ void testDeleteContentByPrefix_HandlesVersioning() bucketField.setAccessible(true); bucketField.set(googleClient, "my-bucket"); - // Blob with 2 versions - Blob blob = mock(Blob.class); - when(blob.getName()).thenReturn("tenantY/versioned-file.txt"); - when(blob.getGeneration()).thenReturn(1L); - + // Single blob with 2 versions returned directly by the combined list call Blob version1 = mock(Blob.class); when(version1.getName()).thenReturn("tenantY/versioned-file.txt"); when(version1.getGeneration()).thenReturn(1L); @@ -351,17 +337,11 @@ void testDeleteContentByPrefix_HandlesVersioning() when(version2.getName()).thenReturn("tenantY/versioned-file.txt"); when(version2.getGeneration()).thenReturn(2L); - Page prefixPage = mock(Page.class); - when(prefixPage.iterateAll()).thenReturn(() -> Collections.singletonList(blob).iterator()); - - Page versionPage = mock(Page.class); - when(versionPage.iterateAll()) + Page page = mock(Page.class); + when(page.iterateAll()) .thenReturn(() -> java.util.Arrays.asList(version1, version2).iterator()); - when(mockStorage.list(anyString(), any())) - .thenReturn(prefixPage); - when(mockStorage.list(anyString(), any(), any())) - .thenReturn(versionPage); + when(mockStorage.list(anyString(), any(), any())).thenReturn(page); when(mockStorage.delete(any(BlobId.class))).thenReturn(true); assertDoesNotThrow(() -> googleClient.deleteContentByPrefix("tenantY/").get()); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java index 96c9cccbd..dfbcd0cc0 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java @@ -190,6 +190,67 @@ void testNullTenant_ThrowsInSharedMTMode() { assertEquals(0, uploadedKeys.size()); } + // ==================== Content ID Validation Integration Tests ==================== + + @Test + void testPathTraversalInContentId_BlockedInSharedMode() { + // A malicious contentId like "../other-tenant/secret" must be rejected + // to prevent tenant A from reading/writing tenant B's namespace + assertThrows( + ServiceException.class, + () -> simulateCreate("tenantA", "../other-tenant/secret", "malicious data")); + assertEquals(0, uploadedKeys.size()); + } + + @Test + void testSlashInContentId_BlockedInSharedMode() { + assertThrows( + ServiceException.class, + () -> simulateCreate("tenantA", "sub/path", "data")); + assertEquals(0, uploadedKeys.size()); + } + + @Test + void testBackslashInContentId_BlockedInSharedMode() { + assertThrows( + ServiceException.class, + () -> simulateCreate("tenantA", "evil\\path", "data")); + assertEquals(0, uploadedKeys.size()); + } + + @Test + void testEmptyContentId_BlockedInSharedMode() { + assertThrows( + ServiceException.class, + () -> simulateCreate("tenantA", "", "data")); + assertEquals(0, uploadedKeys.size()); + } + + @Test + void testDoubleDotInContentId_BlockedInSharedMode() { + assertThrows( + ServiceException.class, + () -> simulateCreate("tenantA", "..otherTenant", "data")); + assertEquals(0, uploadedKeys.size()); + } + + @Test + void testPathTraversalInContentId_FullIsolationScenario() { + // Tenant B uploads a legitimate file + simulateCreate("tenantB", "legit-content-id", "Tenant B secret"); + assertEquals(1, uploadedKeys.size()); + assertEquals("tenantB/legit-content-id", uploadedKeys.get(0)); + + // Tenant A tries to use path traversal to access tenant B's file + assertThrows( + ServiceException.class, + () -> simulateCreate("tenantA", "../tenantB/legit-content-id", "overwrite attempt")); + + // Only the original upload should exist + assertEquals(1, uploadedKeys.size()); + assertTrue(storage.containsKey("tenantB/legit-content-id")); + } + private void simulateCreate(String tenant, String contentId, String content) { AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); MediaData mockMediaData = mock(MediaData.class); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index 26404733d..e952aa102 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -710,4 +710,5 @@ void testValidateTenantId_PathTraversal_ThrowsException() assertThrows(ServiceException.class, () -> handler.createAttachment(context)); } + } From d0cc4130926756a358a917292637f6eae94777f9 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Mon, 30 Mar 2026 08:43:52 +0200 Subject: [PATCH 03/39] revert --- .../attachments/oss/client/AWSClientTest.java | 102 +----- .../oss/client/AzureClientTest.java | 61 ---- .../oss/client/GoogleClientTest.java | 78 ---- .../attachments/oss/client/OSClientTest.java | 41 --- .../oss/configuration/RegistrationTest.java | 102 ------ .../oss/handler/MultiTenantIsolationTest.java | 295 --------------- .../OSSAttachmentsServiceHandlerTest.java | 344 +----------------- ...OSSAttachmentsServiceHandlerTestUtils.java | 3 +- .../oss/handler/TenantCleanupHandlerTest.java | 82 ----- 9 files changed, 7 insertions(+), 1101 deletions(-) delete mode 100644 storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java delete mode 100644 storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java delete mode 100644 storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java index 7fc91a811..032c1a8a2 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java @@ -9,7 +9,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandler; @@ -31,15 +30,10 @@ import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; -import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; -import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.GetObjectResponse; -import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; -import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; -import software.amazon.awssdk.services.s3.model.S3Object; class AWSClientTest { ExecutorService executor = Executors.newCachedThreadPool(); @@ -48,7 +42,7 @@ class AWSClientTest { void testConstructorWithAwsBindingUsesAwsClient() throws NoSuchFieldException, IllegalAccessException { OSSAttachmentsServiceHandler handler = - new OSSAttachmentsServiceHandler(getDummyBinding(), executor, false, null); + new OSSAttachmentsServiceHandler(getDummyBinding(), executor); OSClient client = OSSAttachmentsServiceHandlerTestUtils.getOsClient(handler); assertInstanceOf(AWSClient.class, client); } @@ -231,100 +225,6 @@ void testDeleteContentThrowsOnUnsuccessfulResponse() assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } - @Test - void testDeleteContentByPrefix_DeletesAllMatchingObjects() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - S3Client mockS3Client = mock(S3Client.class); - - // Create mock list response with 2 objects matching the prefix - S3Object obj1 = S3Object.builder().key("tenantA/file1.txt").build(); - S3Object obj2 = S3Object.builder().key("tenantA/file2.txt").build(); - ListObjectsV2Response listResponse = - ListObjectsV2Response.builder() - .contents(obj1, obj2) - .isTruncated(false) - .build(); - - when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))).thenReturn(listResponse); - DeleteObjectsResponse deleteResponse = mock(DeleteObjectsResponse.class); - when(mockS3Client.deleteObjects(any(DeleteObjectsRequest.class))).thenReturn(deleteResponse); - - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - - assertDoesNotThrow(() -> awsClient.deleteContentByPrefix("tenantA/").get()); - verify(mockS3Client).listObjectsV2(any(ListObjectsV2Request.class)); - verify(mockS3Client).deleteObjects(any(DeleteObjectsRequest.class)); - } - - @Test - void testDeleteContentByPrefix_HandlesPagination() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - S3Client mockS3Client = mock(S3Client.class); - - // First page: truncated, has a continuation token - S3Object obj1 = S3Object.builder().key("tenantB/file1.txt").build(); - ListObjectsV2Response firstPage = - ListObjectsV2Response.builder() - .contents(obj1) - .isTruncated(true) - .nextContinuationToken("token-page2") - .build(); - - // Second page: not truncated, final page - S3Object obj2 = S3Object.builder().key("tenantB/file2.txt").build(); - ListObjectsV2Response secondPage = - ListObjectsV2Response.builder() - .contents(obj2) - .isTruncated(false) - .build(); - - when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))) - .thenReturn(firstPage, secondPage); - DeleteObjectsResponse deleteResponse = mock(DeleteObjectsResponse.class); - when(mockS3Client.deleteObjects(any(DeleteObjectsRequest.class))).thenReturn(deleteResponse); - - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - - assertDoesNotThrow(() -> awsClient.deleteContentByPrefix("tenantB/").get()); - // Should call listObjectsV2 twice (two pages) and deleteObjects twice - verify(mockS3Client, org.mockito.Mockito.times(2)) - .listObjectsV2(any(ListObjectsV2Request.class)); - verify(mockS3Client, org.mockito.Mockito.times(2)) - .deleteObjects(any(DeleteObjectsRequest.class)); - } - - @Test - void testDeleteContentByPrefix_EmptyPrefix_NoObjects() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - S3Client mockS3Client = mock(S3Client.class); - - // Empty listing - ListObjectsV2Response emptyResponse = - ListObjectsV2Response.builder() - .contents(java.util.Collections.emptyList()) - .isTruncated(false) - .build(); - - when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))).thenReturn(emptyResponse); - - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - - assertDoesNotThrow(() -> awsClient.deleteContentByPrefix("nonexistent/").get()); - verify(mockS3Client).listObjectsV2(any(ListObjectsV2Request.class)); - // No deleteObjects call when there are no objects - verify(mockS3Client, org.mockito.Mockito.never()) - .deleteObjects(any(DeleteObjectsRequest.class)); - } - private ServiceBinding getDummyBinding() { ServiceBinding binding = mock(ServiceBinding.class); HashMap creds = new HashMap<>(); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java index 59b42bfaf..9b7100468 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java @@ -7,22 +7,16 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import com.azure.core.http.rest.PagedIterable; import com.azure.storage.blob.BlobClient; import com.azure.storage.blob.BlobContainerClient; -import com.azure.storage.blob.models.BlobItem; -import com.azure.storage.blob.models.ListBlobsOptions; import com.azure.storage.blob.specialized.BlobOutputStream; import com.azure.storage.blob.specialized.BlockBlobClient; import com.sap.cds.feature.attachments.oss.handler.ObjectStoreServiceException; import java.io.IOException; import java.io.InputStream; -import java.util.Arrays; -import java.util.Collections; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.stream.Stream; import org.junit.jupiter.api.Test; class AzureClientTest { @@ -195,59 +189,4 @@ void testReadContentThrowsOnRuntimeException() assertThrows(ExecutionException.class, () -> azureClient.readContent("file.txt").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } - - @Test - @SuppressWarnings("unchecked") - void testDeleteContentByPrefix_DeletesAllMatchingBlobs() - throws NoSuchFieldException, IllegalAccessException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - - BlobContainerClient mockContainer = mock(BlobContainerClient.class); - BlobClient mockBlobClient = mock(BlobClient.class); - - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); - - // Create mock blob items matching the prefix - BlobItem blob1 = mock(BlobItem.class); - when(blob1.getName()).thenReturn("tenantA/file1.txt"); - BlobItem blob2 = mock(BlobItem.class); - when(blob2.getName()).thenReturn("tenantA/file2.txt"); - - PagedIterable mockPagedIterable = mock(PagedIterable.class); - when(mockPagedIterable.stream()).thenReturn(Stream.of(blob1, blob2)); - when(mockPagedIterable.iterator()).thenReturn(Arrays.asList(blob1, blob2).iterator()); - when(mockContainer.listBlobs(any(ListBlobsOptions.class), any())).thenReturn(mockPagedIterable); - when(mockContainer.getBlobClient(anyString())).thenReturn(mockBlobClient); - - assertDoesNotThrow(() -> azureClient.deleteContentByPrefix("tenantA/").get()); - } - - @Test - @SuppressWarnings("unchecked") - void testDeleteContentByPrefix_EmptyResult() - throws NoSuchFieldException, IllegalAccessException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - - BlobContainerClient mockContainer = mock(BlobContainerClient.class); - - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); - - // Empty listing - PagedIterable emptyIterable = mock(PagedIterable.class); - when(emptyIterable.stream()).thenReturn(Stream.empty()); - when(emptyIterable.iterator()).thenReturn(Collections.emptyIterator()); - when(mockContainer.listBlobs(any(ListBlobsOptions.class), any())).thenReturn(emptyIterable); - - assertDoesNotThrow(() -> azureClient.deleteContentByPrefix("nonexistent-tenant/").get()); - } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java index f682dfaae..f4c2fd188 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java @@ -272,82 +272,4 @@ void testReadContentThrowsOnRuntimeException() assertThrows(ExecutionException.class, () -> googleClient.readContent("file.txt").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } - - @Test - @SuppressWarnings("unchecked") - void testDeleteContentByPrefix_DeletesAllMatchingBlobs() - throws NoSuchFieldException, IllegalAccessException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); - - Storage mockStorage = mock(Storage.class); - - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - var bucketField = GoogleClient.class.getDeclaredField("bucketName"); - bucketField.setAccessible(true); - bucketField.set(googleClient, "my-bucket"); - - // Mock blobs returned by single list call with prefix + versions - Blob blobA = mock(Blob.class); - when(blobA.getName()).thenReturn("tenantX/file1.txt"); - when(blobA.getGeneration()).thenReturn(1L); - Blob blobB = mock(Blob.class); - when(blobB.getName()).thenReturn("tenantX/file2.txt"); - when(blobB.getGeneration()).thenReturn(2L); - - Page page = mock(Page.class); - when(page.iterateAll()).thenReturn(() -> java.util.Arrays.asList(blobA, blobB).iterator()); - - when(mockStorage.list(anyString(), any(), any())).thenReturn(page); - when(mockStorage.delete(any(BlobId.class))).thenReturn(true); - - assertDoesNotThrow(() -> googleClient.deleteContentByPrefix("tenantX/").get()); - org.mockito.Mockito.verify(mockStorage, org.mockito.Mockito.times(2)) - .delete(any(BlobId.class)); - } - - @Test - @SuppressWarnings("unchecked") - void testDeleteContentByPrefix_HandlesVersioning() - throws NoSuchFieldException, IllegalAccessException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); - - Storage mockStorage = mock(Storage.class); - - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - var bucketField = GoogleClient.class.getDeclaredField("bucketName"); - bucketField.setAccessible(true); - bucketField.set(googleClient, "my-bucket"); - - // Single blob with 2 versions returned directly by the combined list call - Blob version1 = mock(Blob.class); - when(version1.getName()).thenReturn("tenantY/versioned-file.txt"); - when(version1.getGeneration()).thenReturn(1L); - - Blob version2 = mock(Blob.class); - when(version2.getName()).thenReturn("tenantY/versioned-file.txt"); - when(version2.getGeneration()).thenReturn(2L); - - Page page = mock(Page.class); - when(page.iterateAll()) - .thenReturn(() -> java.util.Arrays.asList(version1, version2).iterator()); - - when(mockStorage.list(anyString(), any(), any())).thenReturn(page); - when(mockStorage.delete(any(BlobId.class))).thenReturn(true); - - assertDoesNotThrow(() -> googleClient.deleteContentByPrefix("tenantY/").get()); - - // Should delete both versions - org.mockito.Mockito.verify(mockStorage, org.mockito.Mockito.times(2)) - .delete(any(BlobId.class)); - } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java deleted file mode 100644 index f1861534b..000000000 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * © 2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.oss.client; - -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.concurrent.ExecutionException; -import org.junit.jupiter.api.Test; - -class OSClientTest { - - @Test - void testDeleteContentByPrefix_DefaultThrowsUnsupported() { - // Anonymous implementation that only has the default methods - OSClient client = - new OSClient() { - @Override - public java.util.concurrent.Future uploadContent( - java.io.InputStream content, String completeFileName, String contentType) { - return null; - } - - @Override - public java.util.concurrent.Future deleteContent(String completeFileName) { - return null; - } - - @Override - public java.util.concurrent.Future readContent( - String completeFileName) { - return null; - } - }; - - ExecutionException ex = - assertThrows(ExecutionException.class, () -> client.deleteContentByPrefix("prefix").get()); - assertInstanceOf(UnsupportedOperationException.class, ex.getCause()); - } -} diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java index 5babe024d..a364aa7d7 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java @@ -7,7 +7,6 @@ import static org.mockito.Mockito.*; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandler; -import com.sap.cds.feature.attachments.oss.handler.TenantCleanupHandler; import com.sap.cds.services.environment.CdsEnvironment; import com.sap.cds.services.runtime.CdsRuntime; import com.sap.cds.services.runtime.CdsRuntimeConfigurer; @@ -49,105 +48,4 @@ void testEventHandlersRegistersOSSHandler() { // Assert: OSSAttachmentsServiceHandler should be registered verify(configurer).eventHandler(any(OSSAttachmentsServiceHandler.class)); } - - @Test - void testRegistration_SharedMode_RegistersCleanupHandler() { - Registration registration = new Registration(); - CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); - CdsRuntime cdsRuntime = mock(CdsRuntime.class); - CdsEnvironment environment = mock(CdsEnvironment.class); - ServiceBinding binding = mock(ServiceBinding.class); - - Map credentials = new HashMap<>(); - credentials.put("host", "aws.example.com"); - credentials.put("region", "us-east-1"); - credentials.put("access_key_id", "test-access-key"); - credentials.put("secret_access_key", "test-secret-key"); - credentials.put("bucket", "test-bucket"); - - when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); - when(cdsRuntime.getEnvironment()).thenReturn(environment); - when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); - when(binding.getCredentials()).thenReturn(credentials); - when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); - - // Configure shared mode multitenancy - when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) - .thenReturn(Boolean.TRUE); - when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) - .thenReturn("shared"); - - registration.eventHandlers(configurer); - - // Should register both the handler AND the TenantCleanupHandler - verify(configurer).eventHandler(any(OSSAttachmentsServiceHandler.class)); - verify(configurer).eventHandler(any(TenantCleanupHandler.class)); - } - - @Test - void testRegistration_NonMTMode_NoCleanupHandler() { - Registration registration = new Registration(); - CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); - CdsRuntime cdsRuntime = mock(CdsRuntime.class); - CdsEnvironment environment = mock(CdsEnvironment.class); - ServiceBinding binding = mock(ServiceBinding.class); - - Map credentials = new HashMap<>(); - credentials.put("host", "aws.example.com"); - credentials.put("region", "us-east-1"); - credentials.put("access_key_id", "test-access-key"); - credentials.put("secret_access_key", "test-secret-key"); - credentials.put("bucket", "test-bucket"); - - when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); - when(cdsRuntime.getEnvironment()).thenReturn(environment); - when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); - when(binding.getCredentials()).thenReturn(credentials); - when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); - - // No MT configuration — defaults - when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) - .thenReturn(Boolean.FALSE); - when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) - .thenReturn(null); - - registration.eventHandlers(configurer); - - // Only the handler, NOT the cleanup handler - verify(configurer).eventHandler(any(OSSAttachmentsServiceHandler.class)); - verify(configurer, never()).eventHandler(any(TenantCleanupHandler.class)); - } - - @Test - void testRegistration_PassesMTConfigToHandler() { - Registration registration = new Registration(); - CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); - CdsRuntime cdsRuntime = mock(CdsRuntime.class); - CdsEnvironment environment = mock(CdsEnvironment.class); - ServiceBinding binding = mock(ServiceBinding.class); - - Map credentials = new HashMap<>(); - credentials.put("host", "aws.example.com"); - credentials.put("region", "us-east-1"); - credentials.put("access_key_id", "test-access-key"); - credentials.put("secret_access_key", "test-secret-key"); - credentials.put("bucket", "test-bucket"); - - when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); - when(cdsRuntime.getEnvironment()).thenReturn(environment); - when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); - when(binding.getCredentials()).thenReturn(credentials); - when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); - - when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) - .thenReturn(Boolean.TRUE); - when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) - .thenReturn("shared"); - - registration.eventHandlers(configurer); - - // Verify a handler was registered — we trust the implementation passes the config - // through the constructor (which is verified by OSSAttachmentsServiceHandlerTest) - verify(configurer, times(2)).eventHandler(any()); - } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java deleted file mode 100644 index dfbcd0cc0..000000000 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/MultiTenantIsolationTest.java +++ /dev/null @@ -1,295 +0,0 @@ -/* - * © 2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.oss.handler; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.CALLS_REAL_METHODS; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; -import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; -import com.sap.cds.feature.attachments.oss.client.OSClient; -import com.sap.cds.services.ServiceException; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; -import com.sap.cds.services.request.UserInfo; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - * Integration-level test that verifies complete tenant isolation in shared mode. Uses a tracking - * mock OSClient to record all operations and verify that Tenant A's operations never touch Tenant - * B's key space. - */ -class MultiTenantIsolationTest { - - /** Tracks all upload, read, and delete keys passed to the OSClient. */ - private final List uploadedKeys = new ArrayList<>(); - - private final List readKeys = new ArrayList<>(); - private final List deletedKeys = new ArrayList<>(); - private final Map storage = new HashMap<>(); - - private OSClient trackingClient; - private OSSAttachmentsServiceHandler handler; - - @BeforeEach - void setUp() throws NoSuchFieldException, IllegalAccessException { - uploadedKeys.clear(); - readKeys.clear(); - deletedKeys.clear(); - storage.clear(); - - // Create a tracking OSClient that records all operations - trackingClient = mock(OSClient.class); - - when(trackingClient.uploadContent(any(InputStream.class), anyString(), anyString())) - .thenAnswer( - invocation -> { - InputStream content = invocation.getArgument(0); - String key = invocation.getArgument(1); - uploadedKeys.add(key); - storage.put(key, content.readAllBytes()); - return CompletableFuture.completedFuture(null); - }); - - when(trackingClient.readContent(anyString())) - .thenAnswer( - invocation -> { - String key = invocation.getArgument(0); - readKeys.add(key); - byte[] data = storage.get(key); - if (data != null) { - return CompletableFuture.completedFuture( - (InputStream) new ByteArrayInputStream(data)); - } - return CompletableFuture.completedFuture(null); - }); - - when(trackingClient.deleteContent(anyString())) - .thenAnswer( - invocation -> { - String key = invocation.getArgument(0); - deletedKeys.add(key); - storage.remove(key); - return CompletableFuture.completedFuture(null); - }); - - // Create handler with multitenancy enabled in shared mode - handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, trackingClient); - - var mtEnabledField = - OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtEnabledField.setAccessible(true); - mtEnabledField.set(handler, true); - - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - } - - @Test - void testTwoTenants_Upload_Read_Delete_CompleteIsolation() { - String tenantA = "tenantA"; - String tenantB = "tenantB"; - String contentIdA = "content-id-A"; - String contentIdB = "content-id-B"; - - // Tenant A uploads - simulateCreate(tenantA, contentIdA, "Data from Tenant A"); - // Tenant B uploads - simulateCreate(tenantB, contentIdB, "Data from Tenant B"); - - // Verify keys are prefixed correctly - assertEquals(2, uploadedKeys.size()); - assertTrue(uploadedKeys.contains(tenantA + "/" + contentIdA)); - assertTrue(uploadedKeys.contains(tenantB + "/" + contentIdB)); - - // Verify no cross-tenant key overlap - assertFalse(uploadedKeys.stream().anyMatch(k -> k.startsWith(tenantA) && k.contains(contentIdB))); - assertFalse(uploadedKeys.stream().anyMatch(k -> k.startsWith(tenantB) && k.contains(contentIdA))); - - // Tenant A reads its own content - simulateRead(tenantA, contentIdA); - assertEquals(tenantA + "/" + contentIdA, readKeys.get(0)); - - // Tenant B reads its own content - simulateRead(tenantB, contentIdB); - assertEquals(tenantB + "/" + contentIdB, readKeys.get(1)); - - // Tenant A deletes its content - simulateDelete(tenantA, contentIdA); - assertEquals(tenantA + "/" + contentIdA, deletedKeys.get(0)); - - // Tenant B's content is still in storage - assertTrue(storage.containsKey(tenantB + "/" + contentIdB)); - assertFalse(storage.containsKey(tenantA + "/" + contentIdA)); - - // Tenant B deletes its content - simulateDelete(tenantB, contentIdB); - assertEquals(tenantB + "/" + contentIdB, deletedKeys.get(1)); - - // Both are gone - assertTrue(storage.isEmpty()); - } - - @Test - void testTenantA_CannotAccess_TenantB_Data() { - String tenantA = "tenantA"; - String tenantB = "tenantB"; - String sharedContentId = "same-content-id"; - - // Both tenants upload with the same contentId (UUID) - simulateCreate(tenantA, sharedContentId, "Tenant A secret"); - simulateCreate(tenantB, sharedContentId, "Tenant B secret"); - - // Keys should be different due to tenant prefix - assertEquals(2, uploadedKeys.size()); - assertEquals(tenantA + "/" + sharedContentId, uploadedKeys.get(0)); - assertEquals(tenantB + "/" + sharedContentId, uploadedKeys.get(1)); - - // Storage should have 2 separate entries - assertEquals(2, storage.size()); - - // Reading as tenant A only returns tenant A's data - simulateRead(tenantA, sharedContentId); - assertEquals(tenantA + "/" + sharedContentId, readKeys.get(0)); - - // The read key never touches tenant B's namespace - assertFalse(readKeys.get(0).startsWith(tenantB)); - } - - @Test - void testNullTenant_ThrowsInSharedMTMode() { - String contentId = "content-no-tenant"; - - // In MT shared mode, null tenant must throw (H-1 security fix) - assertThrows( - ServiceException.class, () -> simulateCreate(null, contentId, "No tenant data")); - - // No upload should have occurred - assertEquals(0, uploadedKeys.size()); - } - - // ==================== Content ID Validation Integration Tests ==================== - - @Test - void testPathTraversalInContentId_BlockedInSharedMode() { - // A malicious contentId like "../other-tenant/secret" must be rejected - // to prevent tenant A from reading/writing tenant B's namespace - assertThrows( - ServiceException.class, - () -> simulateCreate("tenantA", "../other-tenant/secret", "malicious data")); - assertEquals(0, uploadedKeys.size()); - } - - @Test - void testSlashInContentId_BlockedInSharedMode() { - assertThrows( - ServiceException.class, - () -> simulateCreate("tenantA", "sub/path", "data")); - assertEquals(0, uploadedKeys.size()); - } - - @Test - void testBackslashInContentId_BlockedInSharedMode() { - assertThrows( - ServiceException.class, - () -> simulateCreate("tenantA", "evil\\path", "data")); - assertEquals(0, uploadedKeys.size()); - } - - @Test - void testEmptyContentId_BlockedInSharedMode() { - assertThrows( - ServiceException.class, - () -> simulateCreate("tenantA", "", "data")); - assertEquals(0, uploadedKeys.size()); - } - - @Test - void testDoubleDotInContentId_BlockedInSharedMode() { - assertThrows( - ServiceException.class, - () -> simulateCreate("tenantA", "..otherTenant", "data")); - assertEquals(0, uploadedKeys.size()); - } - - @Test - void testPathTraversalInContentId_FullIsolationScenario() { - // Tenant B uploads a legitimate file - simulateCreate("tenantB", "legit-content-id", "Tenant B secret"); - assertEquals(1, uploadedKeys.size()); - assertEquals("tenantB/legit-content-id", uploadedKeys.get(0)); - - // Tenant A tries to use path traversal to access tenant B's file - assertThrows( - ServiceException.class, - () -> simulateCreate("tenantA", "../tenantB/legit-content-id", "overwrite attempt")); - - // Only the original upload should exist - assertEquals(1, uploadedKeys.size()); - assertTrue(storage.containsKey("tenantB/legit-content-id")); - } - - private void simulateCreate(String tenant, String contentId, String content) { - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(com.sap.cds.reflect.CdsEntity.class); - UserInfo userInfo = mock(UserInfo.class); - - when(userInfo.getTenant()).thenReturn(tenant); - when(context.getUserInfo()).thenReturn(userInfo); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream(content.getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - - handler.createAttachment(context); - } - - private void simulateRead(String tenant, String contentId) { - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - UserInfo userInfo = mock(UserInfo.class); - - when(userInfo.getTenant()).thenReturn(tenant); - when(context.getUserInfo()).thenReturn(userInfo); - when(context.getContentId()).thenReturn(contentId); - when(context.getData()).thenReturn(mockMediaData); - - handler.readAttachment(context); - } - - private void simulateDelete(String tenant, String contentId) { - AttachmentMarkAsDeletedEventContext context = mock(AttachmentMarkAsDeletedEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(userInfo.getTenant()).thenReturn(tenant); - when(context.getUserInfo()).thenReturn(userInfo); - when(context.getContentId()).thenReturn(contentId); - - handler.markAttachmentAsDeleted(context); - } -} diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index e952aa102..2d973221b 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -11,7 +11,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; import com.sap.cds.feature.attachments.oss.client.OSClient; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; @@ -20,19 +19,16 @@ import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; import com.sap.cds.reflect.CdsEntity; import com.sap.cds.services.ServiceException; -import com.sap.cds.services.request.UserInfo; import com.sap.cloud.environment.servicebinding.api.ServiceBinding; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.Base64; import java.util.HashMap; -import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; class OSSAttachmentsServiceHandlerTest { ExecutorService executor = Executors.newCachedThreadPool(); @@ -49,8 +45,7 @@ void testRestoreAttachmentCallsSetCompleted() { creds.put("bucket", "test-bucket"); when(binding.getCredentials()).thenReturn(creds); - OSSAttachmentsServiceHandler handler = - new OSSAttachmentsServiceHandler(binding, executor, false, null); + OSSAttachmentsServiceHandler handler = new OSSAttachmentsServiceHandler(binding, executor); AttachmentRestoreEventContext context = mock(AttachmentRestoreEventContext.class); handler.restoreAttachment(context); verify(context).setCompleted(); @@ -184,7 +179,7 @@ void testConstructorHandlesInvalidBase64EncodedPrivateKeyData() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + () -> new OSSAttachmentsServiceHandler(binding, executor)); } @Test @@ -200,7 +195,7 @@ void testConstructorHandlesValidBase64ButNoGoogleOrGcp() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + () -> new OSSAttachmentsServiceHandler(binding, executor)); } @Test @@ -212,7 +207,7 @@ void testConstructorHandlesInValidBase64() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + () -> new OSSAttachmentsServiceHandler(binding, executor)); } @Test @@ -226,7 +221,7 @@ void testConstructorHandlesNoValidObjectStoreService() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + () -> new OSSAttachmentsServiceHandler(binding, executor)); } // Helper method to setup common mocks for createAttachment exception tests @@ -382,333 +377,4 @@ void testReadAttachmentHandlesInterruptedException() assertThrows(ServiceException.class, () -> handler.readAttachment(context)); verify(context).setCompleted(); } - - // ==================== Multi-Tenancy Tests (Phase 1 Shared Mode) ==================== - - /** - * Helper to create a handler with MT config injected via reflection. The implementation agent is - * adding multitenancyEnabled and objectStoreKind fields to the handler class. - */ - private OSSAttachmentsServiceHandler createMTHandler( - OSClient mockOsClient, boolean multitenancyEnabled, String objectStoreKind) - throws NoSuchFieldException, IllegalAccessException { - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, multitenancyEnabled); - - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, objectStoreKind); - - return handler; - } - - private static UserInfo mockUserInfo(String tenant) { - UserInfo userInfo = mock(UserInfo.class); - when(userInfo.getTenant()).thenReturn(tenant); - return userInfo; - } - - @Test - void testBuildObjectKey_SharedMode_PrefixesTenantId() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); - - String tenantId = "myTenant"; - String contentId = "content-uuid-123"; - - // Setup create context with tenant - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - UserInfo userInfo = mockUserInfo(tenantId); - when(context.getUserInfo()).thenReturn(userInfo); - when(mockOsClient.uploadContent(any(), anyString(), anyString())) - .thenReturn(CompletableFuture.completedFuture(null)); - - handler.createAttachment(context); - - // Capture the object key passed to uploadContent - ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); - verify(mockOsClient).uploadContent(any(), keyCaptor.capture(), anyString()); - // In shared mode, the key should be tenantId/contentId - org.junit.jupiter.api.Assertions.assertEquals( - tenantId + "/" + contentId, keyCaptor.getValue()); - } - - @Test - void testBuildObjectKey_NonMTMode_NoPrefixing() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - // multitenancy disabled - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, false, null); - - String contentId = "content-uuid-456"; - - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - when(mockOsClient.uploadContent(any(), anyString(), anyString())) - .thenReturn(CompletableFuture.completedFuture(null)); - - handler.createAttachment(context); - - // Capture the object key passed to uploadContent - ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); - verify(mockOsClient).uploadContent(any(), keyCaptor.capture(), anyString()); - // When MT is off, key should be plain contentId with no prefix - org.junit.jupiter.api.Assertions.assertEquals(contentId, keyCaptor.getValue()); - } - - @Test - void testBuildObjectKey_NullTenant_ThrowsInMTMode() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); - - String contentId = "content-uuid-789"; - - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - // null tenant - UserInfo userInfo = mockUserInfo(null); - when(context.getUserInfo()).thenReturn(userInfo); - - // In MT mode, null tenant should throw ServiceException - assertThrows(ServiceException.class, () -> handler.createAttachment(context)); - } - - @Test - void testCreateAttachment_SharedMode_UsesObjectKey() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); - - String tenantId = "tenantX"; - String contentId = "doc-create-123"; - - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("data".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("application/pdf"); - UserInfo userInfoX = mockUserInfo(tenantId); - when(context.getUserInfo()).thenReturn(userInfoX); - when(mockOsClient.uploadContent(any(), anyString(), anyString())) - .thenReturn(CompletableFuture.completedFuture(null)); - - handler.createAttachment(context); - - ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); - verify(mockOsClient).uploadContent(any(), keyCaptor.capture(), anyString()); - org.junit.jupiter.api.Assertions.assertEquals( - tenantId + "/" + contentId, keyCaptor.getValue()); - // contentId stored in the context should remain unprefixed - verify(context).setContentId(contentId); - } - - @Test - void testReadAttachment_SharedMode_UsesObjectKey() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); - - String tenantId = "tenantRead"; - String contentId = "doc-read-456"; - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - when(context.getContentId()).thenReturn(contentId); - when(context.getData()).thenReturn(mockMediaData); - UserInfo userInfoRead = mockUserInfo(tenantId); - when(context.getUserInfo()).thenReturn(userInfoRead); - when(mockOsClient.readContent(anyString())) - .thenReturn( - CompletableFuture.completedFuture(new ByteArrayInputStream("test".getBytes()))); - - handler.readAttachment(context); - - // Verify the read uses the prefixed key - ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); - verify(mockOsClient).readContent(keyCaptor.capture()); - org.junit.jupiter.api.Assertions.assertEquals( - tenantId + "/" + contentId, keyCaptor.getValue()); - } - - @Test - void testDeleteAttachment_SharedMode_UsesObjectKey() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); - - String tenantId = "tenantDel"; - String contentId = "doc-del-789"; - - AttachmentMarkAsDeletedEventContext context = mock(AttachmentMarkAsDeletedEventContext.class); - when(context.getContentId()).thenReturn(contentId); - UserInfo userInfoDel = mockUserInfo(tenantId); - when(context.getUserInfo()).thenReturn(userInfoDel); - when(mockOsClient.deleteContent(anyString())) - .thenReturn(CompletableFuture.completedFuture(null)); - - handler.markAttachmentAsDeleted(context); - - // Verify the delete uses the prefixed key - ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); - verify(mockOsClient).deleteContent(keyCaptor.capture()); - org.junit.jupiter.api.Assertions.assertEquals( - tenantId + "/" + contentId, keyCaptor.getValue()); - } - - @Test - void testCreateAttachment_SingleTenant_NoPrefixing() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - // Backward compatibility: MT disabled - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, false, null); - - String contentId = "doc-single-abc"; - - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("data".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - when(mockOsClient.uploadContent(any(), anyString(), anyString())) - .thenReturn(CompletableFuture.completedFuture(null)); - - handler.createAttachment(context); - - ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); - verify(mockOsClient).uploadContent(any(), keyCaptor.capture(), anyString()); - // No prefix, just contentId - org.junit.jupiter.api.Assertions.assertEquals(contentId, keyCaptor.getValue()); - verify(context).setContentId(contentId); - } - - // ==================== Tenant ID Validation Tests ==================== - - @Test - void testValidateTenantId_EmptyTenant_ThrowsException() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); - - String contentId = "content-id"; - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - UserInfo userInfo = mockUserInfo(""); - when(context.getUserInfo()).thenReturn(userInfo); - - assertThrows(ServiceException.class, () -> handler.createAttachment(context)); - } - - @Test - void testValidateTenantId_SlashInTenant_ThrowsException() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); - - String contentId = "content-id"; - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - UserInfo userInfo = mockUserInfo("tenant/evil"); - when(context.getUserInfo()).thenReturn(userInfo); - - assertThrows(ServiceException.class, () -> handler.createAttachment(context)); - } - - @Test - void testValidateTenantId_BackslashInTenant_ThrowsException() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); - - String contentId = "content-id"; - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - UserInfo userInfo = mockUserInfo("tenant\\evil"); - when(context.getUserInfo()).thenReturn(userInfo); - - assertThrows(ServiceException.class, () -> handler.createAttachment(context)); - } - - @Test - void testValidateTenantId_PathTraversal_ThrowsException() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = createMTHandler(mockOsClient, true, "shared"); - - String contentId = "content-id"; - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - UserInfo userInfo = mockUserInfo("..evil"); - when(context.getUserInfo()).thenReturn(userInfo); - - assertThrows(ServiceException.class, () -> handler.createAttachment(context)); - } - } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java index f96178dd3..615aba16c 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java @@ -39,8 +39,7 @@ public static void testCreateReadDeleteAttachmentFlow( String testFileName = "testFileName-" + System.currentTimeMillis() + ".txt"; String testFileContent = "test"; - OSSAttachmentsServiceHandler handler = - new OSSAttachmentsServiceHandler(binding, executor, false, null); + OSSAttachmentsServiceHandler handler = new OSSAttachmentsServiceHandler(binding, executor); // Create an AttachmentCreateEventContext with mocked data - to upload a test attachment MediaData createMediaData = mock(MediaData.class); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java deleted file mode 100644 index 7f81186ff..000000000 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * © 2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.oss.handler; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.sap.cds.feature.attachments.oss.client.OSClient; -import com.sap.cds.services.mt.UnsubscribeEventContext; -import java.util.concurrent.CompletableFuture; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class TenantCleanupHandlerTest { - - private OSClient osClient; - private UnsubscribeEventContext context; - private TenantCleanupHandler handler; - - @BeforeEach - void setUp() { - osClient = mock(OSClient.class); - context = mock(UnsubscribeEventContext.class); - handler = new TenantCleanupHandler(osClient); - } - - @Test - void testCleanupTenantData_CallsDeleteByPrefix() throws Exception { - String tenantId = "tenant-abc"; - when(context.getTenant()).thenReturn(tenantId); - when(osClient.deleteContentByPrefix(tenantId + "/")) - .thenReturn(CompletableFuture.completedFuture(null)); - - handler.cleanupTenantData(context); - - verify(osClient).deleteContentByPrefix(tenantId + "/"); - } - - @Test - void testCleanupTenantData_UsesCorrectPrefix() throws Exception { - String tenantId = "my-tenant-123"; - when(context.getTenant()).thenReturn(tenantId); - when(osClient.deleteContentByPrefix("my-tenant-123/")) - .thenReturn(CompletableFuture.completedFuture(null)); - - handler.cleanupTenantData(context); - - verify(osClient).deleteContentByPrefix("my-tenant-123/"); - } - - @Test - void testCleanupTenantData_HandlesException() throws Exception { - String tenantId = "tenant-fail"; - when(context.getTenant()).thenReturn(tenantId); - - CompletableFuture failedFuture = new CompletableFuture<>(); - failedFuture.completeExceptionally(new RuntimeException("Storage error")); - when(osClient.deleteContentByPrefix(tenantId + "/")).thenReturn(failedFuture); - - // Should NOT rethrow — the handler logs the error but does not fail the unsubscribe - handler.cleanupTenantData(context); - - verify(osClient).deleteContentByPrefix(tenantId + "/"); - } - - @Test - void testCleanupTenantData_HandlesInterruptedException() throws Exception { - String tenantId = "tenant-interrupt"; - when(context.getTenant()).thenReturn(tenantId); - - CompletableFuture failedFuture = new CompletableFuture<>(); - failedFuture.completeExceptionally(new InterruptedException("Interrupted")); - when(osClient.deleteContentByPrefix(tenantId + "/")).thenReturn(failedFuture); - - // Should NOT rethrow — the handler logs the error but does not fail the unsubscribe - handler.cleanupTenantData(context); - - verify(osClient).deleteContentByPrefix(tenantId + "/"); - } -} From b1972eef3a46d82dff9e31248f1d13ab39c0c320 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Mon, 30 Mar 2026 21:15:34 +0200 Subject: [PATCH 04/39] spotless --- .../sap/cds/feature/attachments/oss/client/AWSClient.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java index 137a6c9fc..513a2b738 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java @@ -25,12 +25,12 @@ import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; import software.amazon.awssdk.services.s3.model.GetObjectRequest; -import software.amazon.awssdk.services.s3.model.S3Error; import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; import software.amazon.awssdk.services.s3.model.ObjectIdentifier; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.S3Error; public class AWSClient implements OSClient { private final S3Client s3Client; @@ -167,9 +167,7 @@ public Future deleteContentByPrefix(String prefix) { } } listReq = - listReq.toBuilder() - .continuationToken(listResp.nextContinuationToken()) - .build(); + listReq.toBuilder().continuationToken(listResp.nextContinuationToken()).build(); } while (listResp.isTruncated()); } catch (RuntimeException e) { throw new ObjectStoreServiceException( From 1df4b6bcfb45426cb2f1d218c81449de18fd2e66 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Mon, 30 Mar 2026 21:29:20 +0200 Subject: [PATCH 05/39] fix tests --- .../feature/attachments/oss/client/AWSClientTest.java | 2 +- .../oss/handler/OSSAttachmentsServiceHandlerTest.java | 11 ++++++----- .../OSSAttachmentsServiceHandlerTestUtils.java | 3 ++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java index 032c1a8a2..658157b3b 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java @@ -42,7 +42,7 @@ class AWSClientTest { void testConstructorWithAwsBindingUsesAwsClient() throws NoSuchFieldException, IllegalAccessException { OSSAttachmentsServiceHandler handler = - new OSSAttachmentsServiceHandler(getDummyBinding(), executor); + new OSSAttachmentsServiceHandler(getDummyBinding(), executor, false, null); OSClient client = OSSAttachmentsServiceHandlerTestUtils.getOsClient(handler); assertInstanceOf(AWSClient.class, client); } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index 2d973221b..62598354c 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -45,7 +45,8 @@ void testRestoreAttachmentCallsSetCompleted() { creds.put("bucket", "test-bucket"); when(binding.getCredentials()).thenReturn(creds); - OSSAttachmentsServiceHandler handler = new OSSAttachmentsServiceHandler(binding, executor); + OSSAttachmentsServiceHandler handler = + new OSSAttachmentsServiceHandler(binding, executor, false, null); AttachmentRestoreEventContext context = mock(AttachmentRestoreEventContext.class); handler.restoreAttachment(context); verify(context).setCompleted(); @@ -179,7 +180,7 @@ void testConstructorHandlesInvalidBase64EncodedPrivateKeyData() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor)); + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); } @Test @@ -195,7 +196,7 @@ void testConstructorHandlesValidBase64ButNoGoogleOrGcp() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor)); + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); } @Test @@ -207,7 +208,7 @@ void testConstructorHandlesInValidBase64() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor)); + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); } @Test @@ -221,7 +222,7 @@ void testConstructorHandlesNoValidObjectStoreService() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor)); + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); } // Helper method to setup common mocks for createAttachment exception tests diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java index 615aba16c..f96178dd3 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java @@ -39,7 +39,8 @@ public static void testCreateReadDeleteAttachmentFlow( String testFileName = "testFileName-" + System.currentTimeMillis() + ".txt"; String testFileContent = "test"; - OSSAttachmentsServiceHandler handler = new OSSAttachmentsServiceHandler(binding, executor); + OSSAttachmentsServiceHandler handler = + new OSSAttachmentsServiceHandler(binding, executor, false, null); // Create an AttachmentCreateEventContext with mocked data - to upload a test attachment MediaData createMediaData = mock(MediaData.class); From b6d76956ab7f799b1f0cc520fb3d01de44d1eb4b Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 09:58:45 +0200 Subject: [PATCH 06/39] splut up test modules --- integration-tests/.cdsrc.json | 5 + integration-tests/{srv => generic}/pom.xml | 4 +- .../integrationtests/Application.java | 0 .../integrationtests/constants/Profiles.java | 0 .../testhandler/EventContextHolder.java | 0 .../testhandler/TestPersistenceHandler.java | 0 .../TestPluginAttachmentsServiceHandler.java | 0 .../src/main/resources/application.yaml | 0 .../src/main/resources/banner.txt | 0 .../src/main/resources/messages.properties | 0 .../resources/spotbugs-exclusion-filter.xml | 0 .../common/JsonToCapMapperTestHelper.java | 0 .../common/MalwareScanResultProvider.java | 0 .../common/MockHttpRequestHelper.java | 0 .../common/TableDataDeleter.java | 0 .../DraftOdataRequestValidationBase.java | 0 ...aRequestValidationWithTestHandlerTest.java | 0 ...thoutTestHandlerAndMalwareScannerTest.java | 0 ...stHandlerAndWithoutMalwareScannerTest.java | 0 .../MediaValidatedAttachmentsDraftTest.java | 0 ...tedAttachmentsSizeValidationDraftTest.java | 0 ...MediaValidatedAttachmentsNonDraftTest.java | 0 .../OdataRequestValidationBase.java | 0 ...aRequestValidationWithTestHandlerTest.java | 0 ...thoutTestHandlerAndMalwareScannerTest.java | 0 ...stHandlerAndWithoutMalwareScannerTest.java | 0 ...mitedAttachmentValidationNonDraftTest.java | 0 .../helper/AttachmentsBuilder.java | 0 .../helper/AttachmentsEntityBuilder.java | 0 .../helper/ItemEntityBuilder.java | 0 .../helper/RootEntityBuilder.java | 0 .../TestPersistenceHandlerTest.java | 0 ...stPluginAttachmentsServiceHandlerTest.java | 0 .../src/test/resources/application.yaml | 0 .../src/test/resources/logback-test.xml | 0 .../src/test/resources/xsuaa-env.json | 0 .../{srv => generic}/test-service.cds | 0 integration-tests/mtx-local/.cdsrc.json | 14 + integration-tests/mtx-local/db/index.cds | 1 + integration-tests/mtx-local/db/schema.cds | 9 + .../mtx-local/mtx/sidecar/package.json | 30 + integration-tests/mtx-local/package-lock.json | 4144 +++++++++++++++++ integration-tests/mtx-local/package.json | 11 + integration-tests/mtx-local/srv/pom.xml | 250 + integration-tests/mtx-local/srv/service.cds | 5 + .../integrationtests/mt/Application.java | 15 + .../system/SubscribeModelTenantsHandler.java | 90 + .../srv/src/main/resources/application.yaml | 23 + .../MultiTenantAttachmentIsolationTest.java | 115 + .../mt/SubscribeAndUnsubscribeTest.java | 72 + .../mt/utils/SubscriptionEndpointClient.java | 67 + integration-tests/pom.xml | 3 +- 52 files changed, 4855 insertions(+), 3 deletions(-) rename integration-tests/{srv => generic}/pom.xml (96%) rename integration-tests/{srv => generic}/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java (100%) rename integration-tests/{srv => generic}/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java (100%) rename integration-tests/{srv => generic}/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java (100%) rename integration-tests/{srv => generic}/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java (100%) rename integration-tests/{srv => generic}/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java (100%) rename integration-tests/{srv => generic}/src/main/resources/application.yaml (100%) rename integration-tests/{srv => generic}/src/main/resources/banner.txt (100%) rename integration-tests/{srv => generic}/src/main/resources/messages.properties (100%) rename integration-tests/{srv => generic}/src/main/resources/spotbugs-exclusion-filter.xml (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java (100%) rename integration-tests/{srv => generic}/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java (100%) rename integration-tests/{srv => generic}/src/test/resources/application.yaml (100%) rename integration-tests/{srv => generic}/src/test/resources/logback-test.xml (100%) rename integration-tests/{srv => generic}/src/test/resources/xsuaa-env.json (100%) rename integration-tests/{srv => generic}/test-service.cds (100%) create mode 100644 integration-tests/mtx-local/.cdsrc.json create mode 100644 integration-tests/mtx-local/db/index.cds create mode 100644 integration-tests/mtx-local/db/schema.cds create mode 100644 integration-tests/mtx-local/mtx/sidecar/package.json create mode 100644 integration-tests/mtx-local/package-lock.json create mode 100644 integration-tests/mtx-local/package.json create mode 100644 integration-tests/mtx-local/srv/pom.xml create mode 100644 integration-tests/mtx-local/srv/service.cds create mode 100644 integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/Application.java create mode 100644 integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java create mode 100644 integration-tests/mtx-local/srv/src/main/resources/application.yaml create mode 100644 integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java create mode 100644 integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/SubscribeAndUnsubscribeTest.java create mode 100644 integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java diff --git a/integration-tests/.cdsrc.json b/integration-tests/.cdsrc.json index c2d556179..0b8f71263 100644 --- a/integration-tests/.cdsrc.json +++ b/integration-tests/.cdsrc.json @@ -7,6 +7,11 @@ }, "fiori": { "draft_messages": false + }, + "build": { + "tasks": [ + { "for": "java", "src": "generic" } + ] } } } \ No newline at end of file diff --git a/integration-tests/srv/pom.xml b/integration-tests/generic/pom.xml similarity index 96% rename from integration-tests/srv/pom.xml rename to integration-tests/generic/pom.xml index 6a5b6eada..265826f00 100644 --- a/integration-tests/srv/pom.xml +++ b/integration-tests/generic/pom.xml @@ -8,10 +8,10 @@ ${revision} - cds-feature-attachments-integration-tests-srv + cds-feature-attachments-integration-tests-generic jar - Integration Tests - Service + Integration Tests - Generic com.sap.cds.feature.attachments.generated diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java similarity index 100% rename from integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java rename to integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java similarity index 100% rename from integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java rename to integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java similarity index 100% rename from integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java rename to integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java similarity index 100% rename from integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java rename to integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java b/integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java similarity index 100% rename from integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java rename to integration-tests/generic/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java diff --git a/integration-tests/srv/src/main/resources/application.yaml b/integration-tests/generic/src/main/resources/application.yaml similarity index 100% rename from integration-tests/srv/src/main/resources/application.yaml rename to integration-tests/generic/src/main/resources/application.yaml diff --git a/integration-tests/srv/src/main/resources/banner.txt b/integration-tests/generic/src/main/resources/banner.txt similarity index 100% rename from integration-tests/srv/src/main/resources/banner.txt rename to integration-tests/generic/src/main/resources/banner.txt diff --git a/integration-tests/srv/src/main/resources/messages.properties b/integration-tests/generic/src/main/resources/messages.properties similarity index 100% rename from integration-tests/srv/src/main/resources/messages.properties rename to integration-tests/generic/src/main/resources/messages.properties diff --git a/integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml b/integration-tests/generic/src/main/resources/spotbugs-exclusion-filter.xml similarity index 100% rename from integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml rename to integration-tests/generic/src/main/resources/spotbugs-exclusion-filter.xml diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java similarity index 100% rename from integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java rename to integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java diff --git a/integration-tests/srv/src/test/resources/application.yaml b/integration-tests/generic/src/test/resources/application.yaml similarity index 100% rename from integration-tests/srv/src/test/resources/application.yaml rename to integration-tests/generic/src/test/resources/application.yaml diff --git a/integration-tests/srv/src/test/resources/logback-test.xml b/integration-tests/generic/src/test/resources/logback-test.xml similarity index 100% rename from integration-tests/srv/src/test/resources/logback-test.xml rename to integration-tests/generic/src/test/resources/logback-test.xml diff --git a/integration-tests/srv/src/test/resources/xsuaa-env.json b/integration-tests/generic/src/test/resources/xsuaa-env.json similarity index 100% rename from integration-tests/srv/src/test/resources/xsuaa-env.json rename to integration-tests/generic/src/test/resources/xsuaa-env.json diff --git a/integration-tests/srv/test-service.cds b/integration-tests/generic/test-service.cds similarity index 100% rename from integration-tests/srv/test-service.cds rename to integration-tests/generic/test-service.cds diff --git a/integration-tests/mtx-local/.cdsrc.json b/integration-tests/mtx-local/.cdsrc.json new file mode 100644 index 000000000..75b24c377 --- /dev/null +++ b/integration-tests/mtx-local/.cdsrc.json @@ -0,0 +1,14 @@ +{ + "profile": "with-mtx-sidecar", + "requires": { + "multitenancy": true, + "extensibility": true, + "toggles": true, + "db": { + "kind": "sqlite" + } + }, + "cdsc": { + "moduleLookupDirectories": ["node_modules/", "target/cds/"] + } +} diff --git a/integration-tests/mtx-local/db/index.cds b/integration-tests/mtx-local/db/index.cds new file mode 100644 index 000000000..3771e1112 --- /dev/null +++ b/integration-tests/mtx-local/db/index.cds @@ -0,0 +1 @@ +using from './schema.cds'; diff --git a/integration-tests/mtx-local/db/schema.cds b/integration-tests/mtx-local/db/schema.cds new file mode 100644 index 000000000..9edbfde50 --- /dev/null +++ b/integration-tests/mtx-local/db/schema.cds @@ -0,0 +1,9 @@ +namespace mt.test.data; + +using { cuid } from '@sap/cds/common'; +using { sap.attachments.Attachments } from 'com.sap.cds/cds-feature-attachments'; + +entity Documents : cuid { + title : String; + attachments : Composition of many Attachments; +} diff --git a/integration-tests/mtx-local/mtx/sidecar/package.json b/integration-tests/mtx-local/mtx/sidecar/package.json new file mode 100644 index 000000000..b2936eb9d --- /dev/null +++ b/integration-tests/mtx-local/mtx/sidecar/package.json @@ -0,0 +1,30 @@ +{ + "name": "mtx-local-sidecar", + "version": "0.0.0", + "dependencies": { + "@sap/cds": "^9", + "@sap/cds-mtxs": "^3", + "@sap/xssec": "^4", + "express": "^4" + }, + "devDependencies": { + "@cap-js/sqlite": "^2" + }, + "cds": { + "profile": "mtx-sidecar", + "[development]": { + "requires": { + "auth": "dummy" + }, + "db": { + "kind": "sqlite", + "credentials": { + "url": "../../db.sqlite" + } + } + } + }, + "scripts": { + "start": "cds-serve --profile development" + } +} diff --git a/integration-tests/mtx-local/package-lock.json b/integration-tests/mtx-local/package-lock.json new file mode 100644 index 000000000..3383043ec --- /dev/null +++ b/integration-tests/mtx-local/package-lock.json @@ -0,0 +1,4144 @@ +{ + "name": "mtx-local-integration-tests", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mtx-local-integration-tests", + "version": "0.0.0", + "workspaces": [ + "mtx/sidecar" + ], + "devDependencies": { + "@sap/cds-dk": "^9", + "@sap/cds-mtxs": "^3" + } + }, + "mtx/sidecar": { + "name": "mtx-local-sidecar", + "version": "0.0.0", + "dependencies": { + "@sap/cds": "^9", + "@sap/cds-mtxs": "^3", + "@sap/xssec": "^4", + "express": "^4" + }, + "devDependencies": { + "@cap-js/sqlite": "^2" + } + }, + "mtx/sidecar/node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "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.14.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/@cap-js/db-service": { + "version": "2.9.0", + "resolved": "https://int.repositories.cloud.sap/artifactory/api/npm/build-milestones-npm/@cap-js/db-service/-/db-service-2.9.0.tgz", + "integrity": "sha512-WCXhoqezaF6A5I2l0MNZeHKXXtHRNEq7Rp0R89/uccOHQIx0DuU0U9NuJJPV/1G5RGk2QKQ9VBo/KYn+MZuuNQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "generic-pool": "^3.9.0" + }, + "peerDependencies": { + "@sap/cds": ">=9.8" + } + }, + "node_modules/@cap-js/sqlite": { + "version": "2.2.0", + "resolved": "https://int.repositories.cloud.sap/artifactory/api/npm/build-milestones-npm/@cap-js/sqlite/-/sqlite-2.2.0.tgz", + "integrity": "sha512-FPj+uVU/14vtGUl2P/Q8y7XhZbsLgrCav2O5PjHPXnupegjby4sMJkgVNxVHnkyKPFgO/W8uEsq9r5TU9VPx8w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@cap-js/db-service": "^2.9.0", + "better-sqlite3": "^12.0.0" + }, + "peerDependencies": { + "@sap/cds": ">=9.8", + "sql.js": "^1.13.0" + }, + "peerDependenciesMeta": { + "sql.js": { + "optional": true + } + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "license": "MIT", + "peer": true, + "engines": { + "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": { + "version": "9.8.4", + "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-9.8.4.tgz", + "integrity": "sha512-1cnFdz4Ex4LV15dztDsSLDiilVdJ2mfTqr5A3TGsRd/6cM31SnHhuZ/Ra0FEErodokNS2pwasGZmSfgHVDW8KA==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@sap/cds-compiler": "^6.4", + "@sap/cds-fiori": "^2", + "express": "^4.22.1 || ^5", + "js-yaml": "^4.1.1" + }, + "bin": { + "cds-deploy": "bin/deploy.js", + "cds-serve": "bin/serve.js" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@eslint/js": "^9 || ^10", + "tar": "^7.5.6" + }, + "peerDependenciesMeta": { + "tar": { + "optional": true + } + } + }, + "node_modules/@sap/cds-compiler": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-6.8.0.tgz", + "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", + "license": "SEE LICENSE IN LICENSE", + "bin": { + "cdsc": "bin/cdsc.js", + "cdshi": "bin/cdshi.js", + "cdsse": "bin/cdsse.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sap/cds-dk": { + "version": "9.8.2", + "resolved": "https://registry.npmjs.org/@sap/cds-dk/-/cds-dk-9.8.2.tgz", + "integrity": "sha512-PGynoQpOCD8Nsr0EEzsEHWg8szYBLVtVd6byxvaaAPYBkNAj2WA+kin6gWaDTiyG3kJa0BSGPk4PCbA2xDoE9A==", + "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/hdi-deploy": "^5", + "axios": "^1", + "express": "^4.22.1 || ^5", + "hdb": "^2.0.0", + "livereload-js": "^4.0.1", + "mustache": "^4.0.1", + "ws": "^8.4.2", + "xml-js": "^1.6.11", + "yaml": "^2" + }, + "bin": { + "cds": "bin/cds.js", + "cds-ts": "bin/cds-ts.js", + "cds-tsx": "bin/cds-tsx.js" + }, + "optionalDependencies": { + "@cap-js/sqlite": ">=1" + } + }, + "node_modules/@sap/cds-dk/node_modules/@cap-js/asyncapi": { + "version": "1.0.3", + "integrity": "sha512-vZSWKAe+3qfvZDXV5SSFiObGWmqyS9MDyEADb5PLVT8kzO39qGaSDPv/GzI/gwvRfCayGAjU4ThiBKrFA7Gclg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "peerDependencies": { + "@sap/cds": ">=7.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/@cap-js/db-service": { + "version": "2.9.0", + "integrity": "sha512-WCXhoqezaF6A5I2l0MNZeHKXXtHRNEq7Rp0R89/uccOHQIx0DuU0U9NuJJPV/1G5RGk2QKQ9VBo/KYn+MZuuNQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "generic-pool": "^3.9.0" + }, + "peerDependencies": { + "@sap/cds": ">=9.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/@cap-js/openapi": { + "version": "1.4.0", + "integrity": "sha512-/LRSwn4SDxAi3qKwl09zoOhEVGaPGlYOPz/0S3UBnaMJVvaLyPiKbbaOtOnrrgulUX5OXt+ujPIQznOsbTzuAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "pluralize": "^8.0.0" + }, + "peerDependencies": { + "@sap/cds": ">=7.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/@cap-js/sqlite": { + "version": "2.2.0", + "integrity": "sha512-FPj+uVU/14vtGUl2P/Q8y7XhZbsLgrCav2O5PjHPXnupegjby4sMJkgVNxVHnkyKPFgO/W8uEsq9r5TU9VPx8w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@cap-js/db-service": "^2.9.0", + "better-sqlite3": "^12.0.0" + }, + "peerDependencies": { + "@sap/cds": ">=9.8", + "sql.js": "^1.13.0" + }, + "peerDependenciesMeta": { + "sql.js": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/@eslint/js": { + "version": "10.0.1", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "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.8.3", + "integrity": "sha512-up/kNVCZK8bOTyye1QN2BjMpH+IgcdyhT3U9mS3TqkpmjNE2JXq8Qz1iNAN+nYcXRWJ1XAUNpgebLfuh/49MGQ==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@sap/cds-compiler": "^6.4", + "@sap/cds-fiori": "^2", + "express": "^4.22.1 || ^5", + "js-yaml": "^4.1.1" + }, + "bin": { + "cds-deploy": "bin/deploy.js", + "cds-serve": "bin/serve.js" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@eslint/js": "^9 || ^10", + "tar": "^7.5.6" + }, + "peerDependenciesMeta": { + "tar": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/cds-compiler": { + "version": "6.8.0", + "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "bin": { + "cdsc": "bin/cdsc.js", + "cdshi": "bin/cdshi.js", + "cdsse": "bin/cdsse.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/cds-fiori": { + "version": "2.3.0", + "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "peerDependencies": { + "@sap/cds": ">=8" + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/cds-mtxs": { + "version": "3.8.1", + "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@sap/hdi-deploy": "^5" + }, + "bin": { + "cds-mtx": "bin/cds-mtx.js", + "cds-mtx-migrate": "bin/cds-mtx-migrate.js" + }, + "peerDependencies": { + "@sap/cds": "^9" + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/hdi": { + "version": "4.8.0", + "integrity": "sha512-tkJmY2ffm6mt4/LFwRBihlQkMxNAXa3ngvRe2N/6+qLIsUNdrH/M03S5mkygXq56K+KoVVZYuradajCusMWwsw==", + "dev": true, + "license": "See LICENSE file", + "dependencies": { + "async": "^3.2.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@sap/hana-client": "^2 >= 2.5", + "hdb": "^2 || ^0" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + }, + "hdb": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/hdi-deploy": { + "version": "5.6.1", + "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", + "dev": true, + "license": "See LICENSE file", + "dependencies": { + "@sap/hdi": "^4.8.0", + "@sap/xsenv": "^6.0.0", + "async": "^3.2.6", + "dotenv": "^16.4.5", + "handlebars": "^4.7.8", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=18.x" + }, + "peerDependencies": { + "@sap/hana-client": "^2 >= 2.6", + "hdb": "^2 || ^0" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + }, + "hdb": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/xsenv": { + "version": "6.1.0", + "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE file", + "dependencies": { + "debug": "4.4.3", + "node-cache": "^5.1.2", + "verror": "1.10.1" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/accepts": { + "version": "2.0.0", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "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/assert-plus": { + "version": "1.0.0", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/async": { + "version": "3.2.6", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "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.13.6", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/base64-js": { + "version": "1.5.1", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/better-sqlite3": { + "version": "12.8.0", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/@sap/cds-dk/node_modules/bindings": { + "version": "1.5.0", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/bl": { + "version": "4.1.0", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/body-parser": { + "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": "^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": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/braces": { + "version": "3.0.3", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sap/cds-dk/node_modules/buffer": { + "version": "5.7.1", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/@sap/cds-dk/node_modules/bytes": { + "version": "3.1.2", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/call-bound": { + "version": "1.0.4", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "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/@sap/cds-dk/node_modules/chownr": { + "version": "1.1.4", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/clone": { + "version": "2.1.2", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "license": "MIT", + "engines": { + "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": "1.0.1", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/content-type": { + "version": "1.0.5", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/cookie": { + "version": "0.7.2", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/cookie-signature": { + "version": "1.2.2", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/core-util-is": { + "version": "1.0.2", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/debug": { + "version": "4.4.3", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/decompress-response": { + "version": "6.0.0", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sap/cds-dk/node_modules/deep-extend": { + "version": "0.6.0", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/detect-libc": { + "version": "2.1.2", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sap/cds-dk/node_modules/dotenv": { + "version": "16.6.1", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@sap/cds-dk/node_modules/dunder-proto": { + "version": "1.0.1", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/ee-first": { + "version": "1.1.1", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/encodeurl": { + "version": "2.0.0", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/end-of-stream": { + "version": "1.4.5", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/es-define-property": { + "version": "1.0.1", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/es-errors": { + "version": "1.3.0", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/es-object-atoms": { + "version": "1.1.1", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/etag": { + "version": "1.8.1", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/expand-template": { + "version": "2.0.3", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sap/cds-dk/node_modules/express": { + "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/extsprintf": { + "version": "1.4.1", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/file-uri-to-path": { + "version": "1.0.0", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/fill-range": { + "version": "7.1.1", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sap/cds-dk/node_modules/finalhandler": { + "version": "2.1.1", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "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": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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.5", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "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" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/forwarded": { + "version": "0.2.0", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/fresh": { + "version": "2.0.0", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/fs-constants": { + "version": "1.0.0", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/function-bind": { + "version": "1.1.2", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/generic-pool": { + "version": "3.9.0", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@sap/cds-dk/node_modules/get-intrinsic": { + "version": "1.3.0", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "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/@sap/cds-dk/node_modules/get-proto": { + "version": "1.0.1", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/github-from-package": { + "version": "0.0.0", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/gopd": { + "version": "1.2.0", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/handlebars": { + "version": "4.7.8", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/has-symbols": { + "version": "1.1.0", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/hdb": { + "version": "2.27.1", + "integrity": "sha512-xYL/W+fq2TyGHyzm8muolQnw8tdh4+2NQ8mQP2FpLSuhfJ8l0jQNSUZoAXic7NfMEan1Jvf8V1L4blwkgTc6+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "iconv-lite": "0.7.0" + }, + "engines": { + "node": ">= 18" + }, + "optionalDependencies": { + "lz4-wasm-nodejs": "0.9.2" + } + }, + "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.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": { + "version": "1.2.1", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/inherits": { + "version": "2.0.4", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@sap/cds-dk/node_modules/ini": { + "version": "1.3.8", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/ipaddr.js": { + "version": "1.9.1", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@sap/cds-dk/node_modules/is-number": { + "version": "7.0.0", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/is-promise": { + "version": "4.0.0", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/js-yaml": { + "version": "4.1.1", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@sap/cds-dk/node_modules/livereload-js": { + "version": "4.0.2", + "integrity": "sha512-Fy7VwgQNiOkynYyNBTo3v9hQUhcW5pFAheJN148+DTgpShjsy/22pLHKKwDK5v0kOsZsJBK+6q1PMgLvRmrwFQ==", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/media-typer": { + "version": "1.1.0", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/merge-descriptors": { + "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/micromatch": { + "version": "4.0.8", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@sap/cds-dk/node_modules/mime-db": { + "version": "1.54.0", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/mime-types": { + "version": "3.0.2", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/mimic-response": { + "version": "3.1.0", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sap/cds-dk/node_modules/minimist": { + "version": "1.2.8", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/mkdirp-classic": { + "version": "0.5.3", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/ms": { + "version": "2.1.3", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/mustache": { + "version": "4.2.0", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/@sap/cds-dk/node_modules/napi-build-utils": { + "version": "2.0.0", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/negotiator": { + "version": "1.0.0", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/neo-async": { + "version": "2.6.2", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/node-abi": { + "version": "3.89.0", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@sap/cds-dk/node_modules/node-cache": { + "version": "5.1.2", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/object-inspect": { + "version": "1.13.4", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/on-finished": { + "version": "2.4.1", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/once": { + "version": "1.4.0", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/@sap/cds-dk/node_modules/parseurl": { + "version": "1.3.3", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/path-to-regexp": { + "version": "8.3.0", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/pluralize": { + "version": "8.0.0", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@sap/cds-dk/node_modules/proxy-addr": { + "version": "2.0.7", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "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.4", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/@sap/cds-dk/node_modules/qs": { + "version": "6.15.0", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/range-parser": { + "version": "1.2.1", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/raw-body": { + "version": "3.0.2", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@sap/cds-dk/node_modules/rc": { + "version": "1.2.8", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/@sap/cds-dk/node_modules/readable-stream": { + "version": "3.6.2", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "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==", + "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", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/safer-buffer": { + "version": "2.1.2", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/sax": { + "version": "1.6.0", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/semver": { + "version": "7.7.4", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@sap/cds-dk/node_modules/send": { + "version": "1.2.1", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "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": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/serve-static": { + "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": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/setprototypeof": { + "version": "1.2.0", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/@sap/cds-dk/node_modules/side-channel": { + "version": "1.1.0", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "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/@sap/cds-dk/node_modules/side-channel-list": { + "version": "1.0.0", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/side-channel-map": { + "version": "1.0.1", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "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/@sap/cds-dk/node_modules/side-channel-weakmap": { + "version": "1.0.2", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "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/@sap/cds-dk/node_modules/simple-concat": { + "version": "1.0.1", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/simple-get": { + "version": "4.0.1", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/source-map": { + "version": "0.6.1", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/statuses": { + "version": "2.0.2", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/string_decoder": { + "version": "1.3.0", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/strip-json-comments": { + "version": "2.0.1", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/tar-fs": { + "version": "2.1.4", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/tar-stream": { + "version": "2.2.0", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sap/cds-dk/node_modules/to-regex-range": { + "version": "5.0.1", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/toidentifier": { + "version": "1.0.1", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/tunnel-agent": { + "version": "0.6.0", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@sap/cds-dk/node_modules/type-is": { + "version": "2.0.1", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/uglify-js": { + "version": "3.19.3", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/unpipe": { + "version": "1.0.0", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/util-deprecate": { + "version": "1.0.2", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/vary": { + "version": "1.1.2", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/verror": { + "version": "1.10.1", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/wordwrap": { + "version": "1.0.0", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/wrappy": { + "version": "1.0.2", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@sap/cds-dk/node_modules/ws": { + "version": "8.20.0", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/xml-js": { + "version": "1.6.11", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/@sap/cds-dk/node_modules/yaml": { + "version": "2.8.3", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/@sap/cds-fiori": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sap/cds-fiori/-/cds-fiori-2.3.0.tgz", + "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", + "license": "SEE LICENSE IN LICENSE", + "peerDependencies": { + "@sap/cds": ">=8" + } + }, + "node_modules/@sap/cds-mtxs": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@sap/cds-mtxs/-/cds-mtxs-3.8.1.tgz", + "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@sap/hdi-deploy": "^5" + }, + "bin": { + "cds-mtx": "bin/cds-mtx.js", + "cds-mtx-migrate": "bin/cds-mtx-migrate.js" + }, + "peerDependencies": { + "@sap/cds": "^9" + } + }, + "node_modules/@sap/hdi": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@sap/hdi/-/hdi-4.8.0.tgz", + "integrity": "sha512-tkJmY2ffm6mt4/LFwRBihlQkMxNAXa3ngvRe2N/6+qLIsUNdrH/M03S5mkygXq56K+KoVVZYuradajCusMWwsw==", + "license": "See LICENSE file", + "dependencies": { + "async": "^3.2.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@sap/hana-client": "^2 >= 2.5", + "hdb": "^2 || ^0" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + }, + "hdb": { + "optional": true + } + } + }, + "node_modules/@sap/hdi-deploy": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@sap/hdi-deploy/-/hdi-deploy-5.6.1.tgz", + "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", + "license": "See LICENSE file", + "dependencies": { + "@sap/hdi": "^4.8.0", + "@sap/xsenv": "^6.0.0", + "async": "^3.2.6", + "dotenv": "^16.4.5", + "handlebars": "^4.7.8", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=18.x" + }, + "peerDependencies": { + "@sap/hana-client": "^2 >= 2.6", + "hdb": "^2 || ^0" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + }, + "hdb": { + "optional": true + } + } + }, + "node_modules/@sap/xsenv": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@sap/xsenv/-/xsenv-6.1.0.tgz", + "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", + "license": "SEE LICENSE IN LICENSE file", + "dependencies": { + "debug": "4.4.3", + "node-cache": "^5.1.2", + "verror": "1.10.1" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@sap/xsenv/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@sap/xsenv/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@sap/xssec": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@sap/xssec/-/xssec-4.13.0.tgz", + "integrity": "sha512-8e+bU+OyAIpAGXQanOopZa5YEK+yHKw84dhhihcCotF40MSNFbVHjQ4xM5hf4QndlqDGfXIuvXmoOMuDATa/gA==", + "license": "SAP DEVELOPER LICENSE AGREEMENT", + "dependencies": { + "debug": "^4.4.3", + "jwt-decode": "^4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sap/xssec/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@sap/xssec/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "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==", + "license": "MIT" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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" + }, + "node_modules/better-sqlite3": { + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "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.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "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/express/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "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": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "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": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "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/express/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/express/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/express/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "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": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "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.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "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/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/mtx-local-sidecar": { + "resolved": "mtx/sidecar", + "link": true + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "license": "MIT", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "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/router/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", + "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "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" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "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/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "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==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/integration-tests/mtx-local/package.json b/integration-tests/mtx-local/package.json new file mode 100644 index 000000000..ccad7b80d --- /dev/null +++ b/integration-tests/mtx-local/package.json @@ -0,0 +1,11 @@ +{ + "name": "mtx-local-integration-tests", + "version": "0.0.0", + "devDependencies": { + "@sap/cds-dk": "^9", + "@sap/cds-mtxs": "^3" + }, + "workspaces": [ + "mtx/sidecar" + ] +} diff --git a/integration-tests/mtx-local/srv/pom.xml b/integration-tests/mtx-local/srv/pom.xml new file mode 100644 index 000000000..5d9601fca --- /dev/null +++ b/integration-tests/mtx-local/srv/pom.xml @@ -0,0 +1,250 @@ + + + 4.0.0 + + + com.sap.cds.integration-tests + cds-feature-attachments-integration-tests-parent + ${revision} + ../../pom.xml + + + cds-feature-attachments-integration-tests-mtx-local + jar + + Integration Tests - MTX Local + + + com.sap.cds.feature.attachments.generated + ${project.basedir}/../mtx/sidecar + true + true + true + + + + + + com.sap.cds + cds-starter-spring-boot + + + + + com.sap.cds + cds-adapter-odata-v4 + runtime + + + + + com.sap.cds + cds-starter-cloudfoundry + + + + + org.xerial + sqlite-jdbc + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.security + spring-security-test + test + + + + + + + + org.apache.maven.plugins + maven-clean-plugin + + + + ${project.basedir}/.. + false + + *.db + *.sqlite* + + + + + + + sidecar-dbs-clean + + clean + + initialize + + + + + + + com.sap.cds + cds-maven-plugin + + + cds.clean + + clean + + + + + cds.install-node + + install-node + + + + + cds.resolve + + resolve + + + ${project.basedir}/.. + ${project.basedir}/.. + + + + + install-dependencies + + npm + + + ${project.basedir}/.. + + install + + + + + + cds.build + + cds + + + ${project.basedir}/.. + + build --for java + + + + + + cds.generate + + generate + + + ${project.basedir}/.. + ${generation-package}.mt.test.cds4j + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + execute-local-integration-tests + + integration-test + + integration-test + + + **/**/*Test.java + + + + + verify-local-integration-tests + + verify + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + copy-cds-models-to-sidecar + + copy-resources + + pre-integration-test + + ${project.basedir}/../node_modules/com.sap.cds/cds-feature-attachments + + + ${project.basedir}/../target/cds/com.sap.cds/cds-feature-attachments + + + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.5.0 + + ${cds.npm.executable} + + ${cds.node.directory}${path.separator}${env.PATH} + + ${skipTests} + ${sidecar.dir} + true + true + run start + + + + start-sidecar + + exec + + pre-integration-test + + + + + + + diff --git a/integration-tests/mtx-local/srv/service.cds b/integration-tests/mtx-local/srv/service.cds new file mode 100644 index 000000000..38b843210 --- /dev/null +++ b/integration-tests/mtx-local/srv/service.cds @@ -0,0 +1,5 @@ +using { mt.test.data as db } from '../db/index'; + +service MtTestService { + entity Documents as projection on db.Documents; +} diff --git a/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/Application.java b/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/Application.java new file mode 100644 index 000000000..fc98c7fb8 --- /dev/null +++ b/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/Application.java @@ -0,0 +1,15 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.mt; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java b/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java new file mode 100644 index 000000000..cde1364f6 --- /dev/null +++ b/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java @@ -0,0 +1,90 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.mt.handlers.system; + +import com.sap.cds.services.application.ApplicationLifecycleService; +import com.sap.cds.services.application.ApplicationPreparedEventContext; +import com.sap.cds.services.application.ApplicationStoppedEventContext; +import com.sap.cds.services.environment.CdsProperties; +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.mt.DeploymentService; +import com.sap.cds.services.runtime.CdsRuntime; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Component +@ServiceName(ApplicationLifecycleService.DEFAULT_NAME) +public class SubscribeModelTenantsHandler implements EventHandler { + + @Autowired private CdsRuntime runtime; + @Autowired private DeploymentService service; + + @On(event = ApplicationLifecycleService.EVENT_APPLICATION_PREPARED) + public void subscribeMockTenants(ApplicationPreparedEventContext context) { + var multiTenancy = runtime.getEnvironment().getCdsProperties().getMultiTenancy(); + + if (Boolean.FALSE.equals(multiTenancy.getMock().isEnabled())) { + return; + } + if (readMockedTenants().isEmpty()) { + return; + } + if (!StringUtils.hasText(multiTenancy.getSidecar().getUrl())) { + return; + } + + readMockedTenants().forEach(this::subscribeTenant); + } + + @On(event = ApplicationLifecycleService.EVENT_APPLICATION_STOPPED) + public void unsubscribeMockTenants(ApplicationStoppedEventContext context) { + var multiTenancy = runtime.getEnvironment().getCdsProperties().getMultiTenancy(); + + if (Boolean.FALSE.equals(multiTenancy.getMock().isEnabled())) { + return; + } + if (readMockedTenants().isEmpty()) { + return; + } + + readMockedTenants().forEach(this::unsubscribeTenant); + } + + private void subscribeTenant(String tenant) { + runtime + .requestContext() + .privilegedUser() + .run( + c -> { + service.subscribe( + tenant, + new HashMap<>( + Collections.singletonMap("subscribedSubdomain", "mt-" + tenant))); + }); + } + + private void unsubscribeTenant(String tenant) { + runtime + .requestContext() + .privilegedUser() + .run( + c -> { + service.unsubscribe(tenant, Collections.emptyMap()); + }); + } + + private List readMockedTenants() { + return runtime.getEnvironment().getCdsProperties().getSecurity().getMock().getTenants().values() + .stream() + .map(CdsProperties.Security.Mock.Tenant::getName) + .collect(Collectors.toList()); + } +} diff --git a/integration-tests/mtx-local/srv/src/main/resources/application.yaml b/integration-tests/mtx-local/srv/src/main/resources/application.yaml new file mode 100644 index 000000000..2bf2b72a3 --- /dev/null +++ b/integration-tests/mtx-local/srv/src/main/resources/application.yaml @@ -0,0 +1,23 @@ +cds: + multi-tenancy: + sidecar: + url: http://localhost:4005 + security: + mock: + users: + - name: user-in-tenant-1 + tenant: tenant-1 + - name: user-in-tenant-2 + tenant: tenant-2 + - name: user-in-tenant-3 + tenant: tenant-3 + +--- +spring: + config.activate.on-profile: local-with-tenants +cds: + security: + mock: + tenants: + - name: tenant-1 + - name: tenant-2 diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java new file mode 100644 index 000000000..14a22d2d4 --- /dev/null +++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java @@ -0,0 +1,115 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.mt; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.junit.jupiter.api.Test; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("local-with-tenants") +class MultiTenantAttachmentIsolationTest { + + private static final String DOCUMENTS_URL = "/odata/v4/MtTestService/Documents"; + + @Autowired MockMvc client; + @Autowired ObjectMapper objectMapper; + + @Test + void createDocumentInTenant1_notVisibleInTenant2() throws Exception { + // Create a document in tenant-1 + client + .perform( + post(DOCUMENTS_URL) + .with(httpBasic("user-in-tenant-1", "")) + .contentType(MediaType.APPLICATION_JSON) + .content("{ \"title\": \"Only in tenant-1\" }")) + .andExpect(status().isCreated()); + + // Read documents in tenant-2 — should NOT see the tenant-1 document + String response = + client + .perform(get(DOCUMENTS_URL).with(httpBasic("user-in-tenant-2", ""))) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + JsonNode values = objectMapper.readTree(response).path("value"); + values.forEach( + node -> assertThat(node.get("title").asText("")).isNotEqualTo("Only in tenant-1")); + } + + @Test + void createDocumentsInBothTenants_eachSeeOnlyOwn() throws Exception { + // Create in tenant-1 + client + .perform( + post(DOCUMENTS_URL) + .with(httpBasic("user-in-tenant-1", "")) + .contentType(MediaType.APPLICATION_JSON) + .content("{ \"title\": \"Doc-T1\" }")) + .andExpect(status().isCreated()); + + // Create in tenant-2 + client + .perform( + post(DOCUMENTS_URL) + .with(httpBasic("user-in-tenant-2", "")) + .contentType(MediaType.APPLICATION_JSON) + .content("{ \"title\": \"Doc-T2\" }")) + .andExpect(status().isCreated()); + + // Read from tenant-1 — should see Doc-T1 but not Doc-T2 + String response1 = + client + .perform(get(DOCUMENTS_URL).with(httpBasic("user-in-tenant-1", ""))) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + JsonNode values1 = objectMapper.readTree(response1).path("value"); + boolean foundT1 = false; + for (JsonNode node : values1) { + assertThat(node.get("title").asText("")).isNotEqualTo("Doc-T2"); + if ("Doc-T1".equals(node.get("title").asText(""))) { + foundT1 = true; + } + } + assertThat(foundT1).isTrue(); + + // Read from tenant-2 — should see Doc-T2 but not Doc-T1 + String response2 = + client + .perform(get(DOCUMENTS_URL).with(httpBasic("user-in-tenant-2", ""))) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + JsonNode values2 = objectMapper.readTree(response2).path("value"); + boolean foundT2 = false; + for (JsonNode node : values2) { + assertThat(node.get("title").asText("")).isNotEqualTo("Doc-T1"); + if ("Doc-T2".equals(node.get("title").asText(""))) { + foundT2 = true; + } + } + assertThat(foundT2).isTrue(); + } +} diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/SubscribeAndUnsubscribeTest.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/SubscribeAndUnsubscribeTest.java new file mode 100644 index 000000000..aaeb0673d --- /dev/null +++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/SubscribeAndUnsubscribeTest.java @@ -0,0 +1,72 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.mt; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sap.cds.feature.attachments.integrationtests.mt.utils.SubscriptionEndpointClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("local-with-tenants") +class SubscribeAndUnsubscribeTest { + + private static final String DOCUMENTS_URL = "/odata/v4/MtTestService/Documents"; + + @Autowired MockMvc client; + @Autowired ObjectMapper objectMapper; + + SubscriptionEndpointClient subscriptionEndpointClient; + + @BeforeEach + void setup() { + subscriptionEndpointClient = new SubscriptionEndpointClient(objectMapper, client); + } + + @Test + void subscribeTenant3_thenServiceIsReachable() throws Exception { + subscriptionEndpointClient.subscribeTenant("tenant-3"); + + client + .perform(get(DOCUMENTS_URL).with(httpBasic("user-in-tenant-3", ""))) + .andExpect(status().isOk()); + } + + @Test + void unsubscribeTenant3_thenServiceFails() throws Exception { + subscriptionEndpointClient.subscribeTenant("tenant-3"); + + // Verify it works + client + .perform(get(DOCUMENTS_URL).with(httpBasic("user-in-tenant-3", ""))) + .andExpect(status().isOk()); + + subscriptionEndpointClient.unsubscribeTenant("tenant-3"); + + // Service should fail after unsubscription + client + .perform(get(DOCUMENTS_URL).with(httpBasic("user-in-tenant-3", ""))) + .andExpect(status().isInternalServerError()); + } + + @AfterEach + void tearDown() { + try { + subscriptionEndpointClient.unsubscribeTenant("tenant-3"); + } catch (Exception ignored) { + // best effort cleanup + } + } +} diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java new file mode 100644 index 000000000..07fc0c8ff --- /dev/null +++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java @@ -0,0 +1,67 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.mt.utils; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.codec.binary.Base64; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +public class SubscriptionEndpointClient { + + private static final String MT_SUBSCRIPTIONS_TENANTS = "/mt/v1.0/subscriptions/tenants/"; + + private final ObjectMapper objectMapper; + private final MockMvc client; + private final String credentials = + "Basic " + new String(Base64.encodeBase64("privileged:".getBytes())); + + public SubscriptionEndpointClient(ObjectMapper objectMapper, MockMvc client) { + this.objectMapper = objectMapper; + this.client = client; + } + + public void subscribeTenant(String tenant) throws Exception { + SubscriptionPayload payload = new SubscriptionPayload(); + payload.subscribedTenantId = tenant; + payload.subscribedSubdomain = tenant.concat("sap.com"); + payload.eventType = "CREATE"; + + client + .perform( + put(MT_SUBSCRIPTIONS_TENANTS.concat(payload.subscribedTenantId)) + .header(HttpHeaders.AUTHORIZATION, credentials) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpect(status().isCreated()); + } + + public void unsubscribeTenant(String tenant) throws Exception { + DeletePayload payload = new DeletePayload(); + payload.subscribedTenantId = tenant; + + client + .perform( + delete(MT_SUBSCRIPTIONS_TENANTS.concat(payload.subscribedTenantId)) + .header(HttpHeaders.AUTHORIZATION, credentials) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpect(status().isNoContent()); + } + + static class SubscriptionPayload { + public String subscribedTenantId; + public String subscribedSubdomain; + public String eventType; + } + + static class DeletePayload { + public String subscribedTenantId; + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 213c34492..593ea7971 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -17,7 +17,8 @@ db - srv + generic + mtx-local/srv From da621f413d0930cf1ceb1604ad10eca534a7d271 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 10:01:51 +0200 Subject: [PATCH 07/39] Remove package-lock.json from mtx-local integration tests The lock file is not needed in version control since Maven handles npm install during the build via cds-maven-plugin. --- integration-tests/mtx-local/.gitignore | 1 + integration-tests/mtx-local/package-lock.json | 4144 ----------------- 2 files changed, 1 insertion(+), 4144 deletions(-) create mode 100644 integration-tests/mtx-local/.gitignore delete mode 100644 integration-tests/mtx-local/package-lock.json diff --git a/integration-tests/mtx-local/.gitignore b/integration-tests/mtx-local/.gitignore new file mode 100644 index 000000000..41fd97059 --- /dev/null +++ b/integration-tests/mtx-local/.gitignore @@ -0,0 +1 @@ +integration-tests/mtx-local/package-lock.json diff --git a/integration-tests/mtx-local/package-lock.json b/integration-tests/mtx-local/package-lock.json deleted file mode 100644 index 3383043ec..000000000 --- a/integration-tests/mtx-local/package-lock.json +++ /dev/null @@ -1,4144 +0,0 @@ -{ - "name": "mtx-local-integration-tests", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "mtx-local-integration-tests", - "version": "0.0.0", - "workspaces": [ - "mtx/sidecar" - ], - "devDependencies": { - "@sap/cds-dk": "^9", - "@sap/cds-mtxs": "^3" - } - }, - "mtx/sidecar": { - "name": "mtx-local-sidecar", - "version": "0.0.0", - "dependencies": { - "@sap/cds": "^9", - "@sap/cds-mtxs": "^3", - "@sap/xssec": "^4", - "express": "^4" - }, - "devDependencies": { - "@cap-js/sqlite": "^2" - } - }, - "mtx/sidecar/node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "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.14.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/@cap-js/db-service": { - "version": "2.9.0", - "resolved": "https://int.repositories.cloud.sap/artifactory/api/npm/build-milestones-npm/@cap-js/db-service/-/db-service-2.9.0.tgz", - "integrity": "sha512-WCXhoqezaF6A5I2l0MNZeHKXXtHRNEq7Rp0R89/uccOHQIx0DuU0U9NuJJPV/1G5RGk2QKQ9VBo/KYn+MZuuNQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "generic-pool": "^3.9.0" - }, - "peerDependencies": { - "@sap/cds": ">=9.8" - } - }, - "node_modules/@cap-js/sqlite": { - "version": "2.2.0", - "resolved": "https://int.repositories.cloud.sap/artifactory/api/npm/build-milestones-npm/@cap-js/sqlite/-/sqlite-2.2.0.tgz", - "integrity": "sha512-FPj+uVU/14vtGUl2P/Q8y7XhZbsLgrCav2O5PjHPXnupegjby4sMJkgVNxVHnkyKPFgO/W8uEsq9r5TU9VPx8w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@cap-js/db-service": "^2.9.0", - "better-sqlite3": "^12.0.0" - }, - "peerDependencies": { - "@sap/cds": ">=9.8", - "sql.js": "^1.13.0" - }, - "peerDependenciesMeta": { - "sql.js": { - "optional": true - } - } - }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "license": "MIT", - "peer": true, - "engines": { - "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": { - "version": "9.8.4", - "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-9.8.4.tgz", - "integrity": "sha512-1cnFdz4Ex4LV15dztDsSLDiilVdJ2mfTqr5A3TGsRd/6cM31SnHhuZ/Ra0FEErodokNS2pwasGZmSfgHVDW8KA==", - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/cds-compiler": "^6.4", - "@sap/cds-fiori": "^2", - "express": "^4.22.1 || ^5", - "js-yaml": "^4.1.1" - }, - "bin": { - "cds-deploy": "bin/deploy.js", - "cds-serve": "bin/serve.js" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@eslint/js": "^9 || ^10", - "tar": "^7.5.6" - }, - "peerDependenciesMeta": { - "tar": { - "optional": true - } - } - }, - "node_modules/@sap/cds-compiler": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-6.8.0.tgz", - "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", - "license": "SEE LICENSE IN LICENSE", - "bin": { - "cdsc": "bin/cdsc.js", - "cdshi": "bin/cdshi.js", - "cdsse": "bin/cdsse.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@sap/cds-dk": { - "version": "9.8.2", - "resolved": "https://registry.npmjs.org/@sap/cds-dk/-/cds-dk-9.8.2.tgz", - "integrity": "sha512-PGynoQpOCD8Nsr0EEzsEHWg8szYBLVtVd6byxvaaAPYBkNAj2WA+kin6gWaDTiyG3kJa0BSGPk4PCbA2xDoE9A==", - "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/hdi-deploy": "^5", - "axios": "^1", - "express": "^4.22.1 || ^5", - "hdb": "^2.0.0", - "livereload-js": "^4.0.1", - "mustache": "^4.0.1", - "ws": "^8.4.2", - "xml-js": "^1.6.11", - "yaml": "^2" - }, - "bin": { - "cds": "bin/cds.js", - "cds-ts": "bin/cds-ts.js", - "cds-tsx": "bin/cds-tsx.js" - }, - "optionalDependencies": { - "@cap-js/sqlite": ">=1" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/asyncapi": { - "version": "1.0.3", - "integrity": "sha512-vZSWKAe+3qfvZDXV5SSFiObGWmqyS9MDyEADb5PLVT8kzO39qGaSDPv/GzI/gwvRfCayGAjU4ThiBKrFA7Gclg==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "peerDependencies": { - "@sap/cds": ">=7.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/db-service": { - "version": "2.9.0", - "integrity": "sha512-WCXhoqezaF6A5I2l0MNZeHKXXtHRNEq7Rp0R89/uccOHQIx0DuU0U9NuJJPV/1G5RGk2QKQ9VBo/KYn+MZuuNQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "generic-pool": "^3.9.0" - }, - "peerDependencies": { - "@sap/cds": ">=9.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/openapi": { - "version": "1.4.0", - "integrity": "sha512-/LRSwn4SDxAi3qKwl09zoOhEVGaPGlYOPz/0S3UBnaMJVvaLyPiKbbaOtOnrrgulUX5OXt+ujPIQznOsbTzuAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "pluralize": "^8.0.0" - }, - "peerDependencies": { - "@sap/cds": ">=7.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/sqlite": { - "version": "2.2.0", - "integrity": "sha512-FPj+uVU/14vtGUl2P/Q8y7XhZbsLgrCav2O5PjHPXnupegjby4sMJkgVNxVHnkyKPFgO/W8uEsq9r5TU9VPx8w==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@cap-js/db-service": "^2.9.0", - "better-sqlite3": "^12.0.0" - }, - "peerDependencies": { - "@sap/cds": ">=9.8", - "sql.js": "^1.13.0" - }, - "peerDependenciesMeta": { - "sql.js": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@eslint/js": { - "version": "10.0.1", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "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.8.3", - "integrity": "sha512-up/kNVCZK8bOTyye1QN2BjMpH+IgcdyhT3U9mS3TqkpmjNE2JXq8Qz1iNAN+nYcXRWJ1XAUNpgebLfuh/49MGQ==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/cds-compiler": "^6.4", - "@sap/cds-fiori": "^2", - "express": "^4.22.1 || ^5", - "js-yaml": "^4.1.1" - }, - "bin": { - "cds-deploy": "bin/deploy.js", - "cds-serve": "bin/serve.js" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@eslint/js": "^9 || ^10", - "tar": "^7.5.6" - }, - "peerDependenciesMeta": { - "tar": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/cds-compiler": { - "version": "6.8.0", - "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "bin": { - "cdsc": "bin/cdsc.js", - "cdshi": "bin/cdshi.js", - "cdsse": "bin/cdsse.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/cds-fiori": { - "version": "2.3.0", - "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "peerDependencies": { - "@sap/cds": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/cds-mtxs": { - "version": "3.8.1", - "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/hdi-deploy": "^5" - }, - "bin": { - "cds-mtx": "bin/cds-mtx.js", - "cds-mtx-migrate": "bin/cds-mtx-migrate.js" - }, - "peerDependencies": { - "@sap/cds": "^9" - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/hdi": { - "version": "4.8.0", - "integrity": "sha512-tkJmY2ffm6mt4/LFwRBihlQkMxNAXa3ngvRe2N/6+qLIsUNdrH/M03S5mkygXq56K+KoVVZYuradajCusMWwsw==", - "dev": true, - "license": "See LICENSE file", - "dependencies": { - "async": "^3.2.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.5", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/hdi-deploy": { - "version": "5.6.1", - "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", - "dev": true, - "license": "See LICENSE file", - "dependencies": { - "@sap/hdi": "^4.8.0", - "@sap/xsenv": "^6.0.0", - "async": "^3.2.6", - "dotenv": "^16.4.5", - "handlebars": "^4.7.8", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=18.x" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.6", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/xsenv": { - "version": "6.1.0", - "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", - "dev": true, - "license": "SEE LICENSE IN LICENSE file", - "dependencies": { - "debug": "4.4.3", - "node-cache": "^5.1.2", - "verror": "1.10.1" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/accepts": { - "version": "2.0.0", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "dependencies": { - "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/assert-plus": { - "version": "1.0.0", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/async": { - "version": "3.2.6", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "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.13.6", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/base64-js": { - "version": "1.5.1", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "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", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/better-sqlite3": { - "version": "12.8.0", - "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - }, - "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x" - } - }, - "node_modules/@sap/cds-dk/node_modules/bindings": { - "version": "1.5.0", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/bl": { - "version": "4.1.0", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/body-parser": { - "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": "^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": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/braces": { - "version": "3.0.3", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/buffer": { - "version": "5.7.1", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "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", - "optional": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/@sap/cds-dk/node_modules/bytes": { - "version": "3.1.2", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/call-bound": { - "version": "1.0.4", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "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/@sap/cds-dk/node_modules/chownr": { - "version": "1.1.4", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/clone": { - "version": "2.1.2", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "dev": true, - "license": "MIT", - "engines": { - "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": "1.0.1", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/content-type": { - "version": "1.0.5", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/cookie": { - "version": "0.7.2", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/cookie-signature": { - "version": "1.2.2", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/core-util-is": { - "version": "1.0.2", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/debug": { - "version": "4.4.3", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/decompress-response": { - "version": "6.0.0", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sap/cds-dk/node_modules/deep-extend": { - "version": "0.6.0", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/detect-libc": { - "version": "2.1.2", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/dotenv": { - "version": "16.6.1", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/@sap/cds-dk/node_modules/dunder-proto": { - "version": "1.0.1", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/ee-first": { - "version": "1.1.1", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/encodeurl": { - "version": "2.0.0", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/end-of-stream": { - "version": "1.4.5", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/es-define-property": { - "version": "1.0.1", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/es-errors": { - "version": "1.3.0", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/es-object-atoms": { - "version": "1.1.1", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/etag": { - "version": "1.8.1", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/expand-template": { - "version": "2.0.3", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "license": "(MIT OR WTFPL)", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sap/cds-dk/node_modules/express": { - "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/extsprintf": { - "version": "1.4.1", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/file-uri-to-path": { - "version": "1.0.0", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/fill-range": { - "version": "7.1.1", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/finalhandler": { - "version": "2.1.1", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "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": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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.5", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "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" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/forwarded": { - "version": "0.2.0", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/fresh": { - "version": "2.0.0", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/fs-constants": { - "version": "1.0.0", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/function-bind": { - "version": "1.1.2", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/generic-pool": { - "version": "3.9.0", - "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/@sap/cds-dk/node_modules/get-intrinsic": { - "version": "1.3.0", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "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/@sap/cds-dk/node_modules/get-proto": { - "version": "1.0.1", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/github-from-package": { - "version": "0.0.0", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/gopd": { - "version": "1.2.0", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/handlebars": { - "version": "4.7.8", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/has-symbols": { - "version": "1.1.0", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/hdb": { - "version": "2.27.1", - "integrity": "sha512-xYL/W+fq2TyGHyzm8muolQnw8tdh4+2NQ8mQP2FpLSuhfJ8l0jQNSUZoAXic7NfMEan1Jvf8V1L4blwkgTc6+A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "iconv-lite": "0.7.0" - }, - "engines": { - "node": ">= 18" - }, - "optionalDependencies": { - "lz4-wasm-nodejs": "0.9.2" - } - }, - "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.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": { - "version": "1.2.1", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "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": "BSD-3-Clause", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/inherits": { - "version": "2.0.4", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@sap/cds-dk/node_modules/ini": { - "version": "1.3.8", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/ipaddr.js": { - "version": "1.9.1", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/@sap/cds-dk/node_modules/is-number": { - "version": "7.0.0", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/is-promise": { - "version": "4.0.0", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/js-yaml": { - "version": "4.1.1", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@sap/cds-dk/node_modules/livereload-js": { - "version": "4.0.2", - "integrity": "sha512-Fy7VwgQNiOkynYyNBTo3v9hQUhcW5pFAheJN148+DTgpShjsy/22pLHKKwDK5v0kOsZsJBK+6q1PMgLvRmrwFQ==", - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/media-typer": { - "version": "1.1.0", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/merge-descriptors": { - "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/micromatch": { - "version": "4.0.8", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@sap/cds-dk/node_modules/mime-db": { - "version": "1.54.0", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/mime-types": { - "version": "3.0.2", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/mimic-response": { - "version": "3.1.0", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sap/cds-dk/node_modules/minimist": { - "version": "1.2.8", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/mkdirp-classic": { - "version": "0.5.3", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/ms": { - "version": "2.1.3", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/mustache": { - "version": "4.2.0", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", - "dev": true, - "license": "MIT", - "bin": { - "mustache": "bin/mustache" - } - }, - "node_modules/@sap/cds-dk/node_modules/napi-build-utils": { - "version": "2.0.0", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/negotiator": { - "version": "1.0.0", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/neo-async": { - "version": "2.6.2", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/node-abi": { - "version": "3.89.0", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@sap/cds-dk/node_modules/node-cache": { - "version": "5.1.2", - "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "2.x" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/object-inspect": { - "version": "1.13.4", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/on-finished": { - "version": "2.4.1", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/once": { - "version": "1.4.0", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/@sap/cds-dk/node_modules/parseurl": { - "version": "1.3.3", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/path-to-regexp": { - "version": "8.3.0", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/pluralize": { - "version": "8.0.0", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "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, - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@sap/cds-dk/node_modules/proxy-addr": { - "version": "2.0.7", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "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.4", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/@sap/cds-dk/node_modules/qs": { - "version": "6.15.0", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/range-parser": { - "version": "1.2.1", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/raw-body": { - "version": "3.0.2", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/@sap/cds-dk/node_modules/rc": { - "version": "1.2.8", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "optional": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/@sap/cds-dk/node_modules/readable-stream": { - "version": "3.6.2", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "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==", - "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", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/safer-buffer": { - "version": "2.1.2", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/sax": { - "version": "1.6.0", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/semver": { - "version": "7.7.4", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@sap/cds-dk/node_modules/send": { - "version": "1.2.1", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "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": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/serve-static": { - "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": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/setprototypeof": { - "version": "1.2.0", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, - "node_modules/@sap/cds-dk/node_modules/side-channel": { - "version": "1.1.0", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "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/@sap/cds-dk/node_modules/side-channel-list": { - "version": "1.0.0", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/side-channel-map": { - "version": "1.0.1", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "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/@sap/cds-dk/node_modules/side-channel-weakmap": { - "version": "1.0.2", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "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/@sap/cds-dk/node_modules/simple-concat": { - "version": "1.0.1", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "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", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/simple-get": { - "version": "4.0.1", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "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", - "optional": true, - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/source-map": { - "version": "0.6.1", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/statuses": { - "version": "2.0.2", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/string_decoder": { - "version": "1.3.0", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/strip-json-comments": { - "version": "2.0.1", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/tar-fs": { - "version": "2.1.4", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/tar-stream": { - "version": "2.2.0", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sap/cds-dk/node_modules/to-regex-range": { - "version": "5.0.1", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/toidentifier": { - "version": "1.0.1", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/tunnel-agent": { - "version": "0.6.0", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@sap/cds-dk/node_modules/type-is": { - "version": "2.0.1", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/uglify-js": { - "version": "3.19.3", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/unpipe": { - "version": "1.0.0", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/util-deprecate": { - "version": "1.0.2", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/vary": { - "version": "1.1.2", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/verror": { - "version": "1.10.1", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/wordwrap": { - "version": "1.0.0", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/wrappy": { - "version": "1.0.2", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@sap/cds-dk/node_modules/ws": { - "version": "8.20.0", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/xml-js": { - "version": "1.6.11", - "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "sax": "^1.2.4" - }, - "bin": { - "xml-js": "bin/cli.js" - } - }, - "node_modules/@sap/cds-dk/node_modules/yaml": { - "version": "2.8.3", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/@sap/cds-fiori": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sap/cds-fiori/-/cds-fiori-2.3.0.tgz", - "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", - "license": "SEE LICENSE IN LICENSE", - "peerDependencies": { - "@sap/cds": ">=8" - } - }, - "node_modules/@sap/cds-mtxs": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@sap/cds-mtxs/-/cds-mtxs-3.8.1.tgz", - "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/hdi-deploy": "^5" - }, - "bin": { - "cds-mtx": "bin/cds-mtx.js", - "cds-mtx-migrate": "bin/cds-mtx-migrate.js" - }, - "peerDependencies": { - "@sap/cds": "^9" - } - }, - "node_modules/@sap/hdi": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@sap/hdi/-/hdi-4.8.0.tgz", - "integrity": "sha512-tkJmY2ffm6mt4/LFwRBihlQkMxNAXa3ngvRe2N/6+qLIsUNdrH/M03S5mkygXq56K+KoVVZYuradajCusMWwsw==", - "license": "See LICENSE file", - "dependencies": { - "async": "^3.2.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.5", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/hdi-deploy": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@sap/hdi-deploy/-/hdi-deploy-5.6.1.tgz", - "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", - "license": "See LICENSE file", - "dependencies": { - "@sap/hdi": "^4.8.0", - "@sap/xsenv": "^6.0.0", - "async": "^3.2.6", - "dotenv": "^16.4.5", - "handlebars": "^4.7.8", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=18.x" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.6", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/xsenv": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sap/xsenv/-/xsenv-6.1.0.tgz", - "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", - "license": "SEE LICENSE IN LICENSE file", - "dependencies": { - "debug": "4.4.3", - "node-cache": "^5.1.2", - "verror": "1.10.1" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@sap/xsenv/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@sap/xsenv/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/@sap/xssec": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@sap/xssec/-/xssec-4.13.0.tgz", - "integrity": "sha512-8e+bU+OyAIpAGXQanOopZa5YEK+yHKw84dhhihcCotF40MSNFbVHjQ4xM5hf4QndlqDGfXIuvXmoOMuDATa/gA==", - "license": "SAP DEVELOPER LICENSE AGREEMENT", - "dependencies": { - "debug": "^4.4.3", - "jwt-decode": "^4" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sap/xssec/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@sap/xssec/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "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==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "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==", - "license": "MIT" - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "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" - }, - "node_modules/better-sqlite3": { - "version": "12.8.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", - "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - }, - "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "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.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "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", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC" - }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "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/express/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "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": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/express/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/express/node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "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": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "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/express/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/express/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/express/node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "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": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "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.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT" - }, - "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==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generic-pool": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", - "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/handlebars": { - "version": "4.7.9", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "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/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "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": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC" - }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jwt-decode": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/mtx-local-sidecar": { - "resolved": "mtx/sidecar", - "link": true - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" - }, - "node_modules/node-abi": { - "version": "3.89.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-cache": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", - "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", - "license": "MIT", - "dependencies": { - "clone": "2.x" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "license": "MIT" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "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/router/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/router/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", - "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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==", - "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" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "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/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "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==", - "license": "ISC" - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "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" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "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", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "license": "MIT" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - } - } -} From 21d5c090dce2540bbb5b133459916b7e4521a97e Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 10:13:23 +0200 Subject: [PATCH 08/39] spotless --- .../handlers/system/SubscribeModelTenantsHandler.java | 11 ++++++++--- .../mt/MultiTenantAttachmentIsolationTest.java | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java b/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java index cde1364f6..3bed0e2c6 100644 --- a/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java +++ b/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java @@ -66,8 +66,7 @@ private void subscribeTenant(String tenant) { c -> { service.subscribe( tenant, - new HashMap<>( - Collections.singletonMap("subscribedSubdomain", "mt-" + tenant))); + new HashMap<>(Collections.singletonMap("subscribedSubdomain", "mt-" + tenant))); }); } @@ -82,7 +81,13 @@ private void unsubscribeTenant(String tenant) { } private List readMockedTenants() { - return runtime.getEnvironment().getCdsProperties().getSecurity().getMock().getTenants().values() + return runtime + .getEnvironment() + .getCdsProperties() + .getSecurity() + .getMock() + .getTenants() + .values() .stream() .map(CdsProperties.Security.Mock.Tenant::getName) .collect(Collectors.toList()); diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java index 14a22d2d4..f8cd24b2c 100644 --- a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java +++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java @@ -11,13 +11,13 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; -import org.junit.jupiter.api.Test; @SpringBootTest @AutoConfigureMockMvc From 6d987ae46223e67e071cd3422a833e4b769f0009 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 10:51:49 +0200 Subject: [PATCH 09/39] unit tests for coverage --- .../oss/handler/TenantCleanupHandler.java | 2 +- .../attachments/oss/client/AWSClientTest.java | 72 ++++ .../oss/client/AzureClientTest.java | 59 +++ .../oss/client/GoogleClientTest.java | 61 ++++ .../attachments/oss/client/OSClientTest.java | 40 ++ .../oss/configuration/RegistrationTest.java | 52 ++- .../OSSAttachmentsServiceHandlerTest.java | 341 ++++++++++++++++++ .../oss/handler/TenantCleanupHandlerTest.java | 62 ++++ 8 files changed, 687 insertions(+), 2 deletions(-) create mode 100644 storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java create mode 100644 storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java index 6b305cebd..f719bb181 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java @@ -1,5 +1,5 @@ /* - * © 2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. */ package com.sap.cds.feature.attachments.oss.handler; diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java index 658157b3b..05e86bff6 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java @@ -9,6 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandler; @@ -17,7 +18,9 @@ import com.sap.cloud.environment.servicebinding.api.ServiceBinding; import java.io.ByteArrayInputStream; import java.io.InputStream; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -30,10 +33,15 @@ import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; +import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.S3Object; class AWSClientTest { ExecutorService executor = Executors.newCachedThreadPool(); @@ -225,6 +233,70 @@ void testDeleteContentThrowsOnUnsuccessfulResponse() assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } + @Test + void testDeleteContentByPrefix() throws Exception { + AWSClient awsClient = new AWSClient(getDummyBinding(), executor); + + S3Client mockS3Client = mock(S3Client.class); + + S3Object obj1 = S3Object.builder().key("prefix/file1.txt").build(); + S3Object obj2 = S3Object.builder().key("prefix/file2.txt").build(); + + ListObjectsV2Response listResponse = mock(ListObjectsV2Response.class); + when(listResponse.contents()).thenReturn(List.of(obj1, obj2)); + when(listResponse.isTruncated()).thenReturn(false); + when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))).thenReturn(listResponse); + + DeleteObjectsResponse deleteResponse = mock(DeleteObjectsResponse.class); + when(deleteResponse.hasErrors()).thenReturn(false); + when(deleteResponse.errors()).thenReturn(Collections.emptyList()); + when(mockS3Client.deleteObjects(any(DeleteObjectsRequest.class))).thenReturn(deleteResponse); + + var field = AWSClient.class.getDeclaredField("s3Client"); + field.setAccessible(true); + field.set(awsClient, mockS3Client); + + awsClient.deleteContentByPrefix("prefix/").get(); + + verify(mockS3Client).deleteObjects(any(DeleteObjectsRequest.class)); + } + + @Test + void testDeleteContentByPrefixEmptyList() throws Exception { + AWSClient awsClient = new AWSClient(getDummyBinding(), executor); + + S3Client mockS3Client = mock(S3Client.class); + + ListObjectsV2Response listResponse = mock(ListObjectsV2Response.class); + when(listResponse.contents()).thenReturn(Collections.emptyList()); + when(listResponse.isTruncated()).thenReturn(false); + when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))).thenReturn(listResponse); + + var field = AWSClient.class.getDeclaredField("s3Client"); + field.setAccessible(true); + field.set(awsClient, mockS3Client); + + assertDoesNotThrow(() -> awsClient.deleteContentByPrefix("prefix/").get()); + } + + @Test + void testDeleteContentByPrefixThrowsOnRuntimeException() throws Exception { + AWSClient awsClient = new AWSClient(getDummyBinding(), executor); + + S3Client mockS3Client = mock(S3Client.class); + when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))) + .thenThrow(new RuntimeException("Simulated failure")); + + var field = AWSClient.class.getDeclaredField("s3Client"); + field.setAccessible(true); + field.set(awsClient, mockS3Client); + + ExecutionException thrown = + assertThrows( + ExecutionException.class, () -> awsClient.deleteContentByPrefix("prefix/").get()); + assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); + } + private ServiceBinding getDummyBinding() { ServiceBinding binding = mock(ServiceBinding.class); HashMap creds = new HashMap<>(); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java index 9b7100468..94fd262de 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java @@ -7,13 +7,17 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import com.azure.core.http.rest.PagedIterable; import com.azure.storage.blob.BlobClient; import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.models.BlobItem; +import com.azure.storage.blob.models.ListBlobsOptions; import com.azure.storage.blob.specialized.BlobOutputStream; import com.azure.storage.blob.specialized.BlockBlobClient; import com.sap.cds.feature.attachments.oss.handler.ObjectStoreServiceException; import java.io.IOException; import java.io.InputStream; +import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -189,4 +193,59 @@ void testReadContentThrowsOnRuntimeException() assertThrows(ExecutionException.class, () -> azureClient.readContent("file.txt").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } + + @Test + void testDeleteContentByPrefix() + throws NoSuchFieldException, IllegalAccessException, InterruptedException, + ExecutionException { + AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); + + BlobContainerClient mockContainer = mock(BlobContainerClient.class); + BlobClient mockBlobClient = mock(BlobClient.class); + + var field = AzureClient.class.getDeclaredField("blobContainerClient"); + field.setAccessible(true); + field.set(azureClient, mockContainer); + var executorField = AzureClient.class.getDeclaredField("executor"); + executorField.setAccessible(true); + executorField.set(azureClient, executor); + + BlobItem item1 = mock(BlobItem.class); + when(item1.getName()).thenReturn("prefix/file1.txt"); + BlobItem item2 = mock(BlobItem.class); + when(item2.getName()).thenReturn("prefix/file2.txt"); + + @SuppressWarnings("unchecked") + PagedIterable pagedIterable = mock(PagedIterable.class); + when(pagedIterable.iterator()).thenReturn(List.of(item1, item2).iterator()); + when(mockContainer.listBlobs(any(ListBlobsOptions.class), isNull())).thenReturn(pagedIterable); + when(mockContainer.getBlobClient(anyString())).thenReturn(mockBlobClient); + + azureClient.deleteContentByPrefix("prefix/").get(); + + verify(mockBlobClient, times(2)).delete(); + } + + @Test + void testDeleteContentByPrefixThrowsOnRuntimeException() + throws NoSuchFieldException, IllegalAccessException { + AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); + + BlobContainerClient mockContainer = mock(BlobContainerClient.class); + + var field = AzureClient.class.getDeclaredField("blobContainerClient"); + field.setAccessible(true); + field.set(azureClient, mockContainer); + var executorField = AzureClient.class.getDeclaredField("executor"); + executorField.setAccessible(true); + executorField.set(azureClient, executor); + + when(mockContainer.listBlobs(any(ListBlobsOptions.class), isNull())) + .thenThrow(new RuntimeException("Simulated failure")); + + ExecutionException thrown = + assertThrows( + ExecutionException.class, () -> azureClient.deleteContentByPrefix("prefix/").get()); + assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java index f4c2fd188..dbc1cc3fb 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java @@ -272,4 +272,65 @@ void testReadContentThrowsOnRuntimeException() assertThrows(ExecutionException.class, () -> googleClient.readContent("file.txt").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } + + @Test + void testDeleteContentByPrefix() + throws NoSuchFieldException, IllegalAccessException, InterruptedException, + ExecutionException { + GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); + + Storage mockStorage = mock(Storage.class); + Page mockPage = mock(Page.class); + Blob mockBlob1 = mock(Blob.class); + when(mockBlob1.getName()).thenReturn("prefix/file1.txt"); + when(mockBlob1.getGeneration()).thenReturn(1L); + Blob mockBlob2 = mock(Blob.class); + when(mockBlob2.getName()).thenReturn("prefix/file2.txt"); + when(mockBlob2.getGeneration()).thenReturn(2L); + + Iterator blobIterator = java.util.List.of(mockBlob1, mockBlob2).iterator(); + when(mockPage.iterateAll()).thenReturn(() -> blobIterator); + when(mockStorage.list(anyString(), any(), any())).thenReturn(mockPage); + when(mockStorage.delete(any(BlobId.class))).thenReturn(true); + + var field = GoogleClient.class.getDeclaredField("storage"); + field.setAccessible(true); + field.set(googleClient, mockStorage); + var executorField = GoogleClient.class.getDeclaredField("executor"); + executorField.setAccessible(true); + executorField.set(googleClient, executor); + var bucketField = GoogleClient.class.getDeclaredField("bucketName"); + bucketField.setAccessible(true); + bucketField.set(googleClient, "my-bucket"); + + googleClient.deleteContentByPrefix("prefix/").get(); + + verify(mockStorage, times(2)).delete(any(BlobId.class)); + } + + @Test + void testDeleteContentByPrefixThrowsOnRuntimeException() + throws NoSuchFieldException, IllegalAccessException { + GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); + + Storage mockStorage = mock(Storage.class); + when(mockStorage.list(anyString(), any(), any())) + .thenThrow(new RuntimeException("Simulated failure")); + + var field = GoogleClient.class.getDeclaredField("storage"); + field.setAccessible(true); + field.set(googleClient, mockStorage); + var executorField = GoogleClient.class.getDeclaredField("executor"); + executorField.setAccessible(true); + executorField.set(googleClient, executor); + var bucketField = GoogleClient.class.getDeclaredField("bucketName"); + bucketField.setAccessible(true); + bucketField.set(googleClient, "my-bucket"); + + ExecutionException thrown = + assertThrows( + ExecutionException.class, + () -> googleClient.deleteContentByPrefix("prefix/").get()); + assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java new file mode 100644 index 000000000..48adf9f63 --- /dev/null +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/OSClientTest.java @@ -0,0 +1,40 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.oss.client; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.Test; + +class OSClientTest { + + @Test + void testDefaultDeleteContentByPrefixThrowsUnsupportedOperationException() { + OSClient client = + new OSClient() { + @Override + public java.util.concurrent.Future uploadContent( + java.io.InputStream content, String completeFileName, String contentType) { + return null; + } + + @Override + public java.util.concurrent.Future deleteContent(String completeFileName) { + return null; + } + + @Override + public java.util.concurrent.Future readContent( + String completeFileName) { + return null; + } + }; + + ExecutionException thrown = + assertThrows(ExecutionException.class, () -> client.deleteContentByPrefix("prefix/").get()); + assertInstanceOf(UnsupportedOperationException.class, thrown.getCause()); + } +} diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java index a364aa7d7..2318424f1 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java @@ -4,7 +4,11 @@ package com.sap.cds.feature.attachments.oss.configuration; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandler; import com.sap.cds.services.environment.CdsEnvironment; @@ -48,4 +52,50 @@ void testEventHandlersRegistersOSSHandler() { // Assert: OSSAttachmentsServiceHandler should be registered verify(configurer).eventHandler(any(OSSAttachmentsServiceHandler.class)); } + + @Test + void testEventHandlersRegistersCleanupHandlerWhenMultitenancyShared() { + Registration registration = new Registration(); + CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); + CdsRuntime cdsRuntime = mock(CdsRuntime.class); + CdsEnvironment environment = mock(CdsEnvironment.class); + ServiceBinding binding = mock(ServiceBinding.class); + + Map credentials = new HashMap<>(); + credentials.put("host", "aws.example.com"); + credentials.put("region", "us-east-1"); + credentials.put("access_key_id", "test-access-key"); + credentials.put("secret_access_key", "test-secret-key"); + credentials.put("bucket", "test-bucket"); + + when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); + when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); + when(binding.getCredentials()).thenReturn(credentials); + when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) + .thenReturn(Boolean.TRUE); + when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) + .thenReturn("shared"); + + registration.eventHandlers(configurer); + + verify(configurer, times(2)).eventHandler(any()); + } + + @Test + void testEventHandlersNoBindingDoesNotRegister() { + Registration registration = new Registration(); + CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); + CdsRuntime cdsRuntime = mock(CdsRuntime.class); + CdsEnvironment environment = mock(CdsEnvironment.class); + + when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); + when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(environment.getServiceBindings()).thenReturn(Stream.empty()); + + registration.eventHandlers(configurer); + + verify(configurer, never()).eventHandler(any()); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index 62598354c..6a066586c 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -19,6 +19,7 @@ import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; import com.sap.cds.reflect.CdsEntity; import com.sap.cds.services.ServiceException; +import com.sap.cds.services.request.UserInfo; import com.sap.cloud.environment.servicebinding.api.ServiceBinding; import java.io.ByteArrayInputStream; import java.io.InputStream; @@ -378,4 +379,344 @@ void testReadAttachmentHandlesInterruptedException() assertThrows(ServiceException.class, () -> handler.readAttachment(context)); verify(context).setCompleted(); } + + @Test + void testCreateAttachmentWithMultitenancyBuildsObjectKey() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + CdsEntity mockEntity = mock(CdsEntity.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getAttachmentEntity()).thenReturn(mockEntity); + when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.getAttachmentIds()).thenReturn(java.util.Map.of("ID", "content123")); + when(context.getData()).thenReturn(mockMediaData); + when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); + when(mockMediaData.getMimeType()).thenReturn("text/plain"); + when(mockMediaData.getFileName()).thenReturn("file.txt"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("myTenant"); + + when(mockOsClient.uploadContent(any(), anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); + + handler.createAttachment(context); + + // Verify the object key includes tenant prefix + verify(mockOsClient).uploadContent(any(), org.mockito.ArgumentMatchers.eq("myTenant/content123"), anyString()); + } + + @Test + void testMultitenancyWithNullTenantThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn(null); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateTenantIdWithSlashThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("tenant/evil"); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateTenantIdWithBackslashThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("tenant\\evil"); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateTenantIdWithDotsThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("..evil"); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateEmptyTenantIdThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + // Need to mock tenant as empty string but not null (null triggers different path) + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn(""); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateContentIdWithSlashThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("content/evil"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("validTenant"); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateContentIdWithNullThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn(null); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("validTenant"); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateContentIdWithBackslashThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("content\\evil"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("validTenant"); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateContentIdWithDotsThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("..evil"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("validTenant"); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateEmptyContentIdThrows() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn(""); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("validTenant"); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testReadAttachmentHandlesExecutionException() + throws NoSuchFieldException, IllegalAccessException, InterruptedException, + ExecutionException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + + var field = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + field.setAccessible(true); + field.set(handler, mockOsClient); + + String contentId = "doc123"; + MediaData mockMediaData = mock(MediaData.class); + + when(context.getContentId()).thenReturn(contentId); + when(context.getData()).thenReturn(mockMediaData); + + @SuppressWarnings("unchecked") + CompletableFuture future = mock(CompletableFuture.class); + when(mockOsClient.readContent(contentId)).thenReturn(future); + when(future.get()).thenThrow(new ExecutionException("failed", new RuntimeException())); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + verify(context).setCompleted(); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java new file mode 100644 index 000000000..f92e76cdf --- /dev/null +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java @@ -0,0 +1,62 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.oss.handler; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.sap.cds.feature.attachments.oss.client.OSClient; +import com.sap.cds.services.mt.UnsubscribeEventContext; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.Test; + +class TenantCleanupHandlerTest { + + @Test + void testCleanupTenantDataCallsDeleteByPrefix() throws Exception { + OSClient mockOsClient = mock(OSClient.class); + UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); + when(context.getTenant()).thenReturn("tenant1"); + when(mockOsClient.deleteContentByPrefix("tenant1/")) + .thenReturn(CompletableFuture.completedFuture(null)); + + TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); + handler.cleanupTenantData(context); + + verify(mockOsClient).deleteContentByPrefix("tenant1/"); + } + + @Test + void testCleanupTenantDataHandlesInterruptedException() throws Exception { + OSClient mockOsClient = mock(OSClient.class); + UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); + when(context.getTenant()).thenReturn("tenant2"); + + @SuppressWarnings("unchecked") + CompletableFuture future = mock(CompletableFuture.class); + when(mockOsClient.deleteContentByPrefix("tenant2/")).thenReturn(future); + when(future.get()).thenThrow(new InterruptedException("interrupted")); + + TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); + handler.cleanupTenantData(context); + + verify(mockOsClient).deleteContentByPrefix("tenant2/"); + } + + @Test + void testCleanupTenantDataHandlesRuntimeException() throws Exception { + OSClient mockOsClient = mock(OSClient.class); + UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); + when(context.getTenant()).thenReturn("tenant3"); + + when(mockOsClient.deleteContentByPrefix("tenant3/")) + .thenReturn(CompletableFuture.failedFuture(new RuntimeException("fail"))); + + TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); + handler.cleanupTenantData(context); + + verify(mockOsClient).deleteContentByPrefix("tenant3/"); + } +} From 33ea3cfc8b72de41f0000b5f9c1c2d87d38dfb91 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 10:53:07 +0200 Subject: [PATCH 10/39] spotless --- .../oss/client/AzureClientTest.java | 4 ++- .../oss/client/GoogleClientTest.java | 7 +++-- .../OSSAttachmentsServiceHandlerTest.java | 31 ++++++++----------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java index 94fd262de..7f6fe6de2 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java @@ -196,7 +196,9 @@ void testReadContentThrowsOnRuntimeException() @Test void testDeleteContentByPrefix() - throws NoSuchFieldException, IllegalAccessException, InterruptedException, + throws NoSuchFieldException, + IllegalAccessException, + InterruptedException, ExecutionException { AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java index dbc1cc3fb..51147491d 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java @@ -275,7 +275,9 @@ void testReadContentThrowsOnRuntimeException() @Test void testDeleteContentByPrefix() - throws NoSuchFieldException, IllegalAccessException, InterruptedException, + throws NoSuchFieldException, + IllegalAccessException, + InterruptedException, ExecutionException { GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); @@ -329,8 +331,7 @@ void testDeleteContentByPrefixThrowsOnRuntimeException() ExecutionException thrown = assertThrows( - ExecutionException.class, - () -> googleClient.deleteContentByPrefix("prefix/").get()); + ExecutionException.class, () -> googleClient.deleteContentByPrefix("prefix/").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index 6a066586c..c4350e2e7 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -418,12 +418,12 @@ void testCreateAttachmentWithMultitenancyBuildsObjectKey() handler.createAttachment(context); // Verify the object key includes tenant prefix - verify(mockOsClient).uploadContent(any(), org.mockito.ArgumentMatchers.eq("myTenant/content123"), anyString()); + verify(mockOsClient) + .uploadContent(any(), org.mockito.ArgumentMatchers.eq("myTenant/content123"), anyString()); } @Test - void testMultitenancyWithNullTenantThrows() - throws NoSuchFieldException, IllegalAccessException { + void testMultitenancyWithNullTenantThrows() throws NoSuchFieldException, IllegalAccessException { OSClient mockOsClient = mock(OSClient.class); OSSAttachmentsServiceHandler handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); @@ -449,8 +449,7 @@ void testMultitenancyWithNullTenantThrows() } @Test - void testValidateTenantIdWithSlashThrows() - throws NoSuchFieldException, IllegalAccessException { + void testValidateTenantIdWithSlashThrows() throws NoSuchFieldException, IllegalAccessException { OSClient mockOsClient = mock(OSClient.class); OSSAttachmentsServiceHandler handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); @@ -503,8 +502,7 @@ void testValidateTenantIdWithBackslashThrows() } @Test - void testValidateTenantIdWithDotsThrows() - throws NoSuchFieldException, IllegalAccessException { + void testValidateTenantIdWithDotsThrows() throws NoSuchFieldException, IllegalAccessException { OSClient mockOsClient = mock(OSClient.class); OSSAttachmentsServiceHandler handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); @@ -530,8 +528,7 @@ void testValidateTenantIdWithDotsThrows() } @Test - void testValidateEmptyTenantIdThrows() - throws NoSuchFieldException, IllegalAccessException { + void testValidateEmptyTenantIdThrows() throws NoSuchFieldException, IllegalAccessException { OSClient mockOsClient = mock(OSClient.class); OSSAttachmentsServiceHandler handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); @@ -558,8 +555,7 @@ void testValidateEmptyTenantIdThrows() } @Test - void testValidateContentIdWithSlashThrows() - throws NoSuchFieldException, IllegalAccessException { + void testValidateContentIdWithSlashThrows() throws NoSuchFieldException, IllegalAccessException { OSClient mockOsClient = mock(OSClient.class); OSSAttachmentsServiceHandler handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); @@ -585,8 +581,7 @@ void testValidateContentIdWithSlashThrows() } @Test - void testValidateContentIdWithNullThrows() - throws NoSuchFieldException, IllegalAccessException { + void testValidateContentIdWithNullThrows() throws NoSuchFieldException, IllegalAccessException { OSClient mockOsClient = mock(OSClient.class); OSSAttachmentsServiceHandler handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); @@ -639,8 +634,7 @@ void testValidateContentIdWithBackslashThrows() } @Test - void testValidateContentIdWithDotsThrows() - throws NoSuchFieldException, IllegalAccessException { + void testValidateContentIdWithDotsThrows() throws NoSuchFieldException, IllegalAccessException { OSClient mockOsClient = mock(OSClient.class); OSSAttachmentsServiceHandler handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); @@ -666,8 +660,7 @@ void testValidateContentIdWithDotsThrows() } @Test - void testValidateEmptyContentIdThrows() - throws NoSuchFieldException, IllegalAccessException { + void testValidateEmptyContentIdThrows() throws NoSuchFieldException, IllegalAccessException { OSClient mockOsClient = mock(OSClient.class); OSSAttachmentsServiceHandler handler = mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); @@ -694,7 +687,9 @@ void testValidateEmptyContentIdThrows() @Test void testReadAttachmentHandlesExecutionException() - throws NoSuchFieldException, IllegalAccessException, InterruptedException, + throws NoSuchFieldException, + IllegalAccessException, + InterruptedException, ExecutionException { OSClient mockOsClient = mock(OSClient.class); OSSAttachmentsServiceHandler handler = From 9658edbd05d5dd4da87b5a315727262032597aff Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 11:11:00 +0200 Subject: [PATCH 11/39] fix tests --- integration-tests/generic/pom.xml | 3 +- integration-tests/srv/pom.xml | 121 +++ .../integrationtests/Application.java | 15 + .../integrationtests/constants/Profiles.java | 15 + .../testhandler/EventContextHolder.java | 8 + .../testhandler/TestPersistenceHandler.java | 49 + .../TestPluginAttachmentsServiceHandler.java | 120 +++ .../srv/src/main/resources/application.yaml | 4 + .../srv/src/main/resources/banner.txt | 9 + .../src/main/resources/messages.properties | 1 + .../resources/spotbugs-exclusion-filter.xml | 26 + .../common/JsonToCapMapperTestHelper.java | 21 + .../common/MalwareScanResultProvider.java | 27 + .../common/MockHttpRequestHelper.java | 167 +++ .../common/TableDataDeleter.java | 21 + .../DraftOdataRequestValidationBase.java | 973 ++++++++++++++++++ ...aRequestValidationWithTestHandlerTest.java | 288 ++++++ ...thoutTestHandlerAndMalwareScannerTest.java | 114 ++ ...stHandlerAndWithoutMalwareScannerTest.java | 90 ++ .../MediaValidatedAttachmentsDraftTest.java | 159 +++ ...tedAttachmentsSizeValidationDraftTest.java | 202 ++++ ...MediaValidatedAttachmentsNonDraftTest.java | 296 ++++++ .../OdataRequestValidationBase.java | 884 ++++++++++++++++ ...aRequestValidationWithTestHandlerTest.java | 245 +++++ ...thoutTestHandlerAndMalwareScannerTest.java | 138 +++ ...stHandlerAndWithoutMalwareScannerTest.java | 116 +++ ...mitedAttachmentValidationNonDraftTest.java | 175 ++++ .../helper/AttachmentsBuilder.java | 33 + .../helper/AttachmentsEntityBuilder.java | 31 + .../helper/ItemEntityBuilder.java | 44 + .../helper/RootEntityBuilder.java | 50 + .../TestPersistenceHandlerTest.java | 116 +++ ...stPluginAttachmentsServiceHandlerTest.java | 266 +++++ .../srv/src/test/resources/application.yaml | 15 + .../srv/src/test/resources/logback-test.xml | 21 + .../srv/src/test/resources/xsuaa-env.json | 30 + integration-tests/srv/test-service.cds | 27 + 37 files changed, 4919 insertions(+), 1 deletion(-) create mode 100644 integration-tests/srv/pom.xml create mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java create mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java create mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java create mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java create mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java create mode 100644 integration-tests/srv/src/main/resources/application.yaml create mode 100644 integration-tests/srv/src/main/resources/banner.txt create mode 100644 integration-tests/srv/src/main/resources/messages.properties create mode 100644 integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java create mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java create mode 100644 integration-tests/srv/src/test/resources/application.yaml create mode 100644 integration-tests/srv/src/test/resources/logback-test.xml create mode 100644 integration-tests/srv/src/test/resources/xsuaa-env.json create mode 100644 integration-tests/srv/test-service.cds diff --git a/integration-tests/generic/pom.xml b/integration-tests/generic/pom.xml index 265826f00..d47623f06 100644 --- a/integration-tests/generic/pom.xml +++ b/integration-tests/generic/pom.xml @@ -97,7 +97,7 @@ ${project.basedir}/.. build --for java - deploy --to h2 --dry > + deploy --to h2 --dry db generic > "${project.basedir}/src/main/resources/schema.sql" @@ -110,6 +110,7 @@ ${project.basedir}/.. + ${project.basedir}/src/main/resources/edmx/csn.json ${generation-package}.integration.test.cds4j diff --git a/integration-tests/srv/pom.xml b/integration-tests/srv/pom.xml new file mode 100644 index 000000000..6a5b6eada --- /dev/null +++ b/integration-tests/srv/pom.xml @@ -0,0 +1,121 @@ + + + 4.0.0 + + + com.sap.cds.integration-tests + cds-feature-attachments-integration-tests-parent + ${revision} + + + cds-feature-attachments-integration-tests-srv + jar + + Integration Tests - Service + + + com.sap.cds.feature.attachments.generated + + + + + + + com.sap.cds + cds-starter-spring-boot + + + + + com.sap.cds + cds-adapter-odata-v4 + runtime + + + + org.springframework.security + spring-security-test + test + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.h2database + h2 + test + + + + org.wiremock.integrations + wiremock-spring-boot + test + + + + + + + + com.sap.cds + cds-maven-plugin + + + cds.clean + + clean + + + + + cds.install-node + + install-node + + + + + cds.resolve + + resolve + + + ${project.basedir}/.. + + + + + cds.build + + cds + + + ${project.basedir}/.. + + build --for java + deploy --to h2 --dry > + "${project.basedir}/src/main/resources/schema.sql" + + + + + + cds.generate + + generate + + + ${project.basedir}/.. + ${generation-package}.integration.test.cds4j + + + + + + + + diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java new file mode 100644 index 000000000..b0b23079c --- /dev/null +++ b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java @@ -0,0 +1,15 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java new file mode 100644 index 000000000..8ac4f8f3e --- /dev/null +++ b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java @@ -0,0 +1,15 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.constants; + +public final class Profiles { + + public static final String TEST_HANDLER_ENABLED = "test-handler-enabled"; + public static final String TEST_HANDLER_DISABLED = "test-handler-disabled"; + public static final String MALWARE_SCAN_ENABLED = "malware-scan-enabled"; + + private Profiles() { + // prevent instantiation + } +} diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java new file mode 100644 index 000000000..280227a89 --- /dev/null +++ b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java @@ -0,0 +1,8 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.testhandler; + +import com.sap.cds.services.EventContext; + +public record EventContextHolder(String event, EventContext context) {} diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java new file mode 100644 index 000000000..207f49183 --- /dev/null +++ b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java @@ -0,0 +1,49 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.testhandler; + +import static com.sap.cds.services.cds.CqnService.EVENT_CREATE; +import static com.sap.cds.services.cds.CqnService.EVENT_UPDATE; + +import com.sap.cds.services.ServiceException; +import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.handler.annotations.Before; +import com.sap.cds.services.handler.annotations.ServiceName; +import com.sap.cds.services.persistence.PersistenceService; +import org.springframework.stereotype.Component; + +@ServiceName(value = "*", type = PersistenceService.class) +@Component +public class TestPersistenceHandler implements EventHandler { + + private volatile boolean throwExceptionOnUpdate = false; + private volatile boolean throwExceptionOnCreate = false; + + @Before(event = EVENT_UPDATE) + public void throwExceptionOnUpdate() { + if (throwExceptionOnUpdate) { + throw new ServiceException("Exception on update"); + } + } + + @Before(event = EVENT_CREATE) + public void throwExceptionOnCreate() { + if (throwExceptionOnCreate) { + throw new ServiceException("Exception on create"); + } + } + + public void reset() { + throwExceptionOnUpdate = false; + throwExceptionOnCreate = false; + } + + public void setThrowExceptionOnUpdate(boolean throwExceptionOnUpdate) { + this.throwExceptionOnUpdate = throwExceptionOnUpdate; + } + + public void setThrowExceptionOnCreate(boolean throwExceptionOnCreate) { + this.throwExceptionOnCreate = throwExceptionOnCreate; + } +} diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java new file mode 100644 index 000000000..341002905 --- /dev/null +++ b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java @@ -0,0 +1,120 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.testhandler; + +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.StatusCode; +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import com.sap.cds.feature.attachments.service.AttachmentService; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; +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.io.ByteArrayInputStream; +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@ServiceName(value = "*", type = AttachmentService.class) +@Profile(Profiles.TEST_HANDLER_ENABLED) +@Component +public class TestPluginAttachmentsServiceHandler implements EventHandler { + + private static final Marker marker = MarkerFactory.getMarker("DUMMY_HANDLER"); + private static final Logger logger = + LoggerFactory.getLogger(TestPluginAttachmentsServiceHandler.class); + + private static final Map documents = new ConcurrentHashMap<>(); + private static final List eventContextHolder = + Collections.synchronizedList(new ArrayList<>()); + + @On(event = AttachmentService.EVENT_CREATE_ATTACHMENT) + public void createAttachment(AttachmentCreateEventContext context) throws IOException { + logger.info(marker, "CREATE Attachment called in dummy handler"); + var contentId = UUID.randomUUID().toString(); + documents.put(contentId, context.getData().getContent().readAllBytes()); + context.setContentId(contentId); + context.getData().setStatus(StatusCode.CLEAN); + context.getData().setScannedAt(Instant.now()); + context.setCompleted(); + eventContextHolder.add( + new EventContextHolder(AttachmentService.EVENT_CREATE_ATTACHMENT, context)); + } + + @On(event = AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED) + public void markAttachmentAsDeleted(AttachmentMarkAsDeletedEventContext context) { + logger.info( + marker, + "DELETE Attachment called in dummy handler for document id {}", + context.getContentId()); + context.setCompleted(); + eventContextHolder.add( + new EventContextHolder(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED, context)); + } + + @On(event = AttachmentService.EVENT_READ_ATTACHMENT) + public void readAttachment(AttachmentReadEventContext context) { + logger.info( + marker, + "READ Attachment called in dummy handler for content id {}", + context.getContentId()); + var contentId = context.getContentId(); + var content = contentId != null ? documents.get(contentId) : null; + var stream = content != null ? new ByteArrayInputStream(content) : null; + context.getData().setContent(stream); + context.setCompleted(); + eventContextHolder.add( + new EventContextHolder(AttachmentService.EVENT_READ_ATTACHMENT, context)); + } + + @On(event = AttachmentService.EVENT_RESTORE_ATTACHMENT) + public void restoreAttachment(AttachmentRestoreEventContext context) { + logger.info( + marker, + "RESTORE Attachment called in dummy handler for timestamp {}", + context.getRestoreTimestamp()); + context.setCompleted(); + eventContextHolder.add( + new EventContextHolder(AttachmentService.EVENT_RESTORE_ATTACHMENT, context)); + } + + public List getEventContextForEvent(String event) { + var context = eventContextHolder.stream().filter(e -> e.event().equals(event)).toList(); + if (event.equals(AttachmentService.EVENT_CREATE_ATTACHMENT) && !context.isEmpty()) { + context.forEach( + c -> { + var createContext = (AttachmentCreateEventContext) c.context(); + createContext + .getData() + .setContent(new ByteArrayInputStream(documents.get(createContext.getContentId()))); + }); + } + return context; + } + + public List getEventContext() { + return eventContextHolder; + } + + public void clearEventContext() { + eventContextHolder.clear(); + } + + public void clearDocuments() { + documents.clear(); + } +} diff --git a/integration-tests/srv/src/main/resources/application.yaml b/integration-tests/srv/src/main/resources/application.yaml new file mode 100644 index 000000000..68a4a6e63 --- /dev/null +++ b/integration-tests/srv/src/main/resources/application.yaml @@ -0,0 +1,4 @@ +--- +spring: + config.activate.on-profile: default + sql.init.schema-locations: classpath:schema.sql diff --git a/integration-tests/srv/src/main/resources/banner.txt b/integration-tests/srv/src/main/resources/banner.txt new file mode 100644 index 000000000..875e346ff --- /dev/null +++ b/integration-tests/srv/src/main/resources/banner.txt @@ -0,0 +1,9 @@ + __ _ _ _ _ __ + / / /\ | | | | | | | | \ \ + / / / \ | |_ | |_ __ _ ___ | |__ _ __ ___ ___ _ __ | |_ ___ \ \ + < < / /\ \ | __| | __| / _` | / __| | '_ \ | '_ ` _ \ / _ \ | '_ \ | __| / __| > > + \ \ / ____ \ | |_ | |_ | (_| | | (__ | | | | | | | | | | | __/ | | | | | |_ \__ \ / / + \_\ /_/ \_\ \__| \__| \__,_| \___| |_| |_| |_| |_| |_| \___| |_| |_| \__| |___/ /_/ + ================================================================================================= + :: Spring Boot :: ${spring-boot.formatted-version} + diff --git a/integration-tests/srv/src/main/resources/messages.properties b/integration-tests/srv/src/main/resources/messages.properties new file mode 100644 index 000000000..81680eda5 --- /dev/null +++ b/integration-tests/srv/src/main/resources/messages.properties @@ -0,0 +1 @@ +not_clean=Error text for not clean \ No newline at end of file diff --git a/integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml b/integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml new file mode 100644 index 000000000..b5e184082 --- /dev/null +++ b/integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java new file mode 100644 index 000000000..5bc8eb32e --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java @@ -0,0 +1,21 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.common; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sap.cds.CdsData; +import com.sap.cds.Struct; +import java.util.HashMap; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +class JsonToCapMapperTestHelper { + + @Autowired private ObjectMapper objectMapper; + + public CdsData mapResponseToSingleResult(String resultBody) throws Exception { + return Struct.access(objectMapper.readValue(resultBody, HashMap.class)).as(CdsData.class); + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java new file mode 100644 index 000000000..f36ec06f0 --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java @@ -0,0 +1,27 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.common; + +import org.springframework.stereotype.Component; + +@Component +public class MalwareScanResultProvider { + + public String buildMalwareScanResult(boolean malware) { + return """ + { + \t"malwareDetected": %s, + \t"encryptedContentDetected": false, + \t"scanSize": 68, + \t"finding": "Win.Test.EICAR_HDB-1", + \t"mimeType": "text/plain", + \t"SHA256": "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + \t"extensions": [ + \t\t"txt" + \t] + } + """ + .formatted(malware); + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java new file mode 100644 index 000000000..426e88f2a --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java @@ -0,0 +1,167 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.common; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.sap.cds.CdsData; +import com.sap.cds.Struct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +@Component +public class MockHttpRequestHelper { + + public static final String ODATA_BASE_URL = "/odata/v4/"; + public static final String IF_MATCH = "If-Match"; + + @Autowired private JsonToCapMapperTestHelper mapper; + @Autowired private MockMvc mvc; + + private String contentType = MediaType.APPLICATION_JSON.toString(); + private String accept = MediaType.APPLICATION_JSON.toString(); + + public MvcResult executeGet(String url) throws Exception { + MockHttpServletRequestBuilder requestBuilder = + MockMvcRequestBuilders.get(url).contentType(contentType).accept(accept); + return mvc.perform(requestBuilder).andReturn(); + } + + public String executeGetWithSingleODataResponseAndAssertStatus(String url, HttpStatus status) + throws Exception { + var result = executeGet(url); + assertThat(result.getResponse().getStatus()).isEqualTo(status.value()); + return result.getResponse().getContentAsString(); + } + + public T executeGetWithSingleODataResponseAndAssertStatus( + String url, Class resultType, HttpStatus status) throws Exception { + var resultBody = executeGetWithSingleODataResponseAndAssertStatus(url, status); + return Struct.access(mapper.mapResponseToSingleResult(resultBody)).as(resultType); + } + + public MvcResult executePost(String url, String body) throws Exception { + return mvc.perform( + MockMvcRequestBuilders.post(url).contentType(contentType).accept(accept).content(body)) + .andReturn(); + } + + public MvcResult executePatch(String url, String body) throws Exception { + return executePatch(url, body, "*"); + } + + public MvcResult executePatch(String url, String body, String etag) throws Exception { + return mvc.perform( + MockMvcRequestBuilders.patch(url) + .contentType(contentType) + .accept(accept) + .header(IF_MATCH, etag) + .content(body)) + .andReturn(); + } + + public void executePostWithMatcher(String url, String body, ResultMatcher matcher) + throws Exception { + mvc.perform( + MockMvcRequestBuilders.post(url).contentType(contentType).accept(accept).content(body)) + .andExpect(matcher); + } + + public MvcResult executeDelete(String url) throws Exception { + return executeDelete(url, "*"); + } + + public MvcResult executeDelete(String url, String etag) throws Exception { + return mvc.perform( + MockMvcRequestBuilders.delete(url) + .contentType(contentType) + .accept(accept) + .header(IF_MATCH, etag)) + .andReturn(); + } + + public void executeDeleteWithMatcher(String url, ResultMatcher matcher) throws Exception { + executeDeleteWithMatcher(url, "*", matcher); + } + + public void executeDeleteWithMatcher(String url, String etag, ResultMatcher matcher) + throws Exception { + mvc.perform( + MockMvcRequestBuilders.delete(url) + .contentType(contentType) + .accept(accept) + .header(IF_MATCH, etag)) + .andExpect(matcher); + } + + public CdsData executePostWithODataResponseAndAssertStatusCreated(String url, String body) + throws Exception { + return executePostWithODataResponseAndAssertStatus(url, body, HttpStatus.CREATED); + } + + public void executePatchWithODataResponseAndAssertStatusOk(String url, String body) + throws Exception { + executePatchWithODataResponseAndAssertStatus(url, body, HttpStatus.OK); + } + + public CdsData executePostWithODataResponseAndAssertStatus( + String url, String body, HttpStatus status) throws Exception { + MvcResult result = executePost(url, body); + String resultBody = result.getResponse().getContentAsString(); + assertThat(result.getResponse().getStatus()) + .as("Unexpected HTTP status, with response body " + resultBody) + .isEqualTo(status.value()); + return mapper.mapResponseToSingleResult(resultBody); + } + + public void executePatchWithODataResponseAndAssertStatus( + String url, String body, HttpStatus status) throws Exception { + executePatchWithODataResponseAndAssertStatus(url, body, "*", status); + } + + public void executePatchWithODataResponseAndAssertStatus( + String url, String body, String etag, HttpStatus status) throws Exception { + MvcResult result = executePatch(url, body, etag); + String resultBody = result.getResponse().getContentAsString(); + assertThat(result.getResponse().getStatus()) + .as("Unexpected HTTP status, with response body " + resultBody) + .isEqualTo(status.value()); + } + + public void executePutWithMatcher(String url, byte[] body, ResultMatcher matcher) + throws Exception { + executePutWithMatcher(url, body, "*", matcher); + } + + public void executePutWithMatcher(String url, byte[] body, String etag, ResultMatcher matcher) + throws Exception { + mvc.perform( + MockMvcRequestBuilders.put(url) + .contentType(contentType) + .accept(accept) + .header(IF_MATCH, etag) + .content(body)) + .andExpect(matcher); + } + + public void setContentType(MediaType contentType) { + this.contentType = contentType.toString(); + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public void resetHelper() { + contentType = MediaType.APPLICATION_JSON.toString(); + accept = MediaType.APPLICATION_JSON.toString(); + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java new file mode 100644 index 000000000..a56d44a7c --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java @@ -0,0 +1,21 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.common; + +import com.sap.cds.ql.Delete; +import com.sap.cds.services.persistence.PersistenceService; +import java.util.Arrays; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class TableDataDeleter { + + @Autowired private PersistenceService persistenceService; + + public void deleteData(String... entityNames) { + Arrays.stream(entityNames) + .forEach(entityName -> persistenceService.run(Delete.from(entityName))); + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java new file mode 100644 index 000000000..843fa2bbc --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java @@ -0,0 +1,973 @@ +/* + * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.draftservice; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.sap.cds.Struct; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.AttachmentEntity; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.DraftRoots; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.DraftRoots_; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.Items; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.TestDraftService_; +import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; +import com.sap.cds.feature.attachments.integrationtests.common.TableDataDeleter; +import com.sap.cds.feature.attachments.integrationtests.testhandler.TestPersistenceHandler; +import com.sap.cds.feature.attachments.integrationtests.testhandler.TestPluginAttachmentsServiceHandler; +import com.sap.cds.feature.attachments.service.AttachmentService; +import com.sap.cds.ql.Select; +import com.sap.cds.ql.StructuredType; +import com.sap.cds.services.persistence.PersistenceService; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +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.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultMatcher; + +@SpringBootTest +@AutoConfigureMockMvc +abstract class DraftOdataRequestValidationBase { + + protected static final Logger logger = + LoggerFactory.getLogger(DraftOdataRequestValidationBase.class); + private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestDraftService/"; + private static final String BASE_ROOT_URL = BASE_URL + "DraftRoots"; + + @Autowired(required = false) + protected TestPluginAttachmentsServiceHandler serviceHandler; + + @Autowired protected MockHttpRequestHelper requestHelper; + @Autowired protected PersistenceService persistenceService; + @Autowired private TableDataDeleter dataDeleter; + @Autowired private TestPersistenceHandler testPersistenceHandler; + + @AfterEach + void teardown() { + dataDeleter.deleteData( + DraftRoots_.CDS_NAME, DraftRoots_.CDS_NAME + "_drafts", "cds.outbox.Messages"); + requestHelper.resetHelper(); + clearServiceHandlerContext(); + testPersistenceHandler.reset(); + } + + @Test + void deepCreateWorks() throws Exception { + var testContentAttachment = "testContent attachment"; + var testContentAttachmentEntity = "testContent attachmentEntity"; + + var selectedRoot = deepCreateAndActivate(testContentAttachment, testContentAttachmentEntity); + + assertThat(selectedRoot.getIsActiveEntity()).isTrue(); + + var selectedAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); + var selectedAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); + + verifyContentId(selectedAttachment.getContentId(), selectedAttachment.getId()); + assertThat(selectedAttachment.getFileName()).isEqualTo("itemAttachment.txt"); + assertThat(selectedAttachment.getMimeType()).contains("text/plain"); + verifyContent(selectedAttachment.getContent(), testContentAttachment); + verifyContentId(selectedAttachmentEntity.getContentId(), selectedAttachmentEntity.getId()); + assertThat(selectedAttachmentEntity.getFileName()).isEqualTo("itemAttachmentEntity.txt"); + assertThat(selectedAttachmentEntity.getMimeType()).contains("image/jpeg"); + verifyContent(selectedAttachmentEntity.getContent(), testContentAttachmentEntity); + verifyOnlyTwoCreateEvents(testContentAttachment, testContentAttachmentEntity); + } + + @Test + void contentCanBeReadFromDraft() throws Exception { + var testContentAttachment = "testContent attachment"; + var testContentAttachmentEntity = "testContent attachmentEntity"; + + var root = deepCreateAndActivate(testContentAttachment, testContentAttachmentEntity); + + var selectedRoot = selectStoredRootData(root); + assertThat(selectedRoot.getItems().get(0).getAttachments()) + .hasSize(1) + .first() + .satisfies(attachment -> verifyContent(attachment.getContent(), testContentAttachment)); + assertThat(selectedRoot.getItems().get(0).getAttachmentEntities()) + .hasSize(1) + .first() + .satisfies( + attachment -> verifyContent(attachment.getContent(), testContentAttachmentEntity)); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var attachmentUrl = + getAttachmentBaseUrl( + selectedRoot.getItems().get(0).getId(), + selectedRoot.getItems().get(0).getAttachments().get(0).getId(), + false) + + "/content"; + var attachmentEntityUrl = + getAttachmentEntityBaseUrl( + selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getId(), false) + + "/content"; + + Awaitility.await() + .atMost(60, TimeUnit.SECONDS) + .pollDelay(1, TimeUnit.SECONDS) + .pollInterval(2, TimeUnit.SECONDS) + .until( + () -> { + var attachmentResponse = requestHelper.executeGet(attachmentUrl); + var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); + var attachmentResponseContent = getResponseContent(attachmentResponse); + var attachmentEntityResponseContent = getResponseContent(attachmentEntityResponse); + var result = + attachmentResponseContent.equals(testContentAttachment) + && attachmentEntityResponseContent.equals(testContentAttachmentEntity); + if (!result) { + logger.info( + "Attachment response content: {}, Attachment Test Content: {}, Attachment Entity response content: {}, Attachment Entity Test Content: {}", + attachmentResponseContent, + testContentAttachment, + attachmentEntityResponseContent, + testContentAttachmentEntity); + } + return result; + }); + clearServiceHandlerContext(); + + var attachmentResponse = requestHelper.executeGet(attachmentUrl); + assertThat(attachmentResponse.getResponse().getContentAsString()) + .isEqualTo(testContentAttachment); + var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); + assertThat(attachmentEntityResponse.getResponse().getContentAsString()) + .isEqualTo(testContentAttachmentEntity); + verifyTwoReadEvents(); + } + + @Test + void deleteAttachmentAndActivateDraft() throws Exception { + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); + var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); + + var attachmentDeleteUrl = + getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), false); + var attachmentEntityDeleteUrl = getAttachmentEntityBaseUrl(itemAttachmentEntity.getId(), false); + + requestHelper.executeDeleteWithMatcher(attachmentDeleteUrl, status().isNoContent()); + requestHelper.executeDeleteWithMatcher(attachmentEntityDeleteUrl, status().isNoContent()); + verifyNoAttachmentEventsCalled(); + + prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); + + var selectedRootAfterDelete = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterDelete.getItems().get(0).getAttachments()).isEmpty(); + assertThat(selectedRootAfterDelete.getItems().get(0).getAttachmentEntities()).isEmpty(); + verifyOnlyTwoDeleteEvents(itemAttachment.getContentId(), itemAttachmentEntity.getContentId()); + } + + @Test + void updateAttachmentAndActivateDraft() throws Exception { + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); + var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); + + var changedAttachmentFileName = "changedAttachmentFileName.txt"; + var changedAttachmentEntityFileName = "changedAttachmentEntityFileName.txt"; + + updateFileName( + selectedRoot, + itemAttachment, + itemAttachmentEntity, + changedAttachmentFileName, + changedAttachmentEntityFileName); + + prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterUpdate = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getFileName()) + .isEqualTo(changedAttachmentFileName); + assertThat( + selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getFileName()) + .isEqualTo(changedAttachmentEntityFileName); + verifyNoAttachmentEventsCalled(); + } + + @Test + void updateAttachmentAndCancelDraft() throws Exception { + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); + var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); + + var originAttachmentFileName = itemAttachment.getFileName(); + var originAttachmentEntityFileName = itemAttachmentEntity.getFileName(); + + updateFileName( + selectedRoot, + itemAttachment, + itemAttachmentEntity, + "changedAttachmentFileName.txt", + "changedAttachmentEntityFileName.txt"); + + cancelDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterUpdate = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getFileName()) + .isEqualTo(originAttachmentFileName); + assertThat( + selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getFileName()) + .isEqualTo(originAttachmentEntityFileName); + verifyNoAttachmentEventsCalled(); + } + + @Test + void createAttachmentAndActivateDraft() throws Exception { + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var itemAttachment = selectedRoot.getItems().get(0); + + var newAttachmentContent = "new attachment content"; + createAttachmentWithContent(newAttachmentContent, itemAttachment.getId()); + var newAttachmentEntityContent = "new attachmentEntity content"; + createAttachmentEntityWithContent(newAttachmentEntityContent, itemAttachment); + + prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterCreate = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterCreate.getItems().get(0).getAttachments()).hasSize(2); + assertThat(selectedRootAfterCreate.getItems().get(0).getAttachmentEntities()).hasSize(2); + verifyOnlyTwoCreateEvents(newAttachmentContent, newAttachmentEntityContent); + } + + @Test + void createAttachmentAndCancelDraft() throws Exception { + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var itemAttachment = selectedRoot.getItems().get(0); + + var newAttachmentContent = "new attachment content"; + createAttachmentWithContent(newAttachmentContent, itemAttachment.getId()); + var newAttachmentEntityContent = "new attachmentEntity content"; + createAttachmentEntityWithContent(newAttachmentEntityContent, itemAttachment); + + cancelDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterCreate = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterCreate.getItems().get(0).getAttachments()).hasSize(1); + assertThat(selectedRootAfterCreate.getItems().get(0).getAttachmentEntities()).hasSize(1); + verifyTwoCreateAndDeleteEvents(newAttachmentContent, newAttachmentEntityContent); + } + + @Test + void deleteContentInDraft() throws Exception { + var selectedRoot = + deepCreateAndActivate( + "testContent attachment for delete", "testContent attachmentEntity for delete"); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); + var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); + + deleteContent(selectedRoot, itemAttachment, itemAttachmentEntity); + verifyNoAttachmentEventsCalled(); + + prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterDelete = selectStoredRootData(selectedRoot); + verifyContent( + selectedRootAfterDelete.getItems().get(0).getAttachments().get(0).getContent(), null); + verifyContent( + selectedRootAfterDelete.getItems().get(0).getAttachmentEntities().get(0).getContent(), + null); + verifyOnlyTwoDeleteEvents(itemAttachment.getContentId(), itemAttachmentEntity.getContentId()); + } + + @Test + void doNotDeleteContentInCancelledDraft() throws Exception { + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); + var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); + + deleteContent(selectedRoot, itemAttachment, itemAttachmentEntity); + + cancelDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterDelete = selectStoredRootData(selectedRoot); + verifyContent( + selectedRootAfterDelete.getItems().get(0).getAttachments().get(0).getContent(), + "testContent attachment"); + verifyContent( + selectedRootAfterDelete.getItems().get(0).getAttachmentEntities().get(0).getContent(), + "testContent attachmentEntity"); + verifyNoAttachmentEventsCalled(); + } + + @Test + void updateContentInDraft() throws Exception { + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); + var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); + + var attachmentContentId = itemAttachment.getContentId(); + var attachmentEntityContentId = itemAttachmentEntity.getContentId(); + + var newAttachmentContent = "new content attachment"; + putNewContentForAttachment( + newAttachmentContent, selectedRoot.getItems().get(0).getId(), itemAttachment.getId()); + var newAttachmentEntityContent = "new content attachmentEntity"; + putNewContentForAttachmentEntity(newAttachmentEntityContent, itemAttachmentEntity.getId()); + + prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterUpdate = selectStoredRootData(selectedRoot); + verifyContent( + selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getContent(), + newAttachmentContent); + verifyContent( + selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getContent(), + newAttachmentEntityContent); + verifyEventContextEmptyForEvent(AttachmentService.EVENT_READ_ATTACHMENT); + verifyTwoUpdateEvents( + newAttachmentContent, + attachmentContentId, + newAttachmentEntityContent, + attachmentEntityContentId); + var selectedRootAfterDeletion = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterDeletion.getItems().get(0).getAttachments().get(0).getContentId()) + .isNotEmpty(); + assertThat( + selectedRootAfterDeletion + .getItems() + .get(0) + .getAttachmentEntities() + .get(0) + .getContentId()) + .isNotEmpty(); + } + + @Test + void contentCanBeReadForActiveRoot() throws Exception { + var attachmentContent = "attachment Content"; + var attachmentEntityContent = "attachmentEntity Content"; + var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); + clearServiceHandlerContext(); + + readAndValidateActiveContent(selectedRoot, attachmentContent, attachmentEntityContent); + } + + @Test + void noChangesOnAttachmentsContentStillAvailable() throws Exception { + var attachmentContent = "attachment Content"; + var attachmentEntityContent = "attachmentEntity Content"; + var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var rootUrl = getRootUrl(selectedRoot.getId(), false); + requestHelper.executePatchWithODataResponseAndAssertStatusOk( + rootUrl, "{\"title\":\"some other title\"}"); + + prepareAndActiveDraft(rootUrl); + verifyNoAttachmentEventsCalled(); + + readAndValidateActiveContent(selectedRoot, attachmentContent, attachmentEntityContent); + } + + @Test + void deleteItemAndActivateDraft() throws Exception { + var attachmentContent = "attachment Content"; + var attachmentEntityContent = "attachmentEntity Content"; + var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var itemUrl = getItemUrl(selectedRoot.getItems().get(0), false); + requestHelper.executeDeleteWithMatcher(itemUrl, status().isNoContent()); + verifyNoAttachmentEventsCalled(); + + prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterDelete = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterDelete.getItems()).isEmpty(); + verifyOnlyTwoDeleteEvents( + selectedRoot.getItems().get(0).getAttachments().get(0).getContentId(), + selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getContentId()); + } + + @Test + void deleteItemAndCancelDraft() throws Exception { + var attachmentContent = "attachment Content"; + var attachmentEntityContent = "attachmentEntity Content"; + var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var itemUrl = getItemUrl(selectedRoot.getItems().get(0), false); + requestHelper.executeDeleteWithMatcher(itemUrl, status().isNoContent()); + + cancelDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterDelete = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterDelete.getItems()).isNotEmpty(); + assertThat(selectedRootAfterDelete.getItems().get(0).getAttachments()).isNotEmpty(); + assertThat(selectedRootAfterDelete.getItems().get(0).getAttachments().get(0).getContentId()) + .isNotEmpty(); + assertThat(selectedRootAfterDelete.getItems().get(0).getAttachmentEntities()).isNotEmpty(); + assertThat( + selectedRootAfterDelete.getItems().get(0).getAttachmentEntities().get(0).getContentId()) + .isNotEmpty(); + verifyNoAttachmentEventsCalled(); + } + + @Test + void noEventsForForDeletedRoot() throws Exception { + var selectedRoot = deepCreateAndActivate("attachmentContent", "attachmentEntityContent"); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var rootUrl = getRootUrl(selectedRoot.getId(), true); + requestHelper.executeDeleteWithMatcher(rootUrl, status().isNoContent()); + + var draftPrepareUrl = rootUrl + "/TestDraftService.draftPrepare"; + requestHelper.executePostWithMatcher( + draftPrepareUrl, "{\"SideEffectsQualifier\":\"\"}", status().isNotFound()); + + var select = Select.from(TestDraftService_.DRAFT_ROOTS); + var result = persistenceService.run(select).listOf(DraftRoots.class); + assertThat(result).isEmpty(); + + var attachmentContentId = selectedRoot.getItems().get(0).getAttachments().get(0).getContentId(); + var attachmentEntityContentId = + selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getContentId(); + + verifyOnlyTwoDeleteEvents(attachmentContentId, attachmentEntityContentId); + } + + @Test + void errorInTransactionAfterCreateCallsDelete() throws Exception { + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + createNewContentAndValidateEvents(selectedRoot); + + prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterCreate = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterCreate.getItems().get(0).getAttachments()).hasSize(2); + assertThat(selectedRootAfterCreate.getItems().get(0).getAttachmentEntities()).hasSize(2); + verifyNoAttachmentEventsCalled(); + } + + @Test + void errorInTransactionAfterCreateCallsDeleteAndNothingForCancel() throws Exception { + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + createNewContentAndValidateEvents(selectedRoot); + + cancelDraft(getRootUrl(selectedRoot.getId(), false)); + var selectedRootAfterCreate = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterCreate.getItems().get(0).getAttachments()).hasSize(1); + assertThat(selectedRootAfterCreate.getItems().get(0).getAttachmentEntities()).hasSize(1); + verifyNoAttachmentEventsCalled(); + } + + @Test + void errorInTransactionAfterUpdateCallsDelete() throws Exception { + var attachmentContent = "testContent attachment"; + var attachmentEntityContent = "testContent attachmentEntity"; + var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); + var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); + + updateContentWithErrorAndValidateEvents(selectedRoot, itemAttachment, itemAttachmentEntity); + + testPersistenceHandler.reset(); + prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); + verifyNothingHasChangedInDraft(selectedRoot, attachmentContent, attachmentEntityContent); + } + + @Test + void errorInTransactionAfterUpdateCallsDeleteEvenIfDraftIsCancelled() throws Exception { + var attachmentContent = "testContent attachment"; + var attachmentEntityContent = "testContent attachmentEntity"; + var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); + var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); + + updateContentWithErrorAndValidateEvents(selectedRoot, itemAttachment, itemAttachmentEntity); + + testPersistenceHandler.reset(); + cancelDraft(getRootUrl(selectedRoot.getId(), false)); + verifyNothingHasChangedInDraft(selectedRoot, attachmentContent, attachmentEntityContent); + } + + @Test + void createAndDeleteAttachmentWorks() throws Exception { + var selectedRoot = + deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); + clearServiceHandlerContext(); + createNewDraftForExistingRoot(selectedRoot.getId()); + + var itemAttachment = selectedRoot.getItems().get(0); + + var newAttachmentContent = "new attachment content"; + createAttachmentWithContent(newAttachmentContent, itemAttachment.getId()); + var newAttachmentEntityContent = "new attachmentEntity content"; + createAttachmentEntityWithContent(newAttachmentEntityContent, itemAttachment); + + var draftRoot = selectStoredRootData(DraftRoots_.CDS_NAME + "_drafts", selectedRoot); + + var existingAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); + var existingAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); + + var newAttachment = + draftRoot.getItems().get(0).getAttachments().stream() + .filter(attachment -> !attachment.getId().equals(existingAttachment.getId())) + .findAny() + .orElseThrow(); + var newAttachmentEntity = + draftRoot.getItems().get(0).getAttachmentEntities().stream() + .filter( + attachmentEntity -> + !attachmentEntity.getId().equals(existingAttachmentEntity.getId())) + .findAny() + .orElseThrow(); + + var attachmentDeleteUrl = + getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), newAttachment.getId(), false); + var attachmentEntityDeleteUrl = getAttachmentEntityBaseUrl(newAttachmentEntity.getId(), false); + + requestHelper.executeDeleteWithMatcher(attachmentDeleteUrl, status().isNoContent()); + requestHelper.executeDeleteWithMatcher(attachmentEntityDeleteUrl, status().isNoContent()); + + verifyTwoCreateAndDeleteEvents(newAttachmentContent, newAttachmentEntityContent); + clearServiceHandlerContext(); + + prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); + verifyNoAttachmentEventsCalled(); + } + + protected DraftRoots deepCreateAndActivate( + String testContentAttachment, String testContentAttachmentEntity) throws Exception { + var responseRoot = createNewDraft(); + var rootUrl = updateRoot(responseRoot); + var responseItem = createItem(rootUrl); + createAttachmentWithContent(testContentAttachment, responseItem.getId()); + createAttachmentEntityWithContent(testContentAttachmentEntity, responseItem); + prepareAndActiveDraft(rootUrl); + + return selectStoredRootData(responseRoot); + } + + private DraftRoots createNewDraft() throws Exception { + var responseRootCdsData = + requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); + return Struct.access(responseRootCdsData).as(DraftRoots.class); + } + + private void createNewDraftForExistingRoot(String rootId) throws Exception { + var url = getRootUrl(rootId, true) + "/TestDraftService.draftEdit"; + requestHelper.executePostWithODataResponseAndAssertStatus( + url, "{\"PreserveChanges\":true}", HttpStatus.OK); + } + + private String updateRoot(DraftRoots responseRoot) throws Exception { + responseRoot.setTitle("some title"); + var rootUrl = getRootUrl(responseRoot.getId(), false); + requestHelper.executePatchWithODataResponseAndAssertStatusOk(rootUrl, responseRoot.toJson()); + return rootUrl; + } + + private String getRootUrl(String rootId, boolean isActiveEntity) { + return BASE_ROOT_URL + "(ID=" + rootId + ",IsActiveEntity=" + isActiveEntity + ")"; + } + + private Items createItem(String rootUrl) throws Exception { + var item = Items.create(); + item.setTitle("some item"); + var itemUrl = rootUrl + "/items"; + var responseItemCdsData = + requestHelper.executePostWithODataResponseAndAssertStatusCreated(itemUrl, item.toJson()); + return Struct.access(responseItemCdsData).as(Items.class); + } + + private void createAttachmentWithContent(String testContentAttachment, String itemId) + throws Exception { + createAttachmentWithContent(testContentAttachment, itemId, status().isNoContent(), false); + } + + private void createAttachmentWithContent( + String testContentAttachment, String itemId, ResultMatcher matcher, boolean withError) + throws Exception { + var responseAttachment = createAttachment(itemId); + if (withError) { + testPersistenceHandler.setThrowExceptionOnUpdate(true); + } + putNewContentForAttachment(testContentAttachment, itemId, responseAttachment.getId(), matcher); + } + + private void putNewContentForAttachment( + String testContentAttachment, String itemId, String attachmentId) throws Exception { + putNewContentForAttachment(testContentAttachment, itemId, attachmentId, status().isNoContent()); + } + + private void putNewContentForAttachment( + String testContentAttachment, String itemId, String attachmentId, ResultMatcher matcher) + throws Exception { + var attachmentPutUrl = getAttachmentBaseUrl(itemId, attachmentId, false) + "/content"; + requestHelper.setContentType("text/plain"); + requestHelper.executePutWithMatcher( + attachmentPutUrl, testContentAttachment.getBytes(StandardCharsets.UTF_8), matcher); + requestHelper.resetHelper(); + } + + private Attachments createAttachment(String itemId) throws Exception { + var itemAttachment = Attachments.create(); + itemAttachment.setFileName("itemAttachment.txt"); + + var attachmentPostUrl = BASE_URL + "Items(ID=" + itemId + ",IsActiveEntity=false)/attachments"; + var responseAttachmentCdsData = + requestHelper.executePostWithODataResponseAndAssertStatusCreated( + attachmentPostUrl, itemAttachment.toJson()); + return Struct.access(responseAttachmentCdsData).as(Attachments.class); + } + + private void createAttachmentEntityWithContent( + String testContentAttachmentEntity, Items responseItem) throws Exception { + createAttachmentEntityWithContent( + testContentAttachmentEntity, responseItem, status().isNoContent(), false); + } + + private void createAttachmentEntityWithContent( + String testContentAttachmentEntity, + Items responseItem, + ResultMatcher matcher, + boolean withError) + throws Exception { + var responseAttachmentEntity = createAttachmentEntity(responseItem); + if (withError) { + testPersistenceHandler.setThrowExceptionOnUpdate(true); + } + putNewContentForAttachmentEntity( + testContentAttachmentEntity, responseAttachmentEntity.getId(), matcher); + } + + private void putNewContentForAttachmentEntity( + String testContentAttachmentEntity, String attachmentId) throws Exception { + putNewContentForAttachmentEntity( + testContentAttachmentEntity, attachmentId, status().isNoContent()); + } + + private void putNewContentForAttachmentEntity( + String testContentAttachmentEntity, String attachmentId, ResultMatcher matcher) + throws Exception { + var attachmentEntityPutUrl = + BASE_URL + "/AttachmentEntity(ID=" + attachmentId + ",IsActiveEntity=false)/content"; + requestHelper.setContentType("image/jpeg"); + requestHelper.executePutWithMatcher( + attachmentEntityPutUrl, + testContentAttachmentEntity.getBytes(StandardCharsets.UTF_8), + matcher); + requestHelper.resetHelper(); + } + + private AttachmentEntity createAttachmentEntity(Items responseItem) throws Exception { + var itemAttachmentEntity = AttachmentEntity.create(); + itemAttachmentEntity.setFileName("itemAttachmentEntity.txt"); + + var attachmentEntityPostUrl = getItemUrl(responseItem, false) + "/attachmentEntities"; + var responseAttachmentEntityCdsData = + requestHelper.executePostWithODataResponseAndAssertStatusCreated( + attachmentEntityPostUrl, itemAttachmentEntity.toJson()); + return Struct.access(responseAttachmentEntityCdsData).as(AttachmentEntity.class); + } + + private String getItemUrl(Items responseItem, boolean isActiveEntity) { + return BASE_URL + + "Items(ID=" + + responseItem.getId() + + ",IsActiveEntity=" + + isActiveEntity + + ")"; + } + + protected String getAttachmentBaseUrl( + String itemId, String attachmentId, boolean isActiveEntity) { + return BASE_URL + + "Items_attachments(up__ID=" + + itemId + + ",ID=" + + attachmentId + + ",IsActiveEntity=" + + isActiveEntity + + ")"; + } + + protected String getAttachmentEntityBaseUrl(String attachmentId, boolean isActiveEntity) { + return BASE_URL + + "AttachmentEntity(ID=" + + attachmentId + + ",IsActiveEntity=" + + isActiveEntity + + ")"; + } + + private void prepareAndActiveDraft(String rootUrl) throws Exception { + var draftPrepareUrl = rootUrl + "/TestDraftService.draftPrepare"; + var draftActivateUrl = rootUrl + "/TestDraftService.draftActivate"; + requestHelper.executePostWithMatcher( + draftPrepareUrl, "{\"SideEffectsQualifier\":\"\"}", status().isOk()); + requestHelper.executePostWithMatcher(draftActivateUrl, "{}", status().isOk()); + } + + private void cancelDraft(String rootUrl) throws Exception { + requestHelper.executeDeleteWithMatcher(rootUrl, status().isNoContent()); + } + + private DraftRoots selectStoredRootData(DraftRoots responseRoot) { + return selectStoredRootData(DraftRoots_.CDS_NAME, responseRoot); + } + + private DraftRoots selectStoredRootData(String entityName, DraftRoots responseRoot) { + var select = + Select.from(entityName) + .where(root -> root.get(DraftRoots.ID).eq(responseRoot.getId())) + .columns( + StructuredType::_all, + root -> + root.to(DraftRoots.ITEMS) + .expand( + StructuredType::_all, + item -> item.to(Items.ATTACHMENTS).expand(), + item -> item.to(Items.ATTACHMENT_ENTITIES).expand())); + return persistenceService.run(select).single(DraftRoots.class); + } + + protected void readAndValidateActiveContent( + DraftRoots selectedRoot, String attachmentContent, String attachmentEntityContent) + throws Exception { + var attachmentUrl = + getAttachmentBaseUrl( + selectedRoot.getItems().get(0).getId(), + selectedRoot.getItems().get(0).getAttachments().get(0).getId(), + true) + + "/content"; + var attachmentEntityUrl = + getAttachmentEntityBaseUrl( + selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getId(), true) + + "/content"; + + Awaitility.await() + .atMost(60, TimeUnit.SECONDS) + .pollDelay(1, TimeUnit.SECONDS) + .pollInterval(2, TimeUnit.SECONDS) + .until( + () -> { + var attachmentResponse = requestHelper.executeGet(attachmentUrl); + var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); + var attachmentContentAsString = attachmentResponse.getResponse().getContentAsString(); + var attachmentEntityContentAsString = + attachmentEntityResponse.getResponse().getContentAsString(); + + var booleanResult = + attachmentContentAsString.equals(attachmentContent) + && attachmentEntityContentAsString.equals(attachmentEntityContent); + + if (!booleanResult) { + logger.info( + "Attachment response content: {}, Attachment Test Content: {}, Attachment Entity response content: {}, Attachment Entity Test Content: {}", + attachmentContentAsString, + attachmentContent, + attachmentEntityContentAsString, + attachmentEntityContent); + } + return booleanResult; + }); + clearServiceHandlerContext(); + + var attachmentResponse = requestHelper.executeGet(attachmentUrl); + var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); + + assertThat(attachmentResponse.getResponse().getContentAsString()).isEqualTo(attachmentContent); + assertThat(attachmentEntityResponse.getResponse().getContentAsString()) + .isEqualTo(attachmentEntityContent); + verifyTwoReadEvents(); + } + + private void deleteContent( + DraftRoots selectedRoot, Attachments itemAttachment, AttachmentEntity itemAttachmentEntity) + throws Exception { + var attachmentUrl = + getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), false) + + "/content"; + var attachmentEntityUrl = + getAttachmentEntityBaseUrl(itemAttachmentEntity.getId(), false) + "/content"; + + requestHelper.executeDeleteWithMatcher(attachmentUrl, status().isNoContent()); + requestHelper.executeDeleteWithMatcher(attachmentEntityUrl, status().isNoContent()); + } + + private void updateFileName( + DraftRoots selectedRoot, + Attachments itemAttachment, + AttachmentEntity itemAttachmentEntity, + String changedAttachmentFileName, + String changedAttachmentEntityFileName) + throws Exception { + updateFileName( + selectedRoot, + itemAttachment, + itemAttachmentEntity, + changedAttachmentFileName, + changedAttachmentEntityFileName, + HttpStatus.OK); + } + + private void updateFileName( + DraftRoots selectedRoot, + Attachments itemAttachment, + AttachmentEntity itemAttachmentEntity, + String changedAttachmentFileName, + String changedAttachmentEntityFileName, + HttpStatus httpStatus) + throws Exception { + var attachmentUrl = + getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), false); + var attachmentEntityUrl = getAttachmentEntityBaseUrl(itemAttachmentEntity.getId(), false); + + requestHelper.executePatchWithODataResponseAndAssertStatus( + attachmentUrl, "{\"fileName\":\"" + changedAttachmentFileName + "\"}", httpStatus); + requestHelper.executePatchWithODataResponseAndAssertStatus( + attachmentEntityUrl, + "{\"fileName\":\"" + changedAttachmentEntityFileName + "\"}", + httpStatus); + } + + private void updateContentWithErrorAndValidateEvents( + DraftRoots selectedRoot, Attachments itemAttachment, AttachmentEntity itemAttachmentEntity) + throws Exception { + testPersistenceHandler.setThrowExceptionOnUpdate(true); + var newAttachmentContent = "new content attachment"; + putNewContentForAttachment( + newAttachmentContent, + selectedRoot.getItems().get(0).getId(), + itemAttachment.getId(), + status().is5xxServerError()); + var newAttachmentEntityContent = "new content attachmentEntity"; + putNewContentForAttachmentEntity( + newAttachmentEntityContent, itemAttachmentEntity.getId(), status().is5xxServerError()); + verifyTwoCreateAndRevertedDeleteEvents(); + clearServiceHandlerContext(); + } + + private void verifyNothingHasChangedInDraft( + DraftRoots selectedRoot, String attachmentContent, String attachmentEntityContent) + throws IOException { + var selectedRootAfterUpdate = selectStoredRootData(selectedRoot); + + verifyContent( + selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getContent(), + attachmentContent); + verifyContent( + selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getContent(), + attachmentEntityContent); + verifyNoAttachmentEventsCalled(); + var selectedRootAfterDeletion = selectStoredRootData(selectedRoot); + assertThat(selectedRootAfterDeletion.getItems().get(0).getAttachments().get(0).getContentId()) + .isNotEmpty(); + assertThat( + selectedRootAfterDeletion + .getItems() + .get(0) + .getAttachmentEntities() + .get(0) + .getContentId()) + .isNotEmpty(); + } + + private void createNewContentAndValidateEvents(DraftRoots selectedRoot) throws Exception { + var itemAttachment = selectedRoot.getItems().get(0); + var newAttachmentContent = "new attachment content"; + createAttachmentWithContent( + newAttachmentContent, itemAttachment.getId(), status().is5xxServerError(), true); + testPersistenceHandler.reset(); + var newAttachmentEntityContent = "new attachmentEntity content"; + createAttachmentEntityWithContent( + newAttachmentEntityContent, itemAttachment, status().is5xxServerError(), true); + verifyTwoCreateAndDeleteEvents(newAttachmentContent, newAttachmentEntityContent); + clearServiceHandlerContext(); + testPersistenceHandler.reset(); + } + + private String getResponseContent(MvcResult attachmentResponse) + throws UnsupportedEncodingException { + return attachmentResponse.getResponse().getStatus() == HttpStatus.OK.value() + ? attachmentResponse.getResponse().getContentAsString() + : ""; + } + + protected abstract void verifyContentId(String contentId, String attachmentId); + + protected abstract void verifyContent(InputStream attachment, String testContent) + throws IOException; + + protected abstract void verifyNoAttachmentEventsCalled(); + + protected abstract void clearServiceHandlerContext(); + + protected abstract void verifyEventContextEmptyForEvent(String... events); + + protected abstract void verifyOnlyTwoCreateEvents( + String newAttachmentContent, String newAttachmentEntityContent); + + protected abstract void verifyTwoCreateAndDeleteEvents( + String newAttachmentContent, String newAttachmentEntityContent); + + protected abstract void verifyTwoReadEvents(); + + protected abstract void verifyOnlyTwoDeleteEvents( + String attachmentContentId, String attachmentEntityContentId); + + protected abstract void verifyTwoUpdateEvents( + String newAttachmentContent, + String attachmentContentId, + String newAttachmentEntityContent, + String attachmentEntityContentId); + + protected abstract void verifyTwoCreateAndRevertedDeleteEvents(); +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java new file mode 100644 index 000000000..4ddb6280a --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java @@ -0,0 +1,288 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.draftservice; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import com.sap.cds.feature.attachments.integrationtests.testhandler.EventContextHolder; +import com.sap.cds.feature.attachments.service.AttachmentService; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles(Profiles.TEST_HANDLER_ENABLED) +class DraftOdataRequestValidationWithTestHandlerTest extends DraftOdataRequestValidationBase { + + private static final Logger logger = + LoggerFactory.getLogger(DraftOdataRequestValidationWithTestHandlerTest.class); + + @Test + void serviceHandlerIsNotEmpty() { + assertThat(serviceHandler).isNotNull(); + verifyNoAttachmentEventsCalled(); + } + + @Override + protected void verifyContentId(String contentId, String attachmentId) { + assertThat(contentId).isNotEmpty().isNotEqualTo(attachmentId); + } + + @Override + protected void verifyContent(InputStream attachment, String testContent) { + assertThat(attachment).isNull(); + } + + @Override + protected void verifyNoAttachmentEventsCalled() { + assertThat(serviceHandler.getEventContext()).isEmpty(); + } + + @Override + protected void clearServiceHandlerContext() { + serviceHandler.clearEventContext(); + } + + @Override + protected void verifyEventContextEmptyForEvent(String... events) { + Arrays.stream(events) + .forEach(event -> assertThat(serviceHandler.getEventContextForEvent(event)).isEmpty()); + } + + @Override + protected void verifyOnlyTwoCreateEvents( + String newAttachmentContent, String newAttachmentEntityContent) { + verifyEventContextEmptyForEvent( + AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED, + AttachmentService.EVENT_READ_ATTACHMENT); + var createEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); + assertThat(createEvents).hasSize(2); + var attachmentContentFound = + isAttachmentContentFoundInCreateEvent(createEvents, newAttachmentContent); + assertThat(attachmentContentFound).isTrue(); + var attachmentEntityContentFound = + isAttachmentContentFoundInCreateEvent(createEvents, newAttachmentEntityContent); + assertThat(attachmentEntityContentFound).isTrue(); + } + + @Override + protected void verifyTwoCreateAndDeleteEvents( + String newAttachmentContent, String newAttachmentEntityContent) { + awaitNumberOfExpectedEvents(4); + verifyEventContextEmptyForEvent(AttachmentService.EVENT_READ_ATTACHMENT); + var createEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); + assertThat(createEvents).hasSize(2); + var attachmentContentFound = + isAttachmentContentFoundInCreateEvent(createEvents, newAttachmentContent); + assertThat(attachmentContentFound).isTrue(); + var attachmentEntityContentFound = + isAttachmentContentFoundInCreateEvent(createEvents, newAttachmentEntityContent); + assertThat(attachmentEntityContentFound).isTrue(); + var deleteEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + assertThat(deleteEvents).hasSize(2); + deleteEvents.forEach( + event -> { + var deleteContext = (AttachmentMarkAsDeletedEventContext) event.context(); + assertThat(deleteContext.getContentId()).isNotEmpty(); + var createEventFound = + createEvents.stream() + .anyMatch( + createEvent -> { + var createContext = (AttachmentCreateEventContext) createEvent.context(); + return createContext.getContentId().equals(deleteContext.getContentId()); + }); + assertThat(createEventFound).isTrue(); + }); + } + + @Override + protected void verifyTwoReadEvents() { + verifyEventContextEmptyForEvent( + AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED, + AttachmentService.EVENT_CREATE_ATTACHMENT); + var readEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_READ_ATTACHMENT); + assertThat(readEvents).hasSize(2); + } + + @Override + protected void verifyOnlyTwoDeleteEvents( + String attachmentContentId, String attachmentEntityContentId) { + awaitNumberOfExpectedEvents(2); + verifyEventContextEmptyForEvent( + AttachmentService.EVENT_CREATE_ATTACHMENT, AttachmentService.EVENT_READ_ATTACHMENT); + var deleteEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + assertThat(deleteEvents).hasSize(2); + verifyDeleteEventContainsContentId(deleteEvents, attachmentContentId); + verifyDeleteEventContainsContentId(deleteEvents, attachmentEntityContentId); + } + + @Override + protected void verifyTwoUpdateEvents( + String newAttachmentContent, + String attachmentContentId, + String newAttachmentEntityContent, + String attachmentEntityContentId) { + awaitNumberOfExpectedEvents(4); + var createEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); + var deleteEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + assertThat(createEvents).hasSize(2); + verifyCreateEventFound(createEvents, newAttachmentContent); + verifyCreateEventFound(createEvents, newAttachmentEntityContent); + assertThat(deleteEvents).hasSize(2); + verifyDeleteEventContainsContentId(deleteEvents, attachmentContentId); + verifyDeleteEventContainsContentId(deleteEvents, attachmentEntityContentId); + } + + @Override + protected void verifyTwoCreateAndRevertedDeleteEvents() { + awaitNumberOfExpectedEvents(4); + var createEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); + var deleteEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + assertThat(createEvents).hasSize(2); + assertThat(deleteEvents).hasSize(2); + deleteEvents.forEach( + event -> { + var deleteContext = (AttachmentMarkAsDeletedEventContext) event.context(); + var createEventFound = + createEvents.stream() + .anyMatch( + createEvent -> { + var createContext = (AttachmentCreateEventContext) createEvent.context(); + return createContext.getContentId().equals(deleteContext.getContentId()); + }); + assertThat(createEventFound).isTrue(); + }); + } + + private void awaitNumberOfExpectedEvents(int expectedEvents) { + Awaitility.await() + .atMost(60, TimeUnit.SECONDS) + .pollDelay(1, TimeUnit.SECONDS) + .pollInterval(2, TimeUnit.SECONDS) + .until( + () -> { + var eventCalls = serviceHandler.getEventContext().size(); + logger.info( + "Waiting for expected size '{}' in handler context, was '{}'", + expectedEvents, + eventCalls); + var numberMatch = eventCalls >= expectedEvents; + if (!numberMatch) { + serviceHandler.getEventContext().forEach(event -> logger.info("Event: {}", event)); + } + return numberMatch; + }); + } + + private void verifyCreateEventFound(List createEvents, String newContent) { + var eventContentFound = + createEvents.stream() + .anyMatch( + event -> { + var createContext = (AttachmentCreateEventContext) event.context(); + try { + return Arrays.equals( + createContext.getData().getContent().readAllBytes(), + newContent.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + assertThat(eventContentFound).isTrue(); + } + + private boolean isAttachmentContentFoundInCreateEvent( + List createEvents, String newAttachmentContent) { + return createEvents.stream() + .anyMatch( + event -> { + var createContext = (AttachmentCreateEventContext) event.context(); + try { + return Arrays.equals( + createContext.getData().getContent().readAllBytes(), + newAttachmentContent.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + private void verifyDeleteEventContainsContentId( + List deleteEvents, String contentId) { + var eventFound = + deleteEvents.stream() + .anyMatch( + event -> { + var deleteContext = (AttachmentMarkAsDeletedEventContext) event.context(); + return deleteContext.getContentId().equals(contentId); + }); + assertThat(eventFound).isTrue(); + } + + // Override flaky tests from base class to disable them. + // These tests are affected by a race condition in the CAP runtime's outbox TaskScheduler + // where the second DELETE event is not processed when two transactions fail in quick succession. + + @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") + @Test + @Override + void errorInTransactionAfterCreateCallsDelete() throws Exception { + super.errorInTransactionAfterCreateCallsDelete(); + } + + @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") + @Test + @Override + void errorInTransactionAfterCreateCallsDeleteAndNothingForCancel() throws Exception { + super.errorInTransactionAfterCreateCallsDeleteAndNothingForCancel(); + } + + @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") + @Test + @Override + void errorInTransactionAfterUpdateCallsDelete() throws Exception { + super.errorInTransactionAfterUpdateCallsDelete(); + } + + @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") + @Test + @Override + void errorInTransactionAfterUpdateCallsDeleteEvenIfDraftIsCancelled() throws Exception { + super.errorInTransactionAfterUpdateCallsDeleteEvenIfDraftIsCancelled(); + } + + @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") + @Test + @Override + void createAttachmentAndCancelDraft() throws Exception { + super.createAttachmentAndCancelDraft(); + } + + @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") + @Test + @Override + void createAndDeleteAttachmentWorks() throws Exception { + super.createAndDeleteAttachmentWorks(); + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java new file mode 100644 index 000000000..817700d8a --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java @@ -0,0 +1,114 @@ +/* + * © 2024-2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.draftservice; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.sap.cds.feature.attachments.integrationtests.common.MalwareScanResultProvider; +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; +import org.wiremock.spring.ConfigureWireMock; +import org.wiremock.spring.EnableWireMock; +import org.wiremock.spring.InjectWireMock; + +@ActiveProfiles(profiles = {Profiles.TEST_HANDLER_DISABLED, Profiles.MALWARE_SCAN_ENABLED}) +@EnableWireMock({@ConfigureWireMock(port = 1111, name = "malware-scanner")}) +class DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest + extends DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest { + + private static final Logger logger = + LoggerFactory.getLogger( + DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.class); + + @InjectWireMock("malware-scanner") + private WireMockServer wiremock; + + @Autowired private MalwareScanResultProvider malwareScanResultProvider; + + @BeforeEach + void setup() { + mockMalwareScanResult(false); + } + + @Override + @AfterEach + void teardown() { + super.teardown(); + wiremock.resetAll(); + } + + @Test + void contentCanNotBeReadForActiveRoot() throws Exception { + wiremock.resetAll(); + mockMalwareScanResult(true); + var attachmentContent = "attachment Content"; + var attachmentEntityContent = "attachmentEntity Content"; + var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); + clearServiceHandlerContext(); + + var attachmentUrl = + getAttachmentBaseUrl( + selectedRoot.getItems().get(0).getId(), + selectedRoot.getItems().get(0).getAttachments().get(0).getId(), + true) + + "/content"; + var attachmentEntityUrl = + getAttachmentEntityBaseUrl( + selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getId(), true) + + "/content"; + + Awaitility.await() + .atMost(60, TimeUnit.SECONDS) + .pollDelay(1, TimeUnit.SECONDS) + .until( + () -> { + var attachmentResponse = requestHelper.executeGet(attachmentUrl); + var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); + + var attachmentResponseContent = attachmentResponse.getResponse().getContentAsString(); + var attachmentEntityResponseContent = + attachmentEntityResponse.getResponse().getContentAsString(); + + logger.info( + "Status should contain 'not_clean' for attachment and attachment entity but was: {} for attachment and {} for attachment entity", + attachmentResponseContent, + attachmentEntityResponseContent); + + return attachmentResponseContent.contains("not_clean") + && attachmentEntityResponseContent.contains("not_clean"); + }); + clearServiceHandlerContext(); + + var attachmentResponse = requestHelper.executeGet(attachmentUrl); + var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); + + assertThat(attachmentResponse.getResponse().getContentAsString()) + .contains("Error text for not clean"); + assertThat(attachmentEntityResponse.getResponse().getContentAsString()) + .contains("Error text for not clean"); + verifyTwoReadEvents(); + } + + private void mockMalwareScanResult(boolean malware) { + wiremock.stubFor( + post(urlEqualTo("/scan")) + .withBasicAuth("test-user", "test-password") + .willReturn( + aResponse() + .withBody(malwareScanResultProvider.buildMalwareScanResult(malware)) + .withStatus(200))); + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java new file mode 100644 index 000000000..7dce33a89 --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java @@ -0,0 +1,90 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.draftservice; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) +class DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest + extends DraftOdataRequestValidationBase { + + @Test + void serviceHandlerIsNull() { + assertThat(serviceHandler).isNull(); + } + + @Override + protected void verifyContentId(String contentId, String attachmentId) { + assertThat(contentId).isEqualTo(attachmentId); + } + + @Override + protected void verifyContent(InputStream attachment, String testContent) throws IOException { + if (Objects.nonNull(testContent)) { + assertThat(attachment.readAllBytes()).isEqualTo(testContent.getBytes(StandardCharsets.UTF_8)); + } else { + assertThat(attachment).isNull(); + } + } + + @Override + protected void verifyNoAttachmentEventsCalled() { + // no service handler - nothing to do + } + + @Override + protected void clearServiceHandlerContext() { + // no service handler - nothing to do + } + + @Override + protected void verifyEventContextEmptyForEvent(String... events) { + // no service handler - nothing to do + } + + @Override + protected void verifyOnlyTwoCreateEvents( + String newAttachmentContent, String newAttachmentEntityContent) { + // no service handler - nothing to do + } + + @Override + protected void verifyTwoCreateAndDeleteEvents( + String newAttachmentContent, String newAttachmentEntityContent) { + // no service handler - nothing to do + } + + @Override + protected void verifyTwoReadEvents() { + // no service handler - nothing to do + } + + @Override + protected void verifyOnlyTwoDeleteEvents( + String attachmentContentId, String attachmentEntityContentId) { + // no service handler - nothing to do + } + + @Override + protected void verifyTwoUpdateEvents( + String newAttachmentContent, + String attachmentContentId, + String newAttachmentEntityContent, + String attachmentEntityContentId) { + // no service handler - nothing to do + } + + @Override + protected void verifyTwoCreateAndRevertedDeleteEvents() { + // no service handler - nothing to do + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java new file mode 100644 index 000000000..f1ddbb49d --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java @@ -0,0 +1,159 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.draftservice; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sap.cds.CdsData; +import com.sap.cds.Struct; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.DraftRoots; +import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.Objects; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) +public class MediaValidatedAttachmentsDraftTest extends DraftOdataRequestValidationBase { + + private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestDraftService/"; + private static final String BASE_ROOT_URL = BASE_URL + "DraftRoots"; + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setup() { + requestHelper.setContentType(MediaType.APPLICATION_JSON); + } + + @ParameterizedTest + @CsvSource({ + "test.png,201", + "test.jpeg,201", + "test.pdf,415", + "test.txt,415", + "'',400", + "' ',400", + ".gitignore,415", + ".env,415", + ".hiddenfile,415" + }) + void shouldValidateMediaType_whenCreatingAttachmentInDraft(String fileName, int expectedStatus) + throws Exception { + String rootId = createDraftRootAndReturnId(); + String metadata = objectMapper.writeValueAsString(Map.of("fileName", fileName)); + + requestHelper.executePostWithMatcher( + buildDraftAttachmentCreationUrl(rootId), metadata, status().is(expectedStatus)); + } + + private String buildDraftAttachmentCreationUrl(String rootId) { + return BASE_ROOT_URL + + "(ID=" + + rootId + + ",IsActiveEntity=false)" + + "/mediaValidatedAttachments"; + } + + @Test + void shouldPass_whenFileNameMissing_inDraft() throws Exception { + String rootId = createDraftRootAndReturnId(); + String metadata = "{}"; + requestHelper.executePostWithMatcher( + buildDraftAttachmentCreationUrl(rootId), metadata, status().isCreated()); + } + + // Helper methods + private String createDraftRootAndReturnId() throws Exception { + CdsData response = + requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); + + DraftRoots draftRoot = Struct.access(response).as(DraftRoots.class); + String payload = objectMapper.writeValueAsString(Map.of("title", "Draft")); + requestHelper.executePatchWithODataResponseAndAssertStatusOk( + getRootUrl(draftRoot.getId(), false), payload); + + return draftRoot.getId(); + } + + private String getRootUrl(String rootId, boolean isActiveEntity) { + return BASE_ROOT_URL + "(ID=" + rootId + ",IsActiveEntity=" + isActiveEntity + ")"; + } + + // Required abstract method implementations + @Override + protected void verifyContentId(String contentId, String attachmentId) { + assertThat(contentId).isEqualTo(attachmentId); + } + + @Override + protected void verifyContent(InputStream attachment, String testContent) throws IOException { + if (Objects.nonNull(testContent)) { + assertThat(attachment.readAllBytes()) + .isEqualTo(testContent.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + } else { + assertThat(attachment).isNull(); + } + } + + @Override + protected void verifyNoAttachmentEventsCalled() { + // Implementation not required for this test + } + + @Override + protected void clearServiceHandlerContext() { + // Implementation not required for this test + } + + @Override + protected void verifyEventContextEmptyForEvent(String... events) { + // Implementation not required for this test + } + + @Override + protected void verifyOnlyTwoCreateEvents( + String newAttachmentContent, String newAttachmentEntityContent) { + // Implementation not required for this test + } + + @Override + protected void verifyTwoCreateAndDeleteEvents( + String newAttachmentContent, String newAttachmentEntityContent) { + // Implementation not required for this test + } + + @Override + protected void verifyTwoReadEvents() { + // Implementation not required for this test + } + + @Override + protected void verifyOnlyTwoDeleteEvents( + String attachmentContentId, String attachmentEntityContentId) { + // Implementation not required for this test + } + + @Override + protected void verifyTwoUpdateEvents( + String newAttachmentContent, + String attachmentContentId, + String newAttachmentEntityContent, + String attachmentEntityContentId) { + // Implementation not required for this test + } + + @Override + protected void verifyTwoCreateAndRevertedDeleteEvents() { + // Implementation not required for this test + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java new file mode 100644 index 000000000..df8db48a9 --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java @@ -0,0 +1,202 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.draftservice; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.sap.cds.Struct; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.DraftRoots; +import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Objects; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) +class SizeLimitedAttachmentsSizeValidationDraftTest extends DraftOdataRequestValidationBase { + + private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestDraftService/"; + private static final String BASE_ROOT_URL = BASE_URL + "DraftRoots"; + + @Test + void uploadContentWithin5MBLimitSucceeds() throws Exception { + // Arrange: Create draft with sizeLimitedAttachments + var draftRoot = createNewDraftWithSizeLimitedAttachments(); + var attachment = draftRoot.getSizeLimitedAttachments().get(0); + attachment.setFileName("test.txt"); + // Act & Assert: Upload 3MB content (within limit) succeeds + byte[] content = new byte[3 * 1024 * 1024]; // 3MB + var url = buildDraftSizeLimitedAttachmentContentUrl(draftRoot.getId(), attachment.getId()); + requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); + requestHelper.executePutWithMatcher(url, content, status().isNoContent()); + } + + @Test + void uploadContentExceeding5MBLimitFails() throws Exception { + // Arrange: Create draft with sizeLimitedAttachments + var draftRoot = createNewDraftWithSizeLimitedAttachments(); + var attachment = draftRoot.getSizeLimitedAttachments().get(0); + attachment.setFileName("test.txt"); + // Act: Try to upload 6MB content (exceeds limit) + byte[] content = new byte[6 * 1024 * 1024]; // 6MB + var url = buildDraftSizeLimitedAttachmentContentUrl(draftRoot.getId(), attachment.getId()); + requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); + requestHelper.executePutWithMatcher(url, content, status().is(413)); + + // Assert: Error response with HTTP 413 status code indicates size limit + // exceeded + } + + @Test + void uploadContentWithinLimitAndActivateDraftSucceeds() throws Exception { + // Arrange: Create draft with sizeLimitedAttachments (no prior activation) + var draftRoot = createNewDraftWithSizeLimitedAttachments(); + var attachment = draftRoot.getSizeLimitedAttachments().get(0); + + // Act: Upload 3MB content (within 5MB limit) + byte[] content = new byte[3 * 1024 * 1024]; // 3MB + var url = buildDraftSizeLimitedAttachmentContentUrl(draftRoot.getId(), attachment.getId()); + requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); + requestHelper.executePutWithMatcher(url, content, status().isNoContent()); + + // Assert: Draft activation succeeds + requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_JSON); + var rootUrl = getRootUrl(draftRoot.getId(), false); + var draftPrepareUrl = rootUrl + "/TestDraftService.draftPrepare"; + var draftActivateUrl = rootUrl + "/TestDraftService.draftActivate"; + requestHelper.executePostWithMatcher( + draftPrepareUrl, "{\"SideEffectsQualifier\":\"\"}", status().isOk()); + requestHelper.executePostWithMatcher(draftActivateUrl, "{}", status().isOk()); + } + + @Test + void uploadContentExceedingLimitOnFirstDraftRejects() throws Exception { + // Arrange: Create draft with sizeLimitedAttachments (no prior activation) + var draftRoot = createNewDraftWithSizeLimitedAttachments(); + var attachment = draftRoot.getSizeLimitedAttachments().get(0); + + // Act & Assert: Upload 6MB content to a brand-new draft attachment fails immediately + byte[] content = new byte[6 * 1024 * 1024]; // 6MB + var url = buildDraftSizeLimitedAttachmentContentUrl(draftRoot.getId(), attachment.getId()); + requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); + requestHelper.executePutWithMatcher(url, content, status().is(413)); + } + + // Helper methods + private DraftRoots createNewDraftWithSizeLimitedAttachments() throws Exception { + // Create new draft + var responseRootCdsData = + requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); + var draftRoot = Struct.access(responseRootCdsData).as(DraftRoots.class); + + // Update root with title + draftRoot.setTitle("Root with sizeLimitedAttachments"); + var rootUrl = getRootUrl(draftRoot.getId(), false); + requestHelper.executePatchWithODataResponseAndAssertStatusOk(rootUrl, draftRoot.toJson()); + + // Create sizeLimitedAttachment + var attachment = Attachments.create(); + attachment.setFileName("testFile.txt"); + attachment.setMimeType("text/plain"); + var attachmentUrl = rootUrl + "/sizeLimitedAttachments"; + var responseAttachmentCdsData = + requestHelper.executePostWithODataResponseAndAssertStatusCreated( + attachmentUrl, attachment.toJson()); + var createdAttachment = Struct.access(responseAttachmentCdsData).as(Attachments.class); + + // Build result with the attachment + draftRoot.setSizeLimitedAttachments(List.of(createdAttachment)); + return draftRoot; + } + + private String getRootUrl(String rootId, boolean isActiveEntity) { + return BASE_ROOT_URL + "(ID=" + rootId + ",IsActiveEntity=" + isActiveEntity + ")"; + } + + private String buildDraftSizeLimitedAttachmentContentUrl(String rootId, String attachmentId) { + return BASE_ROOT_URL + + "(ID=" + + rootId + + ",IsActiveEntity=false)" + + "/sizeLimitedAttachments(ID=" + + attachmentId + + ",up__ID=" + + rootId + + ",IsActiveEntity=false)" + + "/content"; + } + + // Required abstract method implementations + @Override + protected void verifyContentId(String contentId, String attachmentId) { + assertThat(contentId).isEqualTo(attachmentId); + } + + @Override + protected void verifyContent(InputStream attachment, String testContent) throws IOException { + if (Objects.nonNull(testContent)) { + assertThat(attachment.readAllBytes()) + .isEqualTo(testContent.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + } else { + assertThat(attachment).isNull(); + } + } + + @Override + protected void verifyNoAttachmentEventsCalled() { + // no service handler - nothing to do + } + + @Override + protected void clearServiceHandlerContext() { + // no service handler - nothing to do + } + + @Override + protected void verifyEventContextEmptyForEvent(String... events) { + // no service handler - nothing to do + } + + @Override + protected void verifyOnlyTwoCreateEvents( + String newAttachmentContent, String newAttachmentEntityContent) { + // no service handler - nothing to do + } + + @Override + protected void verifyTwoCreateAndDeleteEvents( + String newAttachmentContent, String newAttachmentEntityContent) { + // no service handler - nothing to do + } + + @Override + protected void verifyTwoReadEvents() { + // no service handler - nothing to do + } + + @Override + protected void verifyOnlyTwoDeleteEvents( + String attachmentContentId, String attachmentEntityContentId) { + // no service handler - nothing to do + } + + @Override + protected void verifyTwoUpdateEvents( + String newAttachmentContent, + String attachmentContentId, + String newAttachmentEntityContent, + String attachmentEntityContentId) { + // no service handler - nothing to do + } + + @Override + protected void verifyTwoCreateAndRevertedDeleteEvents() { + // no service handler - nothing to do + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java new file mode 100644 index 000000000..35a3b549b --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java @@ -0,0 +1,296 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sap.cds.Result; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots_; +import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.RootEntityBuilder; +import com.sap.cds.ql.Select; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) +class MediaValidatedAttachmentsNonDraftTest extends OdataRequestValidationBase { + private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots"; + private static final String MEDIA_VALIDATED_ATTACHMENTS = "mediaValidatedAttachments"; + private static final ObjectMapper objectMapper = new ObjectMapper(); + + protected void postServiceRoot(Roots serviceRoot) throws Exception { + String url = MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots"; + requestHelper.executePostWithMatcher(url, serviceRoot.toJson(), status().isCreated()); + } + + private Roots selectStoredRootWithMediaValidatedAttachments() { + Select select = + Select.from(Roots_.class) + .columns(r -> r._all(), r -> r.mediaValidatedAttachments().expand()); + + Result result = persistenceService.run(select); + return result.single(Roots.class); + } + + @BeforeEach + void setup() { + requestHelper.setContentType(MediaType.APPLICATION_JSON); + } + + @ParameterizedTest + @CsvSource({ + "image.jpg,image/jpeg,201", + "image.png,image/png,201", + "document.pdf,application/pdf,415", + "notes.txt,text/plain,415" + }) + void shouldValidateMediaTypes(String fileName, String mediaType, int expectedStatus) + throws Exception { + String rootId = createRootAndReturnId(); + String attachmentMetadata = createAttachmentMetadata(fileName); + + requestHelper.executePostWithMatcher( + createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), + attachmentMetadata, + status().is(expectedStatus)); + } + + @Test + void shouldRejectAttachment_whenFileNameIsEmpty() throws Exception { + String rootId = createRootAndReturnId(); + String fileName = ""; + String attachmentMetadata = createAttachmentMetadata(fileName); + + requestHelper.executePostWithMatcher( + createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), + attachmentMetadata, + status().isBadRequest()); + } + + @Test + void shouldAcceptUppercaseExtension_whenMimeTypeIsAllowed() throws Exception { + String rootId = createRootAndReturnId(); + String attachmentMetadata = createAttachmentMetadata("IMAGE.JPG"); + + requestHelper.executePostWithMatcher( + createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), attachmentMetadata, status().isCreated()); + } + + @Test + void shouldAcceptMixedCaseExtension() throws Exception { + String rootId = createRootAndReturnId(); + String attachmentMetadata = createAttachmentMetadata("image.JpEg"); + + requestHelper.executePostWithMatcher( + createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), attachmentMetadata, status().isCreated()); + } + + @Test + void shouldRejectAttachment_whenFileHasNoExtension() throws Exception { + String rootId = createRootAndReturnId(); + String attachmentMetadata = createAttachmentMetadata("filename"); + + requestHelper.executePostWithMatcher( + createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), + attachmentMetadata, + status().isUnsupportedMediaType()); + } + + @Test + void shouldRejectHiddenFile_whenFileStartsWithDot() throws Exception { + String rootId = createRootAndReturnId(); + String attachmentMetadata = createAttachmentMetadata(".gitignore"); + + requestHelper.executePostWithMatcher( + createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), + attachmentMetadata, + status().isUnsupportedMediaType()); + } + + @ParameterizedTest + @CsvSource({ + // valid cases + "'test1.jpeg|test2.jpeg',201", + // invalid media types + "'test.pdf',415", + "'test1.jpeg|test2.pdf',415", + // invalid filenames + "'',400", + "' ',400", + // edge cases + "'.gitignore',415" + }) + void shouldValidateMediaTypes_forMultipleAttachments(String fileNames, int expectedStatus) + throws Exception { + String payload = buildPayload(fileNames); + requestHelper.executePostWithMatcher(BASE_URL, payload, status().is(expectedStatus)); + } + + @Test + void shouldAcceptWhenMediaValidatedAttachments_hasNoAttachments() throws Exception { + Map payload = new HashMap<>(); + payload.put("title", "Hello World!"); + payload.put("mediaValidatedAttachments", List.of()); + + String payloadStr = objectMapper.writeValueAsString(payload); + requestHelper.executePostWithMatcher(BASE_URL, payloadStr, status().is(201)); + } + + @Test + void shouldAcceptDeepCreate_whenMixedValidAndAllValidAttachments() throws Exception { + Map payload = new HashMap<>(); + payload.put("title", "Hello World!"); + payload.put( + "mediaValidatedAttachments", + List.of(Map.of("fileName", "test1.jpeg"), Map.of("fileName", "test2.jpeg"))); + + payload.put("mimeValidatedAttachments", List.of(Map.of("fileName", "test3.pdf"))); + + requestHelper.executePostWithMatcher( + BASE_URL, objectMapper.writeValueAsString(payload), status().isCreated()); + } + + @Test + void shouldRejectDeepCreate_whenMixedValidAndInvalidAttachments() throws Exception { + Map payload = new HashMap<>(); + payload.put("title", "Hello World!"); + payload.put( + "mediaValidatedAttachments", + List.of(Map.of("fileName", "test1.pdf"), Map.of("fileName", "test2.jpeg"))); + + payload.put("mimeValidatedAttachments", List.of(Map.of("fileName", "test3.pdf"))); + + requestHelper.executePostWithMatcher( + BASE_URL, objectMapper.writeValueAsString(payload), status().isUnsupportedMediaType()); + } + + private String createRootAndReturnId() throws Exception { + // Build the initial Java object.. Root + Roots serviceRoot = buildServiceRoot(); + + // POST the root object to the server to create it in the database + postServiceRoot(serviceRoot); + + // Read the newly created entity back from the database + Roots selectedRoot = selectStoredRootWithMediaValidatedAttachments(); + + return selectedRoot.getId(); + } + + private String buildPayload(String fileNames) throws JsonProcessingException { + List> attachments = new ArrayList<>(); + fileNames = fileNames.replaceAll("^'+|'+$", ""); + for (String name : fileNames.split("\\|")) { + attachments.add(Map.of("fileName", name)); + } + Map payload = new HashMap<>(); + payload.put("title", "Hello World!"); + payload.put("mediaValidatedAttachments", attachments); + + return objectMapper.writeValueAsString(payload); + } + + private String createUrl(String rootId, String path) { + return BASE_URL + "(" + rootId + ")" + (path == null || path.isBlank() ? "" : "/" + path); + } + + private String createAttachmentMetadata(String fileName) throws JsonProcessingException { + return objectMapper.writeValueAsString(Map.of("fileName", fileName)); + } + + // helper method + private Roots buildServiceRoot() { + return RootEntityBuilder.create().setTitle("Root").build(); + } + + // Override abstract methods from OdataRequestValidationBase + + @Override + protected void executeContentRequestAndValidateContent(String url, String content) + throws Exception { + // Implementation not required for this test + } + + @Override + protected void verifyContentId( + Attachments attachmentWithExpectedContent, String attachmentId, String contentId) { + // Implementation not required for this test + } + + @Override + protected void verifyContentAndContentId( + Attachments attachment, String testContent, Attachments itemAttachment) { + // Implementation not required for this test + } + + @Override + protected void verifyContentAndContentIdForAttachmentEntity( + AttachmentEntity attachment, String testContent, AttachmentEntity itemAttachment) { + // Implementation not required for this test + } + + @Override + public void verifySingleCreateAndUpdateEvent(String arg1, String arg2, String arg3) { + // Implementation not required for this test + } + + @Override + public void clearServiceHandlerContext() { + // Implementation not required for this test + } + + @Override + public void verifySingleReadEvent(String arg) { + // Implementation not required for this test + } + + @Override + public void verifyTwoDeleteEvents(AttachmentEntity entity, Attachments attachments) { + // Implementation not required for this test + } + + @Override + public void clearServiceHandlerDocuments() { + // Implementation not required for this test + } + + @Override + public void verifyEventContextEmptyForEvent(String... args) { + // Implementation not required for this test + } + + @Override + public void verifyNoAttachmentEventsCalled() { + // Implementation not required for this test + } + + @Override + public void verifyNumberOfEvents(String arg, int count) { + // Implementation not required for this test + } + + @Override + public void verifySingleCreateEvent(String arg1, String arg2) { + // Implementation not required for this test + } + + @Override + public void verifySingleDeletionEvent(String arg) { + // Implementation not required for this test + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java new file mode 100644 index 000000000..a4774951f --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java @@ -0,0 +1,884 @@ +/* + * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity_; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Items; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Items_; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots_; +import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; +import com.sap.cds.feature.attachments.integrationtests.common.TableDataDeleter; +import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.AttachmentsBuilder; +import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.AttachmentsEntityBuilder; +import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.ItemEntityBuilder; +import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.RootEntityBuilder; +import com.sap.cds.feature.attachments.integrationtests.testhandler.TestPersistenceHandler; +import com.sap.cds.feature.attachments.integrationtests.testhandler.TestPluginAttachmentsServiceHandler; +import com.sap.cds.feature.attachments.service.AttachmentService; +import com.sap.cds.ql.Select; +import com.sap.cds.ql.StructuredType; +import com.sap.cds.ql.cqn.CqnSelect; +import com.sap.cds.services.persistence.PersistenceService; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultMatcher; + +@SpringBootTest +@AutoConfigureMockMvc +abstract class OdataRequestValidationBase { + + protected static final Logger logger = LoggerFactory.getLogger(OdataRequestValidationBase.class); + + @Autowired(required = false) + protected TestPluginAttachmentsServiceHandler serviceHandler; + + @Autowired protected MockHttpRequestHelper requestHelper; + @Autowired protected PersistenceService persistenceService; + @Autowired private TableDataDeleter dataDeleter; + @Autowired private TestPersistenceHandler testPersistenceHandler; + + @AfterEach + void teardown() { + dataDeleter.deleteData(Roots_.CDS_NAME); + clearServiceHandlerContext(); + clearServiceHandlerDocuments(); + requestHelper.resetHelper(); + testPersistenceHandler.reset(); + } + + @Test + void deepCreateWorks() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + verifySelectedRoot(selectedRoot, serviceRoot); + verifyNoAttachmentEventsCalled(); + } + + @Test + void putContentWorksForUrlsWithNavigation() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + var attachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); + + verifyContentAndContentId(attachment, content, itemAttachment); + verifySingleCreateEvent(attachment.getContentId(), content); + } + + @Test + void putContentWorksForUrlsWithoutNavigation() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + + var content = putContentForAttachmentWithoutNavigation(itemAttachment); + var attachment = selectUpdatedAttachment(itemAttachment); + + verifyContentAndContentIdForAttachmentEntity(attachment, content, itemAttachment); + verifySingleCreateEvent(attachment.getContentId(), content); + } + + @Test + void expandReadOfAttachmentsHasNoFilledContent() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + + var url = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); + var responseItem = + requestHelper.executeGetWithSingleODataResponseAndAssertStatus( + url, Items.class, HttpStatus.OK); + + assertThat(responseItem.getAttachments()).hasSameSizeAs(item.getAttachments()); + assertThat(responseItem.getAttachments()) + .allSatisfy( + attachment -> { + assertThat(attachment.getContent()).isNull(); + assertThat(attachment.get("content@mediaContentType")).isNull(); + assertThat(attachment.getContentId()).isNull(); + }); + verifyNoAttachmentEventsCalled(); + } + + @Test + void navigationReadOfAttachmentsHasFilledContent() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + + var url = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); + var responseItem = + requestHelper.executeGetWithSingleODataResponseAndAssertStatus( + url, Items.class, HttpStatus.OK); + + assertThat(responseItem.getAttachments()).hasSameSizeAs(item.getAttachments()); + + var attachmentWithExpectedContent = + responseItem.getAttachments().stream() + .filter(attach -> attach.getId().equals(itemAttachment.getId())) + .findAny() + .orElseThrow(); + assertThat(attachmentWithExpectedContent) + .containsEntry("content@mediaContentType", "application/octet-stream;charset=UTF-8") + .containsEntry(Attachments.FILE_NAME, itemAttachment.getFileName()); + assertThat(attachmentWithExpectedContent.getStatus()).isNotEmpty(); + verifyContentId( + attachmentWithExpectedContent, itemAttachment.getId(), itemAttachment.getContentId()); + verifySingleCreateEvent(attachmentWithExpectedContent.getContentId(), content); + } + + @Test + void navigationReadOfAttachmentsReturnsContent() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); + + var url = + buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()) + + "/content"; + executeContentRequestAndValidateContent(url, content); + verifySingleReadEvent(itemAttachmentAfterChange.getContentId()); + } + + @Test + void navigationDeleteOfContentClears() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); + + executeDeleteAndCheckNoDataCanBeRead( + buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()), + itemAttachmentAfterChange.getContentId()); + + var expandUrl = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); + var responseItem = + requestHelper.executeGetWithSingleODataResponseAndAssertStatus( + expandUrl, Items.class, HttpStatus.OK); + + assertThat(responseItem.getAttachments()).hasSameSizeAs(item.getAttachments()); + assertThat(responseItem.getAttachments()) + .allSatisfy( + attachment -> { + assertThat(attachment.getContent()).isNull(); + assertThat(attachment.get("content@mediaContentType")).isNull(); + assertThat(attachment.getContentId()).isNull(); + }); + verifyNoAttachmentEventsCalled(); + } + + @Test + void navigationDeleteOfAttachmentClearsContentField() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); + + var url = + buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()); + requestHelper.executeDelete(url); + var expandUrl = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); + var responseItem = + requestHelper.executeGetWithSingleODataResponseAndAssertStatus( + expandUrl, Items.class, HttpStatus.OK); + + assertThat(responseItem.getAttachments()).hasSize(1); + assertThat(responseItem.getAttachments()) + .first() + .satisfies( + attachment -> { + assertThat(attachment.getContent()).isNull(); + assertThat(attachment.get("content@mediaContentType")).isNull(); + assertThat(attachment.getContentId()).isNull(); + }); + verifySingleDeletionEvent(itemAttachmentAfterChange.getContentId()); + } + + @Test + void navigationDeleteCallsTwiceReturnsError() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); + + var url = + buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()); + requestHelper.executeDelete(url); + var result = requestHelper.executeDelete(url); + + assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.PRECONDITION_FAILED.value()); + verifySingleDeletionEvent(itemAttachmentAfterChange.getContentId()); + } + + @Test + void directReadOfAttachmentsHasNoContentFilled() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + + var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); + var responseAttachment = + requestHelper.executeGetWithSingleODataResponseAndAssertStatus( + url, Attachments.class, HttpStatus.OK); + + assertThat(responseAttachment.get("content@mediaContentType")).isNull(); + assertThat(responseAttachment.getContentId()).isNull(); + assertThat(responseAttachment.getFileName()).isEqualTo(itemAttachment.getFileName()); + verifyNoAttachmentEventsCalled(); + } + + @Test + void directReadOfAttachmentsHasFilledContent() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + putContentForAttachmentWithoutNavigation(itemAttachment); + clearServiceHandlerContext(); + + var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); + var responseAttachment = + requestHelper.executeGetWithSingleODataResponseAndAssertStatus( + url, Attachments.class, HttpStatus.OK); + + assertThat(responseAttachment) + .containsEntry("content@mediaContentType", "application/octet-stream;charset=UTF-8") + .containsEntry(Attachments.FILE_NAME, itemAttachment.getFileName()); + verifyContentId(responseAttachment, itemAttachment.getId(), itemAttachment.getContentId()); + verifyNoAttachmentEventsCalled(); + } + + @Test + void directReadOfAttachmentsReturnsContent() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + var content = putContentForAttachmentWithoutNavigation(itemAttachment); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentAfterChange = getRandomItemAttachmentEntity(selectedItemAfterChange); + + var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()) + "/content"; + executeContentRequestAndValidateContent(url, content); + verifySingleReadEvent(itemAttachmentAfterChange.getContentId()); + } + + @Test + void directDeleteOfContentClears() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + putContentForAttachmentWithoutNavigation(itemAttachment); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentAfterChange = getRandomItemAttachmentEntity(selectedItemAfterChange); + + executeDeleteAndCheckNoDataCanBeRead( + buildDirectAttachmentEntityUrl(itemAttachment.getId()), + itemAttachmentAfterChange.getContentId()); + + var expandUrl = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); + var responseItem = + requestHelper.executeGetWithSingleODataResponseAndAssertStatus( + expandUrl, Items.class, HttpStatus.OK); + + assertThat(responseItem.getAttachmentEntities()).hasSameSizeAs(item.getAttachmentEntities()); + assertThat(responseItem.getAttachmentEntities()) + .allSatisfy( + attachment -> { + assertThat(attachment.getContent()).isNull(); + assertThat(attachment.get("content@mediaContentType")).isNull(); + assertThat(attachment.getContentId()).isNull(); + }); + verifyNoAttachmentEventsCalled(); + } + + @Test + void directDeleteOfAttachmentClearsContentField() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + putContentForAttachmentWithoutNavigation(itemAttachment); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentAfterChange = getRandomItemAttachmentEntity(selectedItemAfterChange); + + var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); + requestHelper.executeDelete(url); + var expandUrl = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); + var responseItem = + requestHelper.executeGetWithSingleODataResponseAndAssertStatus( + expandUrl, Items.class, HttpStatus.OK); + + assertThat(responseItem.getAttachmentEntities()).isEmpty(); + verifySingleDeletionEvent(itemAttachmentAfterChange.getContentId()); + } + + @Test + void directDeleteCalledTwiceReturnsError() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + putContentForAttachmentWithoutNavigation(itemAttachment); + clearServiceHandlerContext(); + + var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); + requestHelper.executeDelete(url); + MvcResult mvcResult = requestHelper.executeDelete(url); + + assertThat(mvcResult.getResponse().getStatus()) + .isEqualTo(HttpStatus.PRECONDITION_FAILED.value()); + if (Objects.nonNull(serviceHandler)) { + Awaitility.await().until(() -> serviceHandler.getEventContext().size() == 1); + verifyNumberOfEvents(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED, 1); + verifyEventContextEmptyForEvent( + AttachmentService.EVENT_CREATE_ATTACHMENT, AttachmentService.EVENT_READ_ATTACHMENT); + } + } + + @Test + void rootDeleteDeletesAllContents() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachmentEntity = getRandomItemAttachmentEntity(item); + var itemAttachment = getRandomItemAttachment(item); + + putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + putContentForAttachmentWithoutNavigation(itemAttachmentEntity); + verifyNumberOfEvents(AttachmentService.EVENT_CREATE_ATTACHMENT, 2); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentEntityAfterChange = getRandomItemAttachmentEntity(selectedItemAfterChange); + var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); + + var url = + MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots(" + selectedRoot.getId() + ")"; + requestHelper.executeDeleteWithMatcher(url, status().isNoContent()); + + verifyTwoDeleteEvents(itemAttachmentEntityAfterChange, itemAttachmentAfterChange); + } + + @Test + void updateContentWorksForUrlsWithNavigation() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + itemAttachment.setNote("note 1"); + putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + itemAttachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); + itemAttachment.setNote("note 2"); + var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + var attachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); + + verifyContentAndContentId(attachment, content, itemAttachment); + verifySingleCreateAndUpdateEvent( + attachment.getContentId(), itemAttachment.getContentId(), content); + } + + @Test + void updateContentWorksForUrlsWithoutNavigation() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + itemAttachment.setNote("note 1"); + putContentForAttachmentWithoutNavigation(itemAttachment); + itemAttachment = selectUpdatedAttachment(itemAttachment); + itemAttachment.setNote("note 2"); + var content = putContentForAttachmentWithoutNavigation(itemAttachment); + var attachment = selectUpdatedAttachment(itemAttachment); + + verifyContentAndContentIdForAttachmentEntity(attachment, content, itemAttachment); + verifySingleCreateAndUpdateEvent( + attachment.getContentId(), itemAttachment.getContentId(), content); + } + + @Test + void errorInTransactionAfterCreateCallsDelete() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + testPersistenceHandler.setThrowExceptionOnUpdate(true); + putContentForAttachmentWithNavigation( + selectedRoot, itemAttachment, status().is5xxServerError()); + var attachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); + + assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getContentId()); + assertThat(attachment.getContent()).isEqualTo(itemAttachment.getContent()); + } + + @Test + void updateContentWithErrorsResetsForUrlsWithNavigation() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + itemAttachment.setNote("note 1"); + var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + itemAttachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); + itemAttachment.setNote("note 2"); + testPersistenceHandler.setThrowExceptionOnUpdate(true); + putContentForAttachmentWithNavigation( + selectedRoot, itemAttachment, status().is5xxServerError()); + var attachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); + + verifyContentAndContentId(attachment, content, itemAttachment); + assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getContentId()); + verifySingleCreateAndUpdateEvent( + attachment.getContentId(), itemAttachment.getContentId(), content); + } + + @Test + void updateContentWithErrorResetsForUrlsWithoutNavigation() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + itemAttachment.setNote("note 1"); + var content = putContentForAttachmentWithoutNavigation(itemAttachment); + itemAttachment = selectUpdatedAttachment(itemAttachment); + itemAttachment.setNote("note 2"); + testPersistenceHandler.setThrowExceptionOnUpdate(true); + putContentForAttachmentWithoutNavigation(itemAttachment, status().is5xxServerError()); + var attachment = selectUpdatedAttachment(itemAttachment); + + verifyContentAndContentIdForAttachmentEntity(attachment, content, itemAttachment); + assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getContentId()); + verifySingleCreateAndUpdateEvent( + attachment.getContentId(), itemAttachment.getContentId(), content); + } + + @ParameterizedTest + @CsvSource({"status,INFECTED", "contentId,TEST"}) + void statusCannotBeUpdated(String field, String value) throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + putContentForAttachmentWithoutNavigation(itemAttachment); + itemAttachment.setStatus(value); + var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); + + requestHelper.resetHelper(); + requestHelper.executePatchWithODataResponseAndAssertStatus( + url, "{\"" + field + "\":\"" + value + "\"}", HttpStatus.OK); + + selectedRoot = selectStoredRootWithDeepData(); + item = getItemWithAttachmentEntity(selectedRoot); + itemAttachment = getRandomItemAttachmentEntity(item); + assertThat(itemAttachment.get(field)).isNotNull().isNotEqualTo(value); + } + + @Test + void wrongEtagCouldNotBeUpdated() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + + var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); + requestHelper.executePatchWithODataResponseAndAssertStatus( + url, + "{\"fileName\":\"test_for_change.txt\"}", + "W/\"2024-05-06T15:24:29.657713600Z\"", + HttpStatus.PRECONDITION_FAILED); + + var selectedRootAfterChange = selectStoredRootWithDeepData(); + var itemAfterChange = getItemWithAttachmentEntity(selectedRootAfterChange); + var itemAttachmentAfterChange = getRandomItemAttachmentEntity(itemAfterChange); + assertThat(itemAttachmentAfterChange.getFileName()).isEqualTo(itemAttachment.getFileName()); + } + + @Test + void correctEtagCanBeUpdated() throws Exception { + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachmentEntity(selectedRoot); + var itemAttachment = getRandomItemAttachmentEntity(item); + var modifiedAt = itemAttachment.getModifiedAt(); + var eTag = "W/\"" + modifiedAt + "\""; + + var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); + requestHelper.executePatchWithODataResponseAndAssertStatus( + url, "{\"fileName\":\"test_for_change.txt\"}", eTag, HttpStatus.OK); + + var selectedRootAfterChange = selectStoredRootWithDeepData(); + var itemAfterChange = getItemWithAttachmentEntity(selectedRootAfterChange); + var itemAttachmentAfterChange = getRandomItemAttachmentEntity(itemAfterChange); + assertThat(itemAttachmentAfterChange.getFileName()).isEqualTo("test_for_change.txt"); + } + + protected Items selectItem(Items item) { + var selectedRootAfterContentCreated = selectStoredRootWithDeepData(); + return selectedRootAfterContentCreated.getItems().stream() + .filter(i -> i.getId().equals(item.getId())) + .findAny() + .orElseThrow(); + } + + protected Roots buildServiceRootWithDeepData() { + return RootEntityBuilder.create() + .setTitle("some root title") + .addAttachments( + AttachmentsEntityBuilder.create().setFileName("fileRoot.txt").setMimeType("text/plain")) + .addItems( + ItemEntityBuilder.create() + .setTitle("some item 1 title") + .addAttachments( + AttachmentsBuilder.create() + .setFileName("fileItem1.txt") + .setMimeType("text/plain"), + AttachmentsBuilder.create() + .setFileName("fileItem2.txt") + .setMimeType("text/plain")), + ItemEntityBuilder.create() + .setTitle("some item 2 title") + .addAttachmentEntities( + AttachmentsEntityBuilder.create() + .setFileName("fileItem3.text") + .setMimeType("text/plain")) + .addAttachments( + AttachmentsBuilder.create() + .setFileName("fileItem3.text") + .setMimeType("text/plain"))) + .build(); + } + + protected void postServiceRoot(Roots serviceRoot) throws Exception { + var url = MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots"; + requestHelper.executePostWithMatcher(url, serviceRoot.toJson(), status().isCreated()); + } + + protected Roots selectStoredRootWithDeepData() { + CqnSelect select = + Select.from(Roots_.class) + .columns( + StructuredType::_all, + root -> root.attachments().expand(), + root -> + root.items() + .expand( + StructuredType::_all, + item -> item.attachments().expand(), + item -> item.attachmentEntities().expand())); + var result = persistenceService.run(select); + return result.single(Roots.class); + } + + private void verifySelectedRoot(Roots selectedRoot, Roots serviceRoot) { + assertThat(selectedRoot.getId()).isNotEmpty(); + assertThat(selectedRoot.getTitle()).isEqualTo(serviceRoot.getTitle()); + assertThat(selectedRoot.getAttachments()) + .hasSize(1) + .first() + .satisfies( + attachment -> { + assertThat(attachment.getId()).isNotEmpty(); + assertThat(attachment.getFileName()) + .isEqualTo(serviceRoot.getAttachments().get(0).getFileName()); + assertThat(attachment.getMimeType()) + .isEqualTo(serviceRoot.getAttachments().get(0).getMimeType()); + }); + assertThat(selectedRoot.getItems()) + .hasSize(2) + .first() + .satisfies( + item -> { + assertThat(item.getId()).isNotEmpty(); + assertThat(item.getTitle()).isEqualTo(serviceRoot.getItems().get(0).getTitle()); + assertThat(item.getAttachments()).hasSize(2); + }); + assertThat(selectedRoot.getItems().get(1).getId()).isNotEmpty(); + assertThat(selectedRoot.getItems().get(1).getTitle()) + .isEqualTo(serviceRoot.getItems().get(1).getTitle()); + assertThat(selectedRoot.getItems().get(1).getAttachments()).hasSize(1); + } + + protected Attachments getRandomItemAttachment(Items selectedItem) { + return selectedItem.getAttachments().get(0); + } + + protected Attachments getRandomRootSizeLimitedAttachment(Roots selectedRoot) { + return selectedRoot.getSizeLimitedAttachments().get(0); + } + + private AttachmentEntity getRandomItemAttachmentEntity(Items selectedItem) { + return selectedItem.getAttachmentEntities().get(0); + } + + protected Items getItemWithAttachment(Roots selectedRoot) { + return selectedRoot.getItems().stream() + .filter(item -> !item.getAttachments().isEmpty()) + .findAny() + .orElseThrow(); + } + + private Items getItemWithAttachmentEntity(Roots selectedRoot) { + return selectedRoot.getItems().stream() + .filter(item -> !item.getAttachmentEntities().isEmpty()) + .findAny() + .orElseThrow(); + } + + protected String putContentForAttachmentWithNavigation( + Roots selectedRoot, Attachments itemAttachment) throws Exception { + return putContentForAttachmentWithNavigation( + selectedRoot, itemAttachment, status().isNoContent()); + } + + private String putContentForAttachmentWithNavigation( + Roots selectedRoot, Attachments itemAttachment, ResultMatcher matcher) throws Exception { + var selectedItem = + selectedRoot.getItems().stream() + .filter( + item -> + item.getAttachments().stream() + .anyMatch(attach -> attach.getId().equals(itemAttachment.getId()))) + .findAny() + .orElseThrow(); + var url = + buildNavigationAttachmentUrl( + selectedRoot.getId(), selectedItem.getId(), itemAttachment.getId()) + + "/content"; + + var testContent = "testContent" + itemAttachment.getNote(); + requestHelper.setContentType(MediaType.APPLICATION_OCTET_STREAM); + requestHelper.executePutWithMatcher(url, testContent.getBytes(StandardCharsets.UTF_8), matcher); + return testContent; + } + + protected String buildNavigationAttachmentUrl(String rootId, String itemId, String attachmentId) { + return "/odata/v4/TestService/Roots(" + + rootId + + ")/items(" + + itemId + + ")" + + "/attachments(ID=" + + attachmentId + + ",up__ID=" + + itemId + + ")"; + } + + protected String buildNavigationSizeLimitedAttachmentUrl(String rootId, String attachmentId) { + return "/odata/v4/TestService/Roots(" + + rootId + + ")/sizeLimitedAttachments(ID=" + + attachmentId + + ",up__ID=" + + rootId + + ")"; + } + + protected String putContentForSizeLimitedAttachment(Roots selectedRoot, Attachments attachment) + throws Exception { + return putContentForSizeLimitedAttachment(selectedRoot, attachment, status().isNoContent()); + } + + protected String putContentForSizeLimitedAttachment( + Roots selectedRoot, Attachments attachment, ResultMatcher matcher) throws Exception { + var url = + buildNavigationSizeLimitedAttachmentUrl(selectedRoot.getId(), attachment.getId()) + + "/content"; + var testContent = "testContent" + attachment.getNote(); + requestHelper.setContentType(MediaType.APPLICATION_OCTET_STREAM); + requestHelper.executePutWithMatcher(url, testContent.getBytes(StandardCharsets.UTF_8), matcher); + return testContent; + } + + protected String buildExpandAttachmentUrl(String rootId, String itemId) { + return "/odata/v4/TestService/Roots(" + + rootId + + ")/items(" + + itemId + + ")" + + "?$expand=attachments,attachmentEntities"; + } + + private String putContentForAttachmentWithoutNavigation(AttachmentEntity itemAttachment) + throws Exception { + return putContentForAttachmentWithoutNavigation(itemAttachment, status().isNoContent()); + } + + private String putContentForAttachmentWithoutNavigation( + AttachmentEntity itemAttachment, ResultMatcher matcher) throws Exception { + var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()) + "/content"; + var testContent = "testContent" + itemAttachment.getNote(); + requestHelper.setContentType(MediaType.APPLICATION_OCTET_STREAM); + requestHelper.executePutWithMatcher(url, testContent.getBytes(StandardCharsets.UTF_8), matcher); + return testContent; + } + + private String buildDirectAttachmentEntityUrl(String attachmentId) { + return MockHttpRequestHelper.ODATA_BASE_URL + + "TestService/AttachmentEntity(" + + attachmentId + + ")"; + } + + private Attachments selectUpdatedAttachmentWithExpand( + Roots selectedRoot, Attachments itemAttachment) { + CqnSelect attachmentSelect = + Select.from(Items_.class) + .where(a -> a.ID().eq(selectedRoot.getItems().get(0).getId())) + .columns(item -> item.attachments().expand()); + var result = persistenceService.run(attachmentSelect); + var items = result.single(Items.class); + return items.getAttachments().stream() + .filter(attach -> itemAttachment.getId().equals(attach.getId())) + .findAny() + .orElseThrow(); + } + + private AttachmentEntity selectUpdatedAttachment(AttachmentEntity itemAttachment) { + CqnSelect attachmentSelect = + Select.from(AttachmentEntity_.class).where(a -> a.ID().eq(itemAttachment.getId())); + var result = persistenceService.run(attachmentSelect); + return result.single(AttachmentEntity.class); + } + + private void executeDeleteAndCheckNoDataCanBeRead(String baseUrl, String contentId) + throws Exception { + var url = baseUrl + "/content"; + requestHelper.executeDelete(url); + verifySingleDeletionEvent(contentId); + clearServiceHandlerContext(); + var response = requestHelper.executeGet(url); + + assertThat(response.getResponse().getContentLength()).isZero(); + assertThat(response.getResponse().getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + + protected abstract void executeContentRequestAndValidateContent(String url, String content) + throws Exception; + + protected abstract void verifyTwoDeleteEvents( + AttachmentEntity itemAttachmentEntityAfterChange, Attachments itemAttachmentAfterChange); + + protected abstract void verifyNumberOfEvents(String event, int number); + + protected abstract void verifyContentId( + Attachments attachmentWithExpectedContent, String attachmentId, String contentId); + + protected abstract void verifyContentAndContentId( + Attachments attachment, String testContent, Attachments itemAttachment) throws IOException; + + protected abstract void verifyContentAndContentIdForAttachmentEntity( + AttachmentEntity attachment, String testContent, AttachmentEntity itemAttachment) + throws IOException; + + protected abstract void clearServiceHandlerContext(); + + protected abstract void clearServiceHandlerDocuments(); + + protected abstract void verifySingleCreateEvent(String contentId, String content); + + protected abstract void verifySingleCreateAndUpdateEvent( + String resultContentId, String toBeDeletedContentId, String content); + + protected abstract void verifySingleDeletionEvent(String contentId); + + protected abstract void verifySingleReadEvent(String contentId); + + protected abstract void verifyNoAttachmentEventsCalled(); + + protected abstract void verifyEventContextEmptyForEvent(String... events); +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java new file mode 100644 index 000000000..94fca3378 --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java @@ -0,0 +1,245 @@ +/* + * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity; +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import com.sap.cds.feature.attachments.integrationtests.testhandler.EventContextHolder; +import com.sap.cds.feature.attachments.service.AttachmentService; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles(Profiles.TEST_HANDLER_ENABLED) +class OdataRequestValidationWithTestHandlerTest extends OdataRequestValidationBase { + + @Test + void serviceHandlerAvailable() { + assertThat(serviceHandler).isNotNull(); + } + + @Override + protected void executeContentRequestAndValidateContent(String url, String content) + throws Exception { + var response = requestHelper.executeGet(url); + assertThat(response.getResponse().getContentAsString()).isEqualTo(content); + } + + @Override + protected void verifyTwoDeleteEvents( + AttachmentEntity itemAttachmentEntityAfterChange, Attachments itemAttachmentAfterChange) { + waitTillExpectedHandlerMessageSize(2); + verifyEventContextEmptyForEvent( + AttachmentService.EVENT_READ_ATTACHMENT, AttachmentService.EVENT_CREATE_ATTACHMENT); + var deleteEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + assertThat(deleteEvents).hasSize(2); + assertThat( + deleteEvents.stream() + .anyMatch( + verifyContentIdAndUserInfo(itemAttachmentEntityAfterChange.getContentId()))) + .isTrue(); + assertThat( + deleteEvents.stream() + .anyMatch(verifyContentIdAndUserInfo(itemAttachmentAfterChange.getContentId()))) + .isTrue(); + } + + @Override + protected void verifyNumberOfEvents(String event, int number) { + assertThat(serviceHandler.getEventContextForEvent(event)).hasSize(number); + } + + @Override + protected void verifyContentId( + Attachments attachmentWithExpectedContent, String attachmentId, String contentId) { + assertThat(attachmentWithExpectedContent.getContentId()).isNotEmpty().isNotEqualTo(contentId); + } + + @Override + protected void verifyContentAndContentId( + Attachments attachment, String content, Attachments itemAttachment) { + assertThat(attachment.getContent()).isNull(); + assertThat(attachment.getContentId()).isNotEmpty().isNotEqualTo(itemAttachment.getId()); + } + + @Override + protected void verifyContentAndContentIdForAttachmentEntity( + AttachmentEntity attachment, String content, AttachmentEntity itemAttachment) { + assertThat(attachment.getContent()).isNull(); + assertThat(attachment.getContentId()).isNotEmpty().isNotEqualTo(itemAttachment.getId()); + } + + @Override + protected void clearServiceHandlerContext() { + serviceHandler.clearEventContext(); + } + + @Override + protected void clearServiceHandlerDocuments() { + serviceHandler.clearDocuments(); + } + + @Override + protected void verifySingleCreateEvent(String contentId, String content) { + verifyEventContextEmptyForEvent( + AttachmentService.EVENT_READ_ATTACHMENT, + AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + var createEvent = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); + assertThat(createEvent) + .hasSize(1) + .first() + .satisfies( + event -> { + assertThat(event.context()).isInstanceOf(AttachmentCreateEventContext.class); + var createContext = (AttachmentCreateEventContext) event.context(); + assertThat(createContext.getContentId()).isEqualTo(contentId); + assertThat(createContext.getData().getContent().readAllBytes()) + .isEqualTo(content.getBytes(StandardCharsets.UTF_8)); + }); + } + + @Override + protected void verifySingleCreateAndUpdateEvent( + String resultContentId, String toBeDeletedContentId, String content) { + waitTillExpectedHandlerMessageSize(3); + verifyEventContextEmptyForEvent(AttachmentService.EVENT_READ_ATTACHMENT); + var createEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); + assertThat(createEvents).hasSize(2); + verifyCreateEventsContainsContentId(toBeDeletedContentId, createEvents); + verifyCreateEventsContainsContentId(resultContentId, createEvents); + var deleteEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + + var deleteContentId = + !resultContentId.equals(toBeDeletedContentId) + ? toBeDeletedContentId + : createEvents.stream() + .filter( + event -> + !resultContentId.equals( + ((AttachmentCreateEventContext) event.context()).getContentId())) + .findFirst() + .orElseThrow() + .context() + .get(Attachments.CONTENT_ID); + + var eventFound = + deleteEvents.stream() + .anyMatch( + event -> + ((AttachmentMarkAsDeletedEventContext) event.context()) + .getContentId() + .equals(deleteContentId)); + assertThat(eventFound).isTrue(); + } + + @Override + protected void verifySingleDeletionEvent(String contentId) { + waitTillExpectedHandlerMessageSize(1); + verifyEventContextEmptyForEvent( + AttachmentService.EVENT_CREATE_ATTACHMENT, AttachmentService.EVENT_READ_ATTACHMENT); + var deleteEvents = + serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + assertThat(deleteEvents) + .hasSize(1) + .first() + .satisfies( + event -> { + assertThat(event.context()).isInstanceOf(AttachmentMarkAsDeletedEventContext.class); + var deleteContext = (AttachmentMarkAsDeletedEventContext) event.context(); + assertThat(deleteContext.getContentId()).isEqualTo(contentId); + assertThat(deleteContext.getDeletionUserInfo().getName()).isEqualTo("anonymous"); + assertThat(deleteContext.getDeletionUserInfo().getIsSystemUser()).isFalse(); + }); + } + + @Override + protected void verifySingleReadEvent(String contentId) { + verifyEventContextEmptyForEvent( + AttachmentService.EVENT_CREATE_ATTACHMENT, + AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + var readContext = serviceHandler.getEventContext(); + assertThat(readContext) + .hasSize(1) + .first() + .satisfies( + event -> { + assertThat(event.event()).isEqualTo(AttachmentService.EVENT_READ_ATTACHMENT); + assertThat(((AttachmentReadEventContext) event.context()).getContentId()) + .isEqualTo(contentId); + }); + } + + @Override + protected void verifyNoAttachmentEventsCalled() { + assertThat(serviceHandler.getEventContext()).isEmpty(); + } + + @Override + protected void verifyEventContextEmptyForEvent(String... events) { + Arrays.stream(events) + .forEach(event -> assertThat(serviceHandler.getEventContextForEvent(event)).isEmpty()); + } + + private Predicate verifyContentIdAndUserInfo( + String itemAttachmentEntityAfterChange) { + return event -> + ((AttachmentMarkAsDeletedEventContext) event.context()) + .getContentId() + .equals(itemAttachmentEntityAfterChange) + && ((AttachmentMarkAsDeletedEventContext) event.context()) + .getDeletionUserInfo() + .getName() + .equals("anonymous") + && Boolean.FALSE.equals( + ((AttachmentMarkAsDeletedEventContext) event.context()) + .getDeletionUserInfo() + .getIsSystemUser()); + } + + private void verifyCreateEventsContainsContentId( + String contentId, List createEvents) { + assertThat( + createEvents.stream() + .anyMatch( + event -> + ((AttachmentCreateEventContext) event.context()) + .getContentId() + .equals(contentId))) + .isTrue(); + } + + private void waitTillExpectedHandlerMessageSize(int expectedSize) { + Awaitility.await() + .atMost(30, TimeUnit.SECONDS) + .pollDelay(1, TimeUnit.SECONDS) + .until( + () -> { + var eventCalls = serviceHandler.getEventContext().size(); + logger.debug( + "Waiting for expected size '{}' in handler context, was '{}'", + expectedSize, + eventCalls); + var numberMatch = eventCalls >= expectedSize; + if (!numberMatch) { + serviceHandler.getEventContext().forEach(event -> logger.info("Event: {}", event)); + } + return numberMatch; + }); + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java new file mode 100644 index 000000000..725c4d775 --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java @@ -0,0 +1,138 @@ +/* + * © 2024-2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; +import com.sap.cds.feature.attachments.integrationtests.common.MalwareScanResultProvider; +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; +import org.wiremock.spring.ConfigureWireMock; +import org.wiremock.spring.EnableWireMock; +import org.wiremock.spring.InjectWireMock; + +@ActiveProfiles(profiles = {Profiles.TEST_HANDLER_DISABLED, Profiles.MALWARE_SCAN_ENABLED}) +@EnableWireMock({@ConfigureWireMock(port = 1111, name = "malware-scanner")}) +class OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest + extends OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest { + + @InjectWireMock("malware-scanner") + private WireMockServer wiremock; + + @Autowired private MalwareScanResultProvider malwareScanResultProvider; + + @BeforeEach + void setup() { + mockMalwareScanResult(false); + } + + @Override + @AfterEach + void teardown() { + super.teardown(); + wiremock.resetAll(); + } + + @Test + void scannerReturnedMalwareContentCanNotBeRead() throws Exception { + wiremock.resetAll(); + mockMalwareScanResult(true); + + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); + + var url = + buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()); + var contentUrl = url + "/content"; + waitAndVerifyContentErrorResponse(contentUrl, itemAttachmentAfterChange); + clearServiceHandlerContext(); + verifyAttachmentGetResponse(url, itemAttachmentAfterChange); + } + + @Test + void scannerReturnedErrorContentCanNotBeRead() throws Exception { + wiremock.resetAll(); + wiremock.stubFor( + post(urlEqualTo("/scan")) + .withBasicAuth("test-user", "test-password") + .willReturn(aResponse().withStatus(500))); + + var serviceRoot = buildServiceRootWithDeepData(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithDeepData(); + var item = getItemWithAttachment(selectedRoot); + var itemAttachment = getRandomItemAttachment(item); + putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); + clearServiceHandlerContext(); + var selectedItemAfterChange = selectItem(item); + var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); + + var url = + buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()); + var contentUrl = url + "/content"; + waitAndVerifyContentErrorResponse(contentUrl, itemAttachmentAfterChange); + clearServiceHandlerContext(); + verifyAttachmentGetResponse(url, itemAttachmentAfterChange); + } + + private void mockMalwareScanResult(boolean malware) { + wiremock.stubFor( + post(urlEqualTo("/scan")) + .withBasicAuth("test-user", "test-password") + .willReturn( + aResponse() + .withBody(malwareScanResultProvider.buildMalwareScanResult(malware)) + .withStatus(200))); + } + + private void waitAndVerifyContentErrorResponse( + String contentUrl, Attachments itemAttachmentAfterChange) throws Exception { + Awaitility.await() + .atMost(10, TimeUnit.SECONDS) + .until( + () -> { + var response = requestHelper.executeGet(contentUrl); + assertThat(response.getResponse().getStatus()) + .isEqualTo(HttpStatus.METHOD_NOT_ALLOWED.value()); + return response + .getResponse() + .getContentAsString() + .contains("Error text for not clean"); + }); + clearServiceHandlerContext(); + + var response = requestHelper.executeGet(contentUrl); + assertThat(response.getResponse().getStatus()).isEqualTo(HttpStatus.METHOD_NOT_ALLOWED.value()); + assertThat(response.getResponse().getContentAsString()).contains("Error text for not clean"); + verifySingleReadEvent(itemAttachmentAfterChange.getContentId()); + } + + private void verifyAttachmentGetResponse(String url, Attachments itemAttachmentAfterChange) + throws Exception { + var response = requestHelper.executeGet(url); + assertThat(response.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value()); + verifySingleReadEvent(itemAttachmentAfterChange.getContentId()); + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java new file mode 100644 index 000000000..edee93cb5 --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java @@ -0,0 +1,116 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity; +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) +class OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest + extends OdataRequestValidationBase { + + @Test + void serviceHandlerIsNull() { + assertThat(serviceHandler).isNull(); + } + + @Override + protected void executeContentRequestAndValidateContent(String url, String content) + throws Exception { + Awaitility.await() + .atMost(10, TimeUnit.SECONDS) + .until( + () -> { + var response = requestHelper.executeGet(url); + return response.getResponse().getContentAsString().equals(content); + }); + + var response = requestHelper.executeGet(url); + assertThat(response.getResponse().getContentAsString()).isEqualTo(content); + } + + @Override + protected void verifyTwoDeleteEvents( + AttachmentEntity itemAttachmentEntityAfterChange, Attachments itemAttachmentAfterChange) { + // no service handler - nothing to do + } + + @Override + protected void verifyNumberOfEvents(String event, int number) { + // no service handler - nothing to do + } + + @Override + protected void verifyContentId( + Attachments attachmentWithExpectedContent, String attachmentId, String contentId) { + assertThat(attachmentWithExpectedContent.getContentId()).isEqualTo(attachmentId); + } + + @Override + protected void verifyContentAndContentId( + Attachments attachment, String testContent, Attachments itemAttachment) throws IOException { + assertThat(attachment.getContent().readAllBytes()) + .isEqualTo(testContent.getBytes(StandardCharsets.UTF_8)); + assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getId()); + } + + @Override + protected void verifyContentAndContentIdForAttachmentEntity( + AttachmentEntity attachment, String testContent, AttachmentEntity itemAttachment) + throws IOException { + assertThat(attachment.getContent().readAllBytes()) + .isEqualTo(testContent.getBytes(StandardCharsets.UTF_8)); + assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getId()); + } + + @Override + protected void clearServiceHandlerContext() { + // no service handler - nothing to do + } + + @Override + protected void clearServiceHandlerDocuments() { + // no service handler - nothing to do + } + + @Override + protected void verifySingleCreateEvent(String contentId, String content) { + // no service handler - nothing to do + } + + @Override + protected void verifySingleCreateAndUpdateEvent( + String resultContentId, String toBeDeletedContentId, String content) { + // no service handler - nothing to do + } + + @Override + protected void verifySingleDeletionEvent(String contentId) { + // no service handler - nothing to do + } + + @Override + protected void verifySingleReadEvent(String contentId) { + // no service handler - nothing to do + } + + @Override + protected void verifyNoAttachmentEventsCalled() { + // no service handler - nothing to do + } + + @Override + protected void verifyEventContextEmptyForEvent(String... events) { + // no service handler - nothing to do + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java new file mode 100644 index 000000000..9bc77e763 --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java @@ -0,0 +1,175 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity; +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots; +import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; +import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.AttachmentsBuilder; +import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.RootEntityBuilder; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) +class SizeLimitedAttachmentValidationNonDraftTest extends OdataRequestValidationBase { + + @Test + void uploadContentWithin5MBLimitSucceeds() throws Exception { + // Arrange: Create root with sizeLimitedAttachments + var serviceRoot = buildServiceRootWithSizeLimitedAttachments(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithSizeLimitedAttachments(); + var attachment = getRandomRootSizeLimitedAttachment(selectedRoot); + attachment.setFileName("test.txt"); + + // Act & Assert: Upload 3MB content (within limit) succeeds + byte[] content = new byte[3 * 1024 * 1024]; // 3MB + var url = + buildNavigationSizeLimitedAttachmentUrl(selectedRoot.getId(), attachment.getId()) + + "/content"; + requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); + requestHelper.executePutWithMatcher(url, content, status().isNoContent()); + } + + @Test + void uploadContentExceeding5MBLimitFails() throws Exception { + // Arrange: Create root with sizeLimitedAttachments + var serviceRoot = buildServiceRootWithSizeLimitedAttachments(); + postServiceRoot(serviceRoot); + + var selectedRoot = selectStoredRootWithSizeLimitedAttachments(); + var attachment = getRandomRootSizeLimitedAttachment(selectedRoot); + attachment.setFileName("test.txt"); + // Act: Try to upload 6MB content (exceeds limit) + byte[] content = new byte[6 * 1024 * 1024]; // 6MB + var url = + buildNavigationSizeLimitedAttachmentUrl(selectedRoot.getId(), attachment.getId()) + + "/content"; + requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); + requestHelper.executePutWithMatcher(url, content, status().is(413)); + + // Assert: Error response with HTTP 413 status code indicates size limit + // exceeded + } + + // Helper methods + private Roots buildServiceRootWithSizeLimitedAttachments() { + return RootEntityBuilder.create() + .setTitle("Root with sizeLimitedAttachments") + .addSizeLimitedAttachments( + AttachmentsBuilder.create().setFileName("testFile.txt").setMimeType("text/plain")) + .build(); + } + + private Roots selectStoredRootWithSizeLimitedAttachments() { + var select = + com.sap.cds.ql.Select.from( + com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots_ + .class) + .columns(r -> r._all(), r -> r.sizeLimitedAttachments().expand()); + + var result = persistenceService.run(select); + return result.single(Roots.class); + } + + // Required abstract method implementations + @Override + protected void executeContentRequestAndValidateContent(String url, String content) + throws Exception { + Awaitility.await() + .atMost(10, TimeUnit.SECONDS) + .until( + () -> { + var response = requestHelper.executeGet(url); + return response.getResponse().getContentAsString().equals(content); + }); + + var response = requestHelper.executeGet(url); + assertThat(response.getResponse().getContentAsString()).isEqualTo(content); + } + + @Override + protected void verifyTwoDeleteEvents( + AttachmentEntity itemAttachmentEntityAfterChange, Attachments itemAttachmentAfterChange) { + // no service handler - nothing to do + } + + @Override + protected void verifyNumberOfEvents(String event, int number) { + // no service handler - nothing to do + } + + @Override + protected void verifyContentId( + Attachments attachmentWithExpectedContent, String attachmentId, String contentId) { + assertThat(attachmentWithExpectedContent.getContentId()).isEqualTo(attachmentId); + } + + @Override + protected void verifyContentAndContentId( + Attachments attachment, String testContent, Attachments itemAttachment) throws IOException { + assertThat(attachment.getContent().readAllBytes()) + .isEqualTo(testContent.getBytes(StandardCharsets.UTF_8)); + assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getId()); + } + + @Override + protected void verifyContentAndContentIdForAttachmentEntity( + AttachmentEntity attachment, String testContent, AttachmentEntity itemAttachment) + throws IOException { + assertThat(attachment.getContent().readAllBytes()) + .isEqualTo(testContent.getBytes(StandardCharsets.UTF_8)); + assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getId()); + } + + @Override + protected void clearServiceHandlerContext() { + // no service handler - nothing to do + } + + @Override + protected void clearServiceHandlerDocuments() { + // no service handler - nothing to do + } + + @Override + protected void verifySingleCreateEvent(String contentId, String content) { + // no service handler - nothing to do + } + + @Override + protected void verifySingleCreateAndUpdateEvent( + String resultContentId, String toBeDeletedContentId, String content) { + // no service handler - nothing to do + } + + @Override + protected void verifySingleDeletionEvent(String contentId) { + // no service handler - nothing to do + } + + @Override + protected void verifySingleReadEvent(String contentId) { + // no service handler - nothing to do + } + + @Override + protected void verifyNoAttachmentEventsCalled() { + // no service handler - nothing to do + } + + @Override + protected void verifyEventContextEmptyForEvent(String... events) { + // no service handler - nothing to do + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java new file mode 100644 index 000000000..0f5e1bbfe --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java @@ -0,0 +1,33 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper; + +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; + +public class AttachmentsBuilder { + + private Attachments attachment; + + private AttachmentsBuilder() { + attachment = Attachments.create(); + } + + public static AttachmentsBuilder create() { + return new AttachmentsBuilder(); + } + + public AttachmentsBuilder setMimeType(String mimeType) { + attachment.setMimeType(mimeType); + return this; + } + + public AttachmentsBuilder setFileName(String fileName) { + attachment.setFileName(fileName); + return this; + } + + public Attachments build() { + return attachment; + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java new file mode 100644 index 000000000..2da95a144 --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java @@ -0,0 +1,31 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper; + +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity; + +public class AttachmentsEntityBuilder { + + private AttachmentEntity attachmentEntity = AttachmentEntity.create(); + + private AttachmentsEntityBuilder() {} + + public static AttachmentsEntityBuilder create() { + return new AttachmentsEntityBuilder(); + } + + public AttachmentsEntityBuilder setMimeType(String mimeType) { + attachmentEntity.setMimeType(mimeType); + return this; + } + + public AttachmentsEntityBuilder setFileName(String fileName) { + attachmentEntity.setFileName(fileName); + return this; + } + + public AttachmentEntity build() { + return attachmentEntity; + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java new file mode 100644 index 000000000..9b6ab7017 --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java @@ -0,0 +1,44 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper; + +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Items; +import java.util.ArrayList; +import java.util.Arrays; + +public class ItemEntityBuilder { + + private final Items item; + + private ItemEntityBuilder() { + item = Items.create(); + item.setAttachments(new ArrayList<>()); + item.setAttachmentEntities(new ArrayList<>()); + } + + public static ItemEntityBuilder create() { + return new ItemEntityBuilder(); + } + + public ItemEntityBuilder setTitle(String title) { + item.setTitle(title); + return this; + } + + public ItemEntityBuilder addAttachmentEntities(AttachmentsEntityBuilder... attachmentEntities) { + Arrays.stream(attachmentEntities) + .forEach(attachment -> item.getAttachmentEntities().add(attachment.build())); + return this; + } + + public ItemEntityBuilder addAttachments(AttachmentsBuilder... attachmentEntities) { + Arrays.stream(attachmentEntities) + .forEach(attachment -> item.getAttachments().add(attachment.build())); + return this; + } + + public Items build() { + return item; + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java new file mode 100644 index 000000000..9efc70dfc --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java @@ -0,0 +1,50 @@ +/* + * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper; + +import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots; +import java.util.ArrayList; +import java.util.Arrays; + +public class RootEntityBuilder { + + private final Roots rootEntity; + + private RootEntityBuilder() { + rootEntity = Roots.create(); + rootEntity.setAttachments(new ArrayList<>()); + rootEntity.setItems(new ArrayList<>()); + rootEntity.setSizeLimitedAttachments(new ArrayList<>()); + } + + public static RootEntityBuilder create() { + return new RootEntityBuilder(); + } + + public RootEntityBuilder setTitle(String title) { + rootEntity.setTitle(title); + return this; + } + + public RootEntityBuilder addAttachments(AttachmentsEntityBuilder... attachments) { + Arrays.stream(attachments) + .forEach(attachment -> rootEntity.getAttachments().add(attachment.build())); + return this; + } + + public RootEntityBuilder addSizeLimitedAttachments(AttachmentsBuilder... attachments) { + Arrays.stream(attachments) + .forEach(attachment -> rootEntity.getSizeLimitedAttachments().add(attachment.build())); + return this; + } + + public RootEntityBuilder addItems(ItemEntityBuilder... items) { + Arrays.stream(items).forEach(item -> rootEntity.getItems().add(item.build())); + return this; + } + + public Roots build() { + return rootEntity; + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java new file mode 100644 index 000000000..9dcf8875f --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java @@ -0,0 +1,116 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.testhandler; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.sap.cds.services.ServiceException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TestPersistenceHandlerTest { + + private TestPersistenceHandler testPersistenceHandler; + + @BeforeEach + void setUp() { + testPersistenceHandler = new TestPersistenceHandler(); + } + + @Test + void testReset() { + // Set both flags to true + testPersistenceHandler.setThrowExceptionOnUpdate(true); + testPersistenceHandler.setThrowExceptionOnCreate(true); + + // Reset should set both flags to false + testPersistenceHandler.reset(); + + // Verify no exceptions are thrown after reset + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); + } + + @Test + void testThrowExceptionOnUpdateWhenEnabled() { + testPersistenceHandler.setThrowExceptionOnUpdate(true); + + ServiceException exception = + assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnUpdate()); + + assertTrue(exception.getMessage().contains("Exception on update")); + } + + @Test + void testThrowExceptionOnUpdateWhenDisabled() { + testPersistenceHandler.setThrowExceptionOnUpdate(false); + + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); + } + + @Test + void testThrowExceptionOnCreateWhenEnabled() { + testPersistenceHandler.setThrowExceptionOnCreate(true); + + ServiceException exception = + assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnCreate()); + + assertTrue(exception.getMessage().contains("Exception on create")); + } + + @Test + void testThrowExceptionOnCreateWhenDisabled() { + testPersistenceHandler.setThrowExceptionOnCreate(false); + + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); + } + + @Test + void testSetThrowExceptionOnUpdate() { + // Test setting to true + testPersistenceHandler.setThrowExceptionOnUpdate(true); + assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnUpdate()); + + // Test setting to false + testPersistenceHandler.setThrowExceptionOnUpdate(false); + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); + } + + @Test + void testSetThrowExceptionOnCreate() { + // Test setting to true + testPersistenceHandler.setThrowExceptionOnCreate(true); + assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnCreate()); + + // Test setting to false + testPersistenceHandler.setThrowExceptionOnCreate(false); + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); + } + + @Test + void testDefaultBehavior() { + // By default, both flags should be false + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); + } + + @Test + void testIndependentFlagBehavior() { + // Test that the flags work independently + testPersistenceHandler.setThrowExceptionOnUpdate(true); + testPersistenceHandler.setThrowExceptionOnCreate(false); + + assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnUpdate()); + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); + + // Switch them + testPersistenceHandler.setThrowExceptionOnUpdate(false); + testPersistenceHandler.setThrowExceptionOnCreate(true); + + assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); + assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnCreate()); + } +} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java new file mode 100644 index 000000000..bc391fe9b --- /dev/null +++ b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java @@ -0,0 +1,266 @@ +/* + * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.integrationtests.testhandler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.*; + +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.StatusCode; +import com.sap.cds.feature.attachments.service.AttachmentService; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TestPluginAttachmentsServiceHandlerTest { + + private TestPluginAttachmentsServiceHandler cut; + + @BeforeEach + void setup() { + cut = new TestPluginAttachmentsServiceHandler(); + // Clear any previous test data + cut.clearEventContext(); + cut.clearDocuments(); + } + + @Test + void readIsWorking() { + var context = AttachmentReadEventContext.create(); + context.setContentId("test"); + context.setData(MediaData.create()); + + cut.readAttachment(context); + + assertThat(context.getData().getContent()).isNull(); + } + + @Test + void readWithContentIsWorking() throws IOException { + var createContext = AttachmentCreateEventContext.create(); + createContext.setData(MediaData.create()); + createContext + .getData() + .setContent(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))); + cut.createAttachment(createContext); + + var context = AttachmentReadEventContext.create(); + context.setContentId(createContext.getContentId()); + context.setData(MediaData.create()); + + cut.readAttachment(context); + + assertThat(context.getData().getContent().readAllBytes()) + .isEqualTo("test".getBytes(StandardCharsets.UTF_8)); + } + + @Test + void dummyTestForDelete() { + var context = AttachmentMarkAsDeletedEventContext.create(); + context.setContentId("test"); + + assertDoesNotThrow(() -> cut.markAttachmentAsDeleted(context)); + } + + @Test + void dummyTestForCreate() throws IOException { + var context = AttachmentCreateEventContext.create(); + context.setData(MediaData.create()); + var stream = mock(InputStream.class); + when(stream.readAllBytes()).thenReturn("test".getBytes(StandardCharsets.UTF_8)); + context.getData().setContent(stream); + + assertDoesNotThrow(() -> cut.createAttachment(context)); + } + + @Test + void dummyTestForRestore() { + var context = AttachmentRestoreEventContext.create(); + context.setRestoreTimestamp(Instant.now()); + + assertDoesNotThrow(() -> cut.restoreAttachment(context)); + } + + @Test + void testCreateAttachmentSetsContentIdAndStatus() throws IOException { + var context = AttachmentCreateEventContext.create(); + context.setData(MediaData.create()); + context + .getData() + .setContent(new ByteArrayInputStream("test content".getBytes(StandardCharsets.UTF_8))); + + cut.createAttachment(context); + + assertNotNull(context.getContentId()); + assertThat(context.getData().getStatus()).isEqualTo(StatusCode.CLEAN); + } + + @Test + void testEventContextTracking() throws IOException { + // Test create event tracking + var createContext = AttachmentCreateEventContext.create(); + createContext.setData(MediaData.create()); + createContext + .getData() + .setContent(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))); + cut.createAttachment(createContext); + + List createEvents = + cut.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); + assertThat(createEvents).hasSize(1); + assertThat(createEvents.get(0).event()).isEqualTo(AttachmentService.EVENT_CREATE_ATTACHMENT); + + // Test read event tracking + var readContext = AttachmentReadEventContext.create(); + readContext.setContentId("test-id"); + readContext.setData(MediaData.create()); + cut.readAttachment(readContext); + + List readEvents = + cut.getEventContextForEvent(AttachmentService.EVENT_READ_ATTACHMENT); + assertThat(readEvents).hasSize(1); + assertThat(readEvents.get(0).event()).isEqualTo(AttachmentService.EVENT_READ_ATTACHMENT); + + // Test delete event tracking + var deleteContext = AttachmentMarkAsDeletedEventContext.create(); + deleteContext.setContentId("test-id"); + cut.markAttachmentAsDeleted(deleteContext); + + List deleteEvents = + cut.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + assertThat(deleteEvents).hasSize(1); + assertThat(deleteEvents.get(0).event()) + .isEqualTo(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); + + // Test restore event tracking + var restoreContext = AttachmentRestoreEventContext.create(); + restoreContext.setRestoreTimestamp(Instant.now()); + cut.restoreAttachment(restoreContext); + + List restoreEvents = + cut.getEventContextForEvent(AttachmentService.EVENT_RESTORE_ATTACHMENT); + assertThat(restoreEvents).hasSize(1); + assertThat(restoreEvents.get(0).event()).isEqualTo(AttachmentService.EVENT_RESTORE_ATTACHMENT); + } + + @Test + void testGetAllEventContext() throws IOException { + // Create multiple events + var createContext = AttachmentCreateEventContext.create(); + createContext.setData(MediaData.create()); + createContext + .getData() + .setContent(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))); + cut.createAttachment(createContext); + + var readContext = AttachmentReadEventContext.create(); + readContext.setContentId("test-id"); + readContext.setData(MediaData.create()); + cut.readAttachment(readContext); + + List allEvents = cut.getEventContext(); + assertThat(allEvents).hasSize(2); + } + + @Test + void testClearEventContext() throws IOException { + // Add some events + var context = AttachmentCreateEventContext.create(); + context.setData(MediaData.create()); + context.getData().setContent(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))); + cut.createAttachment(context); + + assertThat(cut.getEventContext()).hasSize(1); + + // Clear and verify + cut.clearEventContext(); + assertThat(cut.getEventContext()).isEmpty(); + } + + @Test + void testReadWithNullContentId() { + var context = AttachmentReadEventContext.create(); + context.setContentId(null); + context.setData(MediaData.create()); + + cut.readAttachment(context); + + assertThat(context.getData().getContent()).isNull(); + } + + @Test + void testCreateAttachmentWithEmptyContent() throws IOException { + var context = AttachmentCreateEventContext.create(); + context.setData(MediaData.create()); + context.getData().setContent(new ByteArrayInputStream(new byte[0])); + + cut.createAttachment(context); + + assertNotNull(context.getContentId()); + assertThat(context.getData().getStatus()).isEqualTo(StatusCode.CLEAN); + } + + @Test + void testMultipleCreateAndReadOperations() throws IOException { + // Create first attachment + var createContext1 = AttachmentCreateEventContext.create(); + createContext1.setData(MediaData.create()); + createContext1 + .getData() + .setContent(new ByteArrayInputStream("content1".getBytes(StandardCharsets.UTF_8))); + cut.createAttachment(createContext1); + + // Create second attachment + var createContext2 = AttachmentCreateEventContext.create(); + createContext2.setData(MediaData.create()); + createContext2 + .getData() + .setContent(new ByteArrayInputStream("content2".getBytes(StandardCharsets.UTF_8))); + cut.createAttachment(createContext2); + + // Read first attachment + var readContext1 = AttachmentReadEventContext.create(); + readContext1.setContentId(createContext1.getContentId()); + readContext1.setData(MediaData.create()); + cut.readAttachment(readContext1); + + // Read second attachment + var readContext2 = AttachmentReadEventContext.create(); + readContext2.setContentId(createContext2.getContentId()); + readContext2.setData(MediaData.create()); + cut.readAttachment(readContext2); + + // Verify content + assertThat(readContext1.getData().getContent().readAllBytes()) + .isEqualTo("content1".getBytes(StandardCharsets.UTF_8)); + assertThat(readContext2.getData().getContent().readAllBytes()) + .isEqualTo("content2".getBytes(StandardCharsets.UTF_8)); + } + + @Test + void testRestoreWithSpecificTimestamp() { + Instant timestamp = Instant.parse("2024-01-01T12:00:00Z"); + var context = AttachmentRestoreEventContext.create(); + context.setRestoreTimestamp(timestamp); + + cut.restoreAttachment(context); + + List restoreEvents = + cut.getEventContextForEvent(AttachmentService.EVENT_RESTORE_ATTACHMENT); + assertThat(restoreEvents).hasSize(1); + var restoredContext = (AttachmentRestoreEventContext) restoreEvents.get(0).context(); + assertThat(restoredContext.getRestoreTimestamp()).isEqualTo(timestamp); + } +} diff --git a/integration-tests/srv/src/test/resources/application.yaml b/integration-tests/srv/src/test/resources/application.yaml new file mode 100644 index 000000000..54849379b --- /dev/null +++ b/integration-tests/srv/src/test/resources/application.yaml @@ -0,0 +1,15 @@ +cds: + dataSource: + csv: + paths: "../db/src/gen/csv" + +--- +spring: + config: + activate: + on-profile: malware-scan-enabled + +cds: + environment: + local: + defaultEnvPath: "classpath:xsuaa-env.json" diff --git a/integration-tests/srv/src/test/resources/logback-test.xml b/integration-tests/srv/src/test/resources/logback-test.xml new file mode 100644 index 000000000..023662ed8 --- /dev/null +++ b/integration-tests/srv/src/test/resources/logback-test.xml @@ -0,0 +1,21 @@ + + + + + + %date %-5level [%thread] [%marker]: %msg%nopex [%logger] [%mdc{correlation_id}]%n + + + + + + + + + + + + + + + diff --git a/integration-tests/srv/src/test/resources/xsuaa-env.json b/integration-tests/srv/src/test/resources/xsuaa-env.json new file mode 100644 index 000000000..7adbe70b0 --- /dev/null +++ b/integration-tests/srv/src/test/resources/xsuaa-env.json @@ -0,0 +1,30 @@ +{ + "VCAP_SERVICES": { + "malware-scanner": [ + { + "label": "malware-scanner", + "provider": null, + "plan": "clamav", + "name": "dsr-core-malware-scanner", + "tags": [], + "instance_guid": "2fbe12be-569d-473e-ab0a-eb2f1d18c7e3", + "instance_name": "dsr-core-malware-scanner", + "binding_guid": "16ce4d70-1511-45d0-a788-e365b298ca8a", + "binding_name": null, + "credentials": { + "sync_scan_url": "https://test.scanner.com", + "async_scan_url": "", + "uri": "https://test.scanner.com", + "url": "http://localhost:1111", + "username": "test-user", + "password": "test-password" + }, + "syslog_drain_url": null, + "volume_mounts": [] + } + ] + }, + "VCAP_APPLICATION": { + "application_id": "xsapp!t0815" + } +} diff --git a/integration-tests/srv/test-service.cds b/integration-tests/srv/test-service.cds new file mode 100644 index 000000000..ff68a31ff --- /dev/null +++ b/integration-tests/srv/test-service.cds @@ -0,0 +1,27 @@ +using test.data.model as db from '../db/data-model'; + +annotate db.Roots.sizeLimitedAttachments with { + content @Validation.Maximum: '5MB'; +}; + +// Media type validation for attachments - for testing purposes. +annotate db.Roots.mediaValidatedAttachments with { + content @(Core.AcceptableMediaTypes: [ + 'image/jpeg', + 'image/png' + ]); +} + +annotate db.Roots.mimeValidatedAttachments with { + content @(Core.AcceptableMediaTypes: ['application/pdf']); +} + +service TestService { + entity Roots as projection on db.Roots; + entity AttachmentEntity as projection on db.AttachmentEntity; +} + +service TestDraftService { + @odata.draft.enabled + entity DraftRoots as projection on db.Roots; +} From b02fa9f6f432967cc890fe79fbd611cec478bc5a Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 11:20:53 +0200 Subject: [PATCH 12/39] fix setup --- integration-tests/mtx-local/package-lock.json | 4144 +++++++++++++++++ integration-tests/srv/pom.xml | 121 - .../integrationtests/Application.java | 15 - .../integrationtests/constants/Profiles.java | 15 - .../testhandler/EventContextHolder.java | 8 - .../testhandler/TestPersistenceHandler.java | 49 - .../TestPluginAttachmentsServiceHandler.java | 120 - .../srv/src/main/resources/application.yaml | 4 - .../srv/src/main/resources/banner.txt | 9 - .../src/main/resources/messages.properties | 1 - .../resources/spotbugs-exclusion-filter.xml | 26 - .../common/JsonToCapMapperTestHelper.java | 21 - .../common/MalwareScanResultProvider.java | 27 - .../common/MockHttpRequestHelper.java | 167 - .../common/TableDataDeleter.java | 21 - .../DraftOdataRequestValidationBase.java | 973 ---- ...aRequestValidationWithTestHandlerTest.java | 288 -- ...thoutTestHandlerAndMalwareScannerTest.java | 114 - ...stHandlerAndWithoutMalwareScannerTest.java | 90 - .../MediaValidatedAttachmentsDraftTest.java | 159 - ...tedAttachmentsSizeValidationDraftTest.java | 202 - ...MediaValidatedAttachmentsNonDraftTest.java | 296 -- .../OdataRequestValidationBase.java | 884 ---- ...aRequestValidationWithTestHandlerTest.java | 245 - ...thoutTestHandlerAndMalwareScannerTest.java | 138 - ...stHandlerAndWithoutMalwareScannerTest.java | 116 - ...mitedAttachmentValidationNonDraftTest.java | 175 - .../helper/AttachmentsBuilder.java | 33 - .../helper/AttachmentsEntityBuilder.java | 31 - .../helper/ItemEntityBuilder.java | 44 - .../helper/RootEntityBuilder.java | 50 - .../TestPersistenceHandlerTest.java | 116 - ...stPluginAttachmentsServiceHandlerTest.java | 266 -- .../srv/src/test/resources/application.yaml | 15 - .../srv/src/test/resources/logback-test.xml | 21 - .../srv/src/test/resources/xsuaa-env.json | 30 - integration-tests/srv/test-service.cds | 27 - 37 files changed, 4144 insertions(+), 4917 deletions(-) create mode 100644 integration-tests/mtx-local/package-lock.json delete mode 100644 integration-tests/srv/pom.xml delete mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java delete mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java delete mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java delete mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java delete mode 100644 integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java delete mode 100644 integration-tests/srv/src/main/resources/application.yaml delete mode 100644 integration-tests/srv/src/main/resources/banner.txt delete mode 100644 integration-tests/srv/src/main/resources/messages.properties delete mode 100644 integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java delete mode 100644 integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java delete mode 100644 integration-tests/srv/src/test/resources/application.yaml delete mode 100644 integration-tests/srv/src/test/resources/logback-test.xml delete mode 100644 integration-tests/srv/src/test/resources/xsuaa-env.json delete mode 100644 integration-tests/srv/test-service.cds diff --git a/integration-tests/mtx-local/package-lock.json b/integration-tests/mtx-local/package-lock.json new file mode 100644 index 000000000..3383043ec --- /dev/null +++ b/integration-tests/mtx-local/package-lock.json @@ -0,0 +1,4144 @@ +{ + "name": "mtx-local-integration-tests", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mtx-local-integration-tests", + "version": "0.0.0", + "workspaces": [ + "mtx/sidecar" + ], + "devDependencies": { + "@sap/cds-dk": "^9", + "@sap/cds-mtxs": "^3" + } + }, + "mtx/sidecar": { + "name": "mtx-local-sidecar", + "version": "0.0.0", + "dependencies": { + "@sap/cds": "^9", + "@sap/cds-mtxs": "^3", + "@sap/xssec": "^4", + "express": "^4" + }, + "devDependencies": { + "@cap-js/sqlite": "^2" + } + }, + "mtx/sidecar/node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "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.14.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/@cap-js/db-service": { + "version": "2.9.0", + "resolved": "https://int.repositories.cloud.sap/artifactory/api/npm/build-milestones-npm/@cap-js/db-service/-/db-service-2.9.0.tgz", + "integrity": "sha512-WCXhoqezaF6A5I2l0MNZeHKXXtHRNEq7Rp0R89/uccOHQIx0DuU0U9NuJJPV/1G5RGk2QKQ9VBo/KYn+MZuuNQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "generic-pool": "^3.9.0" + }, + "peerDependencies": { + "@sap/cds": ">=9.8" + } + }, + "node_modules/@cap-js/sqlite": { + "version": "2.2.0", + "resolved": "https://int.repositories.cloud.sap/artifactory/api/npm/build-milestones-npm/@cap-js/sqlite/-/sqlite-2.2.0.tgz", + "integrity": "sha512-FPj+uVU/14vtGUl2P/Q8y7XhZbsLgrCav2O5PjHPXnupegjby4sMJkgVNxVHnkyKPFgO/W8uEsq9r5TU9VPx8w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@cap-js/db-service": "^2.9.0", + "better-sqlite3": "^12.0.0" + }, + "peerDependencies": { + "@sap/cds": ">=9.8", + "sql.js": "^1.13.0" + }, + "peerDependenciesMeta": { + "sql.js": { + "optional": true + } + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "license": "MIT", + "peer": true, + "engines": { + "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": { + "version": "9.8.4", + "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-9.8.4.tgz", + "integrity": "sha512-1cnFdz4Ex4LV15dztDsSLDiilVdJ2mfTqr5A3TGsRd/6cM31SnHhuZ/Ra0FEErodokNS2pwasGZmSfgHVDW8KA==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@sap/cds-compiler": "^6.4", + "@sap/cds-fiori": "^2", + "express": "^4.22.1 || ^5", + "js-yaml": "^4.1.1" + }, + "bin": { + "cds-deploy": "bin/deploy.js", + "cds-serve": "bin/serve.js" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@eslint/js": "^9 || ^10", + "tar": "^7.5.6" + }, + "peerDependenciesMeta": { + "tar": { + "optional": true + } + } + }, + "node_modules/@sap/cds-compiler": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-6.8.0.tgz", + "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", + "license": "SEE LICENSE IN LICENSE", + "bin": { + "cdsc": "bin/cdsc.js", + "cdshi": "bin/cdshi.js", + "cdsse": "bin/cdsse.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sap/cds-dk": { + "version": "9.8.2", + "resolved": "https://registry.npmjs.org/@sap/cds-dk/-/cds-dk-9.8.2.tgz", + "integrity": "sha512-PGynoQpOCD8Nsr0EEzsEHWg8szYBLVtVd6byxvaaAPYBkNAj2WA+kin6gWaDTiyG3kJa0BSGPk4PCbA2xDoE9A==", + "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/hdi-deploy": "^5", + "axios": "^1", + "express": "^4.22.1 || ^5", + "hdb": "^2.0.0", + "livereload-js": "^4.0.1", + "mustache": "^4.0.1", + "ws": "^8.4.2", + "xml-js": "^1.6.11", + "yaml": "^2" + }, + "bin": { + "cds": "bin/cds.js", + "cds-ts": "bin/cds-ts.js", + "cds-tsx": "bin/cds-tsx.js" + }, + "optionalDependencies": { + "@cap-js/sqlite": ">=1" + } + }, + "node_modules/@sap/cds-dk/node_modules/@cap-js/asyncapi": { + "version": "1.0.3", + "integrity": "sha512-vZSWKAe+3qfvZDXV5SSFiObGWmqyS9MDyEADb5PLVT8kzO39qGaSDPv/GzI/gwvRfCayGAjU4ThiBKrFA7Gclg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "peerDependencies": { + "@sap/cds": ">=7.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/@cap-js/db-service": { + "version": "2.9.0", + "integrity": "sha512-WCXhoqezaF6A5I2l0MNZeHKXXtHRNEq7Rp0R89/uccOHQIx0DuU0U9NuJJPV/1G5RGk2QKQ9VBo/KYn+MZuuNQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "generic-pool": "^3.9.0" + }, + "peerDependencies": { + "@sap/cds": ">=9.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/@cap-js/openapi": { + "version": "1.4.0", + "integrity": "sha512-/LRSwn4SDxAi3qKwl09zoOhEVGaPGlYOPz/0S3UBnaMJVvaLyPiKbbaOtOnrrgulUX5OXt+ujPIQznOsbTzuAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "pluralize": "^8.0.0" + }, + "peerDependencies": { + "@sap/cds": ">=7.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/@cap-js/sqlite": { + "version": "2.2.0", + "integrity": "sha512-FPj+uVU/14vtGUl2P/Q8y7XhZbsLgrCav2O5PjHPXnupegjby4sMJkgVNxVHnkyKPFgO/W8uEsq9r5TU9VPx8w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@cap-js/db-service": "^2.9.0", + "better-sqlite3": "^12.0.0" + }, + "peerDependencies": { + "@sap/cds": ">=9.8", + "sql.js": "^1.13.0" + }, + "peerDependenciesMeta": { + "sql.js": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/@eslint/js": { + "version": "10.0.1", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "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.8.3", + "integrity": "sha512-up/kNVCZK8bOTyye1QN2BjMpH+IgcdyhT3U9mS3TqkpmjNE2JXq8Qz1iNAN+nYcXRWJ1XAUNpgebLfuh/49MGQ==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@sap/cds-compiler": "^6.4", + "@sap/cds-fiori": "^2", + "express": "^4.22.1 || ^5", + "js-yaml": "^4.1.1" + }, + "bin": { + "cds-deploy": "bin/deploy.js", + "cds-serve": "bin/serve.js" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@eslint/js": "^9 || ^10", + "tar": "^7.5.6" + }, + "peerDependenciesMeta": { + "tar": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/cds-compiler": { + "version": "6.8.0", + "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "bin": { + "cdsc": "bin/cdsc.js", + "cdshi": "bin/cdshi.js", + "cdsse": "bin/cdsse.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/cds-fiori": { + "version": "2.3.0", + "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "peerDependencies": { + "@sap/cds": ">=8" + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/cds-mtxs": { + "version": "3.8.1", + "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@sap/hdi-deploy": "^5" + }, + "bin": { + "cds-mtx": "bin/cds-mtx.js", + "cds-mtx-migrate": "bin/cds-mtx-migrate.js" + }, + "peerDependencies": { + "@sap/cds": "^9" + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/hdi": { + "version": "4.8.0", + "integrity": "sha512-tkJmY2ffm6mt4/LFwRBihlQkMxNAXa3ngvRe2N/6+qLIsUNdrH/M03S5mkygXq56K+KoVVZYuradajCusMWwsw==", + "dev": true, + "license": "See LICENSE file", + "dependencies": { + "async": "^3.2.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@sap/hana-client": "^2 >= 2.5", + "hdb": "^2 || ^0" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + }, + "hdb": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/hdi-deploy": { + "version": "5.6.1", + "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", + "dev": true, + "license": "See LICENSE file", + "dependencies": { + "@sap/hdi": "^4.8.0", + "@sap/xsenv": "^6.0.0", + "async": "^3.2.6", + "dotenv": "^16.4.5", + "handlebars": "^4.7.8", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=18.x" + }, + "peerDependencies": { + "@sap/hana-client": "^2 >= 2.6", + "hdb": "^2 || ^0" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + }, + "hdb": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/@sap/xsenv": { + "version": "6.1.0", + "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE file", + "dependencies": { + "debug": "4.4.3", + "node-cache": "^5.1.2", + "verror": "1.10.1" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/accepts": { + "version": "2.0.0", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "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/assert-plus": { + "version": "1.0.0", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/async": { + "version": "3.2.6", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "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.13.6", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/base64-js": { + "version": "1.5.1", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/better-sqlite3": { + "version": "12.8.0", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/@sap/cds-dk/node_modules/bindings": { + "version": "1.5.0", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/bl": { + "version": "4.1.0", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/body-parser": { + "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": "^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": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/braces": { + "version": "3.0.3", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sap/cds-dk/node_modules/buffer": { + "version": "5.7.1", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/@sap/cds-dk/node_modules/bytes": { + "version": "3.1.2", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/call-bound": { + "version": "1.0.4", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "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/@sap/cds-dk/node_modules/chownr": { + "version": "1.1.4", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/clone": { + "version": "2.1.2", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "license": "MIT", + "engines": { + "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": "1.0.1", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/content-type": { + "version": "1.0.5", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/cookie": { + "version": "0.7.2", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/cookie-signature": { + "version": "1.2.2", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/core-util-is": { + "version": "1.0.2", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/debug": { + "version": "4.4.3", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/decompress-response": { + "version": "6.0.0", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sap/cds-dk/node_modules/deep-extend": { + "version": "0.6.0", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/detect-libc": { + "version": "2.1.2", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sap/cds-dk/node_modules/dotenv": { + "version": "16.6.1", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@sap/cds-dk/node_modules/dunder-proto": { + "version": "1.0.1", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/ee-first": { + "version": "1.1.1", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/encodeurl": { + "version": "2.0.0", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/end-of-stream": { + "version": "1.4.5", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/es-define-property": { + "version": "1.0.1", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/es-errors": { + "version": "1.3.0", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/es-object-atoms": { + "version": "1.1.1", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/etag": { + "version": "1.8.1", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/expand-template": { + "version": "2.0.3", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sap/cds-dk/node_modules/express": { + "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/extsprintf": { + "version": "1.4.1", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/file-uri-to-path": { + "version": "1.0.0", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/fill-range": { + "version": "7.1.1", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sap/cds-dk/node_modules/finalhandler": { + "version": "2.1.1", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "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": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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.5", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "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" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/forwarded": { + "version": "0.2.0", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/fresh": { + "version": "2.0.0", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/fs-constants": { + "version": "1.0.0", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/function-bind": { + "version": "1.1.2", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/generic-pool": { + "version": "3.9.0", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@sap/cds-dk/node_modules/get-intrinsic": { + "version": "1.3.0", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "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/@sap/cds-dk/node_modules/get-proto": { + "version": "1.0.1", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/github-from-package": { + "version": "0.0.0", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/gopd": { + "version": "1.2.0", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/handlebars": { + "version": "4.7.8", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/has-symbols": { + "version": "1.1.0", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/hdb": { + "version": "2.27.1", + "integrity": "sha512-xYL/W+fq2TyGHyzm8muolQnw8tdh4+2NQ8mQP2FpLSuhfJ8l0jQNSUZoAXic7NfMEan1Jvf8V1L4blwkgTc6+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "iconv-lite": "0.7.0" + }, + "engines": { + "node": ">= 18" + }, + "optionalDependencies": { + "lz4-wasm-nodejs": "0.9.2" + } + }, + "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.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": { + "version": "1.2.1", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/inherits": { + "version": "2.0.4", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@sap/cds-dk/node_modules/ini": { + "version": "1.3.8", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/ipaddr.js": { + "version": "1.9.1", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@sap/cds-dk/node_modules/is-number": { + "version": "7.0.0", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/is-promise": { + "version": "4.0.0", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/js-yaml": { + "version": "4.1.1", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@sap/cds-dk/node_modules/livereload-js": { + "version": "4.0.2", + "integrity": "sha512-Fy7VwgQNiOkynYyNBTo3v9hQUhcW5pFAheJN148+DTgpShjsy/22pLHKKwDK5v0kOsZsJBK+6q1PMgLvRmrwFQ==", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/media-typer": { + "version": "1.1.0", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/merge-descriptors": { + "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/micromatch": { + "version": "4.0.8", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@sap/cds-dk/node_modules/mime-db": { + "version": "1.54.0", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/mime-types": { + "version": "3.0.2", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/mimic-response": { + "version": "3.1.0", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sap/cds-dk/node_modules/minimist": { + "version": "1.2.8", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/mkdirp-classic": { + "version": "0.5.3", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/ms": { + "version": "2.1.3", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/mustache": { + "version": "4.2.0", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/@sap/cds-dk/node_modules/napi-build-utils": { + "version": "2.0.0", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/negotiator": { + "version": "1.0.0", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/neo-async": { + "version": "2.6.2", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/node-abi": { + "version": "3.89.0", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@sap/cds-dk/node_modules/node-cache": { + "version": "5.1.2", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/object-inspect": { + "version": "1.13.4", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/on-finished": { + "version": "2.4.1", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/once": { + "version": "1.4.0", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/@sap/cds-dk/node_modules/parseurl": { + "version": "1.3.3", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/path-to-regexp": { + "version": "8.3.0", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/pluralize": { + "version": "8.0.0", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@sap/cds-dk/node_modules/proxy-addr": { + "version": "2.0.7", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "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.4", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/@sap/cds-dk/node_modules/qs": { + "version": "6.15.0", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/range-parser": { + "version": "1.2.1", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/raw-body": { + "version": "3.0.2", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@sap/cds-dk/node_modules/rc": { + "version": "1.2.8", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/@sap/cds-dk/node_modules/readable-stream": { + "version": "3.6.2", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "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==", + "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", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/safer-buffer": { + "version": "2.1.2", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/sax": { + "version": "1.6.0", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/semver": { + "version": "7.7.4", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@sap/cds-dk/node_modules/send": { + "version": "1.2.1", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "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": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/serve-static": { + "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": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@sap/cds-dk/node_modules/setprototypeof": { + "version": "1.2.0", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/@sap/cds-dk/node_modules/side-channel": { + "version": "1.1.0", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "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/@sap/cds-dk/node_modules/side-channel-list": { + "version": "1.0.0", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@sap/cds-dk/node_modules/side-channel-map": { + "version": "1.0.1", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "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/@sap/cds-dk/node_modules/side-channel-weakmap": { + "version": "1.0.2", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "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/@sap/cds-dk/node_modules/simple-concat": { + "version": "1.0.1", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/simple-get": { + "version": "4.0.1", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/source-map": { + "version": "0.6.1", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/statuses": { + "version": "2.0.2", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/string_decoder": { + "version": "1.3.0", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/strip-json-comments": { + "version": "2.0.1", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/tar-fs": { + "version": "2.1.4", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/@sap/cds-dk/node_modules/tar-stream": { + "version": "2.2.0", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sap/cds-dk/node_modules/to-regex-range": { + "version": "5.0.1", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/toidentifier": { + "version": "1.0.1", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/tunnel-agent": { + "version": "0.6.0", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@sap/cds-dk/node_modules/type-is": { + "version": "2.0.1", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/uglify-js": { + "version": "3.19.3", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/unpipe": { + "version": "1.0.0", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/util-deprecate": { + "version": "1.0.2", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sap/cds-dk/node_modules/vary": { + "version": "1.1.2", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@sap/cds-dk/node_modules/verror": { + "version": "1.10.1", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/@sap/cds-dk/node_modules/wordwrap": { + "version": "1.0.0", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap/cds-dk/node_modules/wrappy": { + "version": "1.0.2", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@sap/cds-dk/node_modules/ws": { + "version": "8.20.0", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@sap/cds-dk/node_modules/xml-js": { + "version": "1.6.11", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/@sap/cds-dk/node_modules/yaml": { + "version": "2.8.3", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/@sap/cds-fiori": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sap/cds-fiori/-/cds-fiori-2.3.0.tgz", + "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", + "license": "SEE LICENSE IN LICENSE", + "peerDependencies": { + "@sap/cds": ">=8" + } + }, + "node_modules/@sap/cds-mtxs": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@sap/cds-mtxs/-/cds-mtxs-3.8.1.tgz", + "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@sap/hdi-deploy": "^5" + }, + "bin": { + "cds-mtx": "bin/cds-mtx.js", + "cds-mtx-migrate": "bin/cds-mtx-migrate.js" + }, + "peerDependencies": { + "@sap/cds": "^9" + } + }, + "node_modules/@sap/hdi": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@sap/hdi/-/hdi-4.8.0.tgz", + "integrity": "sha512-tkJmY2ffm6mt4/LFwRBihlQkMxNAXa3ngvRe2N/6+qLIsUNdrH/M03S5mkygXq56K+KoVVZYuradajCusMWwsw==", + "license": "See LICENSE file", + "dependencies": { + "async": "^3.2.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@sap/hana-client": "^2 >= 2.5", + "hdb": "^2 || ^0" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + }, + "hdb": { + "optional": true + } + } + }, + "node_modules/@sap/hdi-deploy": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@sap/hdi-deploy/-/hdi-deploy-5.6.1.tgz", + "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", + "license": "See LICENSE file", + "dependencies": { + "@sap/hdi": "^4.8.0", + "@sap/xsenv": "^6.0.0", + "async": "^3.2.6", + "dotenv": "^16.4.5", + "handlebars": "^4.7.8", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=18.x" + }, + "peerDependencies": { + "@sap/hana-client": "^2 >= 2.6", + "hdb": "^2 || ^0" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + }, + "hdb": { + "optional": true + } + } + }, + "node_modules/@sap/xsenv": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@sap/xsenv/-/xsenv-6.1.0.tgz", + "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", + "license": "SEE LICENSE IN LICENSE file", + "dependencies": { + "debug": "4.4.3", + "node-cache": "^5.1.2", + "verror": "1.10.1" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@sap/xsenv/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@sap/xsenv/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@sap/xssec": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@sap/xssec/-/xssec-4.13.0.tgz", + "integrity": "sha512-8e+bU+OyAIpAGXQanOopZa5YEK+yHKw84dhhihcCotF40MSNFbVHjQ4xM5hf4QndlqDGfXIuvXmoOMuDATa/gA==", + "license": "SAP DEVELOPER LICENSE AGREEMENT", + "dependencies": { + "debug": "^4.4.3", + "jwt-decode": "^4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sap/xssec/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@sap/xssec/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "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==", + "license": "MIT" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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" + }, + "node_modules/better-sqlite3": { + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "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.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "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/express/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "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": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "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": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "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/express/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/express/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/express/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "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": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "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.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "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/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/mtx-local-sidecar": { + "resolved": "mtx/sidecar", + "link": true + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "license": "MIT", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "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/router/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", + "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "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" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "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/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "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==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/integration-tests/srv/pom.xml b/integration-tests/srv/pom.xml deleted file mode 100644 index 6a5b6eada..000000000 --- a/integration-tests/srv/pom.xml +++ /dev/null @@ -1,121 +0,0 @@ - - - 4.0.0 - - - com.sap.cds.integration-tests - cds-feature-attachments-integration-tests-parent - ${revision} - - - cds-feature-attachments-integration-tests-srv - jar - - Integration Tests - Service - - - com.sap.cds.feature.attachments.generated - - - - - - - com.sap.cds - cds-starter-spring-boot - - - - - com.sap.cds - cds-adapter-odata-v4 - runtime - - - - org.springframework.security - spring-security-test - test - - - - org.springframework.boot - spring-boot-starter-test - test - - - - com.h2database - h2 - test - - - - org.wiremock.integrations - wiremock-spring-boot - test - - - - - - - - com.sap.cds - cds-maven-plugin - - - cds.clean - - clean - - - - - cds.install-node - - install-node - - - - - cds.resolve - - resolve - - - ${project.basedir}/.. - - - - - cds.build - - cds - - - ${project.basedir}/.. - - build --for java - deploy --to h2 --dry > - "${project.basedir}/src/main/resources/schema.sql" - - - - - - cds.generate - - generate - - - ${project.basedir}/.. - ${generation-package}.integration.test.cds4j - - - - - - - - diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java deleted file mode 100644 index b0b23079c..000000000 --- a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/Application.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class Application { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } -} diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java deleted file mode 100644 index 8ac4f8f3e..000000000 --- a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/constants/Profiles.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.constants; - -public final class Profiles { - - public static final String TEST_HANDLER_ENABLED = "test-handler-enabled"; - public static final String TEST_HANDLER_DISABLED = "test-handler-disabled"; - public static final String MALWARE_SCAN_ENABLED = "malware-scan-enabled"; - - private Profiles() { - // prevent instantiation - } -} diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java deleted file mode 100644 index 280227a89..000000000 --- a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/EventContextHolder.java +++ /dev/null @@ -1,8 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.testhandler; - -import com.sap.cds.services.EventContext; - -public record EventContextHolder(String event, EventContext context) {} diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java deleted file mode 100644 index 207f49183..000000000 --- a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandler.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.testhandler; - -import static com.sap.cds.services.cds.CqnService.EVENT_CREATE; -import static com.sap.cds.services.cds.CqnService.EVENT_UPDATE; - -import com.sap.cds.services.ServiceException; -import com.sap.cds.services.handler.EventHandler; -import com.sap.cds.services.handler.annotations.Before; -import com.sap.cds.services.handler.annotations.ServiceName; -import com.sap.cds.services.persistence.PersistenceService; -import org.springframework.stereotype.Component; - -@ServiceName(value = "*", type = PersistenceService.class) -@Component -public class TestPersistenceHandler implements EventHandler { - - private volatile boolean throwExceptionOnUpdate = false; - private volatile boolean throwExceptionOnCreate = false; - - @Before(event = EVENT_UPDATE) - public void throwExceptionOnUpdate() { - if (throwExceptionOnUpdate) { - throw new ServiceException("Exception on update"); - } - } - - @Before(event = EVENT_CREATE) - public void throwExceptionOnCreate() { - if (throwExceptionOnCreate) { - throw new ServiceException("Exception on create"); - } - } - - public void reset() { - throwExceptionOnUpdate = false; - throwExceptionOnCreate = false; - } - - public void setThrowExceptionOnUpdate(boolean throwExceptionOnUpdate) { - this.throwExceptionOnUpdate = throwExceptionOnUpdate; - } - - public void setThrowExceptionOnCreate(boolean throwExceptionOnCreate) { - this.throwExceptionOnCreate = throwExceptionOnCreate; - } -} diff --git a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java b/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java deleted file mode 100644 index 341002905..000000000 --- a/integration-tests/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandler.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.testhandler; - -import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.StatusCode; -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import com.sap.cds.feature.attachments.service.AttachmentService; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; -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.io.ByteArrayInputStream; -import java.io.IOException; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.slf4j.Marker; -import org.slf4j.MarkerFactory; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@ServiceName(value = "*", type = AttachmentService.class) -@Profile(Profiles.TEST_HANDLER_ENABLED) -@Component -public class TestPluginAttachmentsServiceHandler implements EventHandler { - - private static final Marker marker = MarkerFactory.getMarker("DUMMY_HANDLER"); - private static final Logger logger = - LoggerFactory.getLogger(TestPluginAttachmentsServiceHandler.class); - - private static final Map documents = new ConcurrentHashMap<>(); - private static final List eventContextHolder = - Collections.synchronizedList(new ArrayList<>()); - - @On(event = AttachmentService.EVENT_CREATE_ATTACHMENT) - public void createAttachment(AttachmentCreateEventContext context) throws IOException { - logger.info(marker, "CREATE Attachment called in dummy handler"); - var contentId = UUID.randomUUID().toString(); - documents.put(contentId, context.getData().getContent().readAllBytes()); - context.setContentId(contentId); - context.getData().setStatus(StatusCode.CLEAN); - context.getData().setScannedAt(Instant.now()); - context.setCompleted(); - eventContextHolder.add( - new EventContextHolder(AttachmentService.EVENT_CREATE_ATTACHMENT, context)); - } - - @On(event = AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED) - public void markAttachmentAsDeleted(AttachmentMarkAsDeletedEventContext context) { - logger.info( - marker, - "DELETE Attachment called in dummy handler for document id {}", - context.getContentId()); - context.setCompleted(); - eventContextHolder.add( - new EventContextHolder(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED, context)); - } - - @On(event = AttachmentService.EVENT_READ_ATTACHMENT) - public void readAttachment(AttachmentReadEventContext context) { - logger.info( - marker, - "READ Attachment called in dummy handler for content id {}", - context.getContentId()); - var contentId = context.getContentId(); - var content = contentId != null ? documents.get(contentId) : null; - var stream = content != null ? new ByteArrayInputStream(content) : null; - context.getData().setContent(stream); - context.setCompleted(); - eventContextHolder.add( - new EventContextHolder(AttachmentService.EVENT_READ_ATTACHMENT, context)); - } - - @On(event = AttachmentService.EVENT_RESTORE_ATTACHMENT) - public void restoreAttachment(AttachmentRestoreEventContext context) { - logger.info( - marker, - "RESTORE Attachment called in dummy handler for timestamp {}", - context.getRestoreTimestamp()); - context.setCompleted(); - eventContextHolder.add( - new EventContextHolder(AttachmentService.EVENT_RESTORE_ATTACHMENT, context)); - } - - public List getEventContextForEvent(String event) { - var context = eventContextHolder.stream().filter(e -> e.event().equals(event)).toList(); - if (event.equals(AttachmentService.EVENT_CREATE_ATTACHMENT) && !context.isEmpty()) { - context.forEach( - c -> { - var createContext = (AttachmentCreateEventContext) c.context(); - createContext - .getData() - .setContent(new ByteArrayInputStream(documents.get(createContext.getContentId()))); - }); - } - return context; - } - - public List getEventContext() { - return eventContextHolder; - } - - public void clearEventContext() { - eventContextHolder.clear(); - } - - public void clearDocuments() { - documents.clear(); - } -} diff --git a/integration-tests/srv/src/main/resources/application.yaml b/integration-tests/srv/src/main/resources/application.yaml deleted file mode 100644 index 68a4a6e63..000000000 --- a/integration-tests/srv/src/main/resources/application.yaml +++ /dev/null @@ -1,4 +0,0 @@ ---- -spring: - config.activate.on-profile: default - sql.init.schema-locations: classpath:schema.sql diff --git a/integration-tests/srv/src/main/resources/banner.txt b/integration-tests/srv/src/main/resources/banner.txt deleted file mode 100644 index 875e346ff..000000000 --- a/integration-tests/srv/src/main/resources/banner.txt +++ /dev/null @@ -1,9 +0,0 @@ - __ _ _ _ _ __ - / / /\ | | | | | | | | \ \ - / / / \ | |_ | |_ __ _ ___ | |__ _ __ ___ ___ _ __ | |_ ___ \ \ - < < / /\ \ | __| | __| / _` | / __| | '_ \ | '_ ` _ \ / _ \ | '_ \ | __| / __| > > - \ \ / ____ \ | |_ | |_ | (_| | | (__ | | | | | | | | | | | __/ | | | | | |_ \__ \ / / - \_\ /_/ \_\ \__| \__| \__,_| \___| |_| |_| |_| |_| |_| \___| |_| |_| \__| |___/ /_/ - ================================================================================================= - :: Spring Boot :: ${spring-boot.formatted-version} - diff --git a/integration-tests/srv/src/main/resources/messages.properties b/integration-tests/srv/src/main/resources/messages.properties deleted file mode 100644 index 81680eda5..000000000 --- a/integration-tests/srv/src/main/resources/messages.properties +++ /dev/null @@ -1 +0,0 @@ -not_clean=Error text for not clean \ No newline at end of file diff --git a/integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml b/integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml deleted file mode 100644 index b5e184082..000000000 --- a/integration-tests/srv/src/main/resources/spotbugs-exclusion-filter.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java deleted file mode 100644 index 5bc8eb32e..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/JsonToCapMapperTestHelper.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.common; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.sap.cds.CdsData; -import com.sap.cds.Struct; -import java.util.HashMap; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -class JsonToCapMapperTestHelper { - - @Autowired private ObjectMapper objectMapper; - - public CdsData mapResponseToSingleResult(String resultBody) throws Exception { - return Struct.access(objectMapper.readValue(resultBody, HashMap.class)).as(CdsData.class); - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java deleted file mode 100644 index f36ec06f0..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MalwareScanResultProvider.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.common; - -import org.springframework.stereotype.Component; - -@Component -public class MalwareScanResultProvider { - - public String buildMalwareScanResult(boolean malware) { - return """ - { - \t"malwareDetected": %s, - \t"encryptedContentDetected": false, - \t"scanSize": 68, - \t"finding": "Win.Test.EICAR_HDB-1", - \t"mimeType": "text/plain", - \t"SHA256": "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - \t"extensions": [ - \t\t"txt" - \t] - } - """ - .formatted(malware); - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java deleted file mode 100644 index 426e88f2a..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/MockHttpRequestHelper.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.common; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.sap.cds.CdsData; -import com.sap.cds.Struct; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Component; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.ResultMatcher; -import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; - -@Component -public class MockHttpRequestHelper { - - public static final String ODATA_BASE_URL = "/odata/v4/"; - public static final String IF_MATCH = "If-Match"; - - @Autowired private JsonToCapMapperTestHelper mapper; - @Autowired private MockMvc mvc; - - private String contentType = MediaType.APPLICATION_JSON.toString(); - private String accept = MediaType.APPLICATION_JSON.toString(); - - public MvcResult executeGet(String url) throws Exception { - MockHttpServletRequestBuilder requestBuilder = - MockMvcRequestBuilders.get(url).contentType(contentType).accept(accept); - return mvc.perform(requestBuilder).andReturn(); - } - - public String executeGetWithSingleODataResponseAndAssertStatus(String url, HttpStatus status) - throws Exception { - var result = executeGet(url); - assertThat(result.getResponse().getStatus()).isEqualTo(status.value()); - return result.getResponse().getContentAsString(); - } - - public T executeGetWithSingleODataResponseAndAssertStatus( - String url, Class resultType, HttpStatus status) throws Exception { - var resultBody = executeGetWithSingleODataResponseAndAssertStatus(url, status); - return Struct.access(mapper.mapResponseToSingleResult(resultBody)).as(resultType); - } - - public MvcResult executePost(String url, String body) throws Exception { - return mvc.perform( - MockMvcRequestBuilders.post(url).contentType(contentType).accept(accept).content(body)) - .andReturn(); - } - - public MvcResult executePatch(String url, String body) throws Exception { - return executePatch(url, body, "*"); - } - - public MvcResult executePatch(String url, String body, String etag) throws Exception { - return mvc.perform( - MockMvcRequestBuilders.patch(url) - .contentType(contentType) - .accept(accept) - .header(IF_MATCH, etag) - .content(body)) - .andReturn(); - } - - public void executePostWithMatcher(String url, String body, ResultMatcher matcher) - throws Exception { - mvc.perform( - MockMvcRequestBuilders.post(url).contentType(contentType).accept(accept).content(body)) - .andExpect(matcher); - } - - public MvcResult executeDelete(String url) throws Exception { - return executeDelete(url, "*"); - } - - public MvcResult executeDelete(String url, String etag) throws Exception { - return mvc.perform( - MockMvcRequestBuilders.delete(url) - .contentType(contentType) - .accept(accept) - .header(IF_MATCH, etag)) - .andReturn(); - } - - public void executeDeleteWithMatcher(String url, ResultMatcher matcher) throws Exception { - executeDeleteWithMatcher(url, "*", matcher); - } - - public void executeDeleteWithMatcher(String url, String etag, ResultMatcher matcher) - throws Exception { - mvc.perform( - MockMvcRequestBuilders.delete(url) - .contentType(contentType) - .accept(accept) - .header(IF_MATCH, etag)) - .andExpect(matcher); - } - - public CdsData executePostWithODataResponseAndAssertStatusCreated(String url, String body) - throws Exception { - return executePostWithODataResponseAndAssertStatus(url, body, HttpStatus.CREATED); - } - - public void executePatchWithODataResponseAndAssertStatusOk(String url, String body) - throws Exception { - executePatchWithODataResponseAndAssertStatus(url, body, HttpStatus.OK); - } - - public CdsData executePostWithODataResponseAndAssertStatus( - String url, String body, HttpStatus status) throws Exception { - MvcResult result = executePost(url, body); - String resultBody = result.getResponse().getContentAsString(); - assertThat(result.getResponse().getStatus()) - .as("Unexpected HTTP status, with response body " + resultBody) - .isEqualTo(status.value()); - return mapper.mapResponseToSingleResult(resultBody); - } - - public void executePatchWithODataResponseAndAssertStatus( - String url, String body, HttpStatus status) throws Exception { - executePatchWithODataResponseAndAssertStatus(url, body, "*", status); - } - - public void executePatchWithODataResponseAndAssertStatus( - String url, String body, String etag, HttpStatus status) throws Exception { - MvcResult result = executePatch(url, body, etag); - String resultBody = result.getResponse().getContentAsString(); - assertThat(result.getResponse().getStatus()) - .as("Unexpected HTTP status, with response body " + resultBody) - .isEqualTo(status.value()); - } - - public void executePutWithMatcher(String url, byte[] body, ResultMatcher matcher) - throws Exception { - executePutWithMatcher(url, body, "*", matcher); - } - - public void executePutWithMatcher(String url, byte[] body, String etag, ResultMatcher matcher) - throws Exception { - mvc.perform( - MockMvcRequestBuilders.put(url) - .contentType(contentType) - .accept(accept) - .header(IF_MATCH, etag) - .content(body)) - .andExpect(matcher); - } - - public void setContentType(MediaType contentType) { - this.contentType = contentType.toString(); - } - - public void setContentType(String contentType) { - this.contentType = contentType; - } - - public void resetHelper() { - contentType = MediaType.APPLICATION_JSON.toString(); - accept = MediaType.APPLICATION_JSON.toString(); - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java deleted file mode 100644 index a56d44a7c..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/common/TableDataDeleter.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.common; - -import com.sap.cds.ql.Delete; -import com.sap.cds.services.persistence.PersistenceService; -import java.util.Arrays; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@Component -public class TableDataDeleter { - - @Autowired private PersistenceService persistenceService; - - public void deleteData(String... entityNames) { - Arrays.stream(entityNames) - .forEach(entityName -> persistenceService.run(Delete.from(entityName))); - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java deleted file mode 100644 index 843fa2bbc..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationBase.java +++ /dev/null @@ -1,973 +0,0 @@ -/* - * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.draftservice; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.sap.cds.Struct; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.AttachmentEntity; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.DraftRoots; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.DraftRoots_; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.Items; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.TestDraftService_; -import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; -import com.sap.cds.feature.attachments.integrationtests.common.TableDataDeleter; -import com.sap.cds.feature.attachments.integrationtests.testhandler.TestPersistenceHandler; -import com.sap.cds.feature.attachments.integrationtests.testhandler.TestPluginAttachmentsServiceHandler; -import com.sap.cds.feature.attachments.service.AttachmentService; -import com.sap.cds.ql.Select; -import com.sap.cds.ql.StructuredType; -import com.sap.cds.services.persistence.PersistenceService; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.TimeUnit; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.AfterEach; -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.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.HttpStatus; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.ResultMatcher; - -@SpringBootTest -@AutoConfigureMockMvc -abstract class DraftOdataRequestValidationBase { - - protected static final Logger logger = - LoggerFactory.getLogger(DraftOdataRequestValidationBase.class); - private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestDraftService/"; - private static final String BASE_ROOT_URL = BASE_URL + "DraftRoots"; - - @Autowired(required = false) - protected TestPluginAttachmentsServiceHandler serviceHandler; - - @Autowired protected MockHttpRequestHelper requestHelper; - @Autowired protected PersistenceService persistenceService; - @Autowired private TableDataDeleter dataDeleter; - @Autowired private TestPersistenceHandler testPersistenceHandler; - - @AfterEach - void teardown() { - dataDeleter.deleteData( - DraftRoots_.CDS_NAME, DraftRoots_.CDS_NAME + "_drafts", "cds.outbox.Messages"); - requestHelper.resetHelper(); - clearServiceHandlerContext(); - testPersistenceHandler.reset(); - } - - @Test - void deepCreateWorks() throws Exception { - var testContentAttachment = "testContent attachment"; - var testContentAttachmentEntity = "testContent attachmentEntity"; - - var selectedRoot = deepCreateAndActivate(testContentAttachment, testContentAttachmentEntity); - - assertThat(selectedRoot.getIsActiveEntity()).isTrue(); - - var selectedAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); - var selectedAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); - - verifyContentId(selectedAttachment.getContentId(), selectedAttachment.getId()); - assertThat(selectedAttachment.getFileName()).isEqualTo("itemAttachment.txt"); - assertThat(selectedAttachment.getMimeType()).contains("text/plain"); - verifyContent(selectedAttachment.getContent(), testContentAttachment); - verifyContentId(selectedAttachmentEntity.getContentId(), selectedAttachmentEntity.getId()); - assertThat(selectedAttachmentEntity.getFileName()).isEqualTo("itemAttachmentEntity.txt"); - assertThat(selectedAttachmentEntity.getMimeType()).contains("image/jpeg"); - verifyContent(selectedAttachmentEntity.getContent(), testContentAttachmentEntity); - verifyOnlyTwoCreateEvents(testContentAttachment, testContentAttachmentEntity); - } - - @Test - void contentCanBeReadFromDraft() throws Exception { - var testContentAttachment = "testContent attachment"; - var testContentAttachmentEntity = "testContent attachmentEntity"; - - var root = deepCreateAndActivate(testContentAttachment, testContentAttachmentEntity); - - var selectedRoot = selectStoredRootData(root); - assertThat(selectedRoot.getItems().get(0).getAttachments()) - .hasSize(1) - .first() - .satisfies(attachment -> verifyContent(attachment.getContent(), testContentAttachment)); - assertThat(selectedRoot.getItems().get(0).getAttachmentEntities()) - .hasSize(1) - .first() - .satisfies( - attachment -> verifyContent(attachment.getContent(), testContentAttachmentEntity)); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var attachmentUrl = - getAttachmentBaseUrl( - selectedRoot.getItems().get(0).getId(), - selectedRoot.getItems().get(0).getAttachments().get(0).getId(), - false) - + "/content"; - var attachmentEntityUrl = - getAttachmentEntityBaseUrl( - selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getId(), false) - + "/content"; - - Awaitility.await() - .atMost(60, TimeUnit.SECONDS) - .pollDelay(1, TimeUnit.SECONDS) - .pollInterval(2, TimeUnit.SECONDS) - .until( - () -> { - var attachmentResponse = requestHelper.executeGet(attachmentUrl); - var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); - var attachmentResponseContent = getResponseContent(attachmentResponse); - var attachmentEntityResponseContent = getResponseContent(attachmentEntityResponse); - var result = - attachmentResponseContent.equals(testContentAttachment) - && attachmentEntityResponseContent.equals(testContentAttachmentEntity); - if (!result) { - logger.info( - "Attachment response content: {}, Attachment Test Content: {}, Attachment Entity response content: {}, Attachment Entity Test Content: {}", - attachmentResponseContent, - testContentAttachment, - attachmentEntityResponseContent, - testContentAttachmentEntity); - } - return result; - }); - clearServiceHandlerContext(); - - var attachmentResponse = requestHelper.executeGet(attachmentUrl); - assertThat(attachmentResponse.getResponse().getContentAsString()) - .isEqualTo(testContentAttachment); - var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); - assertThat(attachmentEntityResponse.getResponse().getContentAsString()) - .isEqualTo(testContentAttachmentEntity); - verifyTwoReadEvents(); - } - - @Test - void deleteAttachmentAndActivateDraft() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); - var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); - - var attachmentDeleteUrl = - getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), false); - var attachmentEntityDeleteUrl = getAttachmentEntityBaseUrl(itemAttachmentEntity.getId(), false); - - requestHelper.executeDeleteWithMatcher(attachmentDeleteUrl, status().isNoContent()); - requestHelper.executeDeleteWithMatcher(attachmentEntityDeleteUrl, status().isNoContent()); - verifyNoAttachmentEventsCalled(); - - prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); - - var selectedRootAfterDelete = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterDelete.getItems().get(0).getAttachments()).isEmpty(); - assertThat(selectedRootAfterDelete.getItems().get(0).getAttachmentEntities()).isEmpty(); - verifyOnlyTwoDeleteEvents(itemAttachment.getContentId(), itemAttachmentEntity.getContentId()); - } - - @Test - void updateAttachmentAndActivateDraft() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); - var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); - - var changedAttachmentFileName = "changedAttachmentFileName.txt"; - var changedAttachmentEntityFileName = "changedAttachmentEntityFileName.txt"; - - updateFileName( - selectedRoot, - itemAttachment, - itemAttachmentEntity, - changedAttachmentFileName, - changedAttachmentEntityFileName); - - prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterUpdate = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getFileName()) - .isEqualTo(changedAttachmentFileName); - assertThat( - selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getFileName()) - .isEqualTo(changedAttachmentEntityFileName); - verifyNoAttachmentEventsCalled(); - } - - @Test - void updateAttachmentAndCancelDraft() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); - var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); - - var originAttachmentFileName = itemAttachment.getFileName(); - var originAttachmentEntityFileName = itemAttachmentEntity.getFileName(); - - updateFileName( - selectedRoot, - itemAttachment, - itemAttachmentEntity, - "changedAttachmentFileName.txt", - "changedAttachmentEntityFileName.txt"); - - cancelDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterUpdate = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getFileName()) - .isEqualTo(originAttachmentFileName); - assertThat( - selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getFileName()) - .isEqualTo(originAttachmentEntityFileName); - verifyNoAttachmentEventsCalled(); - } - - @Test - void createAttachmentAndActivateDraft() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var itemAttachment = selectedRoot.getItems().get(0); - - var newAttachmentContent = "new attachment content"; - createAttachmentWithContent(newAttachmentContent, itemAttachment.getId()); - var newAttachmentEntityContent = "new attachmentEntity content"; - createAttachmentEntityWithContent(newAttachmentEntityContent, itemAttachment); - - prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterCreate = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterCreate.getItems().get(0).getAttachments()).hasSize(2); - assertThat(selectedRootAfterCreate.getItems().get(0).getAttachmentEntities()).hasSize(2); - verifyOnlyTwoCreateEvents(newAttachmentContent, newAttachmentEntityContent); - } - - @Test - void createAttachmentAndCancelDraft() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var itemAttachment = selectedRoot.getItems().get(0); - - var newAttachmentContent = "new attachment content"; - createAttachmentWithContent(newAttachmentContent, itemAttachment.getId()); - var newAttachmentEntityContent = "new attachmentEntity content"; - createAttachmentEntityWithContent(newAttachmentEntityContent, itemAttachment); - - cancelDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterCreate = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterCreate.getItems().get(0).getAttachments()).hasSize(1); - assertThat(selectedRootAfterCreate.getItems().get(0).getAttachmentEntities()).hasSize(1); - verifyTwoCreateAndDeleteEvents(newAttachmentContent, newAttachmentEntityContent); - } - - @Test - void deleteContentInDraft() throws Exception { - var selectedRoot = - deepCreateAndActivate( - "testContent attachment for delete", "testContent attachmentEntity for delete"); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); - var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); - - deleteContent(selectedRoot, itemAttachment, itemAttachmentEntity); - verifyNoAttachmentEventsCalled(); - - prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterDelete = selectStoredRootData(selectedRoot); - verifyContent( - selectedRootAfterDelete.getItems().get(0).getAttachments().get(0).getContent(), null); - verifyContent( - selectedRootAfterDelete.getItems().get(0).getAttachmentEntities().get(0).getContent(), - null); - verifyOnlyTwoDeleteEvents(itemAttachment.getContentId(), itemAttachmentEntity.getContentId()); - } - - @Test - void doNotDeleteContentInCancelledDraft() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); - var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); - - deleteContent(selectedRoot, itemAttachment, itemAttachmentEntity); - - cancelDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterDelete = selectStoredRootData(selectedRoot); - verifyContent( - selectedRootAfterDelete.getItems().get(0).getAttachments().get(0).getContent(), - "testContent attachment"); - verifyContent( - selectedRootAfterDelete.getItems().get(0).getAttachmentEntities().get(0).getContent(), - "testContent attachmentEntity"); - verifyNoAttachmentEventsCalled(); - } - - @Test - void updateContentInDraft() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); - var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); - - var attachmentContentId = itemAttachment.getContentId(); - var attachmentEntityContentId = itemAttachmentEntity.getContentId(); - - var newAttachmentContent = "new content attachment"; - putNewContentForAttachment( - newAttachmentContent, selectedRoot.getItems().get(0).getId(), itemAttachment.getId()); - var newAttachmentEntityContent = "new content attachmentEntity"; - putNewContentForAttachmentEntity(newAttachmentEntityContent, itemAttachmentEntity.getId()); - - prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterUpdate = selectStoredRootData(selectedRoot); - verifyContent( - selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getContent(), - newAttachmentContent); - verifyContent( - selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getContent(), - newAttachmentEntityContent); - verifyEventContextEmptyForEvent(AttachmentService.EVENT_READ_ATTACHMENT); - verifyTwoUpdateEvents( - newAttachmentContent, - attachmentContentId, - newAttachmentEntityContent, - attachmentEntityContentId); - var selectedRootAfterDeletion = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterDeletion.getItems().get(0).getAttachments().get(0).getContentId()) - .isNotEmpty(); - assertThat( - selectedRootAfterDeletion - .getItems() - .get(0) - .getAttachmentEntities() - .get(0) - .getContentId()) - .isNotEmpty(); - } - - @Test - void contentCanBeReadForActiveRoot() throws Exception { - var attachmentContent = "attachment Content"; - var attachmentEntityContent = "attachmentEntity Content"; - var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); - clearServiceHandlerContext(); - - readAndValidateActiveContent(selectedRoot, attachmentContent, attachmentEntityContent); - } - - @Test - void noChangesOnAttachmentsContentStillAvailable() throws Exception { - var attachmentContent = "attachment Content"; - var attachmentEntityContent = "attachmentEntity Content"; - var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var rootUrl = getRootUrl(selectedRoot.getId(), false); - requestHelper.executePatchWithODataResponseAndAssertStatusOk( - rootUrl, "{\"title\":\"some other title\"}"); - - prepareAndActiveDraft(rootUrl); - verifyNoAttachmentEventsCalled(); - - readAndValidateActiveContent(selectedRoot, attachmentContent, attachmentEntityContent); - } - - @Test - void deleteItemAndActivateDraft() throws Exception { - var attachmentContent = "attachment Content"; - var attachmentEntityContent = "attachmentEntity Content"; - var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var itemUrl = getItemUrl(selectedRoot.getItems().get(0), false); - requestHelper.executeDeleteWithMatcher(itemUrl, status().isNoContent()); - verifyNoAttachmentEventsCalled(); - - prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterDelete = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterDelete.getItems()).isEmpty(); - verifyOnlyTwoDeleteEvents( - selectedRoot.getItems().get(0).getAttachments().get(0).getContentId(), - selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getContentId()); - } - - @Test - void deleteItemAndCancelDraft() throws Exception { - var attachmentContent = "attachment Content"; - var attachmentEntityContent = "attachmentEntity Content"; - var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var itemUrl = getItemUrl(selectedRoot.getItems().get(0), false); - requestHelper.executeDeleteWithMatcher(itemUrl, status().isNoContent()); - - cancelDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterDelete = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterDelete.getItems()).isNotEmpty(); - assertThat(selectedRootAfterDelete.getItems().get(0).getAttachments()).isNotEmpty(); - assertThat(selectedRootAfterDelete.getItems().get(0).getAttachments().get(0).getContentId()) - .isNotEmpty(); - assertThat(selectedRootAfterDelete.getItems().get(0).getAttachmentEntities()).isNotEmpty(); - assertThat( - selectedRootAfterDelete.getItems().get(0).getAttachmentEntities().get(0).getContentId()) - .isNotEmpty(); - verifyNoAttachmentEventsCalled(); - } - - @Test - void noEventsForForDeletedRoot() throws Exception { - var selectedRoot = deepCreateAndActivate("attachmentContent", "attachmentEntityContent"); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var rootUrl = getRootUrl(selectedRoot.getId(), true); - requestHelper.executeDeleteWithMatcher(rootUrl, status().isNoContent()); - - var draftPrepareUrl = rootUrl + "/TestDraftService.draftPrepare"; - requestHelper.executePostWithMatcher( - draftPrepareUrl, "{\"SideEffectsQualifier\":\"\"}", status().isNotFound()); - - var select = Select.from(TestDraftService_.DRAFT_ROOTS); - var result = persistenceService.run(select).listOf(DraftRoots.class); - assertThat(result).isEmpty(); - - var attachmentContentId = selectedRoot.getItems().get(0).getAttachments().get(0).getContentId(); - var attachmentEntityContentId = - selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getContentId(); - - verifyOnlyTwoDeleteEvents(attachmentContentId, attachmentEntityContentId); - } - - @Test - void errorInTransactionAfterCreateCallsDelete() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - createNewContentAndValidateEvents(selectedRoot); - - prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterCreate = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterCreate.getItems().get(0).getAttachments()).hasSize(2); - assertThat(selectedRootAfterCreate.getItems().get(0).getAttachmentEntities()).hasSize(2); - verifyNoAttachmentEventsCalled(); - } - - @Test - void errorInTransactionAfterCreateCallsDeleteAndNothingForCancel() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - createNewContentAndValidateEvents(selectedRoot); - - cancelDraft(getRootUrl(selectedRoot.getId(), false)); - var selectedRootAfterCreate = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterCreate.getItems().get(0).getAttachments()).hasSize(1); - assertThat(selectedRootAfterCreate.getItems().get(0).getAttachmentEntities()).hasSize(1); - verifyNoAttachmentEventsCalled(); - } - - @Test - void errorInTransactionAfterUpdateCallsDelete() throws Exception { - var attachmentContent = "testContent attachment"; - var attachmentEntityContent = "testContent attachmentEntity"; - var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); - var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); - - updateContentWithErrorAndValidateEvents(selectedRoot, itemAttachment, itemAttachmentEntity); - - testPersistenceHandler.reset(); - prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); - verifyNothingHasChangedInDraft(selectedRoot, attachmentContent, attachmentEntityContent); - } - - @Test - void errorInTransactionAfterUpdateCallsDeleteEvenIfDraftIsCancelled() throws Exception { - var attachmentContent = "testContent attachment"; - var attachmentEntityContent = "testContent attachmentEntity"; - var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var itemAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); - var itemAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); - - updateContentWithErrorAndValidateEvents(selectedRoot, itemAttachment, itemAttachmentEntity); - - testPersistenceHandler.reset(); - cancelDraft(getRootUrl(selectedRoot.getId(), false)); - verifyNothingHasChangedInDraft(selectedRoot, attachmentContent, attachmentEntityContent); - } - - @Test - void createAndDeleteAttachmentWorks() throws Exception { - var selectedRoot = - deepCreateAndActivate("testContent attachment", "testContent attachmentEntity"); - clearServiceHandlerContext(); - createNewDraftForExistingRoot(selectedRoot.getId()); - - var itemAttachment = selectedRoot.getItems().get(0); - - var newAttachmentContent = "new attachment content"; - createAttachmentWithContent(newAttachmentContent, itemAttachment.getId()); - var newAttachmentEntityContent = "new attachmentEntity content"; - createAttachmentEntityWithContent(newAttachmentEntityContent, itemAttachment); - - var draftRoot = selectStoredRootData(DraftRoots_.CDS_NAME + "_drafts", selectedRoot); - - var existingAttachment = selectedRoot.getItems().get(0).getAttachments().get(0); - var existingAttachmentEntity = selectedRoot.getItems().get(0).getAttachmentEntities().get(0); - - var newAttachment = - draftRoot.getItems().get(0).getAttachments().stream() - .filter(attachment -> !attachment.getId().equals(existingAttachment.getId())) - .findAny() - .orElseThrow(); - var newAttachmentEntity = - draftRoot.getItems().get(0).getAttachmentEntities().stream() - .filter( - attachmentEntity -> - !attachmentEntity.getId().equals(existingAttachmentEntity.getId())) - .findAny() - .orElseThrow(); - - var attachmentDeleteUrl = - getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), newAttachment.getId(), false); - var attachmentEntityDeleteUrl = getAttachmentEntityBaseUrl(newAttachmentEntity.getId(), false); - - requestHelper.executeDeleteWithMatcher(attachmentDeleteUrl, status().isNoContent()); - requestHelper.executeDeleteWithMatcher(attachmentEntityDeleteUrl, status().isNoContent()); - - verifyTwoCreateAndDeleteEvents(newAttachmentContent, newAttachmentEntityContent); - clearServiceHandlerContext(); - - prepareAndActiveDraft(getRootUrl(selectedRoot.getId(), false)); - verifyNoAttachmentEventsCalled(); - } - - protected DraftRoots deepCreateAndActivate( - String testContentAttachment, String testContentAttachmentEntity) throws Exception { - var responseRoot = createNewDraft(); - var rootUrl = updateRoot(responseRoot); - var responseItem = createItem(rootUrl); - createAttachmentWithContent(testContentAttachment, responseItem.getId()); - createAttachmentEntityWithContent(testContentAttachmentEntity, responseItem); - prepareAndActiveDraft(rootUrl); - - return selectStoredRootData(responseRoot); - } - - private DraftRoots createNewDraft() throws Exception { - var responseRootCdsData = - requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); - return Struct.access(responseRootCdsData).as(DraftRoots.class); - } - - private void createNewDraftForExistingRoot(String rootId) throws Exception { - var url = getRootUrl(rootId, true) + "/TestDraftService.draftEdit"; - requestHelper.executePostWithODataResponseAndAssertStatus( - url, "{\"PreserveChanges\":true}", HttpStatus.OK); - } - - private String updateRoot(DraftRoots responseRoot) throws Exception { - responseRoot.setTitle("some title"); - var rootUrl = getRootUrl(responseRoot.getId(), false); - requestHelper.executePatchWithODataResponseAndAssertStatusOk(rootUrl, responseRoot.toJson()); - return rootUrl; - } - - private String getRootUrl(String rootId, boolean isActiveEntity) { - return BASE_ROOT_URL + "(ID=" + rootId + ",IsActiveEntity=" + isActiveEntity + ")"; - } - - private Items createItem(String rootUrl) throws Exception { - var item = Items.create(); - item.setTitle("some item"); - var itemUrl = rootUrl + "/items"; - var responseItemCdsData = - requestHelper.executePostWithODataResponseAndAssertStatusCreated(itemUrl, item.toJson()); - return Struct.access(responseItemCdsData).as(Items.class); - } - - private void createAttachmentWithContent(String testContentAttachment, String itemId) - throws Exception { - createAttachmentWithContent(testContentAttachment, itemId, status().isNoContent(), false); - } - - private void createAttachmentWithContent( - String testContentAttachment, String itemId, ResultMatcher matcher, boolean withError) - throws Exception { - var responseAttachment = createAttachment(itemId); - if (withError) { - testPersistenceHandler.setThrowExceptionOnUpdate(true); - } - putNewContentForAttachment(testContentAttachment, itemId, responseAttachment.getId(), matcher); - } - - private void putNewContentForAttachment( - String testContentAttachment, String itemId, String attachmentId) throws Exception { - putNewContentForAttachment(testContentAttachment, itemId, attachmentId, status().isNoContent()); - } - - private void putNewContentForAttachment( - String testContentAttachment, String itemId, String attachmentId, ResultMatcher matcher) - throws Exception { - var attachmentPutUrl = getAttachmentBaseUrl(itemId, attachmentId, false) + "/content"; - requestHelper.setContentType("text/plain"); - requestHelper.executePutWithMatcher( - attachmentPutUrl, testContentAttachment.getBytes(StandardCharsets.UTF_8), matcher); - requestHelper.resetHelper(); - } - - private Attachments createAttachment(String itemId) throws Exception { - var itemAttachment = Attachments.create(); - itemAttachment.setFileName("itemAttachment.txt"); - - var attachmentPostUrl = BASE_URL + "Items(ID=" + itemId + ",IsActiveEntity=false)/attachments"; - var responseAttachmentCdsData = - requestHelper.executePostWithODataResponseAndAssertStatusCreated( - attachmentPostUrl, itemAttachment.toJson()); - return Struct.access(responseAttachmentCdsData).as(Attachments.class); - } - - private void createAttachmentEntityWithContent( - String testContentAttachmentEntity, Items responseItem) throws Exception { - createAttachmentEntityWithContent( - testContentAttachmentEntity, responseItem, status().isNoContent(), false); - } - - private void createAttachmentEntityWithContent( - String testContentAttachmentEntity, - Items responseItem, - ResultMatcher matcher, - boolean withError) - throws Exception { - var responseAttachmentEntity = createAttachmentEntity(responseItem); - if (withError) { - testPersistenceHandler.setThrowExceptionOnUpdate(true); - } - putNewContentForAttachmentEntity( - testContentAttachmentEntity, responseAttachmentEntity.getId(), matcher); - } - - private void putNewContentForAttachmentEntity( - String testContentAttachmentEntity, String attachmentId) throws Exception { - putNewContentForAttachmentEntity( - testContentAttachmentEntity, attachmentId, status().isNoContent()); - } - - private void putNewContentForAttachmentEntity( - String testContentAttachmentEntity, String attachmentId, ResultMatcher matcher) - throws Exception { - var attachmentEntityPutUrl = - BASE_URL + "/AttachmentEntity(ID=" + attachmentId + ",IsActiveEntity=false)/content"; - requestHelper.setContentType("image/jpeg"); - requestHelper.executePutWithMatcher( - attachmentEntityPutUrl, - testContentAttachmentEntity.getBytes(StandardCharsets.UTF_8), - matcher); - requestHelper.resetHelper(); - } - - private AttachmentEntity createAttachmentEntity(Items responseItem) throws Exception { - var itemAttachmentEntity = AttachmentEntity.create(); - itemAttachmentEntity.setFileName("itemAttachmentEntity.txt"); - - var attachmentEntityPostUrl = getItemUrl(responseItem, false) + "/attachmentEntities"; - var responseAttachmentEntityCdsData = - requestHelper.executePostWithODataResponseAndAssertStatusCreated( - attachmentEntityPostUrl, itemAttachmentEntity.toJson()); - return Struct.access(responseAttachmentEntityCdsData).as(AttachmentEntity.class); - } - - private String getItemUrl(Items responseItem, boolean isActiveEntity) { - return BASE_URL - + "Items(ID=" - + responseItem.getId() - + ",IsActiveEntity=" - + isActiveEntity - + ")"; - } - - protected String getAttachmentBaseUrl( - String itemId, String attachmentId, boolean isActiveEntity) { - return BASE_URL - + "Items_attachments(up__ID=" - + itemId - + ",ID=" - + attachmentId - + ",IsActiveEntity=" - + isActiveEntity - + ")"; - } - - protected String getAttachmentEntityBaseUrl(String attachmentId, boolean isActiveEntity) { - return BASE_URL - + "AttachmentEntity(ID=" - + attachmentId - + ",IsActiveEntity=" - + isActiveEntity - + ")"; - } - - private void prepareAndActiveDraft(String rootUrl) throws Exception { - var draftPrepareUrl = rootUrl + "/TestDraftService.draftPrepare"; - var draftActivateUrl = rootUrl + "/TestDraftService.draftActivate"; - requestHelper.executePostWithMatcher( - draftPrepareUrl, "{\"SideEffectsQualifier\":\"\"}", status().isOk()); - requestHelper.executePostWithMatcher(draftActivateUrl, "{}", status().isOk()); - } - - private void cancelDraft(String rootUrl) throws Exception { - requestHelper.executeDeleteWithMatcher(rootUrl, status().isNoContent()); - } - - private DraftRoots selectStoredRootData(DraftRoots responseRoot) { - return selectStoredRootData(DraftRoots_.CDS_NAME, responseRoot); - } - - private DraftRoots selectStoredRootData(String entityName, DraftRoots responseRoot) { - var select = - Select.from(entityName) - .where(root -> root.get(DraftRoots.ID).eq(responseRoot.getId())) - .columns( - StructuredType::_all, - root -> - root.to(DraftRoots.ITEMS) - .expand( - StructuredType::_all, - item -> item.to(Items.ATTACHMENTS).expand(), - item -> item.to(Items.ATTACHMENT_ENTITIES).expand())); - return persistenceService.run(select).single(DraftRoots.class); - } - - protected void readAndValidateActiveContent( - DraftRoots selectedRoot, String attachmentContent, String attachmentEntityContent) - throws Exception { - var attachmentUrl = - getAttachmentBaseUrl( - selectedRoot.getItems().get(0).getId(), - selectedRoot.getItems().get(0).getAttachments().get(0).getId(), - true) - + "/content"; - var attachmentEntityUrl = - getAttachmentEntityBaseUrl( - selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getId(), true) - + "/content"; - - Awaitility.await() - .atMost(60, TimeUnit.SECONDS) - .pollDelay(1, TimeUnit.SECONDS) - .pollInterval(2, TimeUnit.SECONDS) - .until( - () -> { - var attachmentResponse = requestHelper.executeGet(attachmentUrl); - var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); - var attachmentContentAsString = attachmentResponse.getResponse().getContentAsString(); - var attachmentEntityContentAsString = - attachmentEntityResponse.getResponse().getContentAsString(); - - var booleanResult = - attachmentContentAsString.equals(attachmentContent) - && attachmentEntityContentAsString.equals(attachmentEntityContent); - - if (!booleanResult) { - logger.info( - "Attachment response content: {}, Attachment Test Content: {}, Attachment Entity response content: {}, Attachment Entity Test Content: {}", - attachmentContentAsString, - attachmentContent, - attachmentEntityContentAsString, - attachmentEntityContent); - } - return booleanResult; - }); - clearServiceHandlerContext(); - - var attachmentResponse = requestHelper.executeGet(attachmentUrl); - var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); - - assertThat(attachmentResponse.getResponse().getContentAsString()).isEqualTo(attachmentContent); - assertThat(attachmentEntityResponse.getResponse().getContentAsString()) - .isEqualTo(attachmentEntityContent); - verifyTwoReadEvents(); - } - - private void deleteContent( - DraftRoots selectedRoot, Attachments itemAttachment, AttachmentEntity itemAttachmentEntity) - throws Exception { - var attachmentUrl = - getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), false) - + "/content"; - var attachmentEntityUrl = - getAttachmentEntityBaseUrl(itemAttachmentEntity.getId(), false) + "/content"; - - requestHelper.executeDeleteWithMatcher(attachmentUrl, status().isNoContent()); - requestHelper.executeDeleteWithMatcher(attachmentEntityUrl, status().isNoContent()); - } - - private void updateFileName( - DraftRoots selectedRoot, - Attachments itemAttachment, - AttachmentEntity itemAttachmentEntity, - String changedAttachmentFileName, - String changedAttachmentEntityFileName) - throws Exception { - updateFileName( - selectedRoot, - itemAttachment, - itemAttachmentEntity, - changedAttachmentFileName, - changedAttachmentEntityFileName, - HttpStatus.OK); - } - - private void updateFileName( - DraftRoots selectedRoot, - Attachments itemAttachment, - AttachmentEntity itemAttachmentEntity, - String changedAttachmentFileName, - String changedAttachmentEntityFileName, - HttpStatus httpStatus) - throws Exception { - var attachmentUrl = - getAttachmentBaseUrl(selectedRoot.getItems().get(0).getId(), itemAttachment.getId(), false); - var attachmentEntityUrl = getAttachmentEntityBaseUrl(itemAttachmentEntity.getId(), false); - - requestHelper.executePatchWithODataResponseAndAssertStatus( - attachmentUrl, "{\"fileName\":\"" + changedAttachmentFileName + "\"}", httpStatus); - requestHelper.executePatchWithODataResponseAndAssertStatus( - attachmentEntityUrl, - "{\"fileName\":\"" + changedAttachmentEntityFileName + "\"}", - httpStatus); - } - - private void updateContentWithErrorAndValidateEvents( - DraftRoots selectedRoot, Attachments itemAttachment, AttachmentEntity itemAttachmentEntity) - throws Exception { - testPersistenceHandler.setThrowExceptionOnUpdate(true); - var newAttachmentContent = "new content attachment"; - putNewContentForAttachment( - newAttachmentContent, - selectedRoot.getItems().get(0).getId(), - itemAttachment.getId(), - status().is5xxServerError()); - var newAttachmentEntityContent = "new content attachmentEntity"; - putNewContentForAttachmentEntity( - newAttachmentEntityContent, itemAttachmentEntity.getId(), status().is5xxServerError()); - verifyTwoCreateAndRevertedDeleteEvents(); - clearServiceHandlerContext(); - } - - private void verifyNothingHasChangedInDraft( - DraftRoots selectedRoot, String attachmentContent, String attachmentEntityContent) - throws IOException { - var selectedRootAfterUpdate = selectStoredRootData(selectedRoot); - - verifyContent( - selectedRootAfterUpdate.getItems().get(0).getAttachments().get(0).getContent(), - attachmentContent); - verifyContent( - selectedRootAfterUpdate.getItems().get(0).getAttachmentEntities().get(0).getContent(), - attachmentEntityContent); - verifyNoAttachmentEventsCalled(); - var selectedRootAfterDeletion = selectStoredRootData(selectedRoot); - assertThat(selectedRootAfterDeletion.getItems().get(0).getAttachments().get(0).getContentId()) - .isNotEmpty(); - assertThat( - selectedRootAfterDeletion - .getItems() - .get(0) - .getAttachmentEntities() - .get(0) - .getContentId()) - .isNotEmpty(); - } - - private void createNewContentAndValidateEvents(DraftRoots selectedRoot) throws Exception { - var itemAttachment = selectedRoot.getItems().get(0); - var newAttachmentContent = "new attachment content"; - createAttachmentWithContent( - newAttachmentContent, itemAttachment.getId(), status().is5xxServerError(), true); - testPersistenceHandler.reset(); - var newAttachmentEntityContent = "new attachmentEntity content"; - createAttachmentEntityWithContent( - newAttachmentEntityContent, itemAttachment, status().is5xxServerError(), true); - verifyTwoCreateAndDeleteEvents(newAttachmentContent, newAttachmentEntityContent); - clearServiceHandlerContext(); - testPersistenceHandler.reset(); - } - - private String getResponseContent(MvcResult attachmentResponse) - throws UnsupportedEncodingException { - return attachmentResponse.getResponse().getStatus() == HttpStatus.OK.value() - ? attachmentResponse.getResponse().getContentAsString() - : ""; - } - - protected abstract void verifyContentId(String contentId, String attachmentId); - - protected abstract void verifyContent(InputStream attachment, String testContent) - throws IOException; - - protected abstract void verifyNoAttachmentEventsCalled(); - - protected abstract void clearServiceHandlerContext(); - - protected abstract void verifyEventContextEmptyForEvent(String... events); - - protected abstract void verifyOnlyTwoCreateEvents( - String newAttachmentContent, String newAttachmentEntityContent); - - protected abstract void verifyTwoCreateAndDeleteEvents( - String newAttachmentContent, String newAttachmentEntityContent); - - protected abstract void verifyTwoReadEvents(); - - protected abstract void verifyOnlyTwoDeleteEvents( - String attachmentContentId, String attachmentEntityContentId); - - protected abstract void verifyTwoUpdateEvents( - String newAttachmentContent, - String attachmentContentId, - String newAttachmentEntityContent, - String attachmentEntityContentId); - - protected abstract void verifyTwoCreateAndRevertedDeleteEvents(); -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java deleted file mode 100644 index 4ddb6280a..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithTestHandlerTest.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.draftservice; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import com.sap.cds.feature.attachments.integrationtests.testhandler.EventContextHolder; -import com.sap.cds.feature.attachments.service.AttachmentService; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.TimeUnit; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.test.context.ActiveProfiles; - -@ActiveProfiles(Profiles.TEST_HANDLER_ENABLED) -class DraftOdataRequestValidationWithTestHandlerTest extends DraftOdataRequestValidationBase { - - private static final Logger logger = - LoggerFactory.getLogger(DraftOdataRequestValidationWithTestHandlerTest.class); - - @Test - void serviceHandlerIsNotEmpty() { - assertThat(serviceHandler).isNotNull(); - verifyNoAttachmentEventsCalled(); - } - - @Override - protected void verifyContentId(String contentId, String attachmentId) { - assertThat(contentId).isNotEmpty().isNotEqualTo(attachmentId); - } - - @Override - protected void verifyContent(InputStream attachment, String testContent) { - assertThat(attachment).isNull(); - } - - @Override - protected void verifyNoAttachmentEventsCalled() { - assertThat(serviceHandler.getEventContext()).isEmpty(); - } - - @Override - protected void clearServiceHandlerContext() { - serviceHandler.clearEventContext(); - } - - @Override - protected void verifyEventContextEmptyForEvent(String... events) { - Arrays.stream(events) - .forEach(event -> assertThat(serviceHandler.getEventContextForEvent(event)).isEmpty()); - } - - @Override - protected void verifyOnlyTwoCreateEvents( - String newAttachmentContent, String newAttachmentEntityContent) { - verifyEventContextEmptyForEvent( - AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED, - AttachmentService.EVENT_READ_ATTACHMENT); - var createEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); - assertThat(createEvents).hasSize(2); - var attachmentContentFound = - isAttachmentContentFoundInCreateEvent(createEvents, newAttachmentContent); - assertThat(attachmentContentFound).isTrue(); - var attachmentEntityContentFound = - isAttachmentContentFoundInCreateEvent(createEvents, newAttachmentEntityContent); - assertThat(attachmentEntityContentFound).isTrue(); - } - - @Override - protected void verifyTwoCreateAndDeleteEvents( - String newAttachmentContent, String newAttachmentEntityContent) { - awaitNumberOfExpectedEvents(4); - verifyEventContextEmptyForEvent(AttachmentService.EVENT_READ_ATTACHMENT); - var createEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); - assertThat(createEvents).hasSize(2); - var attachmentContentFound = - isAttachmentContentFoundInCreateEvent(createEvents, newAttachmentContent); - assertThat(attachmentContentFound).isTrue(); - var attachmentEntityContentFound = - isAttachmentContentFoundInCreateEvent(createEvents, newAttachmentEntityContent); - assertThat(attachmentEntityContentFound).isTrue(); - var deleteEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - assertThat(deleteEvents).hasSize(2); - deleteEvents.forEach( - event -> { - var deleteContext = (AttachmentMarkAsDeletedEventContext) event.context(); - assertThat(deleteContext.getContentId()).isNotEmpty(); - var createEventFound = - createEvents.stream() - .anyMatch( - createEvent -> { - var createContext = (AttachmentCreateEventContext) createEvent.context(); - return createContext.getContentId().equals(deleteContext.getContentId()); - }); - assertThat(createEventFound).isTrue(); - }); - } - - @Override - protected void verifyTwoReadEvents() { - verifyEventContextEmptyForEvent( - AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED, - AttachmentService.EVENT_CREATE_ATTACHMENT); - var readEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_READ_ATTACHMENT); - assertThat(readEvents).hasSize(2); - } - - @Override - protected void verifyOnlyTwoDeleteEvents( - String attachmentContentId, String attachmentEntityContentId) { - awaitNumberOfExpectedEvents(2); - verifyEventContextEmptyForEvent( - AttachmentService.EVENT_CREATE_ATTACHMENT, AttachmentService.EVENT_READ_ATTACHMENT); - var deleteEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - assertThat(deleteEvents).hasSize(2); - verifyDeleteEventContainsContentId(deleteEvents, attachmentContentId); - verifyDeleteEventContainsContentId(deleteEvents, attachmentEntityContentId); - } - - @Override - protected void verifyTwoUpdateEvents( - String newAttachmentContent, - String attachmentContentId, - String newAttachmentEntityContent, - String attachmentEntityContentId) { - awaitNumberOfExpectedEvents(4); - var createEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); - var deleteEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - assertThat(createEvents).hasSize(2); - verifyCreateEventFound(createEvents, newAttachmentContent); - verifyCreateEventFound(createEvents, newAttachmentEntityContent); - assertThat(deleteEvents).hasSize(2); - verifyDeleteEventContainsContentId(deleteEvents, attachmentContentId); - verifyDeleteEventContainsContentId(deleteEvents, attachmentEntityContentId); - } - - @Override - protected void verifyTwoCreateAndRevertedDeleteEvents() { - awaitNumberOfExpectedEvents(4); - var createEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); - var deleteEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - assertThat(createEvents).hasSize(2); - assertThat(deleteEvents).hasSize(2); - deleteEvents.forEach( - event -> { - var deleteContext = (AttachmentMarkAsDeletedEventContext) event.context(); - var createEventFound = - createEvents.stream() - .anyMatch( - createEvent -> { - var createContext = (AttachmentCreateEventContext) createEvent.context(); - return createContext.getContentId().equals(deleteContext.getContentId()); - }); - assertThat(createEventFound).isTrue(); - }); - } - - private void awaitNumberOfExpectedEvents(int expectedEvents) { - Awaitility.await() - .atMost(60, TimeUnit.SECONDS) - .pollDelay(1, TimeUnit.SECONDS) - .pollInterval(2, TimeUnit.SECONDS) - .until( - () -> { - var eventCalls = serviceHandler.getEventContext().size(); - logger.info( - "Waiting for expected size '{}' in handler context, was '{}'", - expectedEvents, - eventCalls); - var numberMatch = eventCalls >= expectedEvents; - if (!numberMatch) { - serviceHandler.getEventContext().forEach(event -> logger.info("Event: {}", event)); - } - return numberMatch; - }); - } - - private void verifyCreateEventFound(List createEvents, String newContent) { - var eventContentFound = - createEvents.stream() - .anyMatch( - event -> { - var createContext = (AttachmentCreateEventContext) event.context(); - try { - return Arrays.equals( - createContext.getData().getContent().readAllBytes(), - newContent.getBytes(StandardCharsets.UTF_8)); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - assertThat(eventContentFound).isTrue(); - } - - private boolean isAttachmentContentFoundInCreateEvent( - List createEvents, String newAttachmentContent) { - return createEvents.stream() - .anyMatch( - event -> { - var createContext = (AttachmentCreateEventContext) event.context(); - try { - return Arrays.equals( - createContext.getData().getContent().readAllBytes(), - newAttachmentContent.getBytes(StandardCharsets.UTF_8)); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } - - private void verifyDeleteEventContainsContentId( - List deleteEvents, String contentId) { - var eventFound = - deleteEvents.stream() - .anyMatch( - event -> { - var deleteContext = (AttachmentMarkAsDeletedEventContext) event.context(); - return deleteContext.getContentId().equals(contentId); - }); - assertThat(eventFound).isTrue(); - } - - // Override flaky tests from base class to disable them. - // These tests are affected by a race condition in the CAP runtime's outbox TaskScheduler - // where the second DELETE event is not processed when two transactions fail in quick succession. - - @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") - @Test - @Override - void errorInTransactionAfterCreateCallsDelete() throws Exception { - super.errorInTransactionAfterCreateCallsDelete(); - } - - @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") - @Test - @Override - void errorInTransactionAfterCreateCallsDeleteAndNothingForCancel() throws Exception { - super.errorInTransactionAfterCreateCallsDeleteAndNothingForCancel(); - } - - @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") - @Test - @Override - void errorInTransactionAfterUpdateCallsDelete() throws Exception { - super.errorInTransactionAfterUpdateCallsDelete(); - } - - @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") - @Test - @Override - void errorInTransactionAfterUpdateCallsDeleteEvenIfDraftIsCancelled() throws Exception { - super.errorInTransactionAfterUpdateCallsDeleteEvenIfDraftIsCancelled(); - } - - @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") - @Test - @Override - void createAttachmentAndCancelDraft() throws Exception { - super.createAttachmentAndCancelDraft(); - } - - @Disabled("Flaky due to CAP runtime outbox race condition - second DELETE event not processed") - @Test - @Override - void createAndDeleteAttachmentWorks() throws Exception { - super.createAndDeleteAttachmentWorks(); - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java deleted file mode 100644 index 817700d8a..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * © 2024-2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.draftservice; - -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.post; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static org.assertj.core.api.Assertions.assertThat; - -import com.github.tomakehurst.wiremock.WireMockServer; -import com.sap.cds.feature.attachments.integrationtests.common.MalwareScanResultProvider; -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import java.util.concurrent.TimeUnit; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.ActiveProfiles; -import org.wiremock.spring.ConfigureWireMock; -import org.wiremock.spring.EnableWireMock; -import org.wiremock.spring.InjectWireMock; - -@ActiveProfiles(profiles = {Profiles.TEST_HANDLER_DISABLED, Profiles.MALWARE_SCAN_ENABLED}) -@EnableWireMock({@ConfigureWireMock(port = 1111, name = "malware-scanner")}) -class DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest - extends DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest { - - private static final Logger logger = - LoggerFactory.getLogger( - DraftOdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.class); - - @InjectWireMock("malware-scanner") - private WireMockServer wiremock; - - @Autowired private MalwareScanResultProvider malwareScanResultProvider; - - @BeforeEach - void setup() { - mockMalwareScanResult(false); - } - - @Override - @AfterEach - void teardown() { - super.teardown(); - wiremock.resetAll(); - } - - @Test - void contentCanNotBeReadForActiveRoot() throws Exception { - wiremock.resetAll(); - mockMalwareScanResult(true); - var attachmentContent = "attachment Content"; - var attachmentEntityContent = "attachmentEntity Content"; - var selectedRoot = deepCreateAndActivate(attachmentContent, attachmentEntityContent); - clearServiceHandlerContext(); - - var attachmentUrl = - getAttachmentBaseUrl( - selectedRoot.getItems().get(0).getId(), - selectedRoot.getItems().get(0).getAttachments().get(0).getId(), - true) - + "/content"; - var attachmentEntityUrl = - getAttachmentEntityBaseUrl( - selectedRoot.getItems().get(0).getAttachmentEntities().get(0).getId(), true) - + "/content"; - - Awaitility.await() - .atMost(60, TimeUnit.SECONDS) - .pollDelay(1, TimeUnit.SECONDS) - .until( - () -> { - var attachmentResponse = requestHelper.executeGet(attachmentUrl); - var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); - - var attachmentResponseContent = attachmentResponse.getResponse().getContentAsString(); - var attachmentEntityResponseContent = - attachmentEntityResponse.getResponse().getContentAsString(); - - logger.info( - "Status should contain 'not_clean' for attachment and attachment entity but was: {} for attachment and {} for attachment entity", - attachmentResponseContent, - attachmentEntityResponseContent); - - return attachmentResponseContent.contains("not_clean") - && attachmentEntityResponseContent.contains("not_clean"); - }); - clearServiceHandlerContext(); - - var attachmentResponse = requestHelper.executeGet(attachmentUrl); - var attachmentEntityResponse = requestHelper.executeGet(attachmentEntityUrl); - - assertThat(attachmentResponse.getResponse().getContentAsString()) - .contains("Error text for not clean"); - assertThat(attachmentEntityResponse.getResponse().getContentAsString()) - .contains("Error text for not clean"); - verifyTwoReadEvents(); - } - - private void mockMalwareScanResult(boolean malware) { - wiremock.stubFor( - post(urlEqualTo("/scan")) - .withBasicAuth("test-user", "test-password") - .willReturn( - aResponse() - .withBody(malwareScanResultProvider.buildMalwareScanResult(malware)) - .withStatus(200))); - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java deleted file mode 100644 index 7dce33a89..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.draftservice; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.Objects; -import org.junit.jupiter.api.Test; -import org.springframework.test.context.ActiveProfiles; - -@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) -class DraftOdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest - extends DraftOdataRequestValidationBase { - - @Test - void serviceHandlerIsNull() { - assertThat(serviceHandler).isNull(); - } - - @Override - protected void verifyContentId(String contentId, String attachmentId) { - assertThat(contentId).isEqualTo(attachmentId); - } - - @Override - protected void verifyContent(InputStream attachment, String testContent) throws IOException { - if (Objects.nonNull(testContent)) { - assertThat(attachment.readAllBytes()).isEqualTo(testContent.getBytes(StandardCharsets.UTF_8)); - } else { - assertThat(attachment).isNull(); - } - } - - @Override - protected void verifyNoAttachmentEventsCalled() { - // no service handler - nothing to do - } - - @Override - protected void clearServiceHandlerContext() { - // no service handler - nothing to do - } - - @Override - protected void verifyEventContextEmptyForEvent(String... events) { - // no service handler - nothing to do - } - - @Override - protected void verifyOnlyTwoCreateEvents( - String newAttachmentContent, String newAttachmentEntityContent) { - // no service handler - nothing to do - } - - @Override - protected void verifyTwoCreateAndDeleteEvents( - String newAttachmentContent, String newAttachmentEntityContent) { - // no service handler - nothing to do - } - - @Override - protected void verifyTwoReadEvents() { - // no service handler - nothing to do - } - - @Override - protected void verifyOnlyTwoDeleteEvents( - String attachmentContentId, String attachmentEntityContentId) { - // no service handler - nothing to do - } - - @Override - protected void verifyTwoUpdateEvents( - String newAttachmentContent, - String attachmentContentId, - String newAttachmentEntityContent, - String attachmentEntityContentId) { - // no service handler - nothing to do - } - - @Override - protected void verifyTwoCreateAndRevertedDeleteEvents() { - // no service handler - nothing to do - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java deleted file mode 100644 index f1ddbb49d..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/MediaValidatedAttachmentsDraftTest.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.draftservice; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.sap.cds.CdsData; -import com.sap.cds.Struct; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.DraftRoots; -import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import java.io.IOException; -import java.io.InputStream; -import java.util.Map; -import java.util.Objects; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.springframework.http.MediaType; -import org.springframework.test.context.ActiveProfiles; - -@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) -public class MediaValidatedAttachmentsDraftTest extends DraftOdataRequestValidationBase { - - private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestDraftService/"; - private static final String BASE_ROOT_URL = BASE_URL + "DraftRoots"; - private static final ObjectMapper objectMapper = new ObjectMapper(); - - @BeforeEach - void setup() { - requestHelper.setContentType(MediaType.APPLICATION_JSON); - } - - @ParameterizedTest - @CsvSource({ - "test.png,201", - "test.jpeg,201", - "test.pdf,415", - "test.txt,415", - "'',400", - "' ',400", - ".gitignore,415", - ".env,415", - ".hiddenfile,415" - }) - void shouldValidateMediaType_whenCreatingAttachmentInDraft(String fileName, int expectedStatus) - throws Exception { - String rootId = createDraftRootAndReturnId(); - String metadata = objectMapper.writeValueAsString(Map.of("fileName", fileName)); - - requestHelper.executePostWithMatcher( - buildDraftAttachmentCreationUrl(rootId), metadata, status().is(expectedStatus)); - } - - private String buildDraftAttachmentCreationUrl(String rootId) { - return BASE_ROOT_URL - + "(ID=" - + rootId - + ",IsActiveEntity=false)" - + "/mediaValidatedAttachments"; - } - - @Test - void shouldPass_whenFileNameMissing_inDraft() throws Exception { - String rootId = createDraftRootAndReturnId(); - String metadata = "{}"; - requestHelper.executePostWithMatcher( - buildDraftAttachmentCreationUrl(rootId), metadata, status().isCreated()); - } - - // Helper methods - private String createDraftRootAndReturnId() throws Exception { - CdsData response = - requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); - - DraftRoots draftRoot = Struct.access(response).as(DraftRoots.class); - String payload = objectMapper.writeValueAsString(Map.of("title", "Draft")); - requestHelper.executePatchWithODataResponseAndAssertStatusOk( - getRootUrl(draftRoot.getId(), false), payload); - - return draftRoot.getId(); - } - - private String getRootUrl(String rootId, boolean isActiveEntity) { - return BASE_ROOT_URL + "(ID=" + rootId + ",IsActiveEntity=" + isActiveEntity + ")"; - } - - // Required abstract method implementations - @Override - protected void verifyContentId(String contentId, String attachmentId) { - assertThat(contentId).isEqualTo(attachmentId); - } - - @Override - protected void verifyContent(InputStream attachment, String testContent) throws IOException { - if (Objects.nonNull(testContent)) { - assertThat(attachment.readAllBytes()) - .isEqualTo(testContent.getBytes(java.nio.charset.StandardCharsets.UTF_8)); - } else { - assertThat(attachment).isNull(); - } - } - - @Override - protected void verifyNoAttachmentEventsCalled() { - // Implementation not required for this test - } - - @Override - protected void clearServiceHandlerContext() { - // Implementation not required for this test - } - - @Override - protected void verifyEventContextEmptyForEvent(String... events) { - // Implementation not required for this test - } - - @Override - protected void verifyOnlyTwoCreateEvents( - String newAttachmentContent, String newAttachmentEntityContent) { - // Implementation not required for this test - } - - @Override - protected void verifyTwoCreateAndDeleteEvents( - String newAttachmentContent, String newAttachmentEntityContent) { - // Implementation not required for this test - } - - @Override - protected void verifyTwoReadEvents() { - // Implementation not required for this test - } - - @Override - protected void verifyOnlyTwoDeleteEvents( - String attachmentContentId, String attachmentEntityContentId) { - // Implementation not required for this test - } - - @Override - protected void verifyTwoUpdateEvents( - String newAttachmentContent, - String attachmentContentId, - String newAttachmentEntityContent, - String attachmentEntityContentId) { - // Implementation not required for this test - } - - @Override - protected void verifyTwoCreateAndRevertedDeleteEvents() { - // Implementation not required for this test - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java deleted file mode 100644 index df8db48a9..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/draftservice/SizeLimitedAttachmentsSizeValidationDraftTest.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.draftservice; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.sap.cds.Struct; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testdraftservice.DraftRoots; -import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import java.util.Objects; -import org.junit.jupiter.api.Test; -import org.springframework.test.context.ActiveProfiles; - -@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) -class SizeLimitedAttachmentsSizeValidationDraftTest extends DraftOdataRequestValidationBase { - - private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestDraftService/"; - private static final String BASE_ROOT_URL = BASE_URL + "DraftRoots"; - - @Test - void uploadContentWithin5MBLimitSucceeds() throws Exception { - // Arrange: Create draft with sizeLimitedAttachments - var draftRoot = createNewDraftWithSizeLimitedAttachments(); - var attachment = draftRoot.getSizeLimitedAttachments().get(0); - attachment.setFileName("test.txt"); - // Act & Assert: Upload 3MB content (within limit) succeeds - byte[] content = new byte[3 * 1024 * 1024]; // 3MB - var url = buildDraftSizeLimitedAttachmentContentUrl(draftRoot.getId(), attachment.getId()); - requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); - requestHelper.executePutWithMatcher(url, content, status().isNoContent()); - } - - @Test - void uploadContentExceeding5MBLimitFails() throws Exception { - // Arrange: Create draft with sizeLimitedAttachments - var draftRoot = createNewDraftWithSizeLimitedAttachments(); - var attachment = draftRoot.getSizeLimitedAttachments().get(0); - attachment.setFileName("test.txt"); - // Act: Try to upload 6MB content (exceeds limit) - byte[] content = new byte[6 * 1024 * 1024]; // 6MB - var url = buildDraftSizeLimitedAttachmentContentUrl(draftRoot.getId(), attachment.getId()); - requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); - requestHelper.executePutWithMatcher(url, content, status().is(413)); - - // Assert: Error response with HTTP 413 status code indicates size limit - // exceeded - } - - @Test - void uploadContentWithinLimitAndActivateDraftSucceeds() throws Exception { - // Arrange: Create draft with sizeLimitedAttachments (no prior activation) - var draftRoot = createNewDraftWithSizeLimitedAttachments(); - var attachment = draftRoot.getSizeLimitedAttachments().get(0); - - // Act: Upload 3MB content (within 5MB limit) - byte[] content = new byte[3 * 1024 * 1024]; // 3MB - var url = buildDraftSizeLimitedAttachmentContentUrl(draftRoot.getId(), attachment.getId()); - requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); - requestHelper.executePutWithMatcher(url, content, status().isNoContent()); - - // Assert: Draft activation succeeds - requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_JSON); - var rootUrl = getRootUrl(draftRoot.getId(), false); - var draftPrepareUrl = rootUrl + "/TestDraftService.draftPrepare"; - var draftActivateUrl = rootUrl + "/TestDraftService.draftActivate"; - requestHelper.executePostWithMatcher( - draftPrepareUrl, "{\"SideEffectsQualifier\":\"\"}", status().isOk()); - requestHelper.executePostWithMatcher(draftActivateUrl, "{}", status().isOk()); - } - - @Test - void uploadContentExceedingLimitOnFirstDraftRejects() throws Exception { - // Arrange: Create draft with sizeLimitedAttachments (no prior activation) - var draftRoot = createNewDraftWithSizeLimitedAttachments(); - var attachment = draftRoot.getSizeLimitedAttachments().get(0); - - // Act & Assert: Upload 6MB content to a brand-new draft attachment fails immediately - byte[] content = new byte[6 * 1024 * 1024]; // 6MB - var url = buildDraftSizeLimitedAttachmentContentUrl(draftRoot.getId(), attachment.getId()); - requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); - requestHelper.executePutWithMatcher(url, content, status().is(413)); - } - - // Helper methods - private DraftRoots createNewDraftWithSizeLimitedAttachments() throws Exception { - // Create new draft - var responseRootCdsData = - requestHelper.executePostWithODataResponseAndAssertStatusCreated(BASE_ROOT_URL, "{}"); - var draftRoot = Struct.access(responseRootCdsData).as(DraftRoots.class); - - // Update root with title - draftRoot.setTitle("Root with sizeLimitedAttachments"); - var rootUrl = getRootUrl(draftRoot.getId(), false); - requestHelper.executePatchWithODataResponseAndAssertStatusOk(rootUrl, draftRoot.toJson()); - - // Create sizeLimitedAttachment - var attachment = Attachments.create(); - attachment.setFileName("testFile.txt"); - attachment.setMimeType("text/plain"); - var attachmentUrl = rootUrl + "/sizeLimitedAttachments"; - var responseAttachmentCdsData = - requestHelper.executePostWithODataResponseAndAssertStatusCreated( - attachmentUrl, attachment.toJson()); - var createdAttachment = Struct.access(responseAttachmentCdsData).as(Attachments.class); - - // Build result with the attachment - draftRoot.setSizeLimitedAttachments(List.of(createdAttachment)); - return draftRoot; - } - - private String getRootUrl(String rootId, boolean isActiveEntity) { - return BASE_ROOT_URL + "(ID=" + rootId + ",IsActiveEntity=" + isActiveEntity + ")"; - } - - private String buildDraftSizeLimitedAttachmentContentUrl(String rootId, String attachmentId) { - return BASE_ROOT_URL - + "(ID=" - + rootId - + ",IsActiveEntity=false)" - + "/sizeLimitedAttachments(ID=" - + attachmentId - + ",up__ID=" - + rootId - + ",IsActiveEntity=false)" - + "/content"; - } - - // Required abstract method implementations - @Override - protected void verifyContentId(String contentId, String attachmentId) { - assertThat(contentId).isEqualTo(attachmentId); - } - - @Override - protected void verifyContent(InputStream attachment, String testContent) throws IOException { - if (Objects.nonNull(testContent)) { - assertThat(attachment.readAllBytes()) - .isEqualTo(testContent.getBytes(java.nio.charset.StandardCharsets.UTF_8)); - } else { - assertThat(attachment).isNull(); - } - } - - @Override - protected void verifyNoAttachmentEventsCalled() { - // no service handler - nothing to do - } - - @Override - protected void clearServiceHandlerContext() { - // no service handler - nothing to do - } - - @Override - protected void verifyEventContextEmptyForEvent(String... events) { - // no service handler - nothing to do - } - - @Override - protected void verifyOnlyTwoCreateEvents( - String newAttachmentContent, String newAttachmentEntityContent) { - // no service handler - nothing to do - } - - @Override - protected void verifyTwoCreateAndDeleteEvents( - String newAttachmentContent, String newAttachmentEntityContent) { - // no service handler - nothing to do - } - - @Override - protected void verifyTwoReadEvents() { - // no service handler - nothing to do - } - - @Override - protected void verifyOnlyTwoDeleteEvents( - String attachmentContentId, String attachmentEntityContentId) { - // no service handler - nothing to do - } - - @Override - protected void verifyTwoUpdateEvents( - String newAttachmentContent, - String attachmentContentId, - String newAttachmentEntityContent, - String attachmentEntityContentId) { - // no service handler - nothing to do - } - - @Override - protected void verifyTwoCreateAndRevertedDeleteEvents() { - // no service handler - nothing to do - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java deleted file mode 100644 index 35a3b549b..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/MediaValidatedAttachmentsNonDraftTest.java +++ /dev/null @@ -1,296 +0,0 @@ -/* - * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice; - -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.sap.cds.Result; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots_; -import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.RootEntityBuilder; -import com.sap.cds.ql.Select; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.springframework.http.MediaType; -import org.springframework.test.context.ActiveProfiles; - -@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) -class MediaValidatedAttachmentsNonDraftTest extends OdataRequestValidationBase { - private static final String BASE_URL = MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots"; - private static final String MEDIA_VALIDATED_ATTACHMENTS = "mediaValidatedAttachments"; - private static final ObjectMapper objectMapper = new ObjectMapper(); - - protected void postServiceRoot(Roots serviceRoot) throws Exception { - String url = MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots"; - requestHelper.executePostWithMatcher(url, serviceRoot.toJson(), status().isCreated()); - } - - private Roots selectStoredRootWithMediaValidatedAttachments() { - Select select = - Select.from(Roots_.class) - .columns(r -> r._all(), r -> r.mediaValidatedAttachments().expand()); - - Result result = persistenceService.run(select); - return result.single(Roots.class); - } - - @BeforeEach - void setup() { - requestHelper.setContentType(MediaType.APPLICATION_JSON); - } - - @ParameterizedTest - @CsvSource({ - "image.jpg,image/jpeg,201", - "image.png,image/png,201", - "document.pdf,application/pdf,415", - "notes.txt,text/plain,415" - }) - void shouldValidateMediaTypes(String fileName, String mediaType, int expectedStatus) - throws Exception { - String rootId = createRootAndReturnId(); - String attachmentMetadata = createAttachmentMetadata(fileName); - - requestHelper.executePostWithMatcher( - createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), - attachmentMetadata, - status().is(expectedStatus)); - } - - @Test - void shouldRejectAttachment_whenFileNameIsEmpty() throws Exception { - String rootId = createRootAndReturnId(); - String fileName = ""; - String attachmentMetadata = createAttachmentMetadata(fileName); - - requestHelper.executePostWithMatcher( - createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), - attachmentMetadata, - status().isBadRequest()); - } - - @Test - void shouldAcceptUppercaseExtension_whenMimeTypeIsAllowed() throws Exception { - String rootId = createRootAndReturnId(); - String attachmentMetadata = createAttachmentMetadata("IMAGE.JPG"); - - requestHelper.executePostWithMatcher( - createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), attachmentMetadata, status().isCreated()); - } - - @Test - void shouldAcceptMixedCaseExtension() throws Exception { - String rootId = createRootAndReturnId(); - String attachmentMetadata = createAttachmentMetadata("image.JpEg"); - - requestHelper.executePostWithMatcher( - createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), attachmentMetadata, status().isCreated()); - } - - @Test - void shouldRejectAttachment_whenFileHasNoExtension() throws Exception { - String rootId = createRootAndReturnId(); - String attachmentMetadata = createAttachmentMetadata("filename"); - - requestHelper.executePostWithMatcher( - createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), - attachmentMetadata, - status().isUnsupportedMediaType()); - } - - @Test - void shouldRejectHiddenFile_whenFileStartsWithDot() throws Exception { - String rootId = createRootAndReturnId(); - String attachmentMetadata = createAttachmentMetadata(".gitignore"); - - requestHelper.executePostWithMatcher( - createUrl(rootId, MEDIA_VALIDATED_ATTACHMENTS), - attachmentMetadata, - status().isUnsupportedMediaType()); - } - - @ParameterizedTest - @CsvSource({ - // valid cases - "'test1.jpeg|test2.jpeg',201", - // invalid media types - "'test.pdf',415", - "'test1.jpeg|test2.pdf',415", - // invalid filenames - "'',400", - "' ',400", - // edge cases - "'.gitignore',415" - }) - void shouldValidateMediaTypes_forMultipleAttachments(String fileNames, int expectedStatus) - throws Exception { - String payload = buildPayload(fileNames); - requestHelper.executePostWithMatcher(BASE_URL, payload, status().is(expectedStatus)); - } - - @Test - void shouldAcceptWhenMediaValidatedAttachments_hasNoAttachments() throws Exception { - Map payload = new HashMap<>(); - payload.put("title", "Hello World!"); - payload.put("mediaValidatedAttachments", List.of()); - - String payloadStr = objectMapper.writeValueAsString(payload); - requestHelper.executePostWithMatcher(BASE_URL, payloadStr, status().is(201)); - } - - @Test - void shouldAcceptDeepCreate_whenMixedValidAndAllValidAttachments() throws Exception { - Map payload = new HashMap<>(); - payload.put("title", "Hello World!"); - payload.put( - "mediaValidatedAttachments", - List.of(Map.of("fileName", "test1.jpeg"), Map.of("fileName", "test2.jpeg"))); - - payload.put("mimeValidatedAttachments", List.of(Map.of("fileName", "test3.pdf"))); - - requestHelper.executePostWithMatcher( - BASE_URL, objectMapper.writeValueAsString(payload), status().isCreated()); - } - - @Test - void shouldRejectDeepCreate_whenMixedValidAndInvalidAttachments() throws Exception { - Map payload = new HashMap<>(); - payload.put("title", "Hello World!"); - payload.put( - "mediaValidatedAttachments", - List.of(Map.of("fileName", "test1.pdf"), Map.of("fileName", "test2.jpeg"))); - - payload.put("mimeValidatedAttachments", List.of(Map.of("fileName", "test3.pdf"))); - - requestHelper.executePostWithMatcher( - BASE_URL, objectMapper.writeValueAsString(payload), status().isUnsupportedMediaType()); - } - - private String createRootAndReturnId() throws Exception { - // Build the initial Java object.. Root - Roots serviceRoot = buildServiceRoot(); - - // POST the root object to the server to create it in the database - postServiceRoot(serviceRoot); - - // Read the newly created entity back from the database - Roots selectedRoot = selectStoredRootWithMediaValidatedAttachments(); - - return selectedRoot.getId(); - } - - private String buildPayload(String fileNames) throws JsonProcessingException { - List> attachments = new ArrayList<>(); - fileNames = fileNames.replaceAll("^'+|'+$", ""); - for (String name : fileNames.split("\\|")) { - attachments.add(Map.of("fileName", name)); - } - Map payload = new HashMap<>(); - payload.put("title", "Hello World!"); - payload.put("mediaValidatedAttachments", attachments); - - return objectMapper.writeValueAsString(payload); - } - - private String createUrl(String rootId, String path) { - return BASE_URL + "(" + rootId + ")" + (path == null || path.isBlank() ? "" : "/" + path); - } - - private String createAttachmentMetadata(String fileName) throws JsonProcessingException { - return objectMapper.writeValueAsString(Map.of("fileName", fileName)); - } - - // helper method - private Roots buildServiceRoot() { - return RootEntityBuilder.create().setTitle("Root").build(); - } - - // Override abstract methods from OdataRequestValidationBase - - @Override - protected void executeContentRequestAndValidateContent(String url, String content) - throws Exception { - // Implementation not required for this test - } - - @Override - protected void verifyContentId( - Attachments attachmentWithExpectedContent, String attachmentId, String contentId) { - // Implementation not required for this test - } - - @Override - protected void verifyContentAndContentId( - Attachments attachment, String testContent, Attachments itemAttachment) { - // Implementation not required for this test - } - - @Override - protected void verifyContentAndContentIdForAttachmentEntity( - AttachmentEntity attachment, String testContent, AttachmentEntity itemAttachment) { - // Implementation not required for this test - } - - @Override - public void verifySingleCreateAndUpdateEvent(String arg1, String arg2, String arg3) { - // Implementation not required for this test - } - - @Override - public void clearServiceHandlerContext() { - // Implementation not required for this test - } - - @Override - public void verifySingleReadEvent(String arg) { - // Implementation not required for this test - } - - @Override - public void verifyTwoDeleteEvents(AttachmentEntity entity, Attachments attachments) { - // Implementation not required for this test - } - - @Override - public void clearServiceHandlerDocuments() { - // Implementation not required for this test - } - - @Override - public void verifyEventContextEmptyForEvent(String... args) { - // Implementation not required for this test - } - - @Override - public void verifyNoAttachmentEventsCalled() { - // Implementation not required for this test - } - - @Override - public void verifyNumberOfEvents(String arg, int count) { - // Implementation not required for this test - } - - @Override - public void verifySingleCreateEvent(String arg1, String arg2) { - // Implementation not required for this test - } - - @Override - public void verifySingleDeletionEvent(String arg) { - // Implementation not required for this test - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java deleted file mode 100644 index a4774951f..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationBase.java +++ /dev/null @@ -1,884 +0,0 @@ -/* - * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity_; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Items; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Items_; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots_; -import com.sap.cds.feature.attachments.integrationtests.common.MockHttpRequestHelper; -import com.sap.cds.feature.attachments.integrationtests.common.TableDataDeleter; -import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.AttachmentsBuilder; -import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.AttachmentsEntityBuilder; -import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.ItemEntityBuilder; -import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.RootEntityBuilder; -import com.sap.cds.feature.attachments.integrationtests.testhandler.TestPersistenceHandler; -import com.sap.cds.feature.attachments.integrationtests.testhandler.TestPluginAttachmentsServiceHandler; -import com.sap.cds.feature.attachments.service.AttachmentService; -import com.sap.cds.ql.Select; -import com.sap.cds.ql.StructuredType; -import com.sap.cds.ql.cqn.CqnSelect; -import com.sap.cds.services.persistence.PersistenceService; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Objects; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.ResultMatcher; - -@SpringBootTest -@AutoConfigureMockMvc -abstract class OdataRequestValidationBase { - - protected static final Logger logger = LoggerFactory.getLogger(OdataRequestValidationBase.class); - - @Autowired(required = false) - protected TestPluginAttachmentsServiceHandler serviceHandler; - - @Autowired protected MockHttpRequestHelper requestHelper; - @Autowired protected PersistenceService persistenceService; - @Autowired private TableDataDeleter dataDeleter; - @Autowired private TestPersistenceHandler testPersistenceHandler; - - @AfterEach - void teardown() { - dataDeleter.deleteData(Roots_.CDS_NAME); - clearServiceHandlerContext(); - clearServiceHandlerDocuments(); - requestHelper.resetHelper(); - testPersistenceHandler.reset(); - } - - @Test - void deepCreateWorks() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - verifySelectedRoot(selectedRoot, serviceRoot); - verifyNoAttachmentEventsCalled(); - } - - @Test - void putContentWorksForUrlsWithNavigation() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - var attachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); - - verifyContentAndContentId(attachment, content, itemAttachment); - verifySingleCreateEvent(attachment.getContentId(), content); - } - - @Test - void putContentWorksForUrlsWithoutNavigation() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - - var content = putContentForAttachmentWithoutNavigation(itemAttachment); - var attachment = selectUpdatedAttachment(itemAttachment); - - verifyContentAndContentIdForAttachmentEntity(attachment, content, itemAttachment); - verifySingleCreateEvent(attachment.getContentId(), content); - } - - @Test - void expandReadOfAttachmentsHasNoFilledContent() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - - var url = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); - var responseItem = - requestHelper.executeGetWithSingleODataResponseAndAssertStatus( - url, Items.class, HttpStatus.OK); - - assertThat(responseItem.getAttachments()).hasSameSizeAs(item.getAttachments()); - assertThat(responseItem.getAttachments()) - .allSatisfy( - attachment -> { - assertThat(attachment.getContent()).isNull(); - assertThat(attachment.get("content@mediaContentType")).isNull(); - assertThat(attachment.getContentId()).isNull(); - }); - verifyNoAttachmentEventsCalled(); - } - - @Test - void navigationReadOfAttachmentsHasFilledContent() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - - var url = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); - var responseItem = - requestHelper.executeGetWithSingleODataResponseAndAssertStatus( - url, Items.class, HttpStatus.OK); - - assertThat(responseItem.getAttachments()).hasSameSizeAs(item.getAttachments()); - - var attachmentWithExpectedContent = - responseItem.getAttachments().stream() - .filter(attach -> attach.getId().equals(itemAttachment.getId())) - .findAny() - .orElseThrow(); - assertThat(attachmentWithExpectedContent) - .containsEntry("content@mediaContentType", "application/octet-stream;charset=UTF-8") - .containsEntry(Attachments.FILE_NAME, itemAttachment.getFileName()); - assertThat(attachmentWithExpectedContent.getStatus()).isNotEmpty(); - verifyContentId( - attachmentWithExpectedContent, itemAttachment.getId(), itemAttachment.getContentId()); - verifySingleCreateEvent(attachmentWithExpectedContent.getContentId(), content); - } - - @Test - void navigationReadOfAttachmentsReturnsContent() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); - - var url = - buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()) - + "/content"; - executeContentRequestAndValidateContent(url, content); - verifySingleReadEvent(itemAttachmentAfterChange.getContentId()); - } - - @Test - void navigationDeleteOfContentClears() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); - - executeDeleteAndCheckNoDataCanBeRead( - buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()), - itemAttachmentAfterChange.getContentId()); - - var expandUrl = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); - var responseItem = - requestHelper.executeGetWithSingleODataResponseAndAssertStatus( - expandUrl, Items.class, HttpStatus.OK); - - assertThat(responseItem.getAttachments()).hasSameSizeAs(item.getAttachments()); - assertThat(responseItem.getAttachments()) - .allSatisfy( - attachment -> { - assertThat(attachment.getContent()).isNull(); - assertThat(attachment.get("content@mediaContentType")).isNull(); - assertThat(attachment.getContentId()).isNull(); - }); - verifyNoAttachmentEventsCalled(); - } - - @Test - void navigationDeleteOfAttachmentClearsContentField() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); - - var url = - buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()); - requestHelper.executeDelete(url); - var expandUrl = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); - var responseItem = - requestHelper.executeGetWithSingleODataResponseAndAssertStatus( - expandUrl, Items.class, HttpStatus.OK); - - assertThat(responseItem.getAttachments()).hasSize(1); - assertThat(responseItem.getAttachments()) - .first() - .satisfies( - attachment -> { - assertThat(attachment.getContent()).isNull(); - assertThat(attachment.get("content@mediaContentType")).isNull(); - assertThat(attachment.getContentId()).isNull(); - }); - verifySingleDeletionEvent(itemAttachmentAfterChange.getContentId()); - } - - @Test - void navigationDeleteCallsTwiceReturnsError() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); - - var url = - buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()); - requestHelper.executeDelete(url); - var result = requestHelper.executeDelete(url); - - assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.PRECONDITION_FAILED.value()); - verifySingleDeletionEvent(itemAttachmentAfterChange.getContentId()); - } - - @Test - void directReadOfAttachmentsHasNoContentFilled() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - - var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); - var responseAttachment = - requestHelper.executeGetWithSingleODataResponseAndAssertStatus( - url, Attachments.class, HttpStatus.OK); - - assertThat(responseAttachment.get("content@mediaContentType")).isNull(); - assertThat(responseAttachment.getContentId()).isNull(); - assertThat(responseAttachment.getFileName()).isEqualTo(itemAttachment.getFileName()); - verifyNoAttachmentEventsCalled(); - } - - @Test - void directReadOfAttachmentsHasFilledContent() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - putContentForAttachmentWithoutNavigation(itemAttachment); - clearServiceHandlerContext(); - - var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); - var responseAttachment = - requestHelper.executeGetWithSingleODataResponseAndAssertStatus( - url, Attachments.class, HttpStatus.OK); - - assertThat(responseAttachment) - .containsEntry("content@mediaContentType", "application/octet-stream;charset=UTF-8") - .containsEntry(Attachments.FILE_NAME, itemAttachment.getFileName()); - verifyContentId(responseAttachment, itemAttachment.getId(), itemAttachment.getContentId()); - verifyNoAttachmentEventsCalled(); - } - - @Test - void directReadOfAttachmentsReturnsContent() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - var content = putContentForAttachmentWithoutNavigation(itemAttachment); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentAfterChange = getRandomItemAttachmentEntity(selectedItemAfterChange); - - var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()) + "/content"; - executeContentRequestAndValidateContent(url, content); - verifySingleReadEvent(itemAttachmentAfterChange.getContentId()); - } - - @Test - void directDeleteOfContentClears() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - putContentForAttachmentWithoutNavigation(itemAttachment); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentAfterChange = getRandomItemAttachmentEntity(selectedItemAfterChange); - - executeDeleteAndCheckNoDataCanBeRead( - buildDirectAttachmentEntityUrl(itemAttachment.getId()), - itemAttachmentAfterChange.getContentId()); - - var expandUrl = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); - var responseItem = - requestHelper.executeGetWithSingleODataResponseAndAssertStatus( - expandUrl, Items.class, HttpStatus.OK); - - assertThat(responseItem.getAttachmentEntities()).hasSameSizeAs(item.getAttachmentEntities()); - assertThat(responseItem.getAttachmentEntities()) - .allSatisfy( - attachment -> { - assertThat(attachment.getContent()).isNull(); - assertThat(attachment.get("content@mediaContentType")).isNull(); - assertThat(attachment.getContentId()).isNull(); - }); - verifyNoAttachmentEventsCalled(); - } - - @Test - void directDeleteOfAttachmentClearsContentField() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - putContentForAttachmentWithoutNavigation(itemAttachment); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentAfterChange = getRandomItemAttachmentEntity(selectedItemAfterChange); - - var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); - requestHelper.executeDelete(url); - var expandUrl = buildExpandAttachmentUrl(selectedRoot.getId(), item.getId()); - var responseItem = - requestHelper.executeGetWithSingleODataResponseAndAssertStatus( - expandUrl, Items.class, HttpStatus.OK); - - assertThat(responseItem.getAttachmentEntities()).isEmpty(); - verifySingleDeletionEvent(itemAttachmentAfterChange.getContentId()); - } - - @Test - void directDeleteCalledTwiceReturnsError() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - putContentForAttachmentWithoutNavigation(itemAttachment); - clearServiceHandlerContext(); - - var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); - requestHelper.executeDelete(url); - MvcResult mvcResult = requestHelper.executeDelete(url); - - assertThat(mvcResult.getResponse().getStatus()) - .isEqualTo(HttpStatus.PRECONDITION_FAILED.value()); - if (Objects.nonNull(serviceHandler)) { - Awaitility.await().until(() -> serviceHandler.getEventContext().size() == 1); - verifyNumberOfEvents(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED, 1); - verifyEventContextEmptyForEvent( - AttachmentService.EVENT_CREATE_ATTACHMENT, AttachmentService.EVENT_READ_ATTACHMENT); - } - } - - @Test - void rootDeleteDeletesAllContents() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachmentEntity = getRandomItemAttachmentEntity(item); - var itemAttachment = getRandomItemAttachment(item); - - putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - putContentForAttachmentWithoutNavigation(itemAttachmentEntity); - verifyNumberOfEvents(AttachmentService.EVENT_CREATE_ATTACHMENT, 2); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentEntityAfterChange = getRandomItemAttachmentEntity(selectedItemAfterChange); - var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); - - var url = - MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots(" + selectedRoot.getId() + ")"; - requestHelper.executeDeleteWithMatcher(url, status().isNoContent()); - - verifyTwoDeleteEvents(itemAttachmentEntityAfterChange, itemAttachmentAfterChange); - } - - @Test - void updateContentWorksForUrlsWithNavigation() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - itemAttachment.setNote("note 1"); - putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - itemAttachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); - itemAttachment.setNote("note 2"); - var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - var attachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); - - verifyContentAndContentId(attachment, content, itemAttachment); - verifySingleCreateAndUpdateEvent( - attachment.getContentId(), itemAttachment.getContentId(), content); - } - - @Test - void updateContentWorksForUrlsWithoutNavigation() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - itemAttachment.setNote("note 1"); - putContentForAttachmentWithoutNavigation(itemAttachment); - itemAttachment = selectUpdatedAttachment(itemAttachment); - itemAttachment.setNote("note 2"); - var content = putContentForAttachmentWithoutNavigation(itemAttachment); - var attachment = selectUpdatedAttachment(itemAttachment); - - verifyContentAndContentIdForAttachmentEntity(attachment, content, itemAttachment); - verifySingleCreateAndUpdateEvent( - attachment.getContentId(), itemAttachment.getContentId(), content); - } - - @Test - void errorInTransactionAfterCreateCallsDelete() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - testPersistenceHandler.setThrowExceptionOnUpdate(true); - putContentForAttachmentWithNavigation( - selectedRoot, itemAttachment, status().is5xxServerError()); - var attachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); - - assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getContentId()); - assertThat(attachment.getContent()).isEqualTo(itemAttachment.getContent()); - } - - @Test - void updateContentWithErrorsResetsForUrlsWithNavigation() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - itemAttachment.setNote("note 1"); - var content = putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - itemAttachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); - itemAttachment.setNote("note 2"); - testPersistenceHandler.setThrowExceptionOnUpdate(true); - putContentForAttachmentWithNavigation( - selectedRoot, itemAttachment, status().is5xxServerError()); - var attachment = selectUpdatedAttachmentWithExpand(selectedRoot, itemAttachment); - - verifyContentAndContentId(attachment, content, itemAttachment); - assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getContentId()); - verifySingleCreateAndUpdateEvent( - attachment.getContentId(), itemAttachment.getContentId(), content); - } - - @Test - void updateContentWithErrorResetsForUrlsWithoutNavigation() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - itemAttachment.setNote("note 1"); - var content = putContentForAttachmentWithoutNavigation(itemAttachment); - itemAttachment = selectUpdatedAttachment(itemAttachment); - itemAttachment.setNote("note 2"); - testPersistenceHandler.setThrowExceptionOnUpdate(true); - putContentForAttachmentWithoutNavigation(itemAttachment, status().is5xxServerError()); - var attachment = selectUpdatedAttachment(itemAttachment); - - verifyContentAndContentIdForAttachmentEntity(attachment, content, itemAttachment); - assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getContentId()); - verifySingleCreateAndUpdateEvent( - attachment.getContentId(), itemAttachment.getContentId(), content); - } - - @ParameterizedTest - @CsvSource({"status,INFECTED", "contentId,TEST"}) - void statusCannotBeUpdated(String field, String value) throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - putContentForAttachmentWithoutNavigation(itemAttachment); - itemAttachment.setStatus(value); - var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); - - requestHelper.resetHelper(); - requestHelper.executePatchWithODataResponseAndAssertStatus( - url, "{\"" + field + "\":\"" + value + "\"}", HttpStatus.OK); - - selectedRoot = selectStoredRootWithDeepData(); - item = getItemWithAttachmentEntity(selectedRoot); - itemAttachment = getRandomItemAttachmentEntity(item); - assertThat(itemAttachment.get(field)).isNotNull().isNotEqualTo(value); - } - - @Test - void wrongEtagCouldNotBeUpdated() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - - var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); - requestHelper.executePatchWithODataResponseAndAssertStatus( - url, - "{\"fileName\":\"test_for_change.txt\"}", - "W/\"2024-05-06T15:24:29.657713600Z\"", - HttpStatus.PRECONDITION_FAILED); - - var selectedRootAfterChange = selectStoredRootWithDeepData(); - var itemAfterChange = getItemWithAttachmentEntity(selectedRootAfterChange); - var itemAttachmentAfterChange = getRandomItemAttachmentEntity(itemAfterChange); - assertThat(itemAttachmentAfterChange.getFileName()).isEqualTo(itemAttachment.getFileName()); - } - - @Test - void correctEtagCanBeUpdated() throws Exception { - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachmentEntity(selectedRoot); - var itemAttachment = getRandomItemAttachmentEntity(item); - var modifiedAt = itemAttachment.getModifiedAt(); - var eTag = "W/\"" + modifiedAt + "\""; - - var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()); - requestHelper.executePatchWithODataResponseAndAssertStatus( - url, "{\"fileName\":\"test_for_change.txt\"}", eTag, HttpStatus.OK); - - var selectedRootAfterChange = selectStoredRootWithDeepData(); - var itemAfterChange = getItemWithAttachmentEntity(selectedRootAfterChange); - var itemAttachmentAfterChange = getRandomItemAttachmentEntity(itemAfterChange); - assertThat(itemAttachmentAfterChange.getFileName()).isEqualTo("test_for_change.txt"); - } - - protected Items selectItem(Items item) { - var selectedRootAfterContentCreated = selectStoredRootWithDeepData(); - return selectedRootAfterContentCreated.getItems().stream() - .filter(i -> i.getId().equals(item.getId())) - .findAny() - .orElseThrow(); - } - - protected Roots buildServiceRootWithDeepData() { - return RootEntityBuilder.create() - .setTitle("some root title") - .addAttachments( - AttachmentsEntityBuilder.create().setFileName("fileRoot.txt").setMimeType("text/plain")) - .addItems( - ItemEntityBuilder.create() - .setTitle("some item 1 title") - .addAttachments( - AttachmentsBuilder.create() - .setFileName("fileItem1.txt") - .setMimeType("text/plain"), - AttachmentsBuilder.create() - .setFileName("fileItem2.txt") - .setMimeType("text/plain")), - ItemEntityBuilder.create() - .setTitle("some item 2 title") - .addAttachmentEntities( - AttachmentsEntityBuilder.create() - .setFileName("fileItem3.text") - .setMimeType("text/plain")) - .addAttachments( - AttachmentsBuilder.create() - .setFileName("fileItem3.text") - .setMimeType("text/plain"))) - .build(); - } - - protected void postServiceRoot(Roots serviceRoot) throws Exception { - var url = MockHttpRequestHelper.ODATA_BASE_URL + "TestService/Roots"; - requestHelper.executePostWithMatcher(url, serviceRoot.toJson(), status().isCreated()); - } - - protected Roots selectStoredRootWithDeepData() { - CqnSelect select = - Select.from(Roots_.class) - .columns( - StructuredType::_all, - root -> root.attachments().expand(), - root -> - root.items() - .expand( - StructuredType::_all, - item -> item.attachments().expand(), - item -> item.attachmentEntities().expand())); - var result = persistenceService.run(select); - return result.single(Roots.class); - } - - private void verifySelectedRoot(Roots selectedRoot, Roots serviceRoot) { - assertThat(selectedRoot.getId()).isNotEmpty(); - assertThat(selectedRoot.getTitle()).isEqualTo(serviceRoot.getTitle()); - assertThat(selectedRoot.getAttachments()) - .hasSize(1) - .first() - .satisfies( - attachment -> { - assertThat(attachment.getId()).isNotEmpty(); - assertThat(attachment.getFileName()) - .isEqualTo(serviceRoot.getAttachments().get(0).getFileName()); - assertThat(attachment.getMimeType()) - .isEqualTo(serviceRoot.getAttachments().get(0).getMimeType()); - }); - assertThat(selectedRoot.getItems()) - .hasSize(2) - .first() - .satisfies( - item -> { - assertThat(item.getId()).isNotEmpty(); - assertThat(item.getTitle()).isEqualTo(serviceRoot.getItems().get(0).getTitle()); - assertThat(item.getAttachments()).hasSize(2); - }); - assertThat(selectedRoot.getItems().get(1).getId()).isNotEmpty(); - assertThat(selectedRoot.getItems().get(1).getTitle()) - .isEqualTo(serviceRoot.getItems().get(1).getTitle()); - assertThat(selectedRoot.getItems().get(1).getAttachments()).hasSize(1); - } - - protected Attachments getRandomItemAttachment(Items selectedItem) { - return selectedItem.getAttachments().get(0); - } - - protected Attachments getRandomRootSizeLimitedAttachment(Roots selectedRoot) { - return selectedRoot.getSizeLimitedAttachments().get(0); - } - - private AttachmentEntity getRandomItemAttachmentEntity(Items selectedItem) { - return selectedItem.getAttachmentEntities().get(0); - } - - protected Items getItemWithAttachment(Roots selectedRoot) { - return selectedRoot.getItems().stream() - .filter(item -> !item.getAttachments().isEmpty()) - .findAny() - .orElseThrow(); - } - - private Items getItemWithAttachmentEntity(Roots selectedRoot) { - return selectedRoot.getItems().stream() - .filter(item -> !item.getAttachmentEntities().isEmpty()) - .findAny() - .orElseThrow(); - } - - protected String putContentForAttachmentWithNavigation( - Roots selectedRoot, Attachments itemAttachment) throws Exception { - return putContentForAttachmentWithNavigation( - selectedRoot, itemAttachment, status().isNoContent()); - } - - private String putContentForAttachmentWithNavigation( - Roots selectedRoot, Attachments itemAttachment, ResultMatcher matcher) throws Exception { - var selectedItem = - selectedRoot.getItems().stream() - .filter( - item -> - item.getAttachments().stream() - .anyMatch(attach -> attach.getId().equals(itemAttachment.getId()))) - .findAny() - .orElseThrow(); - var url = - buildNavigationAttachmentUrl( - selectedRoot.getId(), selectedItem.getId(), itemAttachment.getId()) - + "/content"; - - var testContent = "testContent" + itemAttachment.getNote(); - requestHelper.setContentType(MediaType.APPLICATION_OCTET_STREAM); - requestHelper.executePutWithMatcher(url, testContent.getBytes(StandardCharsets.UTF_8), matcher); - return testContent; - } - - protected String buildNavigationAttachmentUrl(String rootId, String itemId, String attachmentId) { - return "/odata/v4/TestService/Roots(" - + rootId - + ")/items(" - + itemId - + ")" - + "/attachments(ID=" - + attachmentId - + ",up__ID=" - + itemId - + ")"; - } - - protected String buildNavigationSizeLimitedAttachmentUrl(String rootId, String attachmentId) { - return "/odata/v4/TestService/Roots(" - + rootId - + ")/sizeLimitedAttachments(ID=" - + attachmentId - + ",up__ID=" - + rootId - + ")"; - } - - protected String putContentForSizeLimitedAttachment(Roots selectedRoot, Attachments attachment) - throws Exception { - return putContentForSizeLimitedAttachment(selectedRoot, attachment, status().isNoContent()); - } - - protected String putContentForSizeLimitedAttachment( - Roots selectedRoot, Attachments attachment, ResultMatcher matcher) throws Exception { - var url = - buildNavigationSizeLimitedAttachmentUrl(selectedRoot.getId(), attachment.getId()) - + "/content"; - var testContent = "testContent" + attachment.getNote(); - requestHelper.setContentType(MediaType.APPLICATION_OCTET_STREAM); - requestHelper.executePutWithMatcher(url, testContent.getBytes(StandardCharsets.UTF_8), matcher); - return testContent; - } - - protected String buildExpandAttachmentUrl(String rootId, String itemId) { - return "/odata/v4/TestService/Roots(" - + rootId - + ")/items(" - + itemId - + ")" - + "?$expand=attachments,attachmentEntities"; - } - - private String putContentForAttachmentWithoutNavigation(AttachmentEntity itemAttachment) - throws Exception { - return putContentForAttachmentWithoutNavigation(itemAttachment, status().isNoContent()); - } - - private String putContentForAttachmentWithoutNavigation( - AttachmentEntity itemAttachment, ResultMatcher matcher) throws Exception { - var url = buildDirectAttachmentEntityUrl(itemAttachment.getId()) + "/content"; - var testContent = "testContent" + itemAttachment.getNote(); - requestHelper.setContentType(MediaType.APPLICATION_OCTET_STREAM); - requestHelper.executePutWithMatcher(url, testContent.getBytes(StandardCharsets.UTF_8), matcher); - return testContent; - } - - private String buildDirectAttachmentEntityUrl(String attachmentId) { - return MockHttpRequestHelper.ODATA_BASE_URL - + "TestService/AttachmentEntity(" - + attachmentId - + ")"; - } - - private Attachments selectUpdatedAttachmentWithExpand( - Roots selectedRoot, Attachments itemAttachment) { - CqnSelect attachmentSelect = - Select.from(Items_.class) - .where(a -> a.ID().eq(selectedRoot.getItems().get(0).getId())) - .columns(item -> item.attachments().expand()); - var result = persistenceService.run(attachmentSelect); - var items = result.single(Items.class); - return items.getAttachments().stream() - .filter(attach -> itemAttachment.getId().equals(attach.getId())) - .findAny() - .orElseThrow(); - } - - private AttachmentEntity selectUpdatedAttachment(AttachmentEntity itemAttachment) { - CqnSelect attachmentSelect = - Select.from(AttachmentEntity_.class).where(a -> a.ID().eq(itemAttachment.getId())); - var result = persistenceService.run(attachmentSelect); - return result.single(AttachmentEntity.class); - } - - private void executeDeleteAndCheckNoDataCanBeRead(String baseUrl, String contentId) - throws Exception { - var url = baseUrl + "/content"; - requestHelper.executeDelete(url); - verifySingleDeletionEvent(contentId); - clearServiceHandlerContext(); - var response = requestHelper.executeGet(url); - - assertThat(response.getResponse().getContentLength()).isZero(); - assertThat(response.getResponse().getStatus()).isEqualTo(HttpStatus.NO_CONTENT.value()); - } - - protected abstract void executeContentRequestAndValidateContent(String url, String content) - throws Exception; - - protected abstract void verifyTwoDeleteEvents( - AttachmentEntity itemAttachmentEntityAfterChange, Attachments itemAttachmentAfterChange); - - protected abstract void verifyNumberOfEvents(String event, int number); - - protected abstract void verifyContentId( - Attachments attachmentWithExpectedContent, String attachmentId, String contentId); - - protected abstract void verifyContentAndContentId( - Attachments attachment, String testContent, Attachments itemAttachment) throws IOException; - - protected abstract void verifyContentAndContentIdForAttachmentEntity( - AttachmentEntity attachment, String testContent, AttachmentEntity itemAttachment) - throws IOException; - - protected abstract void clearServiceHandlerContext(); - - protected abstract void clearServiceHandlerDocuments(); - - protected abstract void verifySingleCreateEvent(String contentId, String content); - - protected abstract void verifySingleCreateAndUpdateEvent( - String resultContentId, String toBeDeletedContentId, String content); - - protected abstract void verifySingleDeletionEvent(String contentId); - - protected abstract void verifySingleReadEvent(String contentId); - - protected abstract void verifyNoAttachmentEventsCalled(); - - protected abstract void verifyEventContextEmptyForEvent(String... events); -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java deleted file mode 100644 index 94fca3378..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithTestHandlerTest.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity; -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import com.sap.cds.feature.attachments.integrationtests.testhandler.EventContextHolder; -import com.sap.cds.feature.attachments.service.AttachmentService; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.function.Predicate; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.Test; -import org.springframework.test.context.ActiveProfiles; - -@ActiveProfiles(Profiles.TEST_HANDLER_ENABLED) -class OdataRequestValidationWithTestHandlerTest extends OdataRequestValidationBase { - - @Test - void serviceHandlerAvailable() { - assertThat(serviceHandler).isNotNull(); - } - - @Override - protected void executeContentRequestAndValidateContent(String url, String content) - throws Exception { - var response = requestHelper.executeGet(url); - assertThat(response.getResponse().getContentAsString()).isEqualTo(content); - } - - @Override - protected void verifyTwoDeleteEvents( - AttachmentEntity itemAttachmentEntityAfterChange, Attachments itemAttachmentAfterChange) { - waitTillExpectedHandlerMessageSize(2); - verifyEventContextEmptyForEvent( - AttachmentService.EVENT_READ_ATTACHMENT, AttachmentService.EVENT_CREATE_ATTACHMENT); - var deleteEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - assertThat(deleteEvents).hasSize(2); - assertThat( - deleteEvents.stream() - .anyMatch( - verifyContentIdAndUserInfo(itemAttachmentEntityAfterChange.getContentId()))) - .isTrue(); - assertThat( - deleteEvents.stream() - .anyMatch(verifyContentIdAndUserInfo(itemAttachmentAfterChange.getContentId()))) - .isTrue(); - } - - @Override - protected void verifyNumberOfEvents(String event, int number) { - assertThat(serviceHandler.getEventContextForEvent(event)).hasSize(number); - } - - @Override - protected void verifyContentId( - Attachments attachmentWithExpectedContent, String attachmentId, String contentId) { - assertThat(attachmentWithExpectedContent.getContentId()).isNotEmpty().isNotEqualTo(contentId); - } - - @Override - protected void verifyContentAndContentId( - Attachments attachment, String content, Attachments itemAttachment) { - assertThat(attachment.getContent()).isNull(); - assertThat(attachment.getContentId()).isNotEmpty().isNotEqualTo(itemAttachment.getId()); - } - - @Override - protected void verifyContentAndContentIdForAttachmentEntity( - AttachmentEntity attachment, String content, AttachmentEntity itemAttachment) { - assertThat(attachment.getContent()).isNull(); - assertThat(attachment.getContentId()).isNotEmpty().isNotEqualTo(itemAttachment.getId()); - } - - @Override - protected void clearServiceHandlerContext() { - serviceHandler.clearEventContext(); - } - - @Override - protected void clearServiceHandlerDocuments() { - serviceHandler.clearDocuments(); - } - - @Override - protected void verifySingleCreateEvent(String contentId, String content) { - verifyEventContextEmptyForEvent( - AttachmentService.EVENT_READ_ATTACHMENT, - AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - var createEvent = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); - assertThat(createEvent) - .hasSize(1) - .first() - .satisfies( - event -> { - assertThat(event.context()).isInstanceOf(AttachmentCreateEventContext.class); - var createContext = (AttachmentCreateEventContext) event.context(); - assertThat(createContext.getContentId()).isEqualTo(contentId); - assertThat(createContext.getData().getContent().readAllBytes()) - .isEqualTo(content.getBytes(StandardCharsets.UTF_8)); - }); - } - - @Override - protected void verifySingleCreateAndUpdateEvent( - String resultContentId, String toBeDeletedContentId, String content) { - waitTillExpectedHandlerMessageSize(3); - verifyEventContextEmptyForEvent(AttachmentService.EVENT_READ_ATTACHMENT); - var createEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); - assertThat(createEvents).hasSize(2); - verifyCreateEventsContainsContentId(toBeDeletedContentId, createEvents); - verifyCreateEventsContainsContentId(resultContentId, createEvents); - var deleteEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - - var deleteContentId = - !resultContentId.equals(toBeDeletedContentId) - ? toBeDeletedContentId - : createEvents.stream() - .filter( - event -> - !resultContentId.equals( - ((AttachmentCreateEventContext) event.context()).getContentId())) - .findFirst() - .orElseThrow() - .context() - .get(Attachments.CONTENT_ID); - - var eventFound = - deleteEvents.stream() - .anyMatch( - event -> - ((AttachmentMarkAsDeletedEventContext) event.context()) - .getContentId() - .equals(deleteContentId)); - assertThat(eventFound).isTrue(); - } - - @Override - protected void verifySingleDeletionEvent(String contentId) { - waitTillExpectedHandlerMessageSize(1); - verifyEventContextEmptyForEvent( - AttachmentService.EVENT_CREATE_ATTACHMENT, AttachmentService.EVENT_READ_ATTACHMENT); - var deleteEvents = - serviceHandler.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - assertThat(deleteEvents) - .hasSize(1) - .first() - .satisfies( - event -> { - assertThat(event.context()).isInstanceOf(AttachmentMarkAsDeletedEventContext.class); - var deleteContext = (AttachmentMarkAsDeletedEventContext) event.context(); - assertThat(deleteContext.getContentId()).isEqualTo(contentId); - assertThat(deleteContext.getDeletionUserInfo().getName()).isEqualTo("anonymous"); - assertThat(deleteContext.getDeletionUserInfo().getIsSystemUser()).isFalse(); - }); - } - - @Override - protected void verifySingleReadEvent(String contentId) { - verifyEventContextEmptyForEvent( - AttachmentService.EVENT_CREATE_ATTACHMENT, - AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - var readContext = serviceHandler.getEventContext(); - assertThat(readContext) - .hasSize(1) - .first() - .satisfies( - event -> { - assertThat(event.event()).isEqualTo(AttachmentService.EVENT_READ_ATTACHMENT); - assertThat(((AttachmentReadEventContext) event.context()).getContentId()) - .isEqualTo(contentId); - }); - } - - @Override - protected void verifyNoAttachmentEventsCalled() { - assertThat(serviceHandler.getEventContext()).isEmpty(); - } - - @Override - protected void verifyEventContextEmptyForEvent(String... events) { - Arrays.stream(events) - .forEach(event -> assertThat(serviceHandler.getEventContextForEvent(event)).isEmpty()); - } - - private Predicate verifyContentIdAndUserInfo( - String itemAttachmentEntityAfterChange) { - return event -> - ((AttachmentMarkAsDeletedEventContext) event.context()) - .getContentId() - .equals(itemAttachmentEntityAfterChange) - && ((AttachmentMarkAsDeletedEventContext) event.context()) - .getDeletionUserInfo() - .getName() - .equals("anonymous") - && Boolean.FALSE.equals( - ((AttachmentMarkAsDeletedEventContext) event.context()) - .getDeletionUserInfo() - .getIsSystemUser()); - } - - private void verifyCreateEventsContainsContentId( - String contentId, List createEvents) { - assertThat( - createEvents.stream() - .anyMatch( - event -> - ((AttachmentCreateEventContext) event.context()) - .getContentId() - .equals(contentId))) - .isTrue(); - } - - private void waitTillExpectedHandlerMessageSize(int expectedSize) { - Awaitility.await() - .atMost(30, TimeUnit.SECONDS) - .pollDelay(1, TimeUnit.SECONDS) - .until( - () -> { - var eventCalls = serviceHandler.getEventContext().size(); - logger.debug( - "Waiting for expected size '{}' in handler context, was '{}'", - expectedSize, - eventCalls); - var numberMatch = eventCalls >= expectedSize; - if (!numberMatch) { - serviceHandler.getEventContext().forEach(event -> logger.info("Event: {}", event)); - } - return numberMatch; - }); - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java deleted file mode 100644 index 725c4d775..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * © 2024-2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice; - -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.post; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static org.assertj.core.api.Assertions.assertThat; - -import com.github.tomakehurst.wiremock.WireMockServer; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; -import com.sap.cds.feature.attachments.integrationtests.common.MalwareScanResultProvider; -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import java.util.concurrent.TimeUnit; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.test.context.ActiveProfiles; -import org.wiremock.spring.ConfigureWireMock; -import org.wiremock.spring.EnableWireMock; -import org.wiremock.spring.InjectWireMock; - -@ActiveProfiles(profiles = {Profiles.TEST_HANDLER_DISABLED, Profiles.MALWARE_SCAN_ENABLED}) -@EnableWireMock({@ConfigureWireMock(port = 1111, name = "malware-scanner")}) -class OdataRequestValidationWithoutTestHandlerAndMalwareScannerTest - extends OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest { - - @InjectWireMock("malware-scanner") - private WireMockServer wiremock; - - @Autowired private MalwareScanResultProvider malwareScanResultProvider; - - @BeforeEach - void setup() { - mockMalwareScanResult(false); - } - - @Override - @AfterEach - void teardown() { - super.teardown(); - wiremock.resetAll(); - } - - @Test - void scannerReturnedMalwareContentCanNotBeRead() throws Exception { - wiremock.resetAll(); - mockMalwareScanResult(true); - - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); - - var url = - buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()); - var contentUrl = url + "/content"; - waitAndVerifyContentErrorResponse(contentUrl, itemAttachmentAfterChange); - clearServiceHandlerContext(); - verifyAttachmentGetResponse(url, itemAttachmentAfterChange); - } - - @Test - void scannerReturnedErrorContentCanNotBeRead() throws Exception { - wiremock.resetAll(); - wiremock.stubFor( - post(urlEqualTo("/scan")) - .withBasicAuth("test-user", "test-password") - .willReturn(aResponse().withStatus(500))); - - var serviceRoot = buildServiceRootWithDeepData(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithDeepData(); - var item = getItemWithAttachment(selectedRoot); - var itemAttachment = getRandomItemAttachment(item); - putContentForAttachmentWithNavigation(selectedRoot, itemAttachment); - clearServiceHandlerContext(); - var selectedItemAfterChange = selectItem(item); - var itemAttachmentAfterChange = getRandomItemAttachment(selectedItemAfterChange); - - var url = - buildNavigationAttachmentUrl(selectedRoot.getId(), item.getId(), itemAttachment.getId()); - var contentUrl = url + "/content"; - waitAndVerifyContentErrorResponse(contentUrl, itemAttachmentAfterChange); - clearServiceHandlerContext(); - verifyAttachmentGetResponse(url, itemAttachmentAfterChange); - } - - private void mockMalwareScanResult(boolean malware) { - wiremock.stubFor( - post(urlEqualTo("/scan")) - .withBasicAuth("test-user", "test-password") - .willReturn( - aResponse() - .withBody(malwareScanResultProvider.buildMalwareScanResult(malware)) - .withStatus(200))); - } - - private void waitAndVerifyContentErrorResponse( - String contentUrl, Attachments itemAttachmentAfterChange) throws Exception { - Awaitility.await() - .atMost(10, TimeUnit.SECONDS) - .until( - () -> { - var response = requestHelper.executeGet(contentUrl); - assertThat(response.getResponse().getStatus()) - .isEqualTo(HttpStatus.METHOD_NOT_ALLOWED.value()); - return response - .getResponse() - .getContentAsString() - .contains("Error text for not clean"); - }); - clearServiceHandlerContext(); - - var response = requestHelper.executeGet(contentUrl); - assertThat(response.getResponse().getStatus()).isEqualTo(HttpStatus.METHOD_NOT_ALLOWED.value()); - assertThat(response.getResponse().getContentAsString()).contains("Error text for not clean"); - verifySingleReadEvent(itemAttachmentAfterChange.getContentId()); - } - - private void verifyAttachmentGetResponse(String url, Attachments itemAttachmentAfterChange) - throws Exception { - var response = requestHelper.executeGet(url); - assertThat(response.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value()); - verifySingleReadEvent(itemAttachmentAfterChange.getContentId()); - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java deleted file mode 100644 index edee93cb5..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity; -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.TimeUnit; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.Test; -import org.springframework.test.context.ActiveProfiles; - -@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) -class OdataRequestValidationWithoutTestHandlerAndWithoutMalwareScannerTest - extends OdataRequestValidationBase { - - @Test - void serviceHandlerIsNull() { - assertThat(serviceHandler).isNull(); - } - - @Override - protected void executeContentRequestAndValidateContent(String url, String content) - throws Exception { - Awaitility.await() - .atMost(10, TimeUnit.SECONDS) - .until( - () -> { - var response = requestHelper.executeGet(url); - return response.getResponse().getContentAsString().equals(content); - }); - - var response = requestHelper.executeGet(url); - assertThat(response.getResponse().getContentAsString()).isEqualTo(content); - } - - @Override - protected void verifyTwoDeleteEvents( - AttachmentEntity itemAttachmentEntityAfterChange, Attachments itemAttachmentAfterChange) { - // no service handler - nothing to do - } - - @Override - protected void verifyNumberOfEvents(String event, int number) { - // no service handler - nothing to do - } - - @Override - protected void verifyContentId( - Attachments attachmentWithExpectedContent, String attachmentId, String contentId) { - assertThat(attachmentWithExpectedContent.getContentId()).isEqualTo(attachmentId); - } - - @Override - protected void verifyContentAndContentId( - Attachments attachment, String testContent, Attachments itemAttachment) throws IOException { - assertThat(attachment.getContent().readAllBytes()) - .isEqualTo(testContent.getBytes(StandardCharsets.UTF_8)); - assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getId()); - } - - @Override - protected void verifyContentAndContentIdForAttachmentEntity( - AttachmentEntity attachment, String testContent, AttachmentEntity itemAttachment) - throws IOException { - assertThat(attachment.getContent().readAllBytes()) - .isEqualTo(testContent.getBytes(StandardCharsets.UTF_8)); - assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getId()); - } - - @Override - protected void clearServiceHandlerContext() { - // no service handler - nothing to do - } - - @Override - protected void clearServiceHandlerDocuments() { - // no service handler - nothing to do - } - - @Override - protected void verifySingleCreateEvent(String contentId, String content) { - // no service handler - nothing to do - } - - @Override - protected void verifySingleCreateAndUpdateEvent( - String resultContentId, String toBeDeletedContentId, String content) { - // no service handler - nothing to do - } - - @Override - protected void verifySingleDeletionEvent(String contentId) { - // no service handler - nothing to do - } - - @Override - protected void verifySingleReadEvent(String contentId) { - // no service handler - nothing to do - } - - @Override - protected void verifyNoAttachmentEventsCalled() { - // no service handler - nothing to do - } - - @Override - protected void verifyEventContextEmptyForEvent(String... events) { - // no service handler - nothing to do - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java deleted file mode 100644 index 9bc77e763..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/SizeLimitedAttachmentValidationNonDraftTest.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity; -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots; -import com.sap.cds.feature.attachments.integrationtests.constants.Profiles; -import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.AttachmentsBuilder; -import com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper.RootEntityBuilder; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.TimeUnit; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.Test; -import org.springframework.test.context.ActiveProfiles; - -@ActiveProfiles(Profiles.TEST_HANDLER_DISABLED) -class SizeLimitedAttachmentValidationNonDraftTest extends OdataRequestValidationBase { - - @Test - void uploadContentWithin5MBLimitSucceeds() throws Exception { - // Arrange: Create root with sizeLimitedAttachments - var serviceRoot = buildServiceRootWithSizeLimitedAttachments(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithSizeLimitedAttachments(); - var attachment = getRandomRootSizeLimitedAttachment(selectedRoot); - attachment.setFileName("test.txt"); - - // Act & Assert: Upload 3MB content (within limit) succeeds - byte[] content = new byte[3 * 1024 * 1024]; // 3MB - var url = - buildNavigationSizeLimitedAttachmentUrl(selectedRoot.getId(), attachment.getId()) - + "/content"; - requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); - requestHelper.executePutWithMatcher(url, content, status().isNoContent()); - } - - @Test - void uploadContentExceeding5MBLimitFails() throws Exception { - // Arrange: Create root with sizeLimitedAttachments - var serviceRoot = buildServiceRootWithSizeLimitedAttachments(); - postServiceRoot(serviceRoot); - - var selectedRoot = selectStoredRootWithSizeLimitedAttachments(); - var attachment = getRandomRootSizeLimitedAttachment(selectedRoot); - attachment.setFileName("test.txt"); - // Act: Try to upload 6MB content (exceeds limit) - byte[] content = new byte[6 * 1024 * 1024]; // 6MB - var url = - buildNavigationSizeLimitedAttachmentUrl(selectedRoot.getId(), attachment.getId()) - + "/content"; - requestHelper.setContentType(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM); - requestHelper.executePutWithMatcher(url, content, status().is(413)); - - // Assert: Error response with HTTP 413 status code indicates size limit - // exceeded - } - - // Helper methods - private Roots buildServiceRootWithSizeLimitedAttachments() { - return RootEntityBuilder.create() - .setTitle("Root with sizeLimitedAttachments") - .addSizeLimitedAttachments( - AttachmentsBuilder.create().setFileName("testFile.txt").setMimeType("text/plain")) - .build(); - } - - private Roots selectStoredRootWithSizeLimitedAttachments() { - var select = - com.sap.cds.ql.Select.from( - com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots_ - .class) - .columns(r -> r._all(), r -> r.sizeLimitedAttachments().expand()); - - var result = persistenceService.run(select); - return result.single(Roots.class); - } - - // Required abstract method implementations - @Override - protected void executeContentRequestAndValidateContent(String url, String content) - throws Exception { - Awaitility.await() - .atMost(10, TimeUnit.SECONDS) - .until( - () -> { - var response = requestHelper.executeGet(url); - return response.getResponse().getContentAsString().equals(content); - }); - - var response = requestHelper.executeGet(url); - assertThat(response.getResponse().getContentAsString()).isEqualTo(content); - } - - @Override - protected void verifyTwoDeleteEvents( - AttachmentEntity itemAttachmentEntityAfterChange, Attachments itemAttachmentAfterChange) { - // no service handler - nothing to do - } - - @Override - protected void verifyNumberOfEvents(String event, int number) { - // no service handler - nothing to do - } - - @Override - protected void verifyContentId( - Attachments attachmentWithExpectedContent, String attachmentId, String contentId) { - assertThat(attachmentWithExpectedContent.getContentId()).isEqualTo(attachmentId); - } - - @Override - protected void verifyContentAndContentId( - Attachments attachment, String testContent, Attachments itemAttachment) throws IOException { - assertThat(attachment.getContent().readAllBytes()) - .isEqualTo(testContent.getBytes(StandardCharsets.UTF_8)); - assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getId()); - } - - @Override - protected void verifyContentAndContentIdForAttachmentEntity( - AttachmentEntity attachment, String testContent, AttachmentEntity itemAttachment) - throws IOException { - assertThat(attachment.getContent().readAllBytes()) - .isEqualTo(testContent.getBytes(StandardCharsets.UTF_8)); - assertThat(attachment.getContentId()).isEqualTo(itemAttachment.getId()); - } - - @Override - protected void clearServiceHandlerContext() { - // no service handler - nothing to do - } - - @Override - protected void clearServiceHandlerDocuments() { - // no service handler - nothing to do - } - - @Override - protected void verifySingleCreateEvent(String contentId, String content) { - // no service handler - nothing to do - } - - @Override - protected void verifySingleCreateAndUpdateEvent( - String resultContentId, String toBeDeletedContentId, String content) { - // no service handler - nothing to do - } - - @Override - protected void verifySingleDeletionEvent(String contentId) { - // no service handler - nothing to do - } - - @Override - protected void verifySingleReadEvent(String contentId) { - // no service handler - nothing to do - } - - @Override - protected void verifyNoAttachmentEventsCalled() { - // no service handler - nothing to do - } - - @Override - protected void verifyEventContextEmptyForEvent(String... events) { - // no service handler - nothing to do - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java deleted file mode 100644 index 0f5e1bbfe..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsBuilder.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper; - -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.sap.attachments.Attachments; - -public class AttachmentsBuilder { - - private Attachments attachment; - - private AttachmentsBuilder() { - attachment = Attachments.create(); - } - - public static AttachmentsBuilder create() { - return new AttachmentsBuilder(); - } - - public AttachmentsBuilder setMimeType(String mimeType) { - attachment.setMimeType(mimeType); - return this; - } - - public AttachmentsBuilder setFileName(String fileName) { - attachment.setFileName(fileName); - return this; - } - - public Attachments build() { - return attachment; - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java deleted file mode 100644 index 2da95a144..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/AttachmentsEntityBuilder.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper; - -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.AttachmentEntity; - -public class AttachmentsEntityBuilder { - - private AttachmentEntity attachmentEntity = AttachmentEntity.create(); - - private AttachmentsEntityBuilder() {} - - public static AttachmentsEntityBuilder create() { - return new AttachmentsEntityBuilder(); - } - - public AttachmentsEntityBuilder setMimeType(String mimeType) { - attachmentEntity.setMimeType(mimeType); - return this; - } - - public AttachmentsEntityBuilder setFileName(String fileName) { - attachmentEntity.setFileName(fileName); - return this; - } - - public AttachmentEntity build() { - return attachmentEntity; - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java deleted file mode 100644 index 9b6ab7017..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/ItemEntityBuilder.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper; - -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Items; -import java.util.ArrayList; -import java.util.Arrays; - -public class ItemEntityBuilder { - - private final Items item; - - private ItemEntityBuilder() { - item = Items.create(); - item.setAttachments(new ArrayList<>()); - item.setAttachmentEntities(new ArrayList<>()); - } - - public static ItemEntityBuilder create() { - return new ItemEntityBuilder(); - } - - public ItemEntityBuilder setTitle(String title) { - item.setTitle(title); - return this; - } - - public ItemEntityBuilder addAttachmentEntities(AttachmentsEntityBuilder... attachmentEntities) { - Arrays.stream(attachmentEntities) - .forEach(attachment -> item.getAttachmentEntities().add(attachment.build())); - return this; - } - - public ItemEntityBuilder addAttachments(AttachmentsBuilder... attachmentEntities) { - Arrays.stream(attachmentEntities) - .forEach(attachment -> item.getAttachments().add(attachment.build())); - return this; - } - - public Items build() { - return item; - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java deleted file mode 100644 index 9efc70dfc..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/nondraftservice/helper/RootEntityBuilder.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * © 2024-2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.nondraftservice.helper; - -import com.sap.cds.feature.attachments.generated.integration.test.cds4j.testservice.Roots; -import java.util.ArrayList; -import java.util.Arrays; - -public class RootEntityBuilder { - - private final Roots rootEntity; - - private RootEntityBuilder() { - rootEntity = Roots.create(); - rootEntity.setAttachments(new ArrayList<>()); - rootEntity.setItems(new ArrayList<>()); - rootEntity.setSizeLimitedAttachments(new ArrayList<>()); - } - - public static RootEntityBuilder create() { - return new RootEntityBuilder(); - } - - public RootEntityBuilder setTitle(String title) { - rootEntity.setTitle(title); - return this; - } - - public RootEntityBuilder addAttachments(AttachmentsEntityBuilder... attachments) { - Arrays.stream(attachments) - .forEach(attachment -> rootEntity.getAttachments().add(attachment.build())); - return this; - } - - public RootEntityBuilder addSizeLimitedAttachments(AttachmentsBuilder... attachments) { - Arrays.stream(attachments) - .forEach(attachment -> rootEntity.getSizeLimitedAttachments().add(attachment.build())); - return this; - } - - public RootEntityBuilder addItems(ItemEntityBuilder... items) { - Arrays.stream(items).forEach(item -> rootEntity.getItems().add(item.build())); - return this; - } - - public Roots build() { - return rootEntity; - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java deleted file mode 100644 index 9dcf8875f..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPersistenceHandlerTest.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.testhandler; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.sap.cds.services.ServiceException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class TestPersistenceHandlerTest { - - private TestPersistenceHandler testPersistenceHandler; - - @BeforeEach - void setUp() { - testPersistenceHandler = new TestPersistenceHandler(); - } - - @Test - void testReset() { - // Set both flags to true - testPersistenceHandler.setThrowExceptionOnUpdate(true); - testPersistenceHandler.setThrowExceptionOnCreate(true); - - // Reset should set both flags to false - testPersistenceHandler.reset(); - - // Verify no exceptions are thrown after reset - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); - } - - @Test - void testThrowExceptionOnUpdateWhenEnabled() { - testPersistenceHandler.setThrowExceptionOnUpdate(true); - - ServiceException exception = - assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnUpdate()); - - assertTrue(exception.getMessage().contains("Exception on update")); - } - - @Test - void testThrowExceptionOnUpdateWhenDisabled() { - testPersistenceHandler.setThrowExceptionOnUpdate(false); - - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); - } - - @Test - void testThrowExceptionOnCreateWhenEnabled() { - testPersistenceHandler.setThrowExceptionOnCreate(true); - - ServiceException exception = - assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnCreate()); - - assertTrue(exception.getMessage().contains("Exception on create")); - } - - @Test - void testThrowExceptionOnCreateWhenDisabled() { - testPersistenceHandler.setThrowExceptionOnCreate(false); - - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); - } - - @Test - void testSetThrowExceptionOnUpdate() { - // Test setting to true - testPersistenceHandler.setThrowExceptionOnUpdate(true); - assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnUpdate()); - - // Test setting to false - testPersistenceHandler.setThrowExceptionOnUpdate(false); - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); - } - - @Test - void testSetThrowExceptionOnCreate() { - // Test setting to true - testPersistenceHandler.setThrowExceptionOnCreate(true); - assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnCreate()); - - // Test setting to false - testPersistenceHandler.setThrowExceptionOnCreate(false); - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); - } - - @Test - void testDefaultBehavior() { - // By default, both flags should be false - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); - } - - @Test - void testIndependentFlagBehavior() { - // Test that the flags work independently - testPersistenceHandler.setThrowExceptionOnUpdate(true); - testPersistenceHandler.setThrowExceptionOnCreate(false); - - assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnUpdate()); - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnCreate()); - - // Switch them - testPersistenceHandler.setThrowExceptionOnUpdate(false); - testPersistenceHandler.setThrowExceptionOnCreate(true); - - assertDoesNotThrow(() -> testPersistenceHandler.throwExceptionOnUpdate()); - assertThrows(ServiceException.class, () -> testPersistenceHandler.throwExceptionOnCreate()); - } -} diff --git a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java b/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java deleted file mode 100644 index bc391fe9b..000000000 --- a/integration-tests/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java +++ /dev/null @@ -1,266 +0,0 @@ -/* - * © 2024-2024 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.integrationtests.testhandler; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.*; - -import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; -import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.StatusCode; -import com.sap.cds.feature.attachments.service.AttachmentService; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; -import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class TestPluginAttachmentsServiceHandlerTest { - - private TestPluginAttachmentsServiceHandler cut; - - @BeforeEach - void setup() { - cut = new TestPluginAttachmentsServiceHandler(); - // Clear any previous test data - cut.clearEventContext(); - cut.clearDocuments(); - } - - @Test - void readIsWorking() { - var context = AttachmentReadEventContext.create(); - context.setContentId("test"); - context.setData(MediaData.create()); - - cut.readAttachment(context); - - assertThat(context.getData().getContent()).isNull(); - } - - @Test - void readWithContentIsWorking() throws IOException { - var createContext = AttachmentCreateEventContext.create(); - createContext.setData(MediaData.create()); - createContext - .getData() - .setContent(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))); - cut.createAttachment(createContext); - - var context = AttachmentReadEventContext.create(); - context.setContentId(createContext.getContentId()); - context.setData(MediaData.create()); - - cut.readAttachment(context); - - assertThat(context.getData().getContent().readAllBytes()) - .isEqualTo("test".getBytes(StandardCharsets.UTF_8)); - } - - @Test - void dummyTestForDelete() { - var context = AttachmentMarkAsDeletedEventContext.create(); - context.setContentId("test"); - - assertDoesNotThrow(() -> cut.markAttachmentAsDeleted(context)); - } - - @Test - void dummyTestForCreate() throws IOException { - var context = AttachmentCreateEventContext.create(); - context.setData(MediaData.create()); - var stream = mock(InputStream.class); - when(stream.readAllBytes()).thenReturn("test".getBytes(StandardCharsets.UTF_8)); - context.getData().setContent(stream); - - assertDoesNotThrow(() -> cut.createAttachment(context)); - } - - @Test - void dummyTestForRestore() { - var context = AttachmentRestoreEventContext.create(); - context.setRestoreTimestamp(Instant.now()); - - assertDoesNotThrow(() -> cut.restoreAttachment(context)); - } - - @Test - void testCreateAttachmentSetsContentIdAndStatus() throws IOException { - var context = AttachmentCreateEventContext.create(); - context.setData(MediaData.create()); - context - .getData() - .setContent(new ByteArrayInputStream("test content".getBytes(StandardCharsets.UTF_8))); - - cut.createAttachment(context); - - assertNotNull(context.getContentId()); - assertThat(context.getData().getStatus()).isEqualTo(StatusCode.CLEAN); - } - - @Test - void testEventContextTracking() throws IOException { - // Test create event tracking - var createContext = AttachmentCreateEventContext.create(); - createContext.setData(MediaData.create()); - createContext - .getData() - .setContent(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))); - cut.createAttachment(createContext); - - List createEvents = - cut.getEventContextForEvent(AttachmentService.EVENT_CREATE_ATTACHMENT); - assertThat(createEvents).hasSize(1); - assertThat(createEvents.get(0).event()).isEqualTo(AttachmentService.EVENT_CREATE_ATTACHMENT); - - // Test read event tracking - var readContext = AttachmentReadEventContext.create(); - readContext.setContentId("test-id"); - readContext.setData(MediaData.create()); - cut.readAttachment(readContext); - - List readEvents = - cut.getEventContextForEvent(AttachmentService.EVENT_READ_ATTACHMENT); - assertThat(readEvents).hasSize(1); - assertThat(readEvents.get(0).event()).isEqualTo(AttachmentService.EVENT_READ_ATTACHMENT); - - // Test delete event tracking - var deleteContext = AttachmentMarkAsDeletedEventContext.create(); - deleteContext.setContentId("test-id"); - cut.markAttachmentAsDeleted(deleteContext); - - List deleteEvents = - cut.getEventContextForEvent(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - assertThat(deleteEvents).hasSize(1); - assertThat(deleteEvents.get(0).event()) - .isEqualTo(AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED); - - // Test restore event tracking - var restoreContext = AttachmentRestoreEventContext.create(); - restoreContext.setRestoreTimestamp(Instant.now()); - cut.restoreAttachment(restoreContext); - - List restoreEvents = - cut.getEventContextForEvent(AttachmentService.EVENT_RESTORE_ATTACHMENT); - assertThat(restoreEvents).hasSize(1); - assertThat(restoreEvents.get(0).event()).isEqualTo(AttachmentService.EVENT_RESTORE_ATTACHMENT); - } - - @Test - void testGetAllEventContext() throws IOException { - // Create multiple events - var createContext = AttachmentCreateEventContext.create(); - createContext.setData(MediaData.create()); - createContext - .getData() - .setContent(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))); - cut.createAttachment(createContext); - - var readContext = AttachmentReadEventContext.create(); - readContext.setContentId("test-id"); - readContext.setData(MediaData.create()); - cut.readAttachment(readContext); - - List allEvents = cut.getEventContext(); - assertThat(allEvents).hasSize(2); - } - - @Test - void testClearEventContext() throws IOException { - // Add some events - var context = AttachmentCreateEventContext.create(); - context.setData(MediaData.create()); - context.getData().setContent(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))); - cut.createAttachment(context); - - assertThat(cut.getEventContext()).hasSize(1); - - // Clear and verify - cut.clearEventContext(); - assertThat(cut.getEventContext()).isEmpty(); - } - - @Test - void testReadWithNullContentId() { - var context = AttachmentReadEventContext.create(); - context.setContentId(null); - context.setData(MediaData.create()); - - cut.readAttachment(context); - - assertThat(context.getData().getContent()).isNull(); - } - - @Test - void testCreateAttachmentWithEmptyContent() throws IOException { - var context = AttachmentCreateEventContext.create(); - context.setData(MediaData.create()); - context.getData().setContent(new ByteArrayInputStream(new byte[0])); - - cut.createAttachment(context); - - assertNotNull(context.getContentId()); - assertThat(context.getData().getStatus()).isEqualTo(StatusCode.CLEAN); - } - - @Test - void testMultipleCreateAndReadOperations() throws IOException { - // Create first attachment - var createContext1 = AttachmentCreateEventContext.create(); - createContext1.setData(MediaData.create()); - createContext1 - .getData() - .setContent(new ByteArrayInputStream("content1".getBytes(StandardCharsets.UTF_8))); - cut.createAttachment(createContext1); - - // Create second attachment - var createContext2 = AttachmentCreateEventContext.create(); - createContext2.setData(MediaData.create()); - createContext2 - .getData() - .setContent(new ByteArrayInputStream("content2".getBytes(StandardCharsets.UTF_8))); - cut.createAttachment(createContext2); - - // Read first attachment - var readContext1 = AttachmentReadEventContext.create(); - readContext1.setContentId(createContext1.getContentId()); - readContext1.setData(MediaData.create()); - cut.readAttachment(readContext1); - - // Read second attachment - var readContext2 = AttachmentReadEventContext.create(); - readContext2.setContentId(createContext2.getContentId()); - readContext2.setData(MediaData.create()); - cut.readAttachment(readContext2); - - // Verify content - assertThat(readContext1.getData().getContent().readAllBytes()) - .isEqualTo("content1".getBytes(StandardCharsets.UTF_8)); - assertThat(readContext2.getData().getContent().readAllBytes()) - .isEqualTo("content2".getBytes(StandardCharsets.UTF_8)); - } - - @Test - void testRestoreWithSpecificTimestamp() { - Instant timestamp = Instant.parse("2024-01-01T12:00:00Z"); - var context = AttachmentRestoreEventContext.create(); - context.setRestoreTimestamp(timestamp); - - cut.restoreAttachment(context); - - List restoreEvents = - cut.getEventContextForEvent(AttachmentService.EVENT_RESTORE_ATTACHMENT); - assertThat(restoreEvents).hasSize(1); - var restoredContext = (AttachmentRestoreEventContext) restoreEvents.get(0).context(); - assertThat(restoredContext.getRestoreTimestamp()).isEqualTo(timestamp); - } -} diff --git a/integration-tests/srv/src/test/resources/application.yaml b/integration-tests/srv/src/test/resources/application.yaml deleted file mode 100644 index 54849379b..000000000 --- a/integration-tests/srv/src/test/resources/application.yaml +++ /dev/null @@ -1,15 +0,0 @@ -cds: - dataSource: - csv: - paths: "../db/src/gen/csv" - ---- -spring: - config: - activate: - on-profile: malware-scan-enabled - -cds: - environment: - local: - defaultEnvPath: "classpath:xsuaa-env.json" diff --git a/integration-tests/srv/src/test/resources/logback-test.xml b/integration-tests/srv/src/test/resources/logback-test.xml deleted file mode 100644 index 023662ed8..000000000 --- a/integration-tests/srv/src/test/resources/logback-test.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - %date %-5level [%thread] [%marker]: %msg%nopex [%logger] [%mdc{correlation_id}]%n - - - - - - - - - - - - - - - diff --git a/integration-tests/srv/src/test/resources/xsuaa-env.json b/integration-tests/srv/src/test/resources/xsuaa-env.json deleted file mode 100644 index 7adbe70b0..000000000 --- a/integration-tests/srv/src/test/resources/xsuaa-env.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "VCAP_SERVICES": { - "malware-scanner": [ - { - "label": "malware-scanner", - "provider": null, - "plan": "clamav", - "name": "dsr-core-malware-scanner", - "tags": [], - "instance_guid": "2fbe12be-569d-473e-ab0a-eb2f1d18c7e3", - "instance_name": "dsr-core-malware-scanner", - "binding_guid": "16ce4d70-1511-45d0-a788-e365b298ca8a", - "binding_name": null, - "credentials": { - "sync_scan_url": "https://test.scanner.com", - "async_scan_url": "", - "uri": "https://test.scanner.com", - "url": "http://localhost:1111", - "username": "test-user", - "password": "test-password" - }, - "syslog_drain_url": null, - "volume_mounts": [] - } - ] - }, - "VCAP_APPLICATION": { - "application_id": "xsapp!t0815" - } -} diff --git a/integration-tests/srv/test-service.cds b/integration-tests/srv/test-service.cds deleted file mode 100644 index ff68a31ff..000000000 --- a/integration-tests/srv/test-service.cds +++ /dev/null @@ -1,27 +0,0 @@ -using test.data.model as db from '../db/data-model'; - -annotate db.Roots.sizeLimitedAttachments with { - content @Validation.Maximum: '5MB'; -}; - -// Media type validation for attachments - for testing purposes. -annotate db.Roots.mediaValidatedAttachments with { - content @(Core.AcceptableMediaTypes: [ - 'image/jpeg', - 'image/png' - ]); -} - -annotate db.Roots.mimeValidatedAttachments with { - content @(Core.AcceptableMediaTypes: ['application/pdf']); -} - -service TestService { - entity Roots as projection on db.Roots; - entity AttachmentEntity as projection on db.AttachmentEntity; -} - -service TestDraftService { - @odata.draft.enabled - entity DraftRoots as projection on db.Roots; -} From afd2dcc4c044da1885fca32dbd08d5bfb042ede5 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 11:22:34 +0200 Subject: [PATCH 13/39] Remove package-lock.json from mtx-local integration tests Also add package-lock.json to .gitignore to prevent accidental commits. --- .gitignore | 1 + integration-tests/mtx-local/package-lock.json | 4144 ----------------- 2 files changed, 1 insertion(+), 4144 deletions(-) delete mode 100644 integration-tests/mtx-local/package-lock.json diff --git a/.gitignore b/.gitignore index 9ca83a529..ca9f60e59 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ target/ .flattened-pom.xml node_modules +package-lock.json ## PMD .pmd diff --git a/integration-tests/mtx-local/package-lock.json b/integration-tests/mtx-local/package-lock.json deleted file mode 100644 index 3383043ec..000000000 --- a/integration-tests/mtx-local/package-lock.json +++ /dev/null @@ -1,4144 +0,0 @@ -{ - "name": "mtx-local-integration-tests", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "mtx-local-integration-tests", - "version": "0.0.0", - "workspaces": [ - "mtx/sidecar" - ], - "devDependencies": { - "@sap/cds-dk": "^9", - "@sap/cds-mtxs": "^3" - } - }, - "mtx/sidecar": { - "name": "mtx-local-sidecar", - "version": "0.0.0", - "dependencies": { - "@sap/cds": "^9", - "@sap/cds-mtxs": "^3", - "@sap/xssec": "^4", - "express": "^4" - }, - "devDependencies": { - "@cap-js/sqlite": "^2" - } - }, - "mtx/sidecar/node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "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.14.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/@cap-js/db-service": { - "version": "2.9.0", - "resolved": "https://int.repositories.cloud.sap/artifactory/api/npm/build-milestones-npm/@cap-js/db-service/-/db-service-2.9.0.tgz", - "integrity": "sha512-WCXhoqezaF6A5I2l0MNZeHKXXtHRNEq7Rp0R89/uccOHQIx0DuU0U9NuJJPV/1G5RGk2QKQ9VBo/KYn+MZuuNQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "generic-pool": "^3.9.0" - }, - "peerDependencies": { - "@sap/cds": ">=9.8" - } - }, - "node_modules/@cap-js/sqlite": { - "version": "2.2.0", - "resolved": "https://int.repositories.cloud.sap/artifactory/api/npm/build-milestones-npm/@cap-js/sqlite/-/sqlite-2.2.0.tgz", - "integrity": "sha512-FPj+uVU/14vtGUl2P/Q8y7XhZbsLgrCav2O5PjHPXnupegjby4sMJkgVNxVHnkyKPFgO/W8uEsq9r5TU9VPx8w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@cap-js/db-service": "^2.9.0", - "better-sqlite3": "^12.0.0" - }, - "peerDependencies": { - "@sap/cds": ">=9.8", - "sql.js": "^1.13.0" - }, - "peerDependenciesMeta": { - "sql.js": { - "optional": true - } - } - }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "license": "MIT", - "peer": true, - "engines": { - "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": { - "version": "9.8.4", - "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-9.8.4.tgz", - "integrity": "sha512-1cnFdz4Ex4LV15dztDsSLDiilVdJ2mfTqr5A3TGsRd/6cM31SnHhuZ/Ra0FEErodokNS2pwasGZmSfgHVDW8KA==", - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/cds-compiler": "^6.4", - "@sap/cds-fiori": "^2", - "express": "^4.22.1 || ^5", - "js-yaml": "^4.1.1" - }, - "bin": { - "cds-deploy": "bin/deploy.js", - "cds-serve": "bin/serve.js" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@eslint/js": "^9 || ^10", - "tar": "^7.5.6" - }, - "peerDependenciesMeta": { - "tar": { - "optional": true - } - } - }, - "node_modules/@sap/cds-compiler": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-6.8.0.tgz", - "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", - "license": "SEE LICENSE IN LICENSE", - "bin": { - "cdsc": "bin/cdsc.js", - "cdshi": "bin/cdshi.js", - "cdsse": "bin/cdsse.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@sap/cds-dk": { - "version": "9.8.2", - "resolved": "https://registry.npmjs.org/@sap/cds-dk/-/cds-dk-9.8.2.tgz", - "integrity": "sha512-PGynoQpOCD8Nsr0EEzsEHWg8szYBLVtVd6byxvaaAPYBkNAj2WA+kin6gWaDTiyG3kJa0BSGPk4PCbA2xDoE9A==", - "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/hdi-deploy": "^5", - "axios": "^1", - "express": "^4.22.1 || ^5", - "hdb": "^2.0.0", - "livereload-js": "^4.0.1", - "mustache": "^4.0.1", - "ws": "^8.4.2", - "xml-js": "^1.6.11", - "yaml": "^2" - }, - "bin": { - "cds": "bin/cds.js", - "cds-ts": "bin/cds-ts.js", - "cds-tsx": "bin/cds-tsx.js" - }, - "optionalDependencies": { - "@cap-js/sqlite": ">=1" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/asyncapi": { - "version": "1.0.3", - "integrity": "sha512-vZSWKAe+3qfvZDXV5SSFiObGWmqyS9MDyEADb5PLVT8kzO39qGaSDPv/GzI/gwvRfCayGAjU4ThiBKrFA7Gclg==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "peerDependencies": { - "@sap/cds": ">=7.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/db-service": { - "version": "2.9.0", - "integrity": "sha512-WCXhoqezaF6A5I2l0MNZeHKXXtHRNEq7Rp0R89/uccOHQIx0DuU0U9NuJJPV/1G5RGk2QKQ9VBo/KYn+MZuuNQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "generic-pool": "^3.9.0" - }, - "peerDependencies": { - "@sap/cds": ">=9.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/openapi": { - "version": "1.4.0", - "integrity": "sha512-/LRSwn4SDxAi3qKwl09zoOhEVGaPGlYOPz/0S3UBnaMJVvaLyPiKbbaOtOnrrgulUX5OXt+ujPIQznOsbTzuAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "pluralize": "^8.0.0" - }, - "peerDependencies": { - "@sap/cds": ">=7.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/sqlite": { - "version": "2.2.0", - "integrity": "sha512-FPj+uVU/14vtGUl2P/Q8y7XhZbsLgrCav2O5PjHPXnupegjby4sMJkgVNxVHnkyKPFgO/W8uEsq9r5TU9VPx8w==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@cap-js/db-service": "^2.9.0", - "better-sqlite3": "^12.0.0" - }, - "peerDependencies": { - "@sap/cds": ">=9.8", - "sql.js": "^1.13.0" - }, - "peerDependenciesMeta": { - "sql.js": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@eslint/js": { - "version": "10.0.1", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "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.8.3", - "integrity": "sha512-up/kNVCZK8bOTyye1QN2BjMpH+IgcdyhT3U9mS3TqkpmjNE2JXq8Qz1iNAN+nYcXRWJ1XAUNpgebLfuh/49MGQ==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/cds-compiler": "^6.4", - "@sap/cds-fiori": "^2", - "express": "^4.22.1 || ^5", - "js-yaml": "^4.1.1" - }, - "bin": { - "cds-deploy": "bin/deploy.js", - "cds-serve": "bin/serve.js" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@eslint/js": "^9 || ^10", - "tar": "^7.5.6" - }, - "peerDependenciesMeta": { - "tar": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/cds-compiler": { - "version": "6.8.0", - "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "bin": { - "cdsc": "bin/cdsc.js", - "cdshi": "bin/cdshi.js", - "cdsse": "bin/cdsse.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/cds-fiori": { - "version": "2.3.0", - "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "peerDependencies": { - "@sap/cds": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/cds-mtxs": { - "version": "3.8.1", - "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/hdi-deploy": "^5" - }, - "bin": { - "cds-mtx": "bin/cds-mtx.js", - "cds-mtx-migrate": "bin/cds-mtx-migrate.js" - }, - "peerDependencies": { - "@sap/cds": "^9" - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/hdi": { - "version": "4.8.0", - "integrity": "sha512-tkJmY2ffm6mt4/LFwRBihlQkMxNAXa3ngvRe2N/6+qLIsUNdrH/M03S5mkygXq56K+KoVVZYuradajCusMWwsw==", - "dev": true, - "license": "See LICENSE file", - "dependencies": { - "async": "^3.2.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.5", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/hdi-deploy": { - "version": "5.6.1", - "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", - "dev": true, - "license": "See LICENSE file", - "dependencies": { - "@sap/hdi": "^4.8.0", - "@sap/xsenv": "^6.0.0", - "async": "^3.2.6", - "dotenv": "^16.4.5", - "handlebars": "^4.7.8", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=18.x" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.6", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/xsenv": { - "version": "6.1.0", - "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", - "dev": true, - "license": "SEE LICENSE IN LICENSE file", - "dependencies": { - "debug": "4.4.3", - "node-cache": "^5.1.2", - "verror": "1.10.1" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/accepts": { - "version": "2.0.0", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "dependencies": { - "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/assert-plus": { - "version": "1.0.0", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/async": { - "version": "3.2.6", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "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.13.6", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/base64-js": { - "version": "1.5.1", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "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", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/better-sqlite3": { - "version": "12.8.0", - "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - }, - "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x" - } - }, - "node_modules/@sap/cds-dk/node_modules/bindings": { - "version": "1.5.0", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/bl": { - "version": "4.1.0", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/body-parser": { - "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": "^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": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/braces": { - "version": "3.0.3", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/buffer": { - "version": "5.7.1", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "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", - "optional": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/@sap/cds-dk/node_modules/bytes": { - "version": "3.1.2", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/call-bound": { - "version": "1.0.4", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "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/@sap/cds-dk/node_modules/chownr": { - "version": "1.1.4", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/clone": { - "version": "2.1.2", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "dev": true, - "license": "MIT", - "engines": { - "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": "1.0.1", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/content-type": { - "version": "1.0.5", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/cookie": { - "version": "0.7.2", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/cookie-signature": { - "version": "1.2.2", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/core-util-is": { - "version": "1.0.2", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/debug": { - "version": "4.4.3", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/decompress-response": { - "version": "6.0.0", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sap/cds-dk/node_modules/deep-extend": { - "version": "0.6.0", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/detect-libc": { - "version": "2.1.2", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/dotenv": { - "version": "16.6.1", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/@sap/cds-dk/node_modules/dunder-proto": { - "version": "1.0.1", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/ee-first": { - "version": "1.1.1", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/encodeurl": { - "version": "2.0.0", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/end-of-stream": { - "version": "1.4.5", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/es-define-property": { - "version": "1.0.1", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/es-errors": { - "version": "1.3.0", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/es-object-atoms": { - "version": "1.1.1", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/etag": { - "version": "1.8.1", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/expand-template": { - "version": "2.0.3", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "license": "(MIT OR WTFPL)", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sap/cds-dk/node_modules/express": { - "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/extsprintf": { - "version": "1.4.1", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/file-uri-to-path": { - "version": "1.0.0", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/fill-range": { - "version": "7.1.1", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/finalhandler": { - "version": "2.1.1", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "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": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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.5", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "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" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/forwarded": { - "version": "0.2.0", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/fresh": { - "version": "2.0.0", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/fs-constants": { - "version": "1.0.0", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/function-bind": { - "version": "1.1.2", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/generic-pool": { - "version": "3.9.0", - "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/@sap/cds-dk/node_modules/get-intrinsic": { - "version": "1.3.0", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "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/@sap/cds-dk/node_modules/get-proto": { - "version": "1.0.1", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/github-from-package": { - "version": "0.0.0", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/gopd": { - "version": "1.2.0", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/handlebars": { - "version": "4.7.8", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/has-symbols": { - "version": "1.1.0", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/hdb": { - "version": "2.27.1", - "integrity": "sha512-xYL/W+fq2TyGHyzm8muolQnw8tdh4+2NQ8mQP2FpLSuhfJ8l0jQNSUZoAXic7NfMEan1Jvf8V1L4blwkgTc6+A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "iconv-lite": "0.7.0" - }, - "engines": { - "node": ">= 18" - }, - "optionalDependencies": { - "lz4-wasm-nodejs": "0.9.2" - } - }, - "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.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": { - "version": "1.2.1", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "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": "BSD-3-Clause", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/inherits": { - "version": "2.0.4", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@sap/cds-dk/node_modules/ini": { - "version": "1.3.8", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/ipaddr.js": { - "version": "1.9.1", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/@sap/cds-dk/node_modules/is-number": { - "version": "7.0.0", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/is-promise": { - "version": "4.0.0", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/js-yaml": { - "version": "4.1.1", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@sap/cds-dk/node_modules/livereload-js": { - "version": "4.0.2", - "integrity": "sha512-Fy7VwgQNiOkynYyNBTo3v9hQUhcW5pFAheJN148+DTgpShjsy/22pLHKKwDK5v0kOsZsJBK+6q1PMgLvRmrwFQ==", - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/media-typer": { - "version": "1.1.0", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/merge-descriptors": { - "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/micromatch": { - "version": "4.0.8", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@sap/cds-dk/node_modules/mime-db": { - "version": "1.54.0", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/mime-types": { - "version": "3.0.2", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/mimic-response": { - "version": "3.1.0", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sap/cds-dk/node_modules/minimist": { - "version": "1.2.8", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/mkdirp-classic": { - "version": "0.5.3", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/ms": { - "version": "2.1.3", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/mustache": { - "version": "4.2.0", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", - "dev": true, - "license": "MIT", - "bin": { - "mustache": "bin/mustache" - } - }, - "node_modules/@sap/cds-dk/node_modules/napi-build-utils": { - "version": "2.0.0", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/negotiator": { - "version": "1.0.0", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/neo-async": { - "version": "2.6.2", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/node-abi": { - "version": "3.89.0", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@sap/cds-dk/node_modules/node-cache": { - "version": "5.1.2", - "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "2.x" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/object-inspect": { - "version": "1.13.4", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/on-finished": { - "version": "2.4.1", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/once": { - "version": "1.4.0", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/@sap/cds-dk/node_modules/parseurl": { - "version": "1.3.3", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/path-to-regexp": { - "version": "8.3.0", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/pluralize": { - "version": "8.0.0", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "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, - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@sap/cds-dk/node_modules/proxy-addr": { - "version": "2.0.7", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "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.4", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/@sap/cds-dk/node_modules/qs": { - "version": "6.15.0", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/range-parser": { - "version": "1.2.1", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/raw-body": { - "version": "3.0.2", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/@sap/cds-dk/node_modules/rc": { - "version": "1.2.8", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "optional": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/@sap/cds-dk/node_modules/readable-stream": { - "version": "3.6.2", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "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==", - "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", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/safer-buffer": { - "version": "2.1.2", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/sax": { - "version": "1.6.0", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/semver": { - "version": "7.7.4", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@sap/cds-dk/node_modules/send": { - "version": "1.2.1", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "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": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/serve-static": { - "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": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/setprototypeof": { - "version": "1.2.0", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, - "node_modules/@sap/cds-dk/node_modules/side-channel": { - "version": "1.1.0", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "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/@sap/cds-dk/node_modules/side-channel-list": { - "version": "1.0.0", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/side-channel-map": { - "version": "1.0.1", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "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/@sap/cds-dk/node_modules/side-channel-weakmap": { - "version": "1.0.2", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "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/@sap/cds-dk/node_modules/simple-concat": { - "version": "1.0.1", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "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", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/simple-get": { - "version": "4.0.1", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "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", - "optional": true, - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/source-map": { - "version": "0.6.1", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/statuses": { - "version": "2.0.2", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/string_decoder": { - "version": "1.3.0", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/strip-json-comments": { - "version": "2.0.1", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/tar-fs": { - "version": "2.1.4", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/tar-stream": { - "version": "2.2.0", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sap/cds-dk/node_modules/to-regex-range": { - "version": "5.0.1", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/toidentifier": { - "version": "1.0.1", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/tunnel-agent": { - "version": "0.6.0", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@sap/cds-dk/node_modules/type-is": { - "version": "2.0.1", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/uglify-js": { - "version": "3.19.3", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/unpipe": { - "version": "1.0.0", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/util-deprecate": { - "version": "1.0.2", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/vary": { - "version": "1.1.2", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/verror": { - "version": "1.10.1", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/wordwrap": { - "version": "1.0.0", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/wrappy": { - "version": "1.0.2", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@sap/cds-dk/node_modules/ws": { - "version": "8.20.0", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/xml-js": { - "version": "1.6.11", - "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "sax": "^1.2.4" - }, - "bin": { - "xml-js": "bin/cli.js" - } - }, - "node_modules/@sap/cds-dk/node_modules/yaml": { - "version": "2.8.3", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/@sap/cds-fiori": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sap/cds-fiori/-/cds-fiori-2.3.0.tgz", - "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", - "license": "SEE LICENSE IN LICENSE", - "peerDependencies": { - "@sap/cds": ">=8" - } - }, - "node_modules/@sap/cds-mtxs": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@sap/cds-mtxs/-/cds-mtxs-3.8.1.tgz", - "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/hdi-deploy": "^5" - }, - "bin": { - "cds-mtx": "bin/cds-mtx.js", - "cds-mtx-migrate": "bin/cds-mtx-migrate.js" - }, - "peerDependencies": { - "@sap/cds": "^9" - } - }, - "node_modules/@sap/hdi": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@sap/hdi/-/hdi-4.8.0.tgz", - "integrity": "sha512-tkJmY2ffm6mt4/LFwRBihlQkMxNAXa3ngvRe2N/6+qLIsUNdrH/M03S5mkygXq56K+KoVVZYuradajCusMWwsw==", - "license": "See LICENSE file", - "dependencies": { - "async": "^3.2.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.5", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/hdi-deploy": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@sap/hdi-deploy/-/hdi-deploy-5.6.1.tgz", - "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", - "license": "See LICENSE file", - "dependencies": { - "@sap/hdi": "^4.8.0", - "@sap/xsenv": "^6.0.0", - "async": "^3.2.6", - "dotenv": "^16.4.5", - "handlebars": "^4.7.8", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=18.x" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.6", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/xsenv": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sap/xsenv/-/xsenv-6.1.0.tgz", - "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", - "license": "SEE LICENSE IN LICENSE file", - "dependencies": { - "debug": "4.4.3", - "node-cache": "^5.1.2", - "verror": "1.10.1" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@sap/xsenv/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@sap/xsenv/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/@sap/xssec": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@sap/xssec/-/xssec-4.13.0.tgz", - "integrity": "sha512-8e+bU+OyAIpAGXQanOopZa5YEK+yHKw84dhhihcCotF40MSNFbVHjQ4xM5hf4QndlqDGfXIuvXmoOMuDATa/gA==", - "license": "SAP DEVELOPER LICENSE AGREEMENT", - "dependencies": { - "debug": "^4.4.3", - "jwt-decode": "^4" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sap/xssec/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@sap/xssec/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "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==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "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==", - "license": "MIT" - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "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" - }, - "node_modules/better-sqlite3": { - "version": "12.8.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", - "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - }, - "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "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.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "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", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC" - }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "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/express/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "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": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/express/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/express/node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "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": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "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/express/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/express/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/express/node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "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": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "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.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT" - }, - "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==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generic-pool": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", - "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/handlebars": { - "version": "4.7.9", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "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/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "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": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC" - }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jwt-decode": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/mtx-local-sidecar": { - "resolved": "mtx/sidecar", - "link": true - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" - }, - "node_modules/node-abi": { - "version": "3.89.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-cache": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", - "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", - "license": "MIT", - "dependencies": { - "clone": "2.x" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "license": "MIT" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "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/router/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/router/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", - "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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==", - "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" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "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/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "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==", - "license": "ISC" - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "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" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "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", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "license": "MIT" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - } - } -} From 0e38a8aaa5df9b1ae2f7ad531b66e9bb1cd20e77 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 11:28:53 +0200 Subject: [PATCH 14/39] improvements --- .../system/SubscribeModelTenantsHandler.java | 10 ++- .../MultiTenantAttachmentIsolationTest.java | 2 + .../mt/utils/SubscriptionEndpointClient.java | 2 +- .../attachments/oss/client/AzureClient.java | 44 ++++++--- .../attachments/oss/client/GoogleClient.java | 9 +- .../oss/configuration/Registration.java | 15 ++++ .../handler/OSSAttachmentsServiceHandler.java | 5 +- .../oss/handler/TenantCleanupHandler.java | 1 + .../attachments/oss/client/AWSClientTest.java | 49 ++++++++++ .../oss/configuration/RegistrationTest.java | 89 +++++++++++++++++++ .../OSSAttachmentsServiceHandlerTest.java | 66 ++++++++++++++ .../oss/handler/TenantCleanupHandlerTest.java | 30 +++++++ 12 files changed, 300 insertions(+), 22 deletions(-) diff --git a/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java b/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java index 3bed0e2c6..06b260c62 100644 --- a/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java +++ b/integration-tests/mtx-local/srv/src/main/java/com/sap/cds/feature/attachments/integrationtests/mt/handlers/system/SubscribeModelTenantsHandler.java @@ -34,14 +34,15 @@ public void subscribeMockTenants(ApplicationPreparedEventContext context) { if (Boolean.FALSE.equals(multiTenancy.getMock().isEnabled())) { return; } - if (readMockedTenants().isEmpty()) { + List tenants = readMockedTenants(); + if (tenants.isEmpty()) { return; } if (!StringUtils.hasText(multiTenancy.getSidecar().getUrl())) { return; } - readMockedTenants().forEach(this::subscribeTenant); + tenants.forEach(this::subscribeTenant); } @On(event = ApplicationLifecycleService.EVENT_APPLICATION_STOPPED) @@ -51,11 +52,12 @@ public void unsubscribeMockTenants(ApplicationStoppedEventContext context) { if (Boolean.FALSE.equals(multiTenancy.getMock().isEnabled())) { return; } - if (readMockedTenants().isEmpty()) { + List tenants = readMockedTenants(); + if (tenants.isEmpty()) { return; } - readMockedTenants().forEach(this::unsubscribeTenant); + tenants.forEach(this::unsubscribeTenant); } private void subscribeTenant(String tenant) { diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java index f8cd24b2c..d19bf5ed5 100644 --- a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java +++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java @@ -22,6 +22,8 @@ @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("local-with-tenants") +// TODO: Add tests that upload/download actual binary attachment content across tenants +// to verify storage-level isolation (not just entity-level isolation). class MultiTenantAttachmentIsolationTest { private static final String DOCUMENTS_URL = "/odata/v4/MtTestService/Documents"; diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java index 07fc0c8ff..e78fe0b05 100644 --- a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java +++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java @@ -30,7 +30,7 @@ public SubscriptionEndpointClient(ObjectMapper objectMapper, MockMvc client) { public void subscribeTenant(String tenant) throws Exception { SubscriptionPayload payload = new SubscriptionPayload(); payload.subscribedTenantId = tenant; - payload.subscribedSubdomain = tenant.concat("sap.com"); + payload.subscribedSubdomain = tenant.concat(".sap.com"); payload.eventType = "CREATE"; client diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java index a81258823..4f930ac08 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java @@ -15,6 +15,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import org.slf4j.Logger; @@ -97,23 +98,22 @@ public Future deleteContentByPrefix(String prefix) { () -> { try { ListBlobsOptions options = new ListBlobsOptions().setPrefix(prefix); - List blobNames = new ArrayList<>(); + int batchSize = 1000; + List batch = new ArrayList<>(batchSize); for (BlobItem blobItem : blobContainerClient.listBlobs(options, null)) { - blobNames.add(blobItem.getName()); + batch.add(blobItem.getName()); + if (batch.size() >= batchSize) { + deleteBatch(batch); + batch.clear(); + } } - List> deleteFutures = - blobNames.stream() - .map( - name -> - executor.submit( - () -> { - blobContainerClient.getBlobClient(name).delete(); - return (Void) null; - })) - .toList(); - for (Future f : deleteFutures) { - f.get(); + if (!batch.isEmpty()) { + deleteBatch(batch); } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new ObjectStoreServiceException( + "Interrupted while deleting objects by prefix from the Azure Object Store", e); } catch (RuntimeException e) { throw new ObjectStoreServiceException( "Failed to delete objects by prefix from the Azure Object Store", e); @@ -124,4 +124,20 @@ public Future deleteContentByPrefix(String prefix) { return null; }); } + + private void deleteBatch(List blobNames) throws InterruptedException, ExecutionException { + List> deleteFutures = + blobNames.stream() + .map( + name -> + executor.submit( + () -> { + blobContainerClient.getBlobClient(name).delete(); + return (Void) null; + })) + .toList(); + for (Future f : deleteFutures) { + f.get(); + } + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java index e82fb5a64..d41a6767a 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java @@ -146,7 +146,14 @@ public Future deleteContentByPrefix(String prefix) { Storage.BlobListOption.prefix(prefix), Storage.BlobListOption.versions(true)); for (Blob blob : blobs.iterateAll()) { - storage.delete(BlobId.of(bucketName, blob.getName(), blob.getGeneration())); + boolean deleted = + storage.delete(BlobId.of(bucketName, blob.getName(), blob.getGeneration())); + if (!deleted) { + logger.warn( + "Failed to delete blob {} (generation {}) during prefix cleanup", + blob.getName(), + blob.getGeneration()); + } } } catch (RuntimeException e) { throw new ObjectStoreServiceException( diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java index 5a3fff178..2276dd9c0 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java @@ -12,6 +12,7 @@ import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,6 +29,20 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { String objectStoreKind = getObjectStoreKind(env); ExecutorService executor = Executors.newCachedThreadPool(); + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + executor.shutdown(); + try { + if (!executor.awaitTermination(30, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + })); OSSAttachmentsServiceHandler handler = new OSSAttachmentsServiceHandler( bindingOpt.get(), executor, multitenancyEnabled, objectStoreKind); diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java index 1a41d0bb3..3a51830ef 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java @@ -217,8 +217,9 @@ private String getTenant(EventContext context) { return tenant != null ? tenant : "default"; } - private static void validateTenantId(String tenantId) { - if (tenantId.isEmpty() + static void validateTenantId(String tenantId) { + if (tenantId == null + || tenantId.isEmpty() || tenantId.contains("/") || tenantId.contains("\\") || tenantId.contains("..")) { diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java index f719bb181..b35d7f9e4 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java @@ -25,6 +25,7 @@ public TenantCleanupHandler(OSClient osClient) { @After(event = DeploymentService.EVENT_UNSUBSCRIBE) void cleanupTenantData(UnsubscribeEventContext context) { String tenantId = context.getTenant(); + OSSAttachmentsServiceHandler.validateTenantId(tenantId); String prefix = tenantId + "/"; try { osClient.deleteContentByPrefix(prefix).get(); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java index 05e86bff6..b535ce53b 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java @@ -8,7 +8,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -297,6 +299,53 @@ void testDeleteContentByPrefixThrowsOnRuntimeException() throws Exception { assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } + @Test + void testDeleteContentByPrefixWithPagination() throws Exception { + AWSClient awsClient = new AWSClient(getDummyBinding(), executor); + + S3Client mockS3Client = mock(S3Client.class); + + // First page: 2 objects, isTruncated=true + S3Object obj1 = S3Object.builder().key("prefix/file1.txt").build(); + S3Object obj2 = S3Object.builder().key("prefix/file2.txt").build(); + + ListObjectsV2Response firstPage = mock(ListObjectsV2Response.class); + when(firstPage.contents()).thenReturn(List.of(obj1, obj2)); + when(firstPage.isTruncated()).thenReturn(true); + when(firstPage.nextContinuationToken()).thenReturn("token1"); + + // Second page: 1 object, isTruncated=false + S3Object obj3 = S3Object.builder().key("prefix/file3.txt").build(); + + ListObjectsV2Response secondPage = mock(ListObjectsV2Response.class); + when(secondPage.contents()).thenReturn(List.of(obj3)); + when(secondPage.isTruncated()).thenReturn(false); + + // First call returns first page, second call (with token) returns second page + when(mockS3Client.listObjectsV2( + argThat((ListObjectsV2Request req) -> req != null && req.continuationToken() == null))) + .thenReturn(firstPage); + when(mockS3Client.listObjectsV2( + argThat( + (ListObjectsV2Request req) -> + req != null && "token1".equals(req.continuationToken())))) + .thenReturn(secondPage); + + DeleteObjectsResponse deleteResponse = mock(DeleteObjectsResponse.class); + when(deleteResponse.hasErrors()).thenReturn(false); + when(deleteResponse.errors()).thenReturn(Collections.emptyList()); + when(mockS3Client.deleteObjects(any(DeleteObjectsRequest.class))).thenReturn(deleteResponse); + + var field = AWSClient.class.getDeclaredField("s3Client"); + field.setAccessible(true); + field.set(awsClient, mockS3Client); + + awsClient.deleteContentByPrefix("prefix/").get(); + + // deleteObjects should be called twice — once per page + verify(mockS3Client, times(2)).deleteObjects(any(DeleteObjectsRequest.class)); + } + private ServiceBinding getDummyBinding() { ServiceBinding binding = mock(ServiceBinding.class); HashMap creds = new HashMap<>(); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java index 2318424f1..5c8c59498 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java @@ -98,4 +98,93 @@ void testEventHandlersNoBindingDoesNotRegister() { verify(configurer, never()).eventHandler(any()); } + + @Test + void testMtEnabledNonSharedKindRegistersOnlyOSSHandler() { + Registration registration = new Registration(); + CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); + CdsRuntime cdsRuntime = mock(CdsRuntime.class); + CdsEnvironment environment = mock(CdsEnvironment.class); + ServiceBinding binding = mock(ServiceBinding.class); + + Map credentials = new HashMap<>(); + credentials.put("host", "aws.example.com"); + credentials.put("region", "us-east-1"); + credentials.put("access_key_id", "test-access-key"); + credentials.put("secret_access_key", "test-secret-key"); + credentials.put("bucket", "test-bucket"); + + when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); + when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); + when(binding.getCredentials()).thenReturn(credentials); + when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) + .thenReturn(Boolean.TRUE); + when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) + .thenReturn("dedicated"); + + registration.eventHandlers(configurer); + + verify(configurer, times(1)).eventHandler(any(OSSAttachmentsServiceHandler.class)); + verify(configurer, times(1)).eventHandler(any()); + } + + @Test + void testMtEnabledNullKindRegistersOnlyOSSHandler() { + Registration registration = new Registration(); + CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); + CdsRuntime cdsRuntime = mock(CdsRuntime.class); + CdsEnvironment environment = mock(CdsEnvironment.class); + ServiceBinding binding = mock(ServiceBinding.class); + + Map credentials = new HashMap<>(); + credentials.put("host", "aws.example.com"); + credentials.put("region", "us-east-1"); + credentials.put("access_key_id", "test-access-key"); + credentials.put("secret_access_key", "test-secret-key"); + credentials.put("bucket", "test-bucket"); + + when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); + when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); + when(binding.getCredentials()).thenReturn(credentials); + when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) + .thenReturn(Boolean.TRUE); + + registration.eventHandlers(configurer); + + verify(configurer, times(1)).eventHandler(any(OSSAttachmentsServiceHandler.class)); + verify(configurer, times(1)).eventHandler(any()); + } + + @Test + void testMtDisabledSharedKindRegistersOnlyOSSHandler() { + Registration registration = new Registration(); + CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); + CdsRuntime cdsRuntime = mock(CdsRuntime.class); + CdsEnvironment environment = mock(CdsEnvironment.class); + ServiceBinding binding = mock(ServiceBinding.class); + + Map credentials = new HashMap<>(); + credentials.put("host", "aws.example.com"); + credentials.put("region", "us-east-1"); + credentials.put("access_key_id", "test-access-key"); + credentials.put("secret_access_key", "test-secret-key"); + credentials.put("bucket", "test-bucket"); + + when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); + when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); + when(binding.getCredentials()).thenReturn(credentials); + when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) + .thenReturn("shared"); + + registration.eventHandlers(configurer); + + verify(configurer, times(1)).eventHandler(any(OSSAttachmentsServiceHandler.class)); + verify(configurer, times(1)).eventHandler(any()); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index c4350e2e7..538b023a2 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -714,4 +714,70 @@ void testReadAttachmentHandlesExecutionException() assertThrows(ServiceException.class, () -> handler.readAttachment(context)); verify(context).setCompleted(); } + + @Test + void testReadAttachmentWithMultitenancyBuildsObjectKey() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + MediaData mockMediaData = mock(MediaData.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("content123"); + when(context.getData()).thenReturn(mockMediaData); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("myTenant"); + + when(mockOsClient.readContent("myTenant/content123")) + .thenReturn(CompletableFuture.completedFuture(new ByteArrayInputStream("test".getBytes()))); + + handler.readAttachment(context); + + verify(mockOsClient).readContent("myTenant/content123"); + } + + @Test + void testMarkAsDeletedWithMultitenancyBuildsObjectKey() + throws NoSuchFieldException, IllegalAccessException { + OSClient mockOsClient = mock(OSClient.class); + OSSAttachmentsServiceHandler handler = + mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); + + var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); + osClientField.setAccessible(true); + osClientField.set(handler, mockOsClient); + var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); + mtField.setAccessible(true); + mtField.set(handler, true); + var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); + kindField.setAccessible(true); + kindField.set(handler, "shared"); + + AttachmentMarkAsDeletedEventContext context = mock(AttachmentMarkAsDeletedEventContext.class); + UserInfo userInfo = mock(UserInfo.class); + + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("myTenant"); + + when(mockOsClient.deleteContent("myTenant/content123")) + .thenReturn(CompletableFuture.completedFuture(null)); + + handler.markAttachmentAsDeleted(context); + + verify(mockOsClient).deleteContent("myTenant/content123"); + } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java index f92e76cdf..e9a0d3d0f 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java @@ -3,13 +3,16 @@ */ package com.sap.cds.feature.attachments.oss.handler; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.sap.cds.feature.attachments.oss.client.OSClient; +import com.sap.cds.services.ServiceException; import com.sap.cds.services.mt.UnsubscribeEventContext; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import org.junit.jupiter.api.Test; class TenantCleanupHandlerTest { @@ -59,4 +62,31 @@ void testCleanupTenantDataHandlesRuntimeException() throws Exception { verify(mockOsClient).deleteContentByPrefix("tenant3/"); } + + @Test + void testCleanupNullTenantThrowsServiceException() { + OSClient mockOsClient = mock(OSClient.class); + UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); + when(context.getTenant()).thenReturn(null); + + TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); + assertThrows(ServiceException.class, () -> handler.cleanupTenantData(context)); + } + + @Test + void testCleanupHandlesExecutionException() throws Exception { + OSClient mockOsClient = mock(OSClient.class); + UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); + when(context.getTenant()).thenReturn("tenant4"); + + @SuppressWarnings("unchecked") + CompletableFuture future = mock(CompletableFuture.class); + when(mockOsClient.deleteContentByPrefix("tenant4/")).thenReturn(future); + when(future.get()).thenThrow(new ExecutionException("fail", new RuntimeException("cause"))); + + TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); + handler.cleanupTenantData(context); + + verify(mockOsClient).deleteContentByPrefix("tenant4/"); + } } From b3293354780d0243dd8a0341624769ec44279df7 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 13:00:00 +0200 Subject: [PATCH 15/39] update readme --- .../cds-feature-attachments-oss/README.md | 20 ++++++++++++++++++- .../handler/OSSAttachmentsServiceHandler.java | 6 ++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/storage-targets/cds-feature-attachments-oss/README.md b/storage-targets/cds-feature-attachments-oss/README.md index 78b6e9716..2db6f168e 100644 --- a/storage-targets/cds-feature-attachments-oss/README.md +++ b/storage-targets/cds-feature-attachments-oss/README.md @@ -97,4 +97,22 @@ This artifact provides custom handlers for events from the [AttachmentService](. ### Multitenancy -Multitenancy is not directly supported. All attachments are stored in a flat structure within the provided bucket, which might be shared across tenants. +The plugin supports multi-tenancy scenarios with shared object store instances. + +#### Shared Object Store Instance + +To configure a shared object store instance, set the object store kind to `shared`: + +```yaml +cds: + attachments: + objectStore: + kind: shared +``` + +To ensure tenant isolation when using a shared object store instance, the plugin prefixes object keys with the tenant ID. When a tenant unsubscribes, all objects prefixed with that tenant's ID are automatically cleaned up from the shared bucket. + +#### Separate Object Store Instances + +> [!NOTE] +> Separate object store instances per tenant are not yet supported. This feature is planned for a future release. diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java index 3a51830ef..892238344 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java @@ -139,7 +139,8 @@ void createAttachment(AttachmentCreateEventContext context) { @On void markAttachmentAsDeleted(AttachmentMarkAsDeletedEventContext context) { logger.info( - "OS Attachment Service handler called for marking attachment as deleted with document id {}", + "OS Attachment Service handler called for marking attachment as deleted with document id" + + " {}", context.getContentId()); try { @@ -235,7 +236,8 @@ private static void validateContentId(String contentId) { || contentId.contains("\\") || contentId.contains("..")) { throw new ServiceException( - "Invalid content ID for attachment storage: must not be empty or contain path separators"); + "Invalid content ID for attachment storage: must not be empty or contain path" + + " separators"); } } } From 9f9fb848acafb53fba7e02e2848f19852c6964f9 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 13:51:26 +0200 Subject: [PATCH 16/39] reduce diff --- .../oss/configuration/RegistrationTest.java | 120 +- .../OSSAttachmentsServiceHandlerTest.java | 1120 ++++++----------- .../oss/handler/TenantCleanupHandlerTest.java | 41 +- 3 files changed, 467 insertions(+), 814 deletions(-) diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java index 5c8c59498..951740ea3 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java @@ -19,60 +19,52 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class RegistrationTest { - @Test - void testEventHandlersRegistersOSSHandler() { - // Arrange - Registration registration = new Registration(); - CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); - CdsRuntime cdsRuntime = mock(CdsRuntime.class); - CdsEnvironment environment = mock(CdsEnvironment.class); - ServiceBinding binding = mock(ServiceBinding.class); + private Registration registration; + private CdsRuntimeConfigurer configurer; + private CdsEnvironment environment; + private ServiceBinding awsBinding; - // Setup valid AWS credentials for the binding + private static ServiceBinding createAwsBinding() { + ServiceBinding binding = mock(ServiceBinding.class); Map credentials = new HashMap<>(); credentials.put("host", "aws.example.com"); credentials.put("region", "us-east-1"); credentials.put("access_key_id", "test-access-key"); credentials.put("secret_access_key", "test-secret-key"); credentials.put("bucket", "test-bucket"); + when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); + when(binding.getCredentials()).thenReturn(credentials); + return binding; + } + @BeforeEach + void setup() { + registration = new Registration(); + configurer = mock(CdsRuntimeConfigurer.class); + CdsRuntime cdsRuntime = mock(CdsRuntime.class); + environment = mock(CdsEnvironment.class); when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); when(cdsRuntime.getEnvironment()).thenReturn(environment); - when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); - when(binding.getCredentials()).thenReturn(credentials); - when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + awsBinding = createAwsBinding(); + } + + @Test + void testEventHandlersRegistersOSSHandler() { + when(environment.getServiceBindings()).thenReturn(Stream.of(awsBinding)); - // Act registration.eventHandlers(configurer); - // Assert: OSSAttachmentsServiceHandler should be registered verify(configurer).eventHandler(any(OSSAttachmentsServiceHandler.class)); } @Test void testEventHandlersRegistersCleanupHandlerWhenMultitenancyShared() { - Registration registration = new Registration(); - CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); - CdsRuntime cdsRuntime = mock(CdsRuntime.class); - CdsEnvironment environment = mock(CdsEnvironment.class); - ServiceBinding binding = mock(ServiceBinding.class); - - Map credentials = new HashMap<>(); - credentials.put("host", "aws.example.com"); - credentials.put("region", "us-east-1"); - credentials.put("access_key_id", "test-access-key"); - credentials.put("secret_access_key", "test-secret-key"); - credentials.put("bucket", "test-bucket"); - - when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); - when(cdsRuntime.getEnvironment()).thenReturn(environment); - when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); - when(binding.getCredentials()).thenReturn(credentials); - when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + when(environment.getServiceBindings()).thenReturn(Stream.of(awsBinding)); when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) .thenReturn(Boolean.TRUE); when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) @@ -85,13 +77,6 @@ void testEventHandlersRegistersCleanupHandlerWhenMultitenancyShared() { @Test void testEventHandlersNoBindingDoesNotRegister() { - Registration registration = new Registration(); - CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); - CdsRuntime cdsRuntime = mock(CdsRuntime.class); - CdsEnvironment environment = mock(CdsEnvironment.class); - - when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); - when(cdsRuntime.getEnvironment()).thenReturn(environment); when(environment.getServiceBindings()).thenReturn(Stream.empty()); registration.eventHandlers(configurer); @@ -101,24 +86,7 @@ void testEventHandlersNoBindingDoesNotRegister() { @Test void testMtEnabledNonSharedKindRegistersOnlyOSSHandler() { - Registration registration = new Registration(); - CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); - CdsRuntime cdsRuntime = mock(CdsRuntime.class); - CdsEnvironment environment = mock(CdsEnvironment.class); - ServiceBinding binding = mock(ServiceBinding.class); - - Map credentials = new HashMap<>(); - credentials.put("host", "aws.example.com"); - credentials.put("region", "us-east-1"); - credentials.put("access_key_id", "test-access-key"); - credentials.put("secret_access_key", "test-secret-key"); - credentials.put("bucket", "test-bucket"); - - when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); - when(cdsRuntime.getEnvironment()).thenReturn(environment); - when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); - when(binding.getCredentials()).thenReturn(credentials); - when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + when(environment.getServiceBindings()).thenReturn(Stream.of(awsBinding)); when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) .thenReturn(Boolean.TRUE); when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) @@ -132,24 +100,7 @@ void testMtEnabledNonSharedKindRegistersOnlyOSSHandler() { @Test void testMtEnabledNullKindRegistersOnlyOSSHandler() { - Registration registration = new Registration(); - CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); - CdsRuntime cdsRuntime = mock(CdsRuntime.class); - CdsEnvironment environment = mock(CdsEnvironment.class); - ServiceBinding binding = mock(ServiceBinding.class); - - Map credentials = new HashMap<>(); - credentials.put("host", "aws.example.com"); - credentials.put("region", "us-east-1"); - credentials.put("access_key_id", "test-access-key"); - credentials.put("secret_access_key", "test-secret-key"); - credentials.put("bucket", "test-bucket"); - - when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); - when(cdsRuntime.getEnvironment()).thenReturn(environment); - when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); - when(binding.getCredentials()).thenReturn(credentials); - when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + when(environment.getServiceBindings()).thenReturn(Stream.of(awsBinding)); when(environment.getProperty("cds.multitenancy.enabled", Boolean.class, Boolean.FALSE)) .thenReturn(Boolean.TRUE); @@ -161,24 +112,7 @@ void testMtEnabledNullKindRegistersOnlyOSSHandler() { @Test void testMtDisabledSharedKindRegistersOnlyOSSHandler() { - Registration registration = new Registration(); - CdsRuntimeConfigurer configurer = mock(CdsRuntimeConfigurer.class); - CdsRuntime cdsRuntime = mock(CdsRuntime.class); - CdsEnvironment environment = mock(CdsEnvironment.class); - ServiceBinding binding = mock(ServiceBinding.class); - - Map credentials = new HashMap<>(); - credentials.put("host", "aws.example.com"); - credentials.put("region", "us-east-1"); - credentials.put("access_key_id", "test-access-key"); - credentials.put("secret_access_key", "test-secret-key"); - credentials.put("bucket", "test-bucket"); - - when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); - when(cdsRuntime.getEnvironment()).thenReturn(environment); - when(binding.getServiceName()).thenReturn(Optional.of("objectstore")); - when(binding.getCredentials()).thenReturn(credentials); - when(environment.getServiceBindings()).thenReturn(Stream.of(binding)); + when(environment.getServiceBindings()).thenReturn(Stream.of(awsBinding)); when(environment.getProperty("cds.attachments.objectStore.kind", String.class, null)) .thenReturn("shared"); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index 538b023a2..90ff570da 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -3,15 +3,18 @@ */ package com.sap.cds.feature.attachments.oss.handler; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.StatusCode; import com.sap.cds.feature.attachments.oss.client.OSClient; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; @@ -19,24 +22,32 @@ import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; import com.sap.cds.reflect.CdsEntity; import com.sap.cds.services.ServiceException; +import com.sap.cds.services.request.ModifiableUserInfo; import com.sap.cds.services.request.UserInfo; import com.sap.cloud.environment.servicebinding.api.ServiceBinding; import java.io.ByteArrayInputStream; import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.util.Base64; import java.util.HashMap; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; class OSSAttachmentsServiceHandlerTest { - ExecutorService executor = Executors.newCachedThreadPool(); - @Test - void testRestoreAttachmentCallsSetCompleted() { - // Setup a valid AWS binding for the test + private static final ExecutorService executor = Executors.newCachedThreadPool(); + + private OSClient mockOsClient; + private OSSAttachmentsServiceHandler handler; + + private static ServiceBinding createAwsBinding() { ServiceBinding binding = mock(ServiceBinding.class); HashMap creds = new HashMap<>(); creds.put("host", "aws.example.com"); @@ -45,739 +56,446 @@ void testRestoreAttachmentCallsSetCompleted() { creds.put("secret_access_key", "test-secret-key"); creds.put("bucket", "test-bucket"); when(binding.getCredentials()).thenReturn(creds); - - OSSAttachmentsServiceHandler handler = - new OSSAttachmentsServiceHandler(binding, executor, false, null); - AttachmentRestoreEventContext context = mock(AttachmentRestoreEventContext.class); - handler.restoreAttachment(context); - verify(context).setCompleted(); + return binding; } - @Test - void testCreateAttachmentCallsOsClientUploadContent() + private static void injectOsClient(OSSAttachmentsServiceHandler handler, OSClient client) throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - // Mock the handler, but call the real method readAttachment - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - var field = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); field.setAccessible(true); - field.set(handler, mockOsClient); - - String contentId = "doc123"; - String mimeType = "text/plain"; - String fileName = "file.txt"; - - MediaData mockMediaData = mock(MediaData.class); - var mockEntity = mock(com.sap.cds.reflect.CdsEntity.class); - when(mockEntity.getQualifiedName()).thenReturn(fileName); - - InputStream contentStream = new ByteArrayInputStream("test".getBytes()); - - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(context.getAttachmentIds()).thenReturn(java.util.Map.of("ID", contentId)); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(contentStream); - when(mockMediaData.getMimeType()).thenReturn(mimeType); - when(mockOsClient.uploadContent(any(), anyString(), anyString())) - .thenReturn(CompletableFuture.completedFuture(null)); - - when(context.getContentId()).thenReturn(contentId); - - handler.createAttachment(context); - - verify(mockOsClient).uploadContent(contentStream, contentId, mimeType); - verify(context).setIsInternalStored(false); - verify(context).setContentId(contentId); - verify(context).setCompleted(); + field.set(handler, client); } - @Test - void testReadAttachmentCallsOsClientReadContent() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - // Mock the handler, but call the real method readAttachment - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - - var field = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - field.setAccessible(true); - field.set(handler, mockOsClient); - - String contentId = "doc123"; - MediaData mockMediaData = mock(MediaData.class); - - when(context.getContentId()).thenReturn(contentId); - when(context.getData()).thenReturn(mockMediaData); - when(mockOsClient.readContent(contentId)) - .thenReturn(CompletableFuture.completedFuture(new ByteArrayInputStream("test".getBytes()))); - - handler.readAttachment(context); - - verify(mockOsClient).readContent(contentId); - verify(mockMediaData).setContent(any(InputStream.class)); - verify(context).setCompleted(); + private static CdsEntity stubEntity(String name) { + CdsEntity entity = mock(CdsEntity.class); + when(entity.getQualifiedName()).thenReturn(name); + return entity; } - @Test - void testReadAttachmentCallsOsClientReadNullContent() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - // Mock the handler, but call the real method readAttachment - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - - var field = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - field.setAccessible(true); - field.set(handler, mockOsClient); - - String contentId = "doc123"; - MediaData mockMediaData = mock(MediaData.class); - - when(context.getContentId()).thenReturn(contentId); - when(context.getData()).thenReturn(mockMediaData); - when(mockOsClient.readContent(contentId)).thenReturn(CompletableFuture.completedFuture(null)); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - - verify(mockOsClient).readContent(contentId); - verify(context).setCompleted(); + /** + * Creates a real {@link AttachmentCreateEventContext} populated with the given values. + * The only mock used is CdsEntity (a model-level concept not creatable without a full model). + */ + private static AttachmentCreateEventContext createContext( + String contentId, String mimeType, String fileName, byte[] content) { + var ctx = AttachmentCreateEventContext.create(); + ctx.setData(MediaData.create()); + ctx.getData().setContent(new ByteArrayInputStream(content)); + ctx.getData().setMimeType(mimeType); + ctx.getData().setFileName(fileName); + ctx.setAttachmentIds(Map.of(Attachments.ID, contentId)); + ctx.setAttachmentEntity(stubEntity("TestEntity")); + return ctx; } - @Test - void testMarkAttachmentAsDeletedCallsOsClientDeleteContent() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - // Mock the handler, but call the real method readAttachment - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - AttachmentMarkAsDeletedEventContext context = mock(AttachmentMarkAsDeletedEventContext.class); - - var field = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - field.setAccessible(true); - field.set(handler, mockOsClient); - - String contentId = "doc123"; - when(context.getContentId()).thenReturn(contentId); - when(mockOsClient.deleteContent(contentId)).thenReturn(CompletableFuture.completedFuture(null)); - - handler.markAttachmentAsDeleted(context); - - verify(mockOsClient).deleteContent(contentId); - verify(context).setCompleted(); + private static UserInfo userInfoWithTenant(String tenant) { + ModifiableUserInfo userInfo = UserInfo.create(); + userInfo.setTenant(tenant); + return userInfo; } - @Test - void testConstructorHandlesInvalidBase64EncodedPrivateKeyData() { - // Arrange: ServiceBinding with invalid base64EncodedPrivateKeyData (not valid base64) - ServiceBinding binding = mock(ServiceBinding.class); - HashMap creds = new HashMap<>(); - creds.put("base64EncodedPrivateKeyData", "not-a-valid-base64-string"); - when(binding.getCredentials()).thenReturn(creds); - - assertThrows( - ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + @Nested + class ConstructorTests { + + @Test + void testConstructorHandlesInvalidBase64EncodedPrivateKeyData() { + ServiceBinding binding = mock(ServiceBinding.class); + HashMap creds = new HashMap<>(); + creds.put("base64EncodedPrivateKeyData", "not-a-valid-base64-string"); + when(binding.getCredentials()).thenReturn(creds); + + assertThrows( + ObjectStoreServiceException.class, + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + } + + @Test + void testConstructorHandlesValidBase64ButNoGoogleOrGcp() { + String plain = "this is just a dummy string without keywords"; + String base64 = Base64.getEncoder().encodeToString(plain.getBytes(StandardCharsets.UTF_8)); + + ServiceBinding binding = mock(ServiceBinding.class); + HashMap creds = new HashMap<>(); + creds.put("base64EncodedPrivateKeyData", base64); + when(binding.getCredentials()).thenReturn(creds); + + assertThrows( + ObjectStoreServiceException.class, + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + } + + @Test + void testConstructorHandlesInValidBase64() { + ServiceBinding binding = mock(ServiceBinding.class); + HashMap creds = new HashMap<>(); + creds.put("base64EncodedPrivateKeyData", "this is just a dummy string without keywords"); + when(binding.getCredentials()).thenReturn(creds); + + assertThrows( + ObjectStoreServiceException.class, + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + } + + @Test + void testConstructorHandlesNoValidObjectStoreService() { + ServiceBinding binding = mock(ServiceBinding.class); + HashMap creds = new HashMap<>(); + creds.put("someOtherField", "someValue"); + when(binding.getCredentials()).thenReturn(creds); + + assertThrows( + ObjectStoreServiceException.class, + () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + } } - @Test - void testConstructorHandlesValidBase64ButNoGoogleOrGcp() { - String plain = "this is just a dummy string without keywords"; - String base64 = - Base64.getEncoder().encodeToString(plain.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + @Nested + class SingleTenantOperations { - ServiceBinding binding = mock(ServiceBinding.class); - HashMap creds = new HashMap<>(); - creds.put("base64EncodedPrivateKeyData", base64); - when(binding.getCredentials()).thenReturn(creds); + @BeforeEach + void setup() throws NoSuchFieldException, IllegalAccessException { + handler = new OSSAttachmentsServiceHandler(createAwsBinding(), executor, false, null); + mockOsClient = mock(OSClient.class); + injectOsClient(handler, mockOsClient); + } - assertThrows( - ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); - } + @Test + void testRestoreAttachmentCallsSetCompleted() { + var context = AttachmentRestoreEventContext.create(); + context.setRestoreTimestamp(Instant.now()); - @Test - void testConstructorHandlesInValidBase64() { - ServiceBinding binding = mock(ServiceBinding.class); - HashMap creds = new HashMap<>(); - creds.put("base64EncodedPrivateKeyData", "this is just a dummy string without keywords"); - when(binding.getCredentials()).thenReturn(creds); + handler.restoreAttachment(context); - assertThrows( - ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); - } + assertThat(context.isCompleted()).isTrue(); + } - @Test - void testConstructorHandlesNoValidObjectStoreService() { - // Arrange: ServiceBinding with no valid object store credentials - ServiceBinding binding = mock(ServiceBinding.class); - HashMap creds = new HashMap<>(); - // No host, container_uri, or base64EncodedPrivateKeyData - creds.put("someOtherField", "someValue"); - when(binding.getCredentials()).thenReturn(creds); + @Test + void testCreateAttachmentUploadsContent() { + when(mockOsClient.uploadContent(any(), anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); - assertThrows( - ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); - } + var context = createContext("doc123", "text/plain", "file.txt", "test".getBytes()); - // Helper method to setup common mocks for createAttachment exception tests - private AttachmentCreateEventContext setupCreateAttachmentContext( - OSClient mockOsClient, OSSAttachmentsServiceHandler handler, Exception exceptionToThrow) - throws NoSuchFieldException, - IllegalAccessException, - InterruptedException, - ExecutionException { + handler.createAttachment(context); - var field = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - field.setAccessible(true); - field.set(handler, mockOsClient); - - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - CdsEntity mockEntity = mock(CdsEntity.class); - HashMap attachmentIds = new HashMap<>(); - attachmentIds.put("ID", "test-id"); - - when(context.getAttachmentIds()).thenReturn(attachmentIds); - when(context.getData()).thenReturn(mockMediaData); - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(mockMediaData.getFileName()).thenReturn("test.txt"); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - - @SuppressWarnings("unchecked") - CompletableFuture future = mock(CompletableFuture.class); - when(mockOsClient.uploadContent(any(InputStream.class), anyString(), anyString())) - .thenReturn(future); - when(future.get()).thenThrow(exceptionToThrow); - - return context; - } + verify(mockOsClient).uploadContent(any(InputStream.class), eq("doc123"), eq("text/plain")); + assertThat(context.getIsInternalStored()).isFalse(); + assertThat(context.getContentId()).isEqualTo("doc123"); + assertThat(context.getData().getStatus()).isEqualTo(StatusCode.SCANNING); + assertThat(context.isCompleted()).isTrue(); + } - @Test - void testCreateAttachmentExceptionHandling() - throws NoSuchFieldException, - IllegalAccessException, - InterruptedException, - ExecutionException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - // Test InterruptedException - AttachmentCreateEventContext context1 = - setupCreateAttachmentContext( - mockOsClient, handler, new InterruptedException("Thread interrupted")); - assertThrows(ServiceException.class, () -> handler.createAttachment(context1)); - verify(context1).setCompleted(); - - // Test ObjectStoreServiceException - AttachmentCreateEventContext context2 = - setupCreateAttachmentContext( - mockOsClient, handler, new ObjectStoreServiceException("Upload failed")); - assertThrows(ServiceException.class, () -> handler.createAttachment(context2)); - verify(context2).setCompleted(); - - // Test ExecutionException - AttachmentCreateEventContext context3 = - setupCreateAttachmentContext( - mockOsClient, handler, new ExecutionException("Upload failed", new RuntimeException())); - assertThrows(ServiceException.class, () -> handler.createAttachment(context3)); - verify(context3).setCompleted(); - } - - // Helper method to setup common mocks for markAttachmentAsDeleted exception tests - private AttachmentMarkAsDeletedEventContext setupMarkAsDeletedContext( - OSClient mockOsClient, OSSAttachmentsServiceHandler handler, Exception exceptionToThrow) - throws NoSuchFieldException, - IllegalAccessException, - InterruptedException, - ExecutionException { - - var field = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - field.setAccessible(true); - field.set(handler, mockOsClient); - - AttachmentMarkAsDeletedEventContext context = mock(AttachmentMarkAsDeletedEventContext.class); - String contentId = "test-content-id"; - - when(context.getContentId()).thenReturn(contentId); - - @SuppressWarnings("unchecked") - CompletableFuture future = mock(CompletableFuture.class); - when(mockOsClient.deleteContent(contentId)).thenReturn(future); - when(future.get()).thenThrow(exceptionToThrow); - - return context; - } - - @Test - void testMarkAttachmentAsDeletedExceptionHandling() - throws NoSuchFieldException, - IllegalAccessException, - InterruptedException, - ExecutionException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - // Test InterruptedException - AttachmentMarkAsDeletedEventContext context1 = - setupMarkAsDeletedContext( - mockOsClient, handler, new InterruptedException("Thread interrupted")); - assertThrows(ServiceException.class, () -> handler.markAttachmentAsDeleted(context1)); - verify(context1).setCompleted(); - - // Test ObjectStoreServiceException - AttachmentMarkAsDeletedEventContext context2 = - setupMarkAsDeletedContext( - mockOsClient, handler, new ObjectStoreServiceException("Delete failed")); - assertThrows(ServiceException.class, () -> handler.markAttachmentAsDeleted(context2)); - verify(context2).setCompleted(); - - // Test ExecutionException - AttachmentMarkAsDeletedEventContext context3 = - setupMarkAsDeletedContext( - mockOsClient, handler, new ExecutionException("Delete failed", new RuntimeException())); - assertThrows(ServiceException.class, () -> handler.markAttachmentAsDeleted(context3)); - verify(context3).setCompleted(); - } - - @Test - void testReadAttachmentHandlesInterruptedException() - throws NoSuchFieldException, - IllegalAccessException, - InterruptedException, - ExecutionException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - - var field = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - field.setAccessible(true); - field.set(handler, mockOsClient); - - String contentId = "doc123"; - MediaData mockMediaData = mock(MediaData.class); - - when(context.getContentId()).thenReturn(contentId); - when(context.getData()).thenReturn(mockMediaData); - - @SuppressWarnings("unchecked") - CompletableFuture future = mock(CompletableFuture.class); - when(mockOsClient.readContent(contentId)).thenReturn(future); - when(future.get()).thenThrow(new InterruptedException("Thread interrupted")); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - verify(context).setCompleted(); - } - - @Test - void testCreateAttachmentWithMultitenancyBuildsObjectKey() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - CdsEntity mockEntity = mock(CdsEntity.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getAttachmentEntity()).thenReturn(mockEntity); - when(mockEntity.getQualifiedName()).thenReturn("TestEntity"); - when(context.getAttachmentIds()).thenReturn(java.util.Map.of("ID", "content123")); - when(context.getData()).thenReturn(mockMediaData); - when(mockMediaData.getContent()).thenReturn(new ByteArrayInputStream("test".getBytes())); - when(mockMediaData.getMimeType()).thenReturn("text/plain"); - when(mockMediaData.getFileName()).thenReturn("file.txt"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("myTenant"); - - when(mockOsClient.uploadContent(any(), anyString(), anyString())) - .thenReturn(CompletableFuture.completedFuture(null)); - - handler.createAttachment(context); - - // Verify the object key includes tenant prefix - verify(mockOsClient) - .uploadContent(any(), org.mockito.ArgumentMatchers.eq("myTenant/content123"), anyString()); - } - - @Test - void testMultitenancyWithNullTenantThrows() throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("content123"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn(null); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } + @Test + void testReadAttachmentReadsContent() { + when(mockOsClient.readContent("doc123")) + .thenReturn( + CompletableFuture.completedFuture(new ByteArrayInputStream("test".getBytes()))); - @Test - void testValidateTenantIdWithSlashThrows() throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("content123"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("tenant/evil"); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } - - @Test - void testValidateTenantIdWithBackslashThrows() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("content123"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("tenant\\evil"); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } + var context = AttachmentReadEventContext.create(); + context.setContentId("doc123"); + context.setData(MediaData.create()); - @Test - void testValidateTenantIdWithDotsThrows() throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("content123"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("..evil"); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } - - @Test - void testValidateEmptyTenantIdThrows() throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - // Need to mock tenant as empty string but not null (null triggers different path) - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("content123"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn(""); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } - - @Test - void testValidateContentIdWithSlashThrows() throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("content/evil"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("validTenant"); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } - - @Test - void testValidateContentIdWithNullThrows() throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn(null); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("validTenant"); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } - - @Test - void testValidateContentIdWithBackslashThrows() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("content\\evil"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("validTenant"); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } + handler.readAttachment(context); - @Test - void testValidateContentIdWithDotsThrows() throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("..evil"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("validTenant"); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } + verify(mockOsClient).readContent("doc123"); + assertThat(context.getData().getContent()).isNotNull(); + assertThat(context.isCompleted()).isTrue(); + } - @Test - void testValidateEmptyContentIdThrows() throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn(""); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("validTenant"); - - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - } + @Test + void testReadAttachmentWithNullContentThrows() { + when(mockOsClient.readContent("doc123")) + .thenReturn(CompletableFuture.completedFuture(null)); - @Test - void testReadAttachmentHandlesExecutionException() - throws NoSuchFieldException, - IllegalAccessException, - InterruptedException, - ExecutionException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + var context = AttachmentReadEventContext.create(); + context.setContentId("doc123"); + context.setData(MediaData.create()); - var field = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - field.setAccessible(true); - field.set(handler, mockOsClient); + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + assertThat(context.isCompleted()).isTrue(); + } - String contentId = "doc123"; - MediaData mockMediaData = mock(MediaData.class); + @Test + void testMarkAttachmentAsDeletedDeletesContent() { + when(mockOsClient.deleteContent("doc123")) + .thenReturn(CompletableFuture.completedFuture(null)); - when(context.getContentId()).thenReturn(contentId); - when(context.getData()).thenReturn(mockMediaData); + var context = AttachmentMarkAsDeletedEventContext.create(); + context.setContentId("doc123"); - @SuppressWarnings("unchecked") - CompletableFuture future = mock(CompletableFuture.class); - when(mockOsClient.readContent(contentId)).thenReturn(future); - when(future.get()).thenThrow(new ExecutionException("failed", new RuntimeException())); + handler.markAttachmentAsDeleted(context); - assertThrows(ServiceException.class, () -> handler.readAttachment(context)); - verify(context).setCompleted(); + verify(mockOsClient).deleteContent("doc123"); + assertThat(context.isCompleted()).isTrue(); + } } - @Test - void testReadAttachmentWithMultitenancyBuildsObjectKey() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); - MediaData mockMediaData = mock(MediaData.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("content123"); - when(context.getData()).thenReturn(mockMediaData); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("myTenant"); - - when(mockOsClient.readContent("myTenant/content123")) - .thenReturn(CompletableFuture.completedFuture(new ByteArrayInputStream("test".getBytes()))); - - handler.readAttachment(context); - - verify(mockOsClient).readContent("myTenant/content123"); + @Nested + class ExceptionHandling { + + @BeforeEach + void setup() throws NoSuchFieldException, IllegalAccessException { + handler = new OSSAttachmentsServiceHandler(createAwsBinding(), executor, false, null); + mockOsClient = mock(OSClient.class); + injectOsClient(handler, mockOsClient); + } + + @Test + void testCreateAttachmentHandlesInterruptedException() throws Exception { + var context = createContextForUploadException(new InterruptedException("Thread interrupted")); + assertThrows(ServiceException.class, () -> handler.createAttachment(context)); + assertThat(context.isCompleted()).isTrue(); + } + + @Test + void testCreateAttachmentHandlesObjectStoreServiceException() throws Exception { + var context = + createContextForUploadException(new ObjectStoreServiceException("Upload failed")); + assertThrows(ServiceException.class, () -> handler.createAttachment(context)); + assertThat(context.isCompleted()).isTrue(); + } + + @Test + void testCreateAttachmentHandlesExecutionException() throws Exception { + var context = + createContextForUploadException( + new ExecutionException("Upload failed", new RuntimeException())); + assertThrows(ServiceException.class, () -> handler.createAttachment(context)); + assertThat(context.isCompleted()).isTrue(); + } + + @Test + void testMarkAsDeletedHandlesInterruptedException() throws Exception { + var context = + createContextForDeleteException(new InterruptedException("Thread interrupted")); + assertThrows(ServiceException.class, () -> handler.markAttachmentAsDeleted(context)); + assertThat(context.isCompleted()).isTrue(); + } + + @Test + void testMarkAsDeletedHandlesObjectStoreServiceException() throws Exception { + var context = + createContextForDeleteException(new ObjectStoreServiceException("Delete failed")); + assertThrows(ServiceException.class, () -> handler.markAttachmentAsDeleted(context)); + assertThat(context.isCompleted()).isTrue(); + } + + @Test + void testMarkAsDeletedHandlesExecutionException() throws Exception { + var context = + createContextForDeleteException( + new ExecutionException("Delete failed", new RuntimeException())); + assertThrows(ServiceException.class, () -> handler.markAttachmentAsDeleted(context)); + assertThat(context.isCompleted()).isTrue(); + } + + @Test + void testReadAttachmentHandlesInterruptedException() throws Exception { + var context = createContextForReadException(new InterruptedException("Thread interrupted")); + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + assertThat(context.isCompleted()).isTrue(); + } + + @Test + void testReadAttachmentHandlesExecutionException() throws Exception { + var context = + createContextForReadException( + new ExecutionException("failed", new RuntimeException())); + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + assertThat(context.isCompleted()).isTrue(); + } + + private AttachmentCreateEventContext createContextForUploadException(Exception exception) + throws Exception { + @SuppressWarnings("unchecked") + CompletableFuture future = mock(CompletableFuture.class); + when(mockOsClient.uploadContent(any(InputStream.class), anyString(), anyString())) + .thenReturn(future); + when(future.get()).thenThrow(exception); + + return createContext("test-id", "text/plain", "test.txt", "test".getBytes()); + } + + private AttachmentMarkAsDeletedEventContext createContextForDeleteException(Exception exception) + throws Exception { + @SuppressWarnings("unchecked") + CompletableFuture future = mock(CompletableFuture.class); + when(mockOsClient.deleteContent("test-content-id")).thenReturn(future); + when(future.get()).thenThrow(exception); + + var context = AttachmentMarkAsDeletedEventContext.create(); + context.setContentId("test-content-id"); + return context; + } + + private AttachmentReadEventContext createContextForReadException(Exception exception) + throws Exception { + @SuppressWarnings("unchecked") + CompletableFuture future = mock(CompletableFuture.class); + when(mockOsClient.readContent("doc123")).thenReturn(future); + when(future.get()).thenThrow(exception); + + var context = AttachmentReadEventContext.create(); + context.setContentId("doc123"); + context.setData(MediaData.create()); + return context; + } } - @Test - void testMarkAsDeletedWithMultitenancyBuildsObjectKey() - throws NoSuchFieldException, IllegalAccessException { - OSClient mockOsClient = mock(OSClient.class); - OSSAttachmentsServiceHandler handler = - mock(OSSAttachmentsServiceHandler.class, CALLS_REAL_METHODS); - - var osClientField = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - osClientField.setAccessible(true); - osClientField.set(handler, mockOsClient); - var mtField = OSSAttachmentsServiceHandler.class.getDeclaredField("multitenancyEnabled"); - mtField.setAccessible(true); - mtField.set(handler, true); - var kindField = OSSAttachmentsServiceHandler.class.getDeclaredField("objectStoreKind"); - kindField.setAccessible(true); - kindField.set(handler, "shared"); - - AttachmentMarkAsDeletedEventContext context = mock(AttachmentMarkAsDeletedEventContext.class); - UserInfo userInfo = mock(UserInfo.class); - - when(context.getContentId()).thenReturn("content123"); - when(context.getUserInfo()).thenReturn(userInfo); - when(userInfo.getTenant()).thenReturn("myTenant"); - - when(mockOsClient.deleteContent("myTenant/content123")) - .thenReturn(CompletableFuture.completedFuture(null)); - - handler.markAttachmentAsDeleted(context); - - verify(mockOsClient).deleteContent("myTenant/content123"); + @Nested + class MultitenancyTests { + + @BeforeEach + void setup() throws NoSuchFieldException, IllegalAccessException { + handler = new OSSAttachmentsServiceHandler(createAwsBinding(), executor, true, "shared"); + mockOsClient = mock(OSClient.class); + injectOsClient(handler, mockOsClient); + } + + @Test + void testCreateAttachmentWithMultitenancyBuildsObjectKey() { + when(mockOsClient.uploadContent(any(), anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(null)); + + // For multitenancy, getUserInfo() requires a RequestContext, so we mock + // the event context to provide tenant info + CdsEntity entity = stubEntity("TestEntity"); + UserInfo userInfo = userInfoWithTenant("myTenant"); + MediaData data = MediaData.create(); + data.setContent(new ByteArrayInputStream("test".getBytes())); + data.setMimeType("text/plain"); + data.setFileName("file.txt"); + + AttachmentCreateEventContext context = mock(AttachmentCreateEventContext.class); + when(context.getAttachmentEntity()).thenReturn(entity); + when(context.getAttachmentIds()).thenReturn(Map.of(Attachments.ID, "content123")); + when(context.getData()).thenReturn(data); + when(context.getUserInfo()).thenReturn(userInfo); + + handler.createAttachment(context); + + verify(mockOsClient).uploadContent(any(), eq("myTenant/content123"), anyString()); + } + + @Test + void testReadAttachmentWithMultitenancyBuildsObjectKey() { + when(mockOsClient.readContent("myTenant/content123")) + .thenReturn( + CompletableFuture.completedFuture(new ByteArrayInputStream("test".getBytes()))); + + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn("content123"); + when(context.getData()).thenReturn(MediaData.create()); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("myTenant")); + + handler.readAttachment(context); + + verify(mockOsClient).readContent("myTenant/content123"); + } + + @Test + void testMarkAsDeletedWithMultitenancyBuildsObjectKey() { + when(mockOsClient.deleteContent("myTenant/content123")) + .thenReturn(CompletableFuture.completedFuture(null)); + + AttachmentMarkAsDeletedEventContext context = + mock(AttachmentMarkAsDeletedEventContext.class); + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("myTenant")); + + handler.markAttachmentAsDeleted(context); + + verify(mockOsClient).deleteContent("myTenant/content123"); + } + + @Test + void testMultitenancyWithNullTenantThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfoWithTenant(null)); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateTenantIdWithSlashThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("tenant/evil")); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateTenantIdWithBackslashThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("tenant\\evil")); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateTenantIdWithDotsThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("..evil")); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateEmptyTenantIdThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn("content123"); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("")); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateContentIdWithSlashThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn("content/evil"); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("validTenant")); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateContentIdWithNullThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn(null); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("validTenant")); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateContentIdWithBackslashThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn("content\\evil"); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("validTenant")); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateContentIdWithDotsThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn("..evil"); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("validTenant")); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } + + @Test + void testValidateEmptyContentIdThrows() { + AttachmentReadEventContext context = mock(AttachmentReadEventContext.class); + when(context.getContentId()).thenReturn(""); + when(context.getUserInfo()).thenReturn(userInfoWithTenant("validTenant")); + + assertThrows(ServiceException.class, () -> handler.readAttachment(context)); + } } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java index e9a0d3d0f..818d3f86d 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandlerTest.java @@ -13,19 +13,28 @@ import com.sap.cds.services.mt.UnsubscribeEventContext; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class TenantCleanupHandlerTest { + private OSClient mockOsClient; + private TenantCleanupHandler handler; + + @BeforeEach + void setup() { + mockOsClient = mock(OSClient.class); + handler = new TenantCleanupHandler(mockOsClient); + } + @Test void testCleanupTenantDataCallsDeleteByPrefix() throws Exception { - OSClient mockOsClient = mock(OSClient.class); - UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); - when(context.getTenant()).thenReturn("tenant1"); + var context = UnsubscribeEventContext.create(); + context.setTenant("tenant1"); + when(mockOsClient.deleteContentByPrefix("tenant1/")) .thenReturn(CompletableFuture.completedFuture(null)); - TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); handler.cleanupTenantData(context); verify(mockOsClient).deleteContentByPrefix("tenant1/"); @@ -33,16 +42,14 @@ void testCleanupTenantDataCallsDeleteByPrefix() throws Exception { @Test void testCleanupTenantDataHandlesInterruptedException() throws Exception { - OSClient mockOsClient = mock(OSClient.class); - UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); - when(context.getTenant()).thenReturn("tenant2"); + var context = UnsubscribeEventContext.create(); + context.setTenant("tenant2"); @SuppressWarnings("unchecked") CompletableFuture future = mock(CompletableFuture.class); when(mockOsClient.deleteContentByPrefix("tenant2/")).thenReturn(future); when(future.get()).thenThrow(new InterruptedException("interrupted")); - TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); handler.cleanupTenantData(context); verify(mockOsClient).deleteContentByPrefix("tenant2/"); @@ -50,14 +57,12 @@ void testCleanupTenantDataHandlesInterruptedException() throws Exception { @Test void testCleanupTenantDataHandlesRuntimeException() throws Exception { - OSClient mockOsClient = mock(OSClient.class); - UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); - when(context.getTenant()).thenReturn("tenant3"); + var context = UnsubscribeEventContext.create(); + context.setTenant("tenant3"); when(mockOsClient.deleteContentByPrefix("tenant3/")) .thenReturn(CompletableFuture.failedFuture(new RuntimeException("fail"))); - TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); handler.cleanupTenantData(context); verify(mockOsClient).deleteContentByPrefix("tenant3/"); @@ -65,26 +70,22 @@ void testCleanupTenantDataHandlesRuntimeException() throws Exception { @Test void testCleanupNullTenantThrowsServiceException() { - OSClient mockOsClient = mock(OSClient.class); - UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); - when(context.getTenant()).thenReturn(null); + var context = UnsubscribeEventContext.create(); + // tenant is null by default - TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); assertThrows(ServiceException.class, () -> handler.cleanupTenantData(context)); } @Test void testCleanupHandlesExecutionException() throws Exception { - OSClient mockOsClient = mock(OSClient.class); - UnsubscribeEventContext context = mock(UnsubscribeEventContext.class); - when(context.getTenant()).thenReturn("tenant4"); + var context = UnsubscribeEventContext.create(); + context.setTenant("tenant4"); @SuppressWarnings("unchecked") CompletableFuture future = mock(CompletableFuture.class); when(mockOsClient.deleteContentByPrefix("tenant4/")).thenReturn(future); when(future.get()).thenThrow(new ExecutionException("fail", new RuntimeException("cause"))); - TenantCleanupHandler handler = new TenantCleanupHandler(mockOsClient); handler.cleanupTenantData(context); verify(mockOsClient).deleteContentByPrefix("tenant4/"); From cd4ba8b2c210a9a24036e4bf0087dacc6ac3979d Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 14:00:22 +0200 Subject: [PATCH 17/39] test improvements --- ...stPluginAttachmentsServiceHandlerTest.java | 9 +- .../attachments/oss/client/AWSClient.java | 8 + .../attachments/oss/client/AzureClient.java | 5 + .../attachments/oss/client/GoogleClient.java | 6 + .../attachments/oss/client/AWSClientTest.java | 107 +++-------- .../oss/client/AzureClientTest.java | 138 +++----------- .../oss/client/GoogleClientTest.java | 179 +++--------------- .../OSSAttachmentsServiceHandlerTest.java | 16 +- 8 files changed, 99 insertions(+), 369 deletions(-) diff --git a/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java index bc391fe9b..26b418ef7 100644 --- a/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java +++ b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java @@ -6,8 +6,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.*; - import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.StatusCode; import com.sap.cds.feature.attachments.service.AttachmentService; @@ -17,7 +15,6 @@ import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.List; @@ -75,12 +72,10 @@ void dummyTestForDelete() { } @Test - void dummyTestForCreate() throws IOException { + void dummyTestForCreate() { var context = AttachmentCreateEventContext.create(); context.setData(MediaData.create()); - var stream = mock(InputStream.class); - when(stream.readAllBytes()).thenReturn("test".getBytes(StandardCharsets.UTF_8)); - context.getData().setContent(stream); + context.getData().setContent(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))); assertDoesNotThrow(() -> cut.createAttachment(context)); } diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java index 513a2b738..ec2ed5e7b 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java @@ -63,6 +63,14 @@ public AWSClient(ServiceBinding binding, ExecutorService executor) { logger.info("Initialized AWS S3 client"); } + AWSClient( + S3Client s3Client, S3AsyncClient s3AsyncClient, String bucketName, ExecutorService executor) { + this.s3Client = s3Client; + this.s3AsyncClient = s3AsyncClient; + this.bucketName = bucketName; + this.executor = executor; + } + @Override public Future uploadContent( InputStream content, String completeFileName, String contentType) { diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java index 4f930ac08..d83c64ee1 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java @@ -38,6 +38,11 @@ public AzureClient(ServiceBinding binding, ExecutorService executor) { logger.info("Initialized Azure Blob Storage client"); } + AzureClient(BlobContainerClient blobContainerClient, ExecutorService executor) { + this.blobContainerClient = blobContainerClient; + this.executor = executor; + } + @Override public Future uploadContent( InputStream content, String completeFileName, String contentType) { diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java index d41a6767a..ece8e91cb 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java @@ -54,6 +54,12 @@ public GoogleClient(ServiceBinding binding, ExecutorService executor) { logger.info("Initialized client for Google Cloud Storage with binding: {}", binding); } + GoogleClient(Storage storage, String bucketName, ExecutorService executor) { + this.storage = storage; + this.bucketName = bucketName; + this.executor = executor; + } + @Override public Future uploadContent( InputStream content, String completeFileName, String contentType) { diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java index b535ce53b..b3b55dc39 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java @@ -59,10 +59,9 @@ void testConstructorWithAwsBindingUsesAwsClient() @Test void testReadContent() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - // Mock S3Client to return a dummy InputStream S3Client mockS3Client = mock(S3Client.class); + AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + ByteArrayInputStream mockInputStream = new ByteArrayInputStream("test-data".getBytes()); GetObjectResponse mockResponse = mock(GetObjectResponse.class); ResponseInputStream mockResponseInputStream = @@ -70,20 +69,15 @@ void testReadContent() throws Exception { when(mockS3Client.getObject(any(GetObjectRequest.class))).thenReturn(mockResponseInputStream); - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - InputStream result = awsClient.readContent("test.txt").get(); assertNotNull(result); } @Test void testUploadContent() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - // Mock S3AsyncClient to return a successful PutObjectResponse S3AsyncClient mockAsyncClient = mock(S3AsyncClient.class); + AWSClient awsClient = new AWSClient(mock(S3Client.class), mockAsyncClient, "bucket", executor); + PutObjectResponse mockPutRes = mock(PutObjectResponse.class); SdkHttpResponse mockHttpRes = mock(SdkHttpResponse.class); when(mockHttpRes.isSuccessful()).thenReturn(true); @@ -93,50 +87,33 @@ void testUploadContent() throws Exception { when(mockAsyncClient.putObject(any(PutObjectRequest.class), any(AsyncRequestBody.class))) .thenReturn(successFuture); - var field = AWSClient.class.getDeclaredField("s3AsyncClient"); - field.setAccessible(true); - field.set(awsClient, mockAsyncClient); - - // Should not throw - awsClient .uploadContent(new ByteArrayInputStream("test".getBytes()), "test.txt", "text/plain") .get(); } @Test - void testDeleteContent() throws NoSuchFieldException, IllegalAccessException { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - // Mock S3Client to return a DeleteObjectResponse with successful SdkHttpResponse + void testDeleteContent() { S3Client mockS3Client = mock(S3Client.class); + AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + DeleteObjectResponse mockDelRes = mock(DeleteObjectResponse.class); SdkHttpResponse mockHttpRes = mock(SdkHttpResponse.class); when(mockHttpRes.isSuccessful()).thenReturn(true); when(mockDelRes.sdkHttpResponse()).thenReturn(mockHttpRes); when(mockS3Client.deleteObject(any(DeleteObjectRequest.class))).thenReturn(mockDelRes); - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - assertDoesNotThrow(() -> awsClient.deleteContent("test.txt").get()); } @Test void testReadContentThrows() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - // Mock S3Client to return a dummy InputStream S3Client mockS3Client = mock(S3Client.class); + AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); when(mockS3Client.getObject(any(GetObjectRequest.class))) .thenThrow(new RuntimeException("Simulated S3 failure")); - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - ExecutionException thrown = assertThrows(ExecutionException.class, () -> awsClient.readContent("test.txt").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); @@ -144,19 +121,14 @@ void testReadContentThrows() throws Exception { @Test void testUploadContentThrows() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - // Mock S3AsyncClient that always fails S3AsyncClient mockAsyncClient = mock(S3AsyncClient.class); + AWSClient awsClient = new AWSClient(mock(S3Client.class), mockAsyncClient, "bucket", executor); + CompletableFuture failedFuture = new CompletableFuture<>(); failedFuture.completeExceptionally(new RuntimeException("Simulated S3 failure")); when(mockAsyncClient.putObject(any(PutObjectRequest.class), any(AsyncRequestBody.class))) .thenReturn((CompletableFuture) failedFuture); - var field = AWSClient.class.getDeclaredField("s3AsyncClient"); - field.setAccessible(true); - field.set(awsClient, mockAsyncClient); - ExecutionException thrown = assertThrows( ExecutionException.class, @@ -171,18 +143,13 @@ void testUploadContentThrows() throws Exception { @Test void testUploadContentThrowsOnPutResponseNull() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - // Mock S3AsyncClient that returns a null PutObjectResponse S3AsyncClient mockAsyncClient = mock(S3AsyncClient.class); + AWSClient awsClient = new AWSClient(mock(S3Client.class), mockAsyncClient, "bucket", executor); + CompletableFuture nullFuture = CompletableFuture.completedFuture(null); when(mockAsyncClient.putObject(any(PutObjectRequest.class), any(AsyncRequestBody.class))) .thenReturn(nullFuture); - var field = AWSClient.class.getDeclaredField("s3AsyncClient"); - field.setAccessible(true); - field.set(awsClient, mockAsyncClient); - ExecutionException thrown = assertThrows( ExecutionException.class, @@ -197,39 +164,28 @@ void testUploadContentThrowsOnPutResponseNull() throws Exception { @Test void testDeleteContentThrowsOnRuntimeException() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - // Mock S3Client to throw a RuntimeException S3Client mockS3Client = mock(S3Client.class); + AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + when(mockS3Client.deleteObject(any(DeleteObjectRequest.class))) .thenThrow(new RuntimeException("Simulated S3 delete failure")); - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - ExecutionException thrown = assertThrows(ExecutionException.class, () -> awsClient.deleteContent("test.txt").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } @Test - void testDeleteContentThrowsOnUnsuccessfulResponse() - throws NoSuchFieldException, IllegalAccessException { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - - // Mock S3Client to return a DeleteObjectResponse with unsuccessful SdkHttpResponse + void testDeleteContentThrowsOnUnsuccessfulResponse() { S3Client mockS3Client = mock(S3Client.class); + AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + DeleteObjectResponse mockDelRes = mock(DeleteObjectResponse.class); SdkHttpResponse mockHttpRes = mock(SdkHttpResponse.class); when(mockHttpRes.isSuccessful()).thenReturn(false); when(mockDelRes.sdkHttpResponse()).thenReturn(mockHttpRes); when(mockS3Client.deleteObject(any(DeleteObjectRequest.class))).thenReturn(mockDelRes); - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - ExecutionException thrown = assertThrows(ExecutionException.class, () -> awsClient.deleteContent("test.txt").get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); @@ -237,9 +193,8 @@ void testDeleteContentThrowsOnUnsuccessfulResponse() @Test void testDeleteContentByPrefix() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - S3Client mockS3Client = mock(S3Client.class); + AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); S3Object obj1 = S3Object.builder().key("prefix/file1.txt").build(); S3Object obj2 = S3Object.builder().key("prefix/file2.txt").build(); @@ -254,10 +209,6 @@ void testDeleteContentByPrefix() throws Exception { when(deleteResponse.errors()).thenReturn(Collections.emptyList()); when(mockS3Client.deleteObjects(any(DeleteObjectsRequest.class))).thenReturn(deleteResponse); - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - awsClient.deleteContentByPrefix("prefix/").get(); verify(mockS3Client).deleteObjects(any(DeleteObjectsRequest.class)); @@ -265,34 +216,25 @@ void testDeleteContentByPrefix() throws Exception { @Test void testDeleteContentByPrefixEmptyList() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - S3Client mockS3Client = mock(S3Client.class); + AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); ListObjectsV2Response listResponse = mock(ListObjectsV2Response.class); when(listResponse.contents()).thenReturn(Collections.emptyList()); when(listResponse.isTruncated()).thenReturn(false); when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))).thenReturn(listResponse); - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - assertDoesNotThrow(() -> awsClient.deleteContentByPrefix("prefix/").get()); } @Test void testDeleteContentByPrefixThrowsOnRuntimeException() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - S3Client mockS3Client = mock(S3Client.class); + AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))) .thenThrow(new RuntimeException("Simulated failure")); - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - ExecutionException thrown = assertThrows( ExecutionException.class, () -> awsClient.deleteContentByPrefix("prefix/").get()); @@ -301,9 +243,8 @@ void testDeleteContentByPrefixThrowsOnRuntimeException() throws Exception { @Test void testDeleteContentByPrefixWithPagination() throws Exception { - AWSClient awsClient = new AWSClient(getDummyBinding(), executor); - S3Client mockS3Client = mock(S3Client.class); + AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); // First page: 2 objects, isTruncated=true S3Object obj1 = S3Object.builder().key("prefix/file1.txt").build(); @@ -336,10 +277,6 @@ void testDeleteContentByPrefixWithPagination() throws Exception { when(deleteResponse.errors()).thenReturn(Collections.emptyList()); when(mockS3Client.deleteObjects(any(DeleteObjectsRequest.class))).thenReturn(deleteResponse); - var field = AWSClient.class.getDeclaredField("s3Client"); - field.setAccessible(true); - field.set(awsClient, mockS3Client); - awsClient.deleteContentByPrefix("prefix/").get(); // deleteObjects should be called twice — once per page diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java index 7f6fe6de2..f1c55570d 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AzureClientTest.java @@ -27,25 +27,11 @@ class AzureClientTest { ExecutorService executor = Executors.newCachedThreadPool(); @Test - void testReadContent() - throws NoSuchFieldException, - SecurityException, - IllegalArgumentException, - IllegalAccessException, - InterruptedException, - ExecutionException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - - // Mock BlobContainerClient and BlobClient + void testReadContent() throws InterruptedException, ExecutionException { BlobContainerClient mockContainer = mock(BlobContainerClient.class); BlobClient mockBlobClient = mock(BlobClient.class); + AzureClient azureClient = new AzureClient(mockContainer, executor); - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); when(mockContainer.getBlobClient(anyString())).thenReturn(mockBlobClient); // Should not throw @@ -53,26 +39,15 @@ void testReadContent() } @Test - void testUploadContent() - throws NoSuchFieldException, - IllegalAccessException, - InterruptedException, - ExecutionException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - - // Mock BlobContainerClient and BlockBlobClient + void testUploadContent() throws InterruptedException, ExecutionException { BlobContainerClient mockContainer = mock(BlobContainerClient.class); + BlobClient mockBlobClient = mock(BlobClient.class); BlockBlobClient mockBlockBlob = mock(BlockBlobClient.class); BlobOutputStream mockOutputStream = mock(BlobOutputStream.class); + AzureClient azureClient = new AzureClient(mockContainer, executor); - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); - when(mockContainer.getBlobClient(anyString())).thenReturn(mock(BlobClient.class)); - when(mockContainer.getBlobClient(anyString()).getBlockBlobClient()).thenReturn(mockBlockBlob); + when(mockContainer.getBlobClient(anyString())).thenReturn(mockBlobClient); + when(mockBlobClient.getBlockBlobClient()).thenReturn(mockBlockBlob); when(mockBlockBlob.getBlobOutputStream()).thenReturn(mockOutputStream); InputStream mockInput = new java.io.ByteArrayInputStream("test-data".getBytes()); @@ -82,23 +57,15 @@ void testUploadContent() } @Test - void testUploadContentThrowsOnIOException() - throws NoSuchFieldException, IllegalAccessException, IOException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - - // Mock BlobContainerClient and BlockBlobClient + void testUploadContentThrowsOnIOException() throws IOException { BlobContainerClient mockContainer = mock(BlobContainerClient.class); + BlobClient mockBlobClient = mock(BlobClient.class); BlockBlobClient mockBlockBlob = mock(BlockBlobClient.class); BlobOutputStream mockOutputStream = mock(BlobOutputStream.class); + AzureClient azureClient = new AzureClient(mockContainer, executor); - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); - when(mockContainer.getBlobClient(anyString())).thenReturn(mock(BlobClient.class)); - when(mockContainer.getBlobClient(anyString()).getBlockBlobClient()).thenReturn(mockBlockBlob); + when(mockContainer.getBlobClient(anyString())).thenReturn(mockBlobClient); + when(mockBlobClient.getBlockBlobClient()).thenReturn(mockBlockBlob); when(mockBlockBlob.getBlobOutputStream()).thenReturn(mockOutputStream); // Mock InputStream to throw IOException @@ -113,23 +80,11 @@ void testUploadContentThrowsOnIOException() } @Test - void testDeleteContentThrowsOnRuntimeException() - throws NoSuchFieldException, - SecurityException, - IllegalArgumentException, - IllegalAccessException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - - // Mock BlobContainerClient and BlobClient + void testDeleteContentThrowsOnRuntimeException() { BlobContainerClient mockContainer = mock(BlobContainerClient.class); BlobClient mockBlobClient = mock(BlobClient.class); + AzureClient azureClient = new AzureClient(mockContainer, executor); - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); when(mockContainer.getBlobClient(anyString())).thenReturn(mockBlobClient); // Mock delete to throw RuntimeException @@ -141,25 +96,11 @@ void testDeleteContentThrowsOnRuntimeException() } @Test - void testDeleteContent() - throws NoSuchFieldException, - SecurityException, - IllegalArgumentException, - IllegalAccessException, - InterruptedException, - ExecutionException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - - // Mock BlobContainerClient and BlobClient + void testDeleteContent() throws InterruptedException, ExecutionException { BlobContainerClient mockContainer = mock(BlobContainerClient.class); BlobClient mockBlobClient = mock(BlobClient.class); + AzureClient azureClient = new AzureClient(mockContainer, executor); - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); when(mockContainer.getBlobClient(anyString())).thenReturn(mockBlobClient); // Should not throw @@ -167,26 +108,14 @@ void testDeleteContent() } @Test - void testReadContentThrowsOnRuntimeException() - throws NoSuchFieldException, - SecurityException, - IllegalArgumentException, - IllegalAccessException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - - // Mock BlobContainerClient and BlobClient + void testReadContentThrowsOnRuntimeException() { BlobContainerClient mockContainer = mock(BlobContainerClient.class); BlobClient mockBlobClient = mock(BlobClient.class); + AzureClient azureClient = new AzureClient(mockContainer, executor); - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); when(mockContainer.getBlobClient(anyString())).thenReturn(mockBlobClient); - // Mock delete to throw RuntimeException + // Mock openInputStream to throw RuntimeException doThrow(new RuntimeException("Simulated read failure")).when(mockBlobClient).openInputStream(); ExecutionException thrown = @@ -195,22 +124,10 @@ void testReadContentThrowsOnRuntimeException() } @Test - void testDeleteContentByPrefix() - throws NoSuchFieldException, - IllegalAccessException, - InterruptedException, - ExecutionException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - + void testDeleteContentByPrefix() throws InterruptedException, ExecutionException { BlobContainerClient mockContainer = mock(BlobContainerClient.class); BlobClient mockBlobClient = mock(BlobClient.class); - - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); + AzureClient azureClient = new AzureClient(mockContainer, executor); BlobItem item1 = mock(BlobItem.class); when(item1.getName()).thenReturn("prefix/file1.txt"); @@ -229,18 +146,9 @@ void testDeleteContentByPrefix() } @Test - void testDeleteContentByPrefixThrowsOnRuntimeException() - throws NoSuchFieldException, IllegalAccessException { - AzureClient azureClient = mock(AzureClient.class, CALLS_REAL_METHODS); - + void testDeleteContentByPrefixThrowsOnRuntimeException() { BlobContainerClient mockContainer = mock(BlobContainerClient.class); - - var field = AzureClient.class.getDeclaredField("blobContainerClient"); - field.setAccessible(true); - field.set(azureClient, mockContainer); - var executorField = AzureClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(azureClient, executor); + AzureClient azureClient = new AzureClient(mockContainer, executor); when(mockContainer.listBlobs(any(ListBlobsOptions.class), isNull())) .thenThrow(new RuntimeException("Simulated failure")); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java index 51147491d..c61af0a4f 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java @@ -54,18 +54,12 @@ void testConstructorThrowsOnInvalidCredentials() { } @Test - void testDeleteContent() - throws NoSuchFieldException, - IllegalArgumentException, - IllegalAccessException, - InterruptedException, - ExecutionException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); + void testDeleteContent() throws InterruptedException, ExecutionException { + Storage mockStorage = mock(Storage.class); + GoogleClient googleClient = new GoogleClient(mockStorage, "my-bucket", executor); String fileName = "file.txt"; - // Mock storage and paging - Storage mockStorage = mock(Storage.class); Page mockPage = mock(Page.class); Blob mockBlob = mock(Blob.class); when(mockBlob.getName()).thenReturn(fileName); @@ -75,48 +69,19 @@ void testDeleteContent() when(mockStorage.list(anyString(), any(), any())).thenReturn(mockPage); when(mockStorage.delete(any(BlobId.class))).thenReturn(true); - // Inject mock storage and bucketName into googleClient using reflection - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - var bucketField = GoogleClient.class.getDeclaredField("bucketName"); - bucketField.setAccessible(true); - bucketField.set(googleClient, "my-bucket"); - // Should not throw googleClient.deleteContent(fileName).get(); } @Test - void testUploadContent() - throws NoSuchFieldException, - IllegalArgumentException, - IllegalAccessException, - InterruptedException, - ExecutionException, - IOException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); - - // Mock storage and writer + void testUploadContent() throws InterruptedException, ExecutionException, IOException { Storage mockStorage = mock(Storage.class); - WriteChannel mockWriter = mock(WriteChannel.class); + GoogleClient googleClient = new GoogleClient(mockStorage, "my-bucket", executor); - // Inject mock storage and bucketName into googleClient using reflection - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - var bucketField = GoogleClient.class.getDeclaredField("bucketName"); - bucketField.setAccessible(true); - bucketField.set(googleClient, "my-bucket"); + WriteChannel mockWriter = mock(WriteChannel.class); when(mockStorage.writer(any(BlobInfo.class))).thenReturn(mockWriter); - when(mockWriter.write(any(java.nio.ByteBuffer.class))).thenReturn(42); // return any int + when(mockWriter.write(any(java.nio.ByteBuffer.class))).thenReturn(42); InputStream input = new java.io.ByteArrayInputStream("test".getBytes()); // Should not throw @@ -124,44 +89,25 @@ void testUploadContent() } @Test - void testReadContent() - throws NoSuchFieldException, - IllegalArgumentException, - IllegalAccessException, - InterruptedException, - ExecutionException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); - - // Mock storage and read channel + void testReadContent() throws InterruptedException, ExecutionException { Storage mockStorage = mock(Storage.class); + GoogleClient googleClient = new GoogleClient(mockStorage, "my-bucket", executor); + ReadChannel mockReadChannel = mock(ReadChannel.class); when(mockStorage.reader(any(com.google.cloud.storage.BlobId.class))) .thenReturn(mockReadChannel); - // Inject mock storage and bucketName into googleClient using reflection - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - var bucketField = GoogleClient.class.getDeclaredField("bucketName"); - bucketField.setAccessible(true); - bucketField.set(googleClient, "my-bucket"); - // Should not throw googleClient.readContent("file.txt").get(); } @Test - void testDeleteContentDoesNotWork() - throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); + void testDeleteContentDoesNotWork() { + Storage mockStorage = mock(Storage.class); + GoogleClient googleClient = new GoogleClient(mockStorage, "my-bucket", executor); String fileName = "file.txt"; - // Mock storage and paging - Storage mockStorage = mock(Storage.class); Page mockPage = mock(Page.class); Blob mockBlob = mock(Blob.class); when(mockBlob.getName()).thenReturn(fileName); @@ -171,41 +117,17 @@ void testDeleteContentDoesNotWork() when(mockStorage.list(anyString(), any(), any())).thenReturn(mockPage); when(mockStorage.delete(any(BlobId.class))).thenReturn(false); - // Inject mock storage and bucketName into googleClient using reflection - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - var bucketField = GoogleClient.class.getDeclaredField("bucketName"); - bucketField.setAccessible(true); - bucketField.set(googleClient, "my-bucket"); - ExecutionException thrown = assertThrows(ExecutionException.class, () -> googleClient.deleteContent(fileName).get()); assertInstanceOf(ObjectStoreServiceException.class, thrown.getCause()); } @Test - void testUploadContentThrowsOnIOException() - throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException, IOException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); - - // Mock storage and writer + void testUploadContentThrowsOnIOException() throws IOException { Storage mockStorage = mock(Storage.class); - WriteChannel mockWriter = mock(WriteChannel.class); + GoogleClient googleClient = new GoogleClient(mockStorage, "my-bucket", executor); - // Inject mock storage and bucketName into googleClient using reflection - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - var bucketField = GoogleClient.class.getDeclaredField("bucketName"); - bucketField.setAccessible(true); - bucketField.set(googleClient, "my-bucket"); + WriteChannel mockWriter = mock(WriteChannel.class); when(mockStorage.writer(any(BlobInfo.class))).thenReturn(mockWriter); // Simulate IOException on write @@ -223,21 +145,11 @@ void testUploadContentThrowsOnIOException() } @Test - void testDeleteContentThrowsOnRuntimeException() - throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); - - // Mock storage and blob to throw RuntimeException on delete + void testDeleteContentThrowsOnRuntimeException() { Storage mockStorage = mock(Storage.class); - Blob mockBlob = mock(Blob.class); + GoogleClient googleClient = new GoogleClient(mockStorage, "my-bucket", executor); - // Inject mock storage into googleClient using reflection - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); + Blob mockBlob = mock(Blob.class); when(mockStorage.get(any(String.class), any(String.class))).thenReturn(mockBlob); doThrow(new RuntimeException("Simulated delete failure")).when(mockBlob).delete(); @@ -248,22 +160,10 @@ void testDeleteContentThrowsOnRuntimeException() } @Test - void testReadContentThrowsOnRuntimeException() - throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); - - // Mock storage and blob to throw RuntimeException on reader + void testReadContentThrowsOnRuntimeException() { Storage mockStorage = mock(Storage.class); + GoogleClient googleClient = new GoogleClient(mockStorage, "my-bucket", executor); - // Inject mock storage into googleClient using reflection - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - - // Mock blob.reader() to throw RuntimeException doThrow(new RuntimeException("Simulated read failure")) .when(mockStorage) .reader(any(com.google.cloud.storage.BlobId.class)); @@ -274,14 +174,10 @@ void testReadContentThrowsOnRuntimeException() } @Test - void testDeleteContentByPrefix() - throws NoSuchFieldException, - IllegalAccessException, - InterruptedException, - ExecutionException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); - + void testDeleteContentByPrefix() throws InterruptedException, ExecutionException { Storage mockStorage = mock(Storage.class); + GoogleClient googleClient = new GoogleClient(mockStorage, "my-bucket", executor); + Page mockPage = mock(Page.class); Blob mockBlob1 = mock(Blob.class); when(mockBlob1.getName()).thenReturn("prefix/file1.txt"); @@ -295,40 +191,19 @@ void testDeleteContentByPrefix() when(mockStorage.list(anyString(), any(), any())).thenReturn(mockPage); when(mockStorage.delete(any(BlobId.class))).thenReturn(true); - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - var bucketField = GoogleClient.class.getDeclaredField("bucketName"); - bucketField.setAccessible(true); - bucketField.set(googleClient, "my-bucket"); - googleClient.deleteContentByPrefix("prefix/").get(); verify(mockStorage, times(2)).delete(any(BlobId.class)); } @Test - void testDeleteContentByPrefixThrowsOnRuntimeException() - throws NoSuchFieldException, IllegalAccessException { - GoogleClient googleClient = mock(GoogleClient.class, CALLS_REAL_METHODS); - + void testDeleteContentByPrefixThrowsOnRuntimeException() { Storage mockStorage = mock(Storage.class); + GoogleClient googleClient = new GoogleClient(mockStorage, "my-bucket", executor); + when(mockStorage.list(anyString(), any(), any())) .thenThrow(new RuntimeException("Simulated failure")); - var field = GoogleClient.class.getDeclaredField("storage"); - field.setAccessible(true); - field.set(googleClient, mockStorage); - var executorField = GoogleClient.class.getDeclaredField("executor"); - executorField.setAccessible(true); - executorField.set(googleClient, executor); - var bucketField = GoogleClient.class.getDeclaredField("bucketName"); - bucketField.setAccessible(true); - bucketField.set(googleClient, "my-bucket"); - ExecutionException thrown = assertThrows( ExecutionException.class, () -> googleClient.deleteContentByPrefix("prefix/").get()); diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index 90ff570da..36fb3f75c 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -73,8 +73,8 @@ private static CdsEntity stubEntity(String name) { } /** - * Creates a real {@link AttachmentCreateEventContext} populated with the given values. - * The only mock used is CdsEntity (a model-level concept not creatable without a full model). + * Creates a real {@link AttachmentCreateEventContext} populated with the given values. The only + * mock used is CdsEntity (a model-level concept not creatable without a full model). */ private static AttachmentCreateEventContext createContext( String contentId, String mimeType, String fileName, byte[] content) { @@ -204,8 +204,7 @@ void testReadAttachmentReadsContent() { @Test void testReadAttachmentWithNullContentThrows() { - when(mockOsClient.readContent("doc123")) - .thenReturn(CompletableFuture.completedFuture(null)); + when(mockOsClient.readContent("doc123")).thenReturn(CompletableFuture.completedFuture(null)); var context = AttachmentReadEventContext.create(); context.setContentId("doc123"); @@ -266,8 +265,7 @@ void testCreateAttachmentHandlesExecutionException() throws Exception { @Test void testMarkAsDeletedHandlesInterruptedException() throws Exception { - var context = - createContextForDeleteException(new InterruptedException("Thread interrupted")); + var context = createContextForDeleteException(new InterruptedException("Thread interrupted")); assertThrows(ServiceException.class, () -> handler.markAttachmentAsDeleted(context)); assertThat(context.isCompleted()).isTrue(); } @@ -299,8 +297,7 @@ void testReadAttachmentHandlesInterruptedException() throws Exception { @Test void testReadAttachmentHandlesExecutionException() throws Exception { var context = - createContextForReadException( - new ExecutionException("failed", new RuntimeException())); + createContextForReadException(new ExecutionException("failed", new RuntimeException())); assertThrows(ServiceException.class, () -> handler.readAttachment(context)); assertThat(context.isCompleted()).isTrue(); } @@ -398,8 +395,7 @@ void testMarkAsDeletedWithMultitenancyBuildsObjectKey() { when(mockOsClient.deleteContent("myTenant/content123")) .thenReturn(CompletableFuture.completedFuture(null)); - AttachmentMarkAsDeletedEventContext context = - mock(AttachmentMarkAsDeletedEventContext.class); + AttachmentMarkAsDeletedEventContext context = mock(AttachmentMarkAsDeletedEventContext.class); when(context.getContentId()).thenReturn("content123"); when(context.getUserInfo()).thenReturn(userInfoWithTenant("myTenant")); From 31c079d2319b899b37cc01f3c103e64a858ce16f Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 31 Mar 2026 14:00:39 +0200 Subject: [PATCH 18/39] spotless --- ...stPluginAttachmentsServiceHandlerTest.java | 1 + .../attachments/oss/client/AWSClientTest.java | 27 ++++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java index 26b418ef7..160e938fd 100644 --- a/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java +++ b/integration-tests/generic/src/test/java/com/sap/cds/feature/attachments/integrationtests/testhandler/TestPluginAttachmentsServiceHandlerTest.java @@ -6,6 +6,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertNotNull; + import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.StatusCode; import com.sap.cds.feature.attachments.service.AttachmentService; diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java index b3b55dc39..2710e2d8f 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java @@ -60,7 +60,8 @@ void testConstructorWithAwsBindingUsesAwsClient() @Test void testReadContent() throws Exception { S3Client mockS3Client = mock(S3Client.class); - AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + AWSClient awsClient = + new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); ByteArrayInputStream mockInputStream = new ByteArrayInputStream("test-data".getBytes()); GetObjectResponse mockResponse = mock(GetObjectResponse.class); @@ -95,7 +96,8 @@ void testUploadContent() throws Exception { @Test void testDeleteContent() { S3Client mockS3Client = mock(S3Client.class); - AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + AWSClient awsClient = + new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); DeleteObjectResponse mockDelRes = mock(DeleteObjectResponse.class); SdkHttpResponse mockHttpRes = mock(SdkHttpResponse.class); @@ -109,7 +111,8 @@ void testDeleteContent() { @Test void testReadContentThrows() throws Exception { S3Client mockS3Client = mock(S3Client.class); - AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + AWSClient awsClient = + new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); when(mockS3Client.getObject(any(GetObjectRequest.class))) .thenThrow(new RuntimeException("Simulated S3 failure")); @@ -165,7 +168,8 @@ void testUploadContentThrowsOnPutResponseNull() throws Exception { @Test void testDeleteContentThrowsOnRuntimeException() throws Exception { S3Client mockS3Client = mock(S3Client.class); - AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + AWSClient awsClient = + new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); when(mockS3Client.deleteObject(any(DeleteObjectRequest.class))) .thenThrow(new RuntimeException("Simulated S3 delete failure")); @@ -178,7 +182,8 @@ void testDeleteContentThrowsOnRuntimeException() throws Exception { @Test void testDeleteContentThrowsOnUnsuccessfulResponse() { S3Client mockS3Client = mock(S3Client.class); - AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + AWSClient awsClient = + new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); DeleteObjectResponse mockDelRes = mock(DeleteObjectResponse.class); SdkHttpResponse mockHttpRes = mock(SdkHttpResponse.class); @@ -194,7 +199,8 @@ void testDeleteContentThrowsOnUnsuccessfulResponse() { @Test void testDeleteContentByPrefix() throws Exception { S3Client mockS3Client = mock(S3Client.class); - AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + AWSClient awsClient = + new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); S3Object obj1 = S3Object.builder().key("prefix/file1.txt").build(); S3Object obj2 = S3Object.builder().key("prefix/file2.txt").build(); @@ -217,7 +223,8 @@ void testDeleteContentByPrefix() throws Exception { @Test void testDeleteContentByPrefixEmptyList() throws Exception { S3Client mockS3Client = mock(S3Client.class); - AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + AWSClient awsClient = + new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); ListObjectsV2Response listResponse = mock(ListObjectsV2Response.class); when(listResponse.contents()).thenReturn(Collections.emptyList()); @@ -230,7 +237,8 @@ void testDeleteContentByPrefixEmptyList() throws Exception { @Test void testDeleteContentByPrefixThrowsOnRuntimeException() throws Exception { S3Client mockS3Client = mock(S3Client.class); - AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + AWSClient awsClient = + new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))) .thenThrow(new RuntimeException("Simulated failure")); @@ -244,7 +252,8 @@ void testDeleteContentByPrefixThrowsOnRuntimeException() throws Exception { @Test void testDeleteContentByPrefixWithPagination() throws Exception { S3Client mockS3Client = mock(S3Client.class); - AWSClient awsClient = new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); + AWSClient awsClient = + new AWSClient(mockS3Client, mock(S3AsyncClient.class), "bucket", executor); // First page: 2 objects, isTruncated=true S3Object obj1 = S3Object.builder().key("prefix/file1.txt").build(); From 41b257268ebf67b24ad4f4a1da50cae32bc3c9c6 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 1 Apr 2026 14:11:56 +0200 Subject: [PATCH 19/39] Add mtx bookshop --- samples/bookshop-mtx/.cdsrc.json | 19 + samples/bookshop-mtx/.gitignore | 34 ++ .../bookshop-mtx/Makefile_20260401103121.mta | 54 +++ samples/bookshop-mtx/README.md | 128 +++++++ .../bookshop-mtx/app/_i18n/i18n.properties | 15 + .../bookshop-mtx/app/_i18n/i18n_de.properties | 15 + .../app/admin-books/dist/Component-dbg.js | 8 + .../app/admin-books/dist/Component-preload.js | 8 + .../admin-books/dist/Component-preload.js.map | 1 + .../app/admin-books/dist/Component.js | 2 + .../app/admin-books/dist/Component.js.map | 1 + .../app/admin-books/dist/admin-books.zip | Bin 0 -> 4359 bytes .../app/admin-books/dist/i18n/i18n.properties | 3 + .../admin-books/dist/i18n/i18n_de.properties | 3 + .../app/admin-books/dist/manifest.json | 158 +++++++++ .../app/admin-books/fiori-service.cds | 113 ++++++ .../bookshop-mtx/app/admin-books/package.json | 13 + samples/bookshop-mtx/app/admin-books/ui5.yaml | 21 ++ .../app/admin-books/webapp/Component.js | 8 + .../admin-books/webapp/i18n/i18n.properties | 3 + .../webapp/i18n/i18n_de.properties | 3 + .../app/admin-books/webapp/manifest.json | 145 ++++++++ .../bookshop-mtx/app/admin-books/xs-app.json | 18 + .../app/appconfig/fioriSandboxConfig.json | 95 +++++ .../app/browse/dist/Component-dbg.js | 7 + .../app/browse/dist/Component-preload.js | 8 + .../app/browse/dist/Component-preload.js.map | 1 + .../bookshop-mtx/app/browse/dist/Component.js | 2 + .../app/browse/dist/Component.js.map | 1 + .../bookshop-mtx/app/browse/dist/browse.zip | Bin 0 -> 4490 bytes .../app/browse/dist/i18n/i18n.properties | 3 + .../app/browse/dist/i18n/i18n_de.properties | 3 + .../app/browse/dist/manifest.json | 150 ++++++++ .../bookshop-mtx/app/browse/fiori-service.cds | 51 +++ samples/bookshop-mtx/app/browse/package.json | 13 + samples/bookshop-mtx/app/browse/ui5.yaml | 21 ++ .../app/browse/webapp/Component.js | 7 + .../app/browse/webapp/i18n/i18n.properties | 3 + .../app/browse/webapp/i18n/i18n_de.properties | 3 + .../app/browse/webapp/manifest.json | 137 ++++++++ samples/bookshop-mtx/app/browse/xs-app.json | 18 + samples/bookshop-mtx/app/common.cds | 264 ++++++++++++++ samples/bookshop-mtx/app/index.html | 32 ++ .../portal/portal-site/CommonDataModel.json | 102 ++++++ .../portal-site/i18n/catalog.properties | 1 + .../portal-site/i18n/catalog_de.properties | 1 + .../portal/portal-site/i18n/group.properties | 1 + .../portal-site/i18n/group_de.properties | 1 + samples/bookshop-mtx/app/router/package.json | 9 + samples/bookshop-mtx/app/router/xs-app.json | 16 + samples/bookshop-mtx/app/services.cds | 6 + .../db/data/sap.capire.bookshop-Authors.csv | 5 + .../db/data/sap.capire.bookshop-Books.csv | 6 + .../data/sap.capire.bookshop-Books_texts.csv | 5 + .../db/data/sap.capire.bookshop-Genres.csv | 16 + samples/bookshop-mtx/db/package.json | 14 + samples/bookshop-mtx/db/schema.cds | 37 ++ samples/bookshop-mtx/db/undeploy.json | 7 + samples/bookshop-mtx/mta.yaml | 151 ++++++++ samples/bookshop-mtx/mtx/sidecar/package.json | 26 ++ samples/bookshop-mtx/package.json | 13 + samples/bookshop-mtx/pom.xml | 192 ++++++++++ .../bookshop-mtx/resources/admin-books.zip | Bin 0 -> 4359 bytes samples/bookshop-mtx/resources/browse.zip | Bin 0 -> 4490 bytes samples/bookshop-mtx/sonar-project.properties | 23 ++ samples/bookshop-mtx/srv/admin-service.cds | 6 + samples/bookshop-mtx/srv/attachments.cds | 49 +++ samples/bookshop-mtx/srv/cat-service.cds | 34 ++ samples/bookshop-mtx/srv/pom.xml | 166 +++++++++ .../java/customer/bookshop/Application.java | 13 + .../handlers/CatalogServiceHandler.java | 63 ++++ .../bookshop/handlers/RestHandler.java | 330 ++++++++++++++++++ .../srv/src/main/resources/application.yaml | 63 ++++ .../customer/bookshop/ApplicationTest.java | 21 ++ .../handlers/CatalogServiceHandlerTest.java | 159 +++++++++ samples/bookshop-mtx/xs-security.json | 40 +++ 76 files changed, 3168 insertions(+) create mode 100644 samples/bookshop-mtx/.cdsrc.json create mode 100644 samples/bookshop-mtx/.gitignore create mode 100644 samples/bookshop-mtx/Makefile_20260401103121.mta create mode 100644 samples/bookshop-mtx/README.md create mode 100644 samples/bookshop-mtx/app/_i18n/i18n.properties create mode 100644 samples/bookshop-mtx/app/_i18n/i18n_de.properties create mode 100644 samples/bookshop-mtx/app/admin-books/dist/Component-dbg.js create mode 100644 samples/bookshop-mtx/app/admin-books/dist/Component-preload.js create mode 100644 samples/bookshop-mtx/app/admin-books/dist/Component-preload.js.map create mode 100644 samples/bookshop-mtx/app/admin-books/dist/Component.js create mode 100644 samples/bookshop-mtx/app/admin-books/dist/Component.js.map create mode 100644 samples/bookshop-mtx/app/admin-books/dist/admin-books.zip create mode 100644 samples/bookshop-mtx/app/admin-books/dist/i18n/i18n.properties create mode 100644 samples/bookshop-mtx/app/admin-books/dist/i18n/i18n_de.properties create mode 100644 samples/bookshop-mtx/app/admin-books/dist/manifest.json create mode 100644 samples/bookshop-mtx/app/admin-books/fiori-service.cds create mode 100644 samples/bookshop-mtx/app/admin-books/package.json create mode 100644 samples/bookshop-mtx/app/admin-books/ui5.yaml create mode 100644 samples/bookshop-mtx/app/admin-books/webapp/Component.js create mode 100644 samples/bookshop-mtx/app/admin-books/webapp/i18n/i18n.properties create mode 100644 samples/bookshop-mtx/app/admin-books/webapp/i18n/i18n_de.properties create mode 100644 samples/bookshop-mtx/app/admin-books/webapp/manifest.json create mode 100644 samples/bookshop-mtx/app/admin-books/xs-app.json create mode 100644 samples/bookshop-mtx/app/appconfig/fioriSandboxConfig.json create mode 100644 samples/bookshop-mtx/app/browse/dist/Component-dbg.js create mode 100644 samples/bookshop-mtx/app/browse/dist/Component-preload.js create mode 100644 samples/bookshop-mtx/app/browse/dist/Component-preload.js.map create mode 100644 samples/bookshop-mtx/app/browse/dist/Component.js create mode 100644 samples/bookshop-mtx/app/browse/dist/Component.js.map create mode 100644 samples/bookshop-mtx/app/browse/dist/browse.zip create mode 100644 samples/bookshop-mtx/app/browse/dist/i18n/i18n.properties create mode 100644 samples/bookshop-mtx/app/browse/dist/i18n/i18n_de.properties create mode 100644 samples/bookshop-mtx/app/browse/dist/manifest.json create mode 100644 samples/bookshop-mtx/app/browse/fiori-service.cds create mode 100644 samples/bookshop-mtx/app/browse/package.json create mode 100644 samples/bookshop-mtx/app/browse/ui5.yaml create mode 100644 samples/bookshop-mtx/app/browse/webapp/Component.js create mode 100644 samples/bookshop-mtx/app/browse/webapp/i18n/i18n.properties create mode 100644 samples/bookshop-mtx/app/browse/webapp/i18n/i18n_de.properties create mode 100644 samples/bookshop-mtx/app/browse/webapp/manifest.json create mode 100644 samples/bookshop-mtx/app/browse/xs-app.json create mode 100644 samples/bookshop-mtx/app/common.cds create mode 100644 samples/bookshop-mtx/app/index.html create mode 100644 samples/bookshop-mtx/app/portal/portal-site/CommonDataModel.json create mode 100644 samples/bookshop-mtx/app/portal/portal-site/i18n/catalog.properties create mode 100644 samples/bookshop-mtx/app/portal/portal-site/i18n/catalog_de.properties create mode 100644 samples/bookshop-mtx/app/portal/portal-site/i18n/group.properties create mode 100644 samples/bookshop-mtx/app/portal/portal-site/i18n/group_de.properties create mode 100644 samples/bookshop-mtx/app/router/package.json create mode 100644 samples/bookshop-mtx/app/router/xs-app.json create mode 100644 samples/bookshop-mtx/app/services.cds create mode 100644 samples/bookshop-mtx/db/data/sap.capire.bookshop-Authors.csv create mode 100644 samples/bookshop-mtx/db/data/sap.capire.bookshop-Books.csv create mode 100644 samples/bookshop-mtx/db/data/sap.capire.bookshop-Books_texts.csv create mode 100644 samples/bookshop-mtx/db/data/sap.capire.bookshop-Genres.csv create mode 100644 samples/bookshop-mtx/db/package.json create mode 100644 samples/bookshop-mtx/db/schema.cds create mode 100644 samples/bookshop-mtx/db/undeploy.json create mode 100644 samples/bookshop-mtx/mta.yaml create mode 100644 samples/bookshop-mtx/mtx/sidecar/package.json create mode 100644 samples/bookshop-mtx/package.json create mode 100644 samples/bookshop-mtx/pom.xml create mode 100644 samples/bookshop-mtx/resources/admin-books.zip create mode 100644 samples/bookshop-mtx/resources/browse.zip create mode 100644 samples/bookshop-mtx/sonar-project.properties create mode 100644 samples/bookshop-mtx/srv/admin-service.cds create mode 100644 samples/bookshop-mtx/srv/attachments.cds create mode 100644 samples/bookshop-mtx/srv/cat-service.cds create mode 100644 samples/bookshop-mtx/srv/pom.xml create mode 100644 samples/bookshop-mtx/srv/src/main/java/customer/bookshop/Application.java create mode 100644 samples/bookshop-mtx/srv/src/main/java/customer/bookshop/handlers/CatalogServiceHandler.java create mode 100644 samples/bookshop-mtx/srv/src/main/java/customer/bookshop/handlers/RestHandler.java create mode 100644 samples/bookshop-mtx/srv/src/main/resources/application.yaml create mode 100644 samples/bookshop-mtx/srv/src/test/java/customer/bookshop/ApplicationTest.java create mode 100644 samples/bookshop-mtx/srv/src/test/java/customer/bookshop/handlers/CatalogServiceHandlerTest.java create mode 100644 samples/bookshop-mtx/xs-security.json diff --git a/samples/bookshop-mtx/.cdsrc.json b/samples/bookshop-mtx/.cdsrc.json new file mode 100644 index 000000000..7deba9f32 --- /dev/null +++ b/samples/bookshop-mtx/.cdsrc.json @@ -0,0 +1,19 @@ +{ + "profiles": [ + "with-mtx-sidecar", + "java" + ], + "requires": { + "[production]": { + "multitenancy": true, + "extensibility": true, + "toggles": true, + "auth": "xsuaa" + }, + "[with-mtx]": { + "multitenancy": true, + "extensibility": true, + "toggles": true + } + } +} diff --git a/samples/bookshop-mtx/.gitignore b/samples/bookshop-mtx/.gitignore new file mode 100644 index 000000000..2ecc68df6 --- /dev/null +++ b/samples/bookshop-mtx/.gitignore @@ -0,0 +1,34 @@ +**/gen/ +**/edmx/ +*.db +*.sqlite +*.sqlite-wal +*.sqlite-shm +schema*.sql +default-env.json + +**/bin/ +**/target/ +.flattened-pom.xml +.classpath +.project +.settings + +**/node/ +**/node_modules/ + +**/.mta/ +*.mtar + +*.log* +gc_history* +hs_err* +*.tgz +*.iml + +.vscode +.idea +.reloadtrigger + +# added by cds +.cdsrc-private.json diff --git a/samples/bookshop-mtx/Makefile_20260401103121.mta b/samples/bookshop-mtx/Makefile_20260401103121.mta new file mode 100644 index 000000000..e4a9e790c --- /dev/null +++ b/samples/bookshop-mtx/Makefile_20260401103121.mta @@ -0,0 +1,54 @@ +# Generated with Cloud MTA Build Tool version 1.2.34 +version=0.0.1 +MBT=/Users/I569613/.nvm/versions/node/v22.20.0/lib/node_modules/mbt/unpacked_bin/mbt +ifndef p +$(error platform flag is expected. e.g. use make -f makefile.mta p=cf) +endif +target_provided=true +ifndef t +t="$(CURDIR)" +target_provided=false +endif +ifndef strict +strict=true +endif +ifndef mtar +mtar="*" +endif +modules := $(shell $(MBT) provide modules -d=dev) +modules := $(subst ],,$(subst [,,$(modules))) +# List of all the recipes to be executed during the build process +.PHONY: all pre_validate pre_build validate $(modules) post_build meta mtar cleanup +# Default target compile all +all: pre_validate pre_build validate $(modules) post_build meta mtar cleanup +# Validate mta.yaml +pre_validate: + @$(MBT) validate -r=${strict} -x="paths" +pre_build: pre_validate + @$(MBT) project build -p=pre + + +# Execute module build +define build_rule +$(1): validate + @$(MBT) module build -m=$(1) -p=${p} -t=${t} +endef + +$(foreach mod,$(modules),$(eval $(call build_rule,$(mod))))# Create META-INF folder with MANIFEST.MF & mtad.yaml +meta: $(modules) post_build + @$(MBT) gen meta -p=${p} -t=${t} + +post_build: $(modules) + @$(MBT) project build -p=post -t=${t} + +# Validate mta.yaml +validate: pre_build + @$(MBT) validate -r=${strict} + +# Pack as MTAR artifact +mtar: $(modules) meta + @$(MBT) gen mtar --mtar=${mtar} --target_provided=${target_provided} -t=${t} + +cleanup: mtar +# Remove tmp folder + @$(MBT) clean -t=${t} \ No newline at end of file diff --git a/samples/bookshop-mtx/README.md b/samples/bookshop-mtx/README.md new file mode 100644 index 000000000..babcff672 --- /dev/null +++ b/samples/bookshop-mtx/README.md @@ -0,0 +1,128 @@ +# Bookshop Sample - Attachments Plugin + +This sample demonstrates how to use the `cds-feature-attachments` plugin in a CAP Java application. It extends the classic CAP bookshop sample to include file attachments for books. + +## What This Sample Demonstrates + +- Integration of the latest attachments plugin with CAP Java +- Extending existing entities with attachment capabilities +- UI integration with Fiori elements applications +- Basic attachment operations (upload, download, delete) + +## Prerequisites + +- Java 17 or higher +- Maven 3.6 or higher +- Node.js 18 or higher +- npm + +## Getting Started + +1. **Clone and navigate to the sample**: + ```bash + cd samples/bookshop + ``` + +2. **Install dependencies**: + ```bash + mvn clean compile + ``` + +3. **Run the application**: + ```bash + mvn spring-boot:run + ``` + +4. **Access the application**: + - Browse Books: http://localhost:8080/browse/index.html + - Admin Books: http://localhost:8080/admin-books/index.html + +## Using Attachments + +Once the application is running: + +1. Navigate to the Books app (browse or admin) +2. Select any book to open its details +3. Scroll down to find the "Attachments" section +4. Use the attachment controls to: + - Upload files by clicking the upload button + - View uploaded files in the attachment list + - Download files by clicking on them + - Delete files using the delete button + +## Implementation Details + +### Maven Configuration + +The attachments plugin is added to `srv/pom.xml`: + +```xml + + com.sap.cds + cds-feature-attachments + +``` + +The `cds-maven-plugin` includes the `resolve` goal to make CDS models from dependencies available: + +```xml + + com.sap.cds + cds-maven-plugin + + + cds.resolve + + resolve + + + + + +``` + +### CDS Model Extension + +The `srv/attachments.cds` file extends the Books entity with attachments: + +```cds +using { sap.capire.bookshop as my } from '../db/schema'; +using { sap.attachments.Attachments } from 'com.sap.cds/cds-feature-attachments'; + +extend my.Books with { + attachments: Composition of many Attachments; +} +``` + +### UI Integration + +The same file adds UI facets for both services to display attachments in the Fiori apps: + +```cds +using { CatalogService as service } from '../app/services'; +annotate service.Books with @( + UI.Facets: [ + { + $Type : 'UI.ReferenceFacet', + ID : 'AttachmentsFacet', + Label : '{i18n>attachments}', + Target : 'attachments/@UI.LineItem' + } + ] +); +``` + +## Storage Configuration + +This sample uses the default in-memory storage, which stores attachments directly in the H2 database. For production scenarios, consider using object store backends. + +## Advanced Configuration + +For advanced topics like object store integration, malware scanning, and security configuration, see the [main project documentation](../../README.md). + +## Troubleshooting + +- **Port conflicts**: If port 8080 is in use, specify a different port: `mvn spring-boot:run -Dspring-boot.run.arguments="--server.port=8081"` +- **Memory issues**: Increase JVM heap size: `export MAVEN_OPTS="-Xmx2g"` +- **File upload issues**: Check browser developer console for error messages +- **View loading issues**: If your view doesn’t load, try clearing the cache and local storage for `localhost` (and the relevant port) in your browser settings or try a private/incognito tab. diff --git a/samples/bookshop-mtx/app/_i18n/i18n.properties b/samples/bookshop-mtx/app/_i18n/i18n.properties new file mode 100644 index 000000000..7326bbb72 --- /dev/null +++ b/samples/bookshop-mtx/app/_i18n/i18n.properties @@ -0,0 +1,15 @@ +Books = Books +Book = Book +ID = ID +Title = Title +Author = Author +Authors = Authors +AuthorID = Author ID +AuthorName = Author Name +Name = Name +Age = Age +Stock = Stock +Order = Order +Orders = Orders +Price = Price +Genre = Genre \ No newline at end of file diff --git a/samples/bookshop-mtx/app/_i18n/i18n_de.properties b/samples/bookshop-mtx/app/_i18n/i18n_de.properties new file mode 100644 index 000000000..cb712c12c --- /dev/null +++ b/samples/bookshop-mtx/app/_i18n/i18n_de.properties @@ -0,0 +1,15 @@ +Books = Bücher +Book = Buch +ID = ID +Title = Titel +Author = Autor +Authors = Autoren +AuthorID = ID des Autors +AuthorName = Name des Autors +Name = Name +Age = Alter +Stock = Bestand +Order = Bestellung +Orders = Bestellungen +Price = Preis +Genre = Genre \ No newline at end of file diff --git a/samples/bookshop-mtx/app/admin-books/dist/Component-dbg.js b/samples/bookshop-mtx/app/admin-books/dist/Component-dbg.js new file mode 100644 index 000000000..e98677ee9 --- /dev/null +++ b/samples/bookshop-mtx/app/admin-books/dist/Component-dbg.js @@ -0,0 +1,8 @@ +sap.ui.define(["sap/fe/core/AppComponent"], function (AppComponent) { + "use strict"; + return AppComponent.extend("books.Component", { + metadata: { manifest: "json" } + }); +}); + +/* eslint no-undef:0 */ diff --git a/samples/bookshop-mtx/app/admin-books/dist/Component-preload.js b/samples/bookshop-mtx/app/admin-books/dist/Component-preload.js new file mode 100644 index 000000000..5bacffae6 --- /dev/null +++ b/samples/bookshop-mtx/app/admin-books/dist/Component-preload.js @@ -0,0 +1,8 @@ +//@ui5-bundle bookshop/admin-books/Component-preload.js +sap.ui.predefine("bookshop/admin-books/Component", ["sap/fe/core/AppComponent"],function(e){"use strict";return e.extend("books.Component",{metadata:{manifest:"json"}})}); +sap.ui.require.preload({ + "bookshop/admin-books/i18n/i18n.properties":'appTitle=Manage Books\nappSubTitle=Manage bookshop inventory\nappDescription=Manage your bookshop inventory with ease.\n', + "bookshop/admin-books/i18n/i18n_de.properties":'appTitle=B\\u00fccher verwalten\nappSubTitle=Verwalten Sie den Bestand der Buchhandlungen\nappDescription=Verwalten Sie den Bestand Ihrer Buchhandlung ganz einfach.\n', + "bookshop/admin-books/manifest.json":'{"_version":"1.49.0","sap.app":{"applicationVersion":{"version":"1.0.0"},"id":"bookshop.admin-books","type":"application","title":"{{appTitle}}","description":"{{appDescription}}","i18n":{"bundleUrl":"i18n/i18n.properties","supportedLocales":["","de"]},"dataSources":{"AdminService":{"uri":"/odata/v4/AdminService/","type":"OData","settings":{"odataVersion":"4.0"}}},"crossNavigation":{"inbounds":{"intent-Books-manage":{"signature":{"parameters":{},"additionalParameters":"allowed"},"semanticObject":"Books","action":"manage"}}}},"sap.ui":{"technology":"UI5","fullWidth":false,"deviceTypes":{"desktop":true,"tablet":true,"phone":true}},"sap.ui5":{"dependencies":{"minUI5Version":"1.115.1","libs":{"sap.fe.templates":{}}},"models":{"i18n":{"type":"sap.ui.model.resource.ResourceModel","uri":"i18n/i18n.properties","settings":{"supportedLocales":["","de"]}},"":{"dataSource":"AdminService","settings":{"operationMode":"Server","autoExpandSelect":true,"earlyRequests":true,"groupProperties":{"default":{"submit":"Auto"}}}}},"routing":{"routes":[{"pattern":":?query:","name":"BooksList","target":"BooksList"},{"pattern":"Books({key}):?query:","name":"BooksDetails","target":"BooksDetails"},{"pattern":"Books({key}/author({key2}):?query:","name":"AuthorsDetails","target":"AuthorsDetails"}],"targets":{"BooksList":{"type":"Component","id":"BooksList","name":"sap.fe.templates.ListReport","options":{"settings":{"contextPath":"/Books","initialLoad":true,"navigation":{"Books":{"detail":{"route":"BooksDetails"}}}}}},"BooksDetails":{"type":"Component","id":"BooksDetailsList","name":"sap.fe.templates.ObjectPage","options":{"settings":{"contextPath":"/Books","editableHeaderContent":false,"navigation":{"Authors":{"detail":{"route":"AuthorsDetails"}}}}}},"AuthorsDetails":{"type":"Component","id":"AuthorsDetailsList","name":"sap.fe.templates.ObjectPage","options":{"settings":{"contextPath":"/Authors"}}}}},"contentDensities":{"compact":true,"cozy":true},"flexBundle":false},"sap.fiori":{"registrationIds":[],"archeType":"transactional"}}' +}); +//# sourceMappingURL=Component-preload.js.map diff --git a/samples/bookshop-mtx/app/admin-books/dist/Component-preload.js.map b/samples/bookshop-mtx/app/admin-books/dist/Component-preload.js.map new file mode 100644 index 000000000..9164d03e2 --- /dev/null +++ b/samples/bookshop-mtx/app/admin-books/dist/Component-preload.js.map @@ -0,0 +1 @@ +{"version":3,"file":"Component-preload.js","sections":[{"offset":{"line":1,"column":0},"map":{"version":3,"names":["sap","ui","define","AppComponent","extend","metadata","manifest"],"sources":["Component-dbg.js"],"mappings":"AAAAA,IAAIC,GAAGC,4CAAO,CAAC,4BAA6B,SAAUC,GACpD,aACA,OAAOA,EAAaC,OAAO,kBAAmB,CAC5CC,SAAU,CAAEC,SAAU,SAE1B","ignoreList":[],"sourceRoot":""}},{"offset":{"line":2,"column":0},"map":{"version":3,"names":[],"sources":["Component-preload.js?bundle-code-0"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.require.preload({\n"],"sourceRoot":""}}]} \ No newline at end of file diff --git a/samples/bookshop-mtx/app/admin-books/dist/Component.js b/samples/bookshop-mtx/app/admin-books/dist/Component.js new file mode 100644 index 000000000..60e1e4c86 --- /dev/null +++ b/samples/bookshop-mtx/app/admin-books/dist/Component.js @@ -0,0 +1,2 @@ +sap.ui.define(["sap/fe/core/AppComponent"],function(e){"use strict";return e.extend("books.Component",{metadata:{manifest:"json"}})}); +//# sourceMappingURL=Component.js.map \ No newline at end of file diff --git a/samples/bookshop-mtx/app/admin-books/dist/Component.js.map b/samples/bookshop-mtx/app/admin-books/dist/Component.js.map new file mode 100644 index 000000000..f9544db1a --- /dev/null +++ b/samples/bookshop-mtx/app/admin-books/dist/Component.js.map @@ -0,0 +1 @@ +{"version":3,"file":"Component.js","names":["sap","ui","define","AppComponent","extend","metadata","manifest"],"sources":["Component-dbg.js"],"mappings":"AAAAA,IAAIC,GAAGC,OAAO,CAAC,4BAA6B,SAAUC,GACpD,aACA,OAAOA,EAAaC,OAAO,kBAAmB,CAC5CC,SAAU,CAAEC,SAAU,SAE1B","ignoreList":[]} \ No newline at end of file diff --git a/samples/bookshop-mtx/app/admin-books/dist/admin-books.zip b/samples/bookshop-mtx/app/admin-books/dist/admin-books.zip new file mode 100644 index 0000000000000000000000000000000000000000..291adfc4306eda8d7dcd2e2bad6c8fddc6136f03 GIT binary patch literal 4359 zcmZ{n2Q*ym+J;AqE~A7HJ<8~P^k_jCWeB21B$yCp^iFgM(FReXkLVIzL=B=Folhr9 z!ibWn(az+Y|NlPDI+?YeS$prb*R}3vulK#*`+-96@M!=5A|gQ8qagEd5$#b9001Bt zTXfj66On=o{r6Jk@`u0Fu-I06A8IqZQoF*2dLM(B2gR zA2(h=%z%LbgO+UH?C=<*nW?h(`{Km$Xj#od^46InA4x^Q-jJ$Rj)_D+J9;j3Z>lmN zuGSP^wIG6(U4eN!X2D;^$32ll19^TDn!lo%pTNNcD?n5x>={bj4@N6u+>oWGfh_oC z1cN~zkSrTv4c@VKCUQMGBR)Q7_OO>?gekoH#)66Hz#HbwCzI7dxo_1Xo^8>#O>5 z+7p{a(f%5;9NyQM|Q(dQ_eV4YfBu4R=f51m0T3N!sORTIK%@6-4zrhsBSK!p8+KYBzTB& z?%c+}@~Hi1Wcd}T1{bt1+SAihqjYww57BiJ<;1>TzhW8SUWVn30wkfw%Dg#i5ZJ}} zMyZK`7+;4%`Y?M2hZvBvwsr6i)rN7*dV|6d$)n}b(N-Y9?FCC%8j)Adx5PI55 zcnn0}n;tRC^P#at=45EBcK3+?mS>6Oj$No9e3afcG>lepoLNQ65){t9s0V$AwEb*k zWjn8ETfUbYKLM_)YL)gp)@Wha*VmIUlZHBXLw4YS~%Y> zaQiYuPx2(&!$wS0sLS*q8?i8QW z8!P!L8J>EWI=z?s`r6>g967Jip&l_7tT(aRXh{ZVS1#>dddjN24DtSP!X0HR{gUZb zDDJbcrZvvbf%vvDW8nwOn?iRsbL$TKHYk%ZmMuMV5ROImUl!lI8dY09jNUGdKR#nQ_B%(&vt7q}5 z_Gt-tl$1n9oAwOP8V}j$T$2wVv$LN|I4(O!oVY865R-+w15N_8Fk#Js-Mb*b&pxNyH8!Kbmg@kl(rl}Qd37Y zGXWU}si;viZh-%1d}UBV_s zH}N?$CJrxZF;fFa;5uCulyrm?om6?WLr+<6_-x^3cLu2F$56a(#%9lC?bs=QW^YyY zUS7+$*cfvey$#gHR9)~n)#2v03{e~7sJ&2*!H4#p6T++Ax`@+GspA6xeI)-m_WoXE z0!}VA4hSo2Y}aK*j=C#g;JJB#s`we;9$OGe9=PCsA9GS<$3Bp84=;PvL2gPys&==p zZJzg{sWPE_s)@j$$THV!9+ugauyV|j$?`qA$vPNWp?}?@*e#}X%|(d$r*83tCeqD! zIu6=h>DyqV2g`&_15uHX>7Y7)Hf?xoE}E|wVgpi`@F{A9@CiY>l4f(}2)rOK-NTb9 z?%Z^l?+z99@!&8?=DvmhaBL+mr~R=WYUPH>gVrWPsx;fWsk5O;`)|XpDkhVk(T_0& z1FAnmEN%R(r$O4C&XrIH)P!X{_1>6~Q z{b^NQIv)a@!mT^2O|1->Dx1|=5ZJqgdSK5qH~;JGteC&R8cCnZO%t;F1TN{mZ=2|(0$QI(zpI5V_(shvg@%g&VsEq z|8+vCZi?r92W;eiz0hYM+CWp%hmS$)A0^FOhhPdb5nKf5Y_FbFau;P1_&efAt5Q}n zv0z-gh+`78+cz?2xQb#(@Cq%^@EKDhX9{X6o++tENhT~vw?%M;@Q`GPdiS7ndMDj zzDpgT_k)CeyjnHNLgr>$AJ%YApKKf4{iyK?#1cvLYlqaFuMAQx5C45Lfc7 zGYyQyB=()T2}7CHT`BQh+{Nq5n81gW&65vchdRFB!P3`#ms@JpUv)L@(6QbAsqwk& zuo7w{`@MB~79)eSsC)KB@|Q);>4of8$uFq2nrG%) zD3P2{5>*xZCP2N4VSl3Ks__i7$9aojEmjg20JwSOocPChe%ozyywY#G4ZG-*^-g(h zR1)D8H)X7;1TvNzXCB#dc)7)e;o@tnys#K~ZB!mCEuYdaiI{ggO5`%$&W00(2@sG> zKdABi@+BLdx5vfOOaKgaKe&IBu)^IrIhd6m`NAv=@x2b*efid4Uj&R zTI9;ll)B&Eo5O#H88rZ|@J0416>_A_-c87Ju8jj$aN#e2XYjr|fEPBA!}|d~j&p*F zL8B>Gr%(D6(xv@p~ z+73uLS9^+l_Lu4@uW47iV>;8l)7`l7kV$Z}Vygw`KcAThJ9nj)1ejyL?B739?3I_R zfR&RI*3=R3q}YFF2u18kOes@X{hC71;uQDOCvBRTgK8YdwpIPak>|cgcTnuv<0yGBXLI{bL%e5S9vCY7->jw(K!BxQf z)1XQGck|Ujzc2qVb{ZNG0Px;?@3;GJ+SRG>GA-va?b3|+N7U8n?lOu4+s2hy?~jbD zk?=A@jNocO{3GIOKD&%qzl`|5wDw2L)s%D@Q+(~JlHZ&7kC?0R<1!|N@Dk=y82KaW zs-av)QDW2c|H?K0Q`!G-oXcyB6z~r&FUSi!n_t>S{}%KE-HTm21lZOA*msEmkyuxQ IQT)F97uYjmE&u=k literal 0 HcmV?d00001 diff --git a/samples/bookshop-mtx/app/admin-books/dist/i18n/i18n.properties b/samples/bookshop-mtx/app/admin-books/dist/i18n/i18n.properties new file mode 100644 index 000000000..9a23ee40a --- /dev/null +++ b/samples/bookshop-mtx/app/admin-books/dist/i18n/i18n.properties @@ -0,0 +1,3 @@ +appTitle=Manage Books +appSubTitle=Manage bookshop inventory +appDescription=Manage your bookshop inventory with ease. diff --git a/samples/bookshop-mtx/app/admin-books/dist/i18n/i18n_de.properties b/samples/bookshop-mtx/app/admin-books/dist/i18n/i18n_de.properties new file mode 100644 index 000000000..27e8a53d7 --- /dev/null +++ b/samples/bookshop-mtx/app/admin-books/dist/i18n/i18n_de.properties @@ -0,0 +1,3 @@ +appTitle=B\u00fccher verwalten +appSubTitle=Verwalten Sie den Bestand der Buchhandlungen +appDescription=Verwalten Sie den Bestand Ihrer Buchhandlung ganz einfach. diff --git a/samples/bookshop-mtx/app/admin-books/dist/manifest.json b/samples/bookshop-mtx/app/admin-books/dist/manifest.json new file mode 100644 index 000000000..1edc63fa0 --- /dev/null +++ b/samples/bookshop-mtx/app/admin-books/dist/manifest.json @@ -0,0 +1,158 @@ +{ + "_version": "1.49.0", + "sap.app": { + "applicationVersion": { + "version": "1.0.0" + }, + "id": "bookshop.admin-books", + "type": "application", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "i18n": { + "bundleUrl": "i18n/i18n.properties", + "supportedLocales": [ + "", + "de" + ] + }, + "dataSources": { + "AdminService": { + "uri": "/odata/v4/AdminService/", + "type": "OData", + "settings": { + "odataVersion": "4.0" + } + } + }, + "crossNavigation": { + "inbounds": { + "intent-Books-manage": { + "signature": { + "parameters": {}, + "additionalParameters": "allowed" + }, + "semanticObject": "Books", + "action": "manage" + } + } + } + }, + "sap.ui": { + "technology": "UI5", + "fullWidth": false, + "deviceTypes": { + "desktop": true, + "tablet": true, + "phone": true + } + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.115.1", + "libs": { + "sap.fe.templates": {} + } + }, + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties", + "settings": { + "supportedLocales": [ + "", + "de" + ] + } + }, + "": { + "dataSource": "AdminService", + "settings": { + "operationMode": "Server", + "autoExpandSelect": true, + "earlyRequests": true, + "groupProperties": { + "default": { + "submit": "Auto" + } + } + } + } + }, + "routing": { + "routes": [ + { + "pattern": ":?query:", + "name": "BooksList", + "target": "BooksList" + }, + { + "pattern": "Books({key}):?query:", + "name": "BooksDetails", + "target": "BooksDetails" + }, + { + "pattern": "Books({key}/author({key2}):?query:", + "name": "AuthorsDetails", + "target": "AuthorsDetails" + } + ], + "targets": { + "BooksList": { + "type": "Component", + "id": "BooksList", + "name": "sap.fe.templates.ListReport", + "options": { + "settings": { + "contextPath": "/Books", + "initialLoad": true, + "navigation": { + "Books": { + "detail": { + "route": "BooksDetails" + } + } + } + } + } + }, + "BooksDetails": { + "type": "Component", + "id": "BooksDetailsList", + "name": "sap.fe.templates.ObjectPage", + "options": { + "settings": { + "contextPath": "/Books", + "editableHeaderContent": false, + "navigation": { + "Authors": { + "detail": { + "route": "AuthorsDetails" + } + } + } + } + } + }, + "AuthorsDetails": { + "type": "Component", + "id": "AuthorsDetailsList", + "name": "sap.fe.templates.ObjectPage", + "options": { + "settings": { + "contextPath": "/Authors" + } + } + } + } + }, + "contentDensities": { + "compact": true, + "cozy": true + }, + "flexBundle": false + }, + "sap.fiori": { + "registrationIds": [], + "archeType": "transactional" + } +} \ No newline at end of file diff --git a/samples/bookshop-mtx/app/admin-books/fiori-service.cds b/samples/bookshop-mtx/app/admin-books/fiori-service.cds new file mode 100644 index 000000000..36fa09086 --- /dev/null +++ b/samples/bookshop-mtx/app/admin-books/fiori-service.cds @@ -0,0 +1,113 @@ +using {AdminService} from '../../srv/admin-service.cds'; + +//////////////////////////////////////////////////////////////////////////// +// +// Books Object Page +// +annotate AdminService.Books with @(UI: { + HeaderInfo : { + TypeName : '{i18n>Book}', + TypeNamePlural: '{i18n>Books}', + Title : {Value: title}, + Description : {Value: author.name} + }, + Facets : [ + { + $Type : 'UI.ReferenceFacet', + Label : '{i18n>General}', + Target: '@UI.FieldGroup#General' + }, + { + $Type : 'UI.ReferenceFacet', + Label : '{i18n>Translations}', + Target: 'texts/@UI.LineItem' + }, + { + $Type : 'UI.ReferenceFacet', + Label : '{i18n>Details}', + Target: '@UI.FieldGroup#Details' + }, + { + $Type : 'UI.ReferenceFacet', + Label : '{i18n>Admin}', + Target: '@UI.FieldGroup#Admin' + } + ], + FieldGroup #General: {Data: [ + {Value: title}, + {Value: author_ID}, + {Value: genre_ID}, + {Value: descr} + ]}, + FieldGroup #Details: {Data: [ + {Value: stock}, + {Value: price} + ]}, + FieldGroup #Admin : {Data: [ + {Value: createdBy}, + {Value: createdAt}, + {Value: modifiedBy}, + {Value: modifiedAt} + ]} +}); + + +//////////////////////////////////////////////////////////// +// +// Draft for Localized Data +// +annotate sap.capire.bookshop.Books with @fiori.draft.enabled; +annotate AdminService.Books with @odata.draft.enabled; + +annotate AdminService.Books.texts with @(UI: { + Identification : [{Value: title}], + SelectionFields: [ + locale, + title + ], + LineItem : [ + { + Value: locale, + Label: 'Locale' + }, + { + Value: title, + Label: 'Title' + }, + { + Value: descr, + Label: 'Description' + } + ] +}); + +annotate AdminService.Books.texts with { + ID @UI.Hidden; + ID_texts @UI.Hidden; +}; + +// Add Value Help for Locales +annotate AdminService.Books.texts { + locale @( + ValueList.entity: 'Languages', + Common.ValueListWithFixedValues //show as drop down, not a dialog + ) +}; + +// In addition we need to expose Languages through AdminService as a target for ValueList +using {sap} from '@sap/cds/common'; + +extend service AdminService { + @readonly + entity Languages as projection on sap.common.Languages; +} + +// Workaround for Fiori popup for asking user to enter a new UUID on Create +annotate AdminService.Books with { + ID @Core.Computed; +} + +// Show Genre as drop down, not a dialog +annotate AdminService.Books with { + genre @Common.ValueListWithFixedValues; +} diff --git a/samples/bookshop-mtx/app/admin-books/package.json b/samples/bookshop-mtx/app/admin-books/package.json new file mode 100644 index 000000000..7adc1936c --- /dev/null +++ b/samples/bookshop-mtx/app/admin-books/package.json @@ -0,0 +1,13 @@ +{ + "name": "browse", + "version": "1.0.0", + "main": "webapp/index.html", + "scripts": { + "build": "ui5 build preload --clean-dest", + "start": "ui5 serve" + }, + "devDependencies": { + "@ui5/cli": "^4", + "ui5-task-zipper": "^3" + } +} diff --git a/samples/bookshop-mtx/app/admin-books/ui5.yaml b/samples/bookshop-mtx/app/admin-books/ui5.yaml new file mode 100644 index 000000000..f57db9288 --- /dev/null +++ b/samples/bookshop-mtx/app/admin-books/ui5.yaml @@ -0,0 +1,21 @@ +# yaml-language-server: $schema=https://sap.github.io/ui5-tooling/schema/ui5.yaml.json +specVersion: '4.0' +metadata: + name: admin-books +type: application +resources: + configuration: + propertiesFileSourceEncoding: UTF-8 +builder: + resources: + excludes: + - "/test/**" + - "/localService/**" + customTasks: + - name: ui5-task-zipper + afterTask: generateVersionInfo + configuration: + archiveName: admin-books + relativePaths: true + additionalFiles: + - xs-app.json diff --git a/samples/bookshop-mtx/app/admin-books/webapp/Component.js b/samples/bookshop-mtx/app/admin-books/webapp/Component.js new file mode 100644 index 000000000..e98677ee9 --- /dev/null +++ b/samples/bookshop-mtx/app/admin-books/webapp/Component.js @@ -0,0 +1,8 @@ +sap.ui.define(["sap/fe/core/AppComponent"], function (AppComponent) { + "use strict"; + return AppComponent.extend("books.Component", { + metadata: { manifest: "json" } + }); +}); + +/* eslint no-undef:0 */ diff --git a/samples/bookshop-mtx/app/admin-books/webapp/i18n/i18n.properties b/samples/bookshop-mtx/app/admin-books/webapp/i18n/i18n.properties new file mode 100644 index 000000000..9a23ee40a --- /dev/null +++ b/samples/bookshop-mtx/app/admin-books/webapp/i18n/i18n.properties @@ -0,0 +1,3 @@ +appTitle=Manage Books +appSubTitle=Manage bookshop inventory +appDescription=Manage your bookshop inventory with ease. diff --git a/samples/bookshop-mtx/app/admin-books/webapp/i18n/i18n_de.properties b/samples/bookshop-mtx/app/admin-books/webapp/i18n/i18n_de.properties new file mode 100644 index 000000000..01d56a22c --- /dev/null +++ b/samples/bookshop-mtx/app/admin-books/webapp/i18n/i18n_de.properties @@ -0,0 +1,3 @@ +appTitle=Bücher verwalten +appSubTitle=Verwalten Sie den Bestand der Buchhandlungen +appDescription=Verwalten Sie den Bestand Ihrer Buchhandlung ganz einfach. diff --git a/samples/bookshop-mtx/app/admin-books/webapp/manifest.json b/samples/bookshop-mtx/app/admin-books/webapp/manifest.json new file mode 100644 index 000000000..4bcc54ceb --- /dev/null +++ b/samples/bookshop-mtx/app/admin-books/webapp/manifest.json @@ -0,0 +1,145 @@ +{ + "_version": "1.49.0", + "sap.app": { + "applicationVersion": { + "version": "1.0.0" + }, + "id": "bookshop.admin-books", + "type": "application", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "i18n": "i18n/i18n.properties", + "dataSources": { + "AdminService": { + "uri": "/odata/v4/AdminService/", + "type": "OData", + "settings": { + "odataVersion": "4.0" + } + } + }, + "crossNavigation": { + "inbounds": { + "intent-Books-manage": { + "signature": { + "parameters": {}, + "additionalParameters": "allowed" + }, + "semanticObject": "Books", + "action": "manage" + } + } + } + }, + "sap.ui": { + "technology": "UI5", + "fullWidth": false, + "deviceTypes": { + "desktop": true, + "tablet": true, + "phone": true + } + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.115.1", + "libs": { + "sap.fe.templates": {} + } + }, + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties" + }, + "": { + "dataSource": "AdminService", + "settings": { + "operationMode": "Server", + "autoExpandSelect": true, + "earlyRequests": true, + "groupProperties": { + "default": { + "submit": "Auto" + } + } + } + } + }, + "routing": { + "routes": [ + { + "pattern": ":?query:", + "name": "BooksList", + "target": "BooksList" + }, + { + "pattern": "Books({key}):?query:", + "name": "BooksDetails", + "target": "BooksDetails" + }, + { + "pattern": "Books({key}/author({key2}):?query:", + "name": "AuthorsDetails", + "target": "AuthorsDetails" + } + ], + "targets": { + "BooksList": { + "type": "Component", + "id": "BooksList", + "name": "sap.fe.templates.ListReport", + "options": { + "settings": { + "contextPath": "/Books", + "initialLoad": true, + "navigation": { + "Books": { + "detail": { + "route": "BooksDetails" + } + } + } + } + } + }, + "BooksDetails": { + "type": "Component", + "id": "BooksDetailsList", + "name": "sap.fe.templates.ObjectPage", + "options": { + "settings": { + "contextPath": "/Books", + "editableHeaderContent": false, + "navigation": { + "Authors": { + "detail": { + "route": "AuthorsDetails" + } + } + } + } + } + }, + "AuthorsDetails": { + "type": "Component", + "id": "AuthorsDetailsList", + "name": "sap.fe.templates.ObjectPage", + "options": { + "settings": { + "contextPath": "/Authors" + } + } + } + } + }, + "contentDensities": { + "compact": true, + "cozy": true + } + }, + "sap.fiori": { + "registrationIds": [], + "archeType": "transactional" + } +} diff --git a/samples/bookshop-mtx/app/admin-books/xs-app.json b/samples/bookshop-mtx/app/admin-books/xs-app.json new file mode 100644 index 000000000..bc90058f1 --- /dev/null +++ b/samples/bookshop-mtx/app/admin-books/xs-app.json @@ -0,0 +1,18 @@ +{ + "authenticationMethod": "route", + "routes": [ + { + "source": "^/?odata/(.*)$", + "target": "/odata/$1", + "destination": "srv-api", + "authenticationType": "xsuaa", + "csrfProtection": true + }, + { + "source": "^(.*)$", + "service": "html5-apps-repo-rt", + "authenticationType": "xsuaa", + "target": "$1" + } + ] +} diff --git a/samples/bookshop-mtx/app/appconfig/fioriSandboxConfig.json b/samples/bookshop-mtx/app/appconfig/fioriSandboxConfig.json new file mode 100644 index 000000000..ff2ac499b --- /dev/null +++ b/samples/bookshop-mtx/app/appconfig/fioriSandboxConfig.json @@ -0,0 +1,95 @@ +{ + "services": { + "LaunchPage": { + "adapter": { + "config": { + "catalogs": [], + "groups": [ + { + "id": "Bookshop", + "title": "Bookshop", + "isPreset": true, + "isVisible": true, + "isGroupLocked": false, + "tiles": [ + { + "id": "BrowseBooks", + "tileType": "sap.ushell.ui.tile.StaticTile", + "properties": { + "title": "Browse Books", + "targetURL": "#Books-display" + } + } + ] + }, + { + "id": "Administration", + "title": "Administration", + "isPreset": true, + "isVisible": true, + "isGroupLocked": false, + "tiles": [ + { + "id": "ManageBooks", + "tileType": "sap.ushell.ui.tile.StaticTile", + "properties": { + "title": "Manage Books", + "targetURL": "#Books-manage" + } + } + ] + } + ] + } + } + }, + "NavTargetResolution": { + "config": { + "enableClientSideTargetResolution": true + } + }, + "ClientSideTargetResolution": { + "adapter": { + "config": { + "inbounds": { + "BrowseBooks": { + "semanticObject": "Books", + "action": "display", + "title": "Browse Books", + "signature": { + "parameters": { + "Books.ID": { + "renameTo": "ID" + }, + "Authors.books.ID": { + "renameTo": "ID" + } + }, + "additionalParameters": "ignored" + }, + "resolutionResult": { + "applicationType": "SAPUI5", + "additionalInformation": "SAPUI5.Component=bookshop", + "url": "browse/webapp" + } + }, + "ManageBooks": { + "semanticObject": "Books", + "action": "manage", + "title": "Manage Books", + "signature": { + "parameters": {}, + "additionalParameters": "allowed" + }, + "resolutionResult": { + "applicationType": "SAPUI5", + "additionalInformation": "SAPUI5.Component=books", + "url": "admin-books/webapp" + } + } + } + } + } + } + } +} diff --git a/samples/bookshop-mtx/app/browse/dist/Component-dbg.js b/samples/bookshop-mtx/app/browse/dist/Component-dbg.js new file mode 100644 index 000000000..4020679f8 --- /dev/null +++ b/samples/bookshop-mtx/app/browse/dist/Component-dbg.js @@ -0,0 +1,7 @@ +sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) { + "use strict"; + return AppComponent.extend("bookshop.Component", { + metadata: { manifest: "json" } + }); +}); +/* eslint no-undef:0 */ diff --git a/samples/bookshop-mtx/app/browse/dist/Component-preload.js b/samples/bookshop-mtx/app/browse/dist/Component-preload.js new file mode 100644 index 000000000..2553610fb --- /dev/null +++ b/samples/bookshop-mtx/app/browse/dist/Component-preload.js @@ -0,0 +1,8 @@ +//@ui5-bundle bookshop/browse/Component-preload.js +sap.ui.predefine("bookshop/browse/Component", ["sap/fe/core/AppComponent"],function(e){"use strict";return e.extend("bookshop.Component",{metadata:{manifest:"json"}})}); +sap.ui.require.preload({ + "bookshop/browse/i18n/i18n.properties":'appTitle=Browse Books\nappSubTitle=Find all your favorite books\nappDescription=This application lets you find the next books you want to read.\n', + "bookshop/browse/i18n/i18n_de.properties":'appTitle=B\\u00fccher anschauen\nappSubTitle=Finden sie ihre n\\u00e4chste Lekt\\u00fcre\nappDescription=Finden Sie die nachsten B\\u00fccher, die Sie lesen m\\u00f6chten.\n', + "bookshop/browse/manifest.json":'{"_version":"1.49.0","sap.app":{"id":"bookshop.browse","applicationVersion":{"version":"1.0.0"},"type":"application","title":"{{appTitle}}","description":"{{appDescription}}","i18n":{"bundleUrl":"i18n/i18n.properties","supportedLocales":["","de"]},"dataSources":{"CatalogService":{"uri":"/odata/v4/CatalogService/","type":"OData","settings":{"odataVersion":"4.0"}}},"crossNavigation":{"inbounds":{"intent1":{"signature":{"parameters":{"Books.ID":{"renameTo":"ID"},"Authors.books.ID":{"renameTo":"ID"}},"additionalParameters":"ignored"},"semanticObject":"Books","action":"display","title":"{{appTitle}}","subTitle":"{{appSubTitle}}","icon":"sap-icon://course-book","indicatorDataSource":{"dataSource":"CatalogService","path":"Books/$count","refresh":1800}}}}},"sap.ui":{"technology":"UI5","fullWidth":false,"deviceTypes":{"desktop":true,"tablet":true,"phone":true}},"sap.ui5":{"dependencies":{"minUI5Version":"1.115.1","libs":{"sap.fe.templates":{}}},"models":{"i18n":{"type":"sap.ui.model.resource.ResourceModel","uri":"i18n/i18n.properties","settings":{"supportedLocales":["","de"]}},"":{"dataSource":"CatalogService","settings":{"operationMode":"Server","autoExpandSelect":true,"earlyRequests":true,"groupProperties":{"default":{"submit":"Auto"}}}}},"routing":{"routes":[{"pattern":":?query:","name":"BooksList","target":"BooksList"},{"pattern":"Books({key}):?query:","name":"BooksDetails","target":"BooksDetails"}],"targets":{"BooksList":{"type":"Component","id":"BooksList","name":"sap.fe.templates.ListReport","options":{"settings":{"contextPath":"/Books","initialLoad":true,"navigation":{"Books":{"detail":{"route":"BooksDetails"}}}}}},"BooksDetails":{"type":"Component","id":"BooksDetailsList","name":"sap.fe.templates.ObjectPage","options":{"settings":{"contextPath":"/Books"}}}}},"contentDensities":{"compact":true,"cozy":true},"flexBundle":false},"sap.fiori":{"registrationIds":[],"archeType":"transactional"}}' +}); +//# sourceMappingURL=Component-preload.js.map diff --git a/samples/bookshop-mtx/app/browse/dist/Component-preload.js.map b/samples/bookshop-mtx/app/browse/dist/Component-preload.js.map new file mode 100644 index 000000000..03c8337d2 --- /dev/null +++ b/samples/bookshop-mtx/app/browse/dist/Component-preload.js.map @@ -0,0 +1 @@ +{"version":3,"file":"Component-preload.js","sections":[{"offset":{"line":1,"column":0},"map":{"version":3,"names":["sap","ui","define","AppComponent","extend","metadata","manifest"],"sources":["Component-dbg.js"],"mappings":"AAAAA,IAAIC,GAAGC,uCAAO,CAAC,4BAA6B,SAASC,GACpD,aACA,OAAOA,EAAaC,OAAO,qBAAsB,CAChDC,SAAU,CAAEC,SAAU,SAExB","ignoreList":[],"sourceRoot":""}},{"offset":{"line":2,"column":0},"map":{"version":3,"names":[],"sources":["Component-preload.js?bundle-code-0"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.require.preload({\n"],"sourceRoot":""}}]} \ No newline at end of file diff --git a/samples/bookshop-mtx/app/browse/dist/Component.js b/samples/bookshop-mtx/app/browse/dist/Component.js new file mode 100644 index 000000000..1005152b5 --- /dev/null +++ b/samples/bookshop-mtx/app/browse/dist/Component.js @@ -0,0 +1,2 @@ +sap.ui.define(["sap/fe/core/AppComponent"],function(e){"use strict";return e.extend("bookshop.Component",{metadata:{manifest:"json"}})}); +//# sourceMappingURL=Component.js.map \ No newline at end of file diff --git a/samples/bookshop-mtx/app/browse/dist/Component.js.map b/samples/bookshop-mtx/app/browse/dist/Component.js.map new file mode 100644 index 000000000..94e303bb9 --- /dev/null +++ b/samples/bookshop-mtx/app/browse/dist/Component.js.map @@ -0,0 +1 @@ +{"version":3,"file":"Component.js","names":["sap","ui","define","AppComponent","extend","metadata","manifest"],"sources":["Component-dbg.js"],"mappings":"AAAAA,IAAIC,GAAGC,OAAO,CAAC,4BAA6B,SAASC,GACpD,aACA,OAAOA,EAAaC,OAAO,qBAAsB,CAChDC,SAAU,CAAEC,SAAU,SAExB","ignoreList":[]} \ No newline at end of file diff --git a/samples/bookshop-mtx/app/browse/dist/browse.zip b/samples/bookshop-mtx/app/browse/dist/browse.zip new file mode 100644 index 0000000000000000000000000000000000000000..5c2d37ef02d5988425eee7814c987a6efa96e801 GIT binary patch literal 4490 zcmZ{o2{e>_`^P7{$!=11Etbeu86ui72!qF#HT#xzF!r6t&}e38MwTLDDI_#1vS+fB zog(|1vK3L*cY2@qUp)WQoO8{&?{n_+IoEaFzu))z{%(CZH4O*=U|;~m+eY71HD?+H z(f|Pa>;M2W008i`^Kx}|Li$O$ArW4+W|oK#U{FMNDO;?H$7}Hq5Gub2?vW5yx-z-e zN1{FEO*sq zjhH59d-~Fj=v{41eNCvmPV$vYhCo!3%^i$P&zFnaeX0t^E;r^pM|r|F=Ff?0uEZLo zUmE+ec@|1m^P&Hg?%eYr@KkOAR^1Hte4)cU^lj(Da3M>U2@RT+cuI~+#1*FNk4#{Dszm)o;yc|_1>lpy+3 zY3@t(p(glmZlJ{;pu%uN?K5+W(rqm_m6ixei}G>aQQ#xO#=*Lfg3_d=&ex2QPwWBx zoenz3r9iX$lGJn#%u3|D&RKo+LNNDc;nDv5x$$8%tM3!xvpa0w80!(ew2eAceGwfQ ze#@jP8y)68k^0o6D-# z?lSXQ=9nKfBU$fa%$$JHtpHJ9TYUF!#NDw`e@MD z$LG7woi6ExWsaSg?_XCK>dF-VvA)^9ens6~SWFZL9v&)qNhhv(o35!;8o##7V%A37 zqbu!&fIdu+O{sal$-K}fw?05}$GZZ-jWT?uMxK5A{+AENi3~*qn{;a^6x&aXC0$t+ zq*r3URSfYt2c?-~S^t_^N;eR}EsnV{bw9eakfj%aQ9BXkdFxu&{MH6gZW%sL0$d(^=uG_U~(oE(ZOW-0$$Ndb;U_|@j#W~8p6;n=*LkB}c-OBX5=@$Gy1%M8u7n+dql ztQ|jed@^lbLh14eiP~cAi;oZ1x@1kifF$-_X+@3RaCQT zjZ-?M_qA7WqqMPok%9fJL4QHRbR!zivzN=~3)zv`Ds2^R^IbX4iLc!8dIFM^efeIp zL>%lUtd4|1i%>ip-(~Yi{;cyAb!NL+t0@?|v!^!TblRLxzImC5RFsU3Fh5``$NEaL zcMIV^2G?e6*1zulbgA`UUA>)?*zQls_YoydQsP%vZ5*8rHC7iZ%R;6!cBh8S3K65t z$%WodkV?LM(&esX3~43o;CdiC$G6TUk*(_Hg#I4}EnnI$v?f1&mIvg;?-(zBE)b9C{QHO1!;H9ts)>_sP@i!$uN|t%fXU2}W?fu|4qRLw(5F;ReIZMe zsxe<%;7w3m*C)ZF6@(jh(7mQ)4m$R)3Mko2dD?j^zm;`=Bf~~JdGK}KJtc)@3qF%?cRhM$k_JFp&z8{L?!3*Ev>TfsHi=M7{~&&jVJ3FjA(yi|ZRz0wx8 zdSWUtZ$>tZBC3E*Wo-zCG z6}Qt2NS&g(SVKF#H{H@ltDV@?y%A0s@{YiN_UrHW)7NGXBU}j9nc?1EePdww^s@io;j)_JJ7en^ z84Hm#!G1EVRfcJu*vfS*>Bne#sla-mvny6ZmwqVwVotlt4!^Vqm!mVki5VOl`fQM? zZm16UYVrtN z3pi1Ob!)Q(SND=f?#Qg3i`23bRxNaNih;ZNATsjm?inEC>iqhAGZ9j}i=1}F_(zlR zYNvzCb=(4A%pyF31G!Ty&2p3I8u=oI2ufA zVIFJ$l=VEDJOj|IG#*zjnWyimD8q1F7cG933pXIU)P7!Uvf38jK<|z{`&3W&Jg9q> z$DZw#r;#nuCT&ui>*-XcP=Z4Cd~6SNQPnk4390OfiMdl~Hr2?%Xf=?K*QVzj z7EsW{ApjoQN5c`s(xCfq?=9MzE-$1&#qeW^N@_F+1r`eA)w=eZC8h}xt zRA1}aInAJv9{e*-;}P z>xZqJhGu^3n;CSV*A^d78Af+zv6_7RM0G>W@|_k)uej?A9P(F0&`kx-rQuuWkv8t^ zAb-a0RQyy3Un}IAhYOV`d!enW$BY~yfq{Rk_WK?jQXwuxE7BfCG+N|VIX6nR*yiv6 z_jYDye4E8|d7sPl(z(<3BIiAW5D5CVxGC8_=dxT@{|Z9Ol}%C|3f?@8oMl%9ig@*U zmE@9=H8@pz%v&Uf{iDTg=me)|CiPD3EpKJB4D>Awo zsSaF%!?g__yT*-l9MgGZZb53Wb=|l6=BSr#@_h{)5w!ibXT3V5xy`FzDlea9PPC@K z6k)_x;xWAvMb*ltc4N279)UaY&g!j*`1k0KuOi zNiLhHRZ~CyvL{18XI>sL?)M!pVzx?LVMIyN0!K_=22M{Cz4A6h1S9C!VmxxbNkJr) zzUMG-WGf~QF%C%D&2*Ie>gPVxPgfj_AZHey`*tRcCw3?%34XVV@#edQLZMcbgbrxI zx%;DPggl?}BsS6+nhEfOv&;5i8w8lvyV&B=6=2K>)CTBuMWEiy)RzJ*1TuSPH{;s z9ltI{TwM(G2t2?omljKlhFDifNjp9JQpu!Y>KkM~gt>G*^rX>f3xUZ}$;0v8h(?^~ zyzbVDw^QPc1Ll9G0t4mfmtGWclhWB=zvGmvAf%+7w>M=JAiVA+{xiV0!kX9>3tptK zNLlStH8kjgaGNiVd6MT0llvkmJHf!)n=yI|ANo!LfQ9n7H5rRFmn-3$hpK4af*BV5 zjHd@bbKrmgVxnl&CfHj&H>|6oT%#e3WGzYfF3uAZCd`cuo7R$jbH64HjN@nSwu1@ zKxKciC<4#GaeX*F!^|@s&vHsJUX=Bt4?jkAjrvb#UB+LMdF14K!wB0XM;WlcEZZT!_D9rFpL!V8PeJ{f(UceRN5oOfco=bq_E0uQ4dfqbM{V6< znjGDsg#Ksp{)jp1tqxr7cO6dT~vy6ZQimH8L`FZyrlfaW@ literal 0 HcmV?d00001 diff --git a/samples/bookshop-mtx/app/browse/dist/i18n/i18n.properties b/samples/bookshop-mtx/app/browse/dist/i18n/i18n.properties new file mode 100644 index 000000000..21436e8e4 --- /dev/null +++ b/samples/bookshop-mtx/app/browse/dist/i18n/i18n.properties @@ -0,0 +1,3 @@ +appTitle=Browse Books +appSubTitle=Find all your favorite books +appDescription=This application lets you find the next books you want to read. diff --git a/samples/bookshop-mtx/app/browse/dist/i18n/i18n_de.properties b/samples/bookshop-mtx/app/browse/dist/i18n/i18n_de.properties new file mode 100644 index 000000000..b0d5d250a --- /dev/null +++ b/samples/bookshop-mtx/app/browse/dist/i18n/i18n_de.properties @@ -0,0 +1,3 @@ +appTitle=B\u00fccher anschauen +appSubTitle=Finden sie ihre n\u00e4chste Lekt\u00fcre +appDescription=Finden Sie die nachsten B\u00fccher, die Sie lesen m\u00f6chten. diff --git a/samples/bookshop-mtx/app/browse/dist/manifest.json b/samples/bookshop-mtx/app/browse/dist/manifest.json new file mode 100644 index 000000000..ca3e63b13 --- /dev/null +++ b/samples/bookshop-mtx/app/browse/dist/manifest.json @@ -0,0 +1,150 @@ +{ + "_version": "1.49.0", + "sap.app": { + "id": "bookshop.browse", + "applicationVersion": { + "version": "1.0.0" + }, + "type": "application", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "i18n": { + "bundleUrl": "i18n/i18n.properties", + "supportedLocales": [ + "", + "de" + ] + }, + "dataSources": { + "CatalogService": { + "uri": "/odata/v4/CatalogService/", + "type": "OData", + "settings": { + "odataVersion": "4.0" + } + } + }, + "crossNavigation": { + "inbounds": { + "intent1": { + "signature": { + "parameters": { + "Books.ID": { + "renameTo": "ID" + }, + "Authors.books.ID": { + "renameTo": "ID" + } + }, + "additionalParameters": "ignored" + }, + "semanticObject": "Books", + "action": "display", + "title": "{{appTitle}}", + "subTitle": "{{appSubTitle}}", + "icon": "sap-icon://course-book", + "indicatorDataSource": { + "dataSource": "CatalogService", + "path": "Books/$count", + "refresh": 1800 + } + } + } + } + }, + "sap.ui": { + "technology": "UI5", + "fullWidth": false, + "deviceTypes": { + "desktop": true, + "tablet": true, + "phone": true + } + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.115.1", + "libs": { + "sap.fe.templates": {} + } + }, + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties", + "settings": { + "supportedLocales": [ + "", + "de" + ] + } + }, + "": { + "dataSource": "CatalogService", + "settings": { + "operationMode": "Server", + "autoExpandSelect": true, + "earlyRequests": true, + "groupProperties": { + "default": { + "submit": "Auto" + } + } + } + } + }, + "routing": { + "routes": [ + { + "pattern": ":?query:", + "name": "BooksList", + "target": "BooksList" + }, + { + "pattern": "Books({key}):?query:", + "name": "BooksDetails", + "target": "BooksDetails" + } + ], + "targets": { + "BooksList": { + "type": "Component", + "id": "BooksList", + "name": "sap.fe.templates.ListReport", + "options": { + "settings": { + "contextPath": "/Books", + "initialLoad": true, + "navigation": { + "Books": { + "detail": { + "route": "BooksDetails" + } + } + } + } + } + }, + "BooksDetails": { + "type": "Component", + "id": "BooksDetailsList", + "name": "sap.fe.templates.ObjectPage", + "options": { + "settings": { + "contextPath": "/Books" + } + } + } + } + }, + "contentDensities": { + "compact": true, + "cozy": true + }, + "flexBundle": false + }, + "sap.fiori": { + "registrationIds": [], + "archeType": "transactional" + } +} \ No newline at end of file diff --git a/samples/bookshop-mtx/app/browse/fiori-service.cds b/samples/bookshop-mtx/app/browse/fiori-service.cds new file mode 100644 index 000000000..b49a94f18 --- /dev/null +++ b/samples/bookshop-mtx/app/browse/fiori-service.cds @@ -0,0 +1,51 @@ +using {CatalogService} from '../../srv/cat-service.cds'; + +//////////////////////////////////////////////////////////////////////////// +// +// Books Object Page +// +annotate CatalogService.Books with @(UI: { + HeaderInfo : { + TypeName : '{i18n>Book}', + TypeNamePlural: '{i18n>Books}', + Title : {Value: title}, + Description : {Value: author} + }, + HeaderFacets : [{ + $Type : 'UI.ReferenceFacet', + Label : '{i18n>Description}', + Target: '@UI.FieldGroup#Descr' + }], + Facets : [{ + $Type : 'UI.ReferenceFacet', + Label : '{i18n>Details}', + Target: '@UI.FieldGroup#Price' + }], + FieldGroup #Descr: {Data: [{Value: descr}]}, + FieldGroup #Price: {Data: [{Value: price}]} +}); + +//////////////////////////////////////////////////////////////////////////// +// +// Books List Page +// +annotate CatalogService.Books with @(UI: { + SelectionFields: [ + ID, + price, + currency_code + ], + LineItem : [ + { + Value: ID, + Label: '{i18n>Title}' + }, + { + Value: author, + Label: '{i18n>Author}' + }, + {Value: genre.name}, + {Value: price}, + {Value: currency.symbol} + ] +}); diff --git a/samples/bookshop-mtx/app/browse/package.json b/samples/bookshop-mtx/app/browse/package.json new file mode 100644 index 000000000..7adc1936c --- /dev/null +++ b/samples/bookshop-mtx/app/browse/package.json @@ -0,0 +1,13 @@ +{ + "name": "browse", + "version": "1.0.0", + "main": "webapp/index.html", + "scripts": { + "build": "ui5 build preload --clean-dest", + "start": "ui5 serve" + }, + "devDependencies": { + "@ui5/cli": "^4", + "ui5-task-zipper": "^3" + } +} diff --git a/samples/bookshop-mtx/app/browse/ui5.yaml b/samples/bookshop-mtx/app/browse/ui5.yaml new file mode 100644 index 000000000..42e8f5910 --- /dev/null +++ b/samples/bookshop-mtx/app/browse/ui5.yaml @@ -0,0 +1,21 @@ +# yaml-language-server: $schema=https://sap.github.io/ui5-tooling/schema/ui5.yaml.json +specVersion: '4.0' +metadata: + name: browse +type: application +resources: + configuration: + propertiesFileSourceEncoding: UTF-8 +builder: + resources: + excludes: + - "/test/**" + - "/localService/**" + customTasks: + - name: ui5-task-zipper + afterTask: generateVersionInfo + configuration: + archiveName: browse + relativePaths: true + additionalFiles: + - xs-app.json diff --git a/samples/bookshop-mtx/app/browse/webapp/Component.js b/samples/bookshop-mtx/app/browse/webapp/Component.js new file mode 100644 index 000000000..4020679f8 --- /dev/null +++ b/samples/bookshop-mtx/app/browse/webapp/Component.js @@ -0,0 +1,7 @@ +sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) { + "use strict"; + return AppComponent.extend("bookshop.Component", { + metadata: { manifest: "json" } + }); +}); +/* eslint no-undef:0 */ diff --git a/samples/bookshop-mtx/app/browse/webapp/i18n/i18n.properties b/samples/bookshop-mtx/app/browse/webapp/i18n/i18n.properties new file mode 100644 index 000000000..21436e8e4 --- /dev/null +++ b/samples/bookshop-mtx/app/browse/webapp/i18n/i18n.properties @@ -0,0 +1,3 @@ +appTitle=Browse Books +appSubTitle=Find all your favorite books +appDescription=This application lets you find the next books you want to read. diff --git a/samples/bookshop-mtx/app/browse/webapp/i18n/i18n_de.properties b/samples/bookshop-mtx/app/browse/webapp/i18n/i18n_de.properties new file mode 100644 index 000000000..ea86c3f29 --- /dev/null +++ b/samples/bookshop-mtx/app/browse/webapp/i18n/i18n_de.properties @@ -0,0 +1,3 @@ +appTitle=Bücher anschauen +appSubTitle=Finden sie ihre nächste Lektüre +appDescription=Finden Sie die nachsten Bücher, die Sie lesen möchten. diff --git a/samples/bookshop-mtx/app/browse/webapp/manifest.json b/samples/bookshop-mtx/app/browse/webapp/manifest.json new file mode 100644 index 000000000..cd4b1c3cf --- /dev/null +++ b/samples/bookshop-mtx/app/browse/webapp/manifest.json @@ -0,0 +1,137 @@ +{ + "_version": "1.49.0", + "sap.app": { + "id": "bookshop.browse", + "applicationVersion": { + "version": "1.0.0" + }, + "type": "application", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "i18n": "i18n/i18n.properties", + "dataSources": { + "CatalogService": { + "uri": "/odata/v4/CatalogService/", + "type": "OData", + "settings": { + "odataVersion": "4.0" + } + } + }, + "crossNavigation": { + "inbounds": { + "intent1": { + "signature": { + "parameters": { + "Books.ID": { + "renameTo": "ID" + }, + "Authors.books.ID": { + "renameTo": "ID" + } + }, + "additionalParameters": "ignored" + }, + "semanticObject": "Books", + "action": "display", + "title": "{{appTitle}}", + "subTitle": "{{appSubTitle}}", + "icon": "sap-icon://course-book", + "indicatorDataSource": { + "dataSource": "CatalogService", + "path": "Books/$count", + "refresh": 1800 + } + } + } + } + }, + "sap.ui": { + "technology": "UI5", + "fullWidth": false, + "deviceTypes": { + "desktop": true, + "tablet": true, + "phone": true + } + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.115.1", + "libs": { + "sap.fe.templates": {} + } + }, + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties" + }, + "": { + "dataSource": "CatalogService", + "settings": { + "operationMode": "Server", + "autoExpandSelect": true, + "earlyRequests": true, + "groupProperties": { + "default": { + "submit": "Auto" + } + } + } + } + }, + "routing": { + "routes": [ + { + "pattern": ":?query:", + "name": "BooksList", + "target": "BooksList" + }, + { + "pattern": "Books({key}):?query:", + "name": "BooksDetails", + "target": "BooksDetails" + } + ], + "targets": { + "BooksList": { + "type": "Component", + "id": "BooksList", + "name": "sap.fe.templates.ListReport", + "options": { + "settings": { + "contextPath": "/Books", + "initialLoad": true, + "navigation": { + "Books": { + "detail": { + "route": "BooksDetails" + } + } + } + } + } + }, + "BooksDetails": { + "type": "Component", + "id": "BooksDetailsList", + "name": "sap.fe.templates.ObjectPage", + "options": { + "settings": { + "contextPath": "/Books" + } + } + } + } + }, + "contentDensities": { + "compact": true, + "cozy": true + } + }, + "sap.fiori": { + "registrationIds": [], + "archeType": "transactional" + } +} diff --git a/samples/bookshop-mtx/app/browse/xs-app.json b/samples/bookshop-mtx/app/browse/xs-app.json new file mode 100644 index 000000000..bc90058f1 --- /dev/null +++ b/samples/bookshop-mtx/app/browse/xs-app.json @@ -0,0 +1,18 @@ +{ + "authenticationMethod": "route", + "routes": [ + { + "source": "^/?odata/(.*)$", + "target": "/odata/$1", + "destination": "srv-api", + "authenticationType": "xsuaa", + "csrfProtection": true + }, + { + "source": "^(.*)$", + "service": "html5-apps-repo-rt", + "authenticationType": "xsuaa", + "target": "$1" + } + ] +} diff --git a/samples/bookshop-mtx/app/common.cds b/samples/bookshop-mtx/app/common.cds new file mode 100644 index 000000000..69627beb5 --- /dev/null +++ b/samples/bookshop-mtx/app/common.cds @@ -0,0 +1,264 @@ +/* + Common Annotations shared by all apps +*/ + +using {sap.capire.bookshop as my} from '../db/schema'; +using { + sap.common, + sap.common.Currencies +} from '@sap/cds/common'; + +//////////////////////////////////////////////////////////////////////////// +// +// Books Lists +// +annotate my.Books with @( + Common.SemanticKey: [ID], + UI : { + Identification : [{Value: title}], + SelectionFields: [ + ID, + author_ID, + price, + currency_code + ], + LineItem : [ + { + Value: ID, + Label: '{i18n>Title}' + }, + { + Value: author.ID, + Label: '{i18n>Author}' + }, + {Value: genre.name}, + {Value: stock}, + {Value: price}, + {Value: currency.symbol} + ] + } +) { + ID @Common : { + SemanticObject : 'Books', + Text : title, + TextArrangement: #TextOnly + }; + author @ValueList.entity: 'Authors'; +}; + +annotate Currencies with { + symbol @Common.Label: '{i18n>Currency}'; +} + +//////////////////////////////////////////////////////////////////////////// +// +// Books Elements +// +annotate my.Books with { + ID @title: '{i18n>ID}'; + title @title: '{i18n>Title}'; + genre @title: '{i18n>Genre}' @Common : { + Text : genre.name, + TextArrangement: #TextOnly + }; + author @title: '{i18n>Author}' @Common : { + Text : author.name, + TextArrangement: #TextOnly + }; + price @title: '{i18n>Price}' @Measures.ISOCurrency: currency_code; + stock @title: '{i18n>Stock}'; + descr @title: '{i18n>Description}' @UI.MultiLineText; + image @title: '{i18n>Image}'; +} + +//////////////////////////////////////////////////////////////////////////// +// +// Genres List +// +annotate my.Genres with @( + Common.SemanticKey: [name], + UI : { + SelectionFields: [name], + LineItem : [ + {Value: name}, + { + Value: parent.name, + Label: 'Main Genre' + } + ] + } +); + +annotate my.Genres with { + ID @Common.Text: name @Common.TextArrangement: #TextOnly; +} + +//////////////////////////////////////////////////////////////////////////// +// +// Genre Details +// +annotate my.Genres with @(UI: { + Identification: [{Value: name}], + HeaderInfo : { + TypeName : '{i18n>Genre}', + TypeNamePlural: '{i18n>Genres}', + Title : {Value: name}, + Description : {Value: ID} + }, + Facets : [{ + $Type : 'UI.ReferenceFacet', + Label : '{i18n>SubGenres}', + Target: 'children/@UI.LineItem' + }] +}); + +//////////////////////////////////////////////////////////////////////////// +// +// Genres Elements +// +annotate my.Genres with { + ID @title: '{i18n>ID}'; + name @title: '{i18n>Genre}'; +} + +//////////////////////////////////////////////////////////////////////////// +// +// Authors List +// +annotate my.Authors with @( + Common.SemanticKey: [ID], + UI : { + Identification : [{Value: name}], + SelectionFields: [name], + LineItem : [ + {Value: ID}, + {Value: dateOfBirth}, + {Value: dateOfDeath}, + {Value: placeOfBirth}, + {Value: placeOfDeath} + ] + } +) { + ID @Common: { + SemanticObject : 'Authors', + Text : name, + TextArrangement: #TextOnly + }; +}; + +//////////////////////////////////////////////////////////////////////////// +// +// Author Details +// +annotate my.Authors with @(UI: { + HeaderInfo: { + TypeName : '{i18n>Author}', + TypeNamePlural: '{i18n>Authors}', + Title : {Value: name}, + Description : {Value: dateOfBirth} + }, + Facets : [{ + $Type : 'UI.ReferenceFacet', + Target: 'books/@UI.LineItem' + }] +}); + + +//////////////////////////////////////////////////////////////////////////// +// +// Authors Elements +// +annotate my.Authors with { + ID @title: '{i18n>ID}'; + name @title: '{i18n>Name}'; + dateOfBirth @title: '{i18n>DateOfBirth}'; + dateOfDeath @title: '{i18n>DateOfDeath}'; + placeOfBirth @title: '{i18n>PlaceOfBirth}'; + placeOfDeath @title: '{i18n>PlaceOfDeath}'; +} + +//////////////////////////////////////////////////////////////////////////// +// +// Languages List +// +annotate common.Languages with @( + Common.SemanticKey: [code], + Identification : [{Value: code}], + UI : { + SelectionFields: [ + name, + descr + ], + LineItem : [ + {Value: code}, + {Value: name} + ] + } +); + +//////////////////////////////////////////////////////////////////////////// +// +// Language Details +// +annotate common.Languages with @(UI: { + HeaderInfo : { + TypeName : '{i18n>Language}', + TypeNamePlural: '{i18n>Languages}', + Title : {Value: name}, + Description : {Value: descr} + }, + Facets : [{ + $Type : 'UI.ReferenceFacet', + Label : '{i18n>Details}', + Target: '@UI.FieldGroup#Details' + }], + FieldGroup #Details: {Data: [ + {Value: code}, + {Value: name}, + {Value: descr} + ]} +}); + +//////////////////////////////////////////////////////////////////////////// +// +// Currencies List +// +annotate common.Currencies with @( + Common.SemanticKey: [code], + Identification : [{Value: code}], + UI : { + SelectionFields: [ + name, + descr + ], + LineItem : [ + {Value: descr}, + {Value: symbol}, + {Value: code} + ] + } +); + +//////////////////////////////////////////////////////////////////////////// +// +// Currency Details +// +annotate common.Currencies with @(UI: { + HeaderInfo : { + TypeName : '{i18n>Currency}', + TypeNamePlural: '{i18n>Currencies}', + Title : {Value: descr}, + Description : {Value: code} + }, + Facets : [{ + $Type : 'UI.ReferenceFacet', + Label : '{i18n>Details}', + Target: '@UI.FieldGroup#Details' + }], + FieldGroup #Details: {Data: [ + {Value: name}, + {Value: symbol}, + {Value: code}, + {Value: descr} + ]} +}); diff --git a/samples/bookshop-mtx/app/index.html b/samples/bookshop-mtx/app/index.html new file mode 100644 index 000000000..70f631507 --- /dev/null +++ b/samples/bookshop-mtx/app/index.html @@ -0,0 +1,32 @@ + + + + + + + + Bookshop + + + + + + + + + + diff --git a/samples/bookshop-mtx/app/portal/portal-site/CommonDataModel.json b/samples/bookshop-mtx/app/portal/portal-site/CommonDataModel.json new file mode 100644 index 000000000..627dacb54 --- /dev/null +++ b/samples/bookshop-mtx/app/portal/portal-site/CommonDataModel.json @@ -0,0 +1,102 @@ +{ + "_version": "3.0.0", + "identification": { + "title": "bookshop", + "id": "bookshop-flp", + "entityType": "bundle" + }, + "payload": { + "catalogs": [ + { + "_version": "3.0.0", + "identification": { + "id": "bookshop-catalog", + "title": "{{title}}", + "entityType": "catalog", + "i18n": "i18n/catalog.properties" + }, + "payload": { + "viz": [ + { + "appId": "bookshop.admin-books", + "vizId": "Books-manage" + }, + { + "appId": "bookshop.browse", + "vizId": "Books-display" + } + ] + } + } + ], + "groups": [ + { + "_version": "3.0.0", + "identification": { + "id": "bookshop-group", + "title": "{{title}}", + "entityType": "group", + "i18n": "i18n/group.properties" + }, + "payload": { + "viz": [ + { + "id": "bookshopadmin-books-001", + "appId": "bookshop.admin-books", + "vizId": "Books-manage" + }, + { + "id": "bookshopbrowse-001", + "appId": "bookshop.browse", + "vizId": "Books-display" + } + ] + } + } + ], + "sites": [ + { + "_version": "3.0.0", + "identification": { + "id": "bookshop-site", + "entityType": "site", + "title": "bookshop", + "description": "A simple CAP project." + }, + "payload": { + "config": { + "ushellConfig": { + "renderers": { + "fiori2": { + "componentData": { + "config": { + "applications": { + "Shell-home": {} + }, + "enableSearch": true, + "enablePersonalization": true, + "enableSetTheme": true, + "enableSetLanguage": true + } + } + } + } + } + }, + "groupsOrder": ["bookshop-group"], + "sap.cloud.portal": { + "config": { + "theme.id": "sap_horizon", + "theme.active": [ + "sap_horizon", + "sap_horizon_dark", + "sap_horizon_hcw", + "sap_horizon_hcb" + ] + } + } + } + } + ] + } +} diff --git a/samples/bookshop-mtx/app/portal/portal-site/i18n/catalog.properties b/samples/bookshop-mtx/app/portal/portal-site/i18n/catalog.properties new file mode 100644 index 000000000..3eb834376 --- /dev/null +++ b/samples/bookshop-mtx/app/portal/portal-site/i18n/catalog.properties @@ -0,0 +1 @@ +title=Application Title diff --git a/samples/bookshop-mtx/app/portal/portal-site/i18n/catalog_de.properties b/samples/bookshop-mtx/app/portal/portal-site/i18n/catalog_de.properties new file mode 100644 index 000000000..e74142ab8 --- /dev/null +++ b/samples/bookshop-mtx/app/portal/portal-site/i18n/catalog_de.properties @@ -0,0 +1 @@ +title=Name der Anwendung diff --git a/samples/bookshop-mtx/app/portal/portal-site/i18n/group.properties b/samples/bookshop-mtx/app/portal/portal-site/i18n/group.properties new file mode 100644 index 000000000..3eb834376 --- /dev/null +++ b/samples/bookshop-mtx/app/portal/portal-site/i18n/group.properties @@ -0,0 +1 @@ +title=Application Title diff --git a/samples/bookshop-mtx/app/portal/portal-site/i18n/group_de.properties b/samples/bookshop-mtx/app/portal/portal-site/i18n/group_de.properties new file mode 100644 index 000000000..e74142ab8 --- /dev/null +++ b/samples/bookshop-mtx/app/portal/portal-site/i18n/group_de.properties @@ -0,0 +1 @@ +title=Name der Anwendung diff --git a/samples/bookshop-mtx/app/router/package.json b/samples/bookshop-mtx/app/router/package.json new file mode 100644 index 000000000..92b6a2821 --- /dev/null +++ b/samples/bookshop-mtx/app/router/package.json @@ -0,0 +1,9 @@ +{ + "name": "approuter", + "dependencies": { + "@sap/approuter": "^20.0.0" + }, + "scripts": { + "start": "node node_modules/@sap/approuter/approuter.js" + } +} diff --git a/samples/bookshop-mtx/app/router/xs-app.json b/samples/bookshop-mtx/app/router/xs-app.json new file mode 100644 index 000000000..819cdeac6 --- /dev/null +++ b/samples/bookshop-mtx/app/router/xs-app.json @@ -0,0 +1,16 @@ +{ + "welcomeFile": "/cp.portal", + "routes": [ + { + "source": "^/-/cds/.*", + "destination": "mtx-api", + "authenticationType": "none" + }, + { + "source": "^/(.*)$", + "target": "$1", + "destination": "srv-api", + "csrfProtection": true + } + ] +} diff --git a/samples/bookshop-mtx/app/services.cds b/samples/bookshop-mtx/app/services.cds new file mode 100644 index 000000000..87e7b310f --- /dev/null +++ b/samples/bookshop-mtx/app/services.cds @@ -0,0 +1,6 @@ +/* + This model controls what gets served to Fiori frontends... +*/ +using from './common'; +using from './browse/fiori-service'; +using from './admin-books/fiori-service'; diff --git a/samples/bookshop-mtx/db/data/sap.capire.bookshop-Authors.csv b/samples/bookshop-mtx/db/data/sap.capire.bookshop-Authors.csv new file mode 100644 index 000000000..5272ee157 --- /dev/null +++ b/samples/bookshop-mtx/db/data/sap.capire.bookshop-Authors.csv @@ -0,0 +1,5 @@ +ID;name;dateOfBirth;placeOfBirth;dateOfDeath;placeOfDeath +10fef92e-975f-4c41-8045-c58e5c27a040;Emily Brontë;1818-07-30;Thornton, Yorkshire;1848-12-19;Haworth, Yorkshire +d4585e0e-ab3b-4424-b2ac-f2bfa785f068;Charlotte Brontë;1818-04-21;Thornton, Yorkshire;1855-03-31;Haworth, Yorkshire +4cf60975-300d-4dbe-8598-57b02e62bae2;Edgar Allen Poe;1809-01-19;Boston, Massachusetts;1849-10-07;Baltimore, Maryland +df9fb9fa-f121-45b5-8be5-8ff7ad5219a2;Richard Carpenter;1929-08-14;King’s Lynn, Norfolk;2012-02-26;Hertfordshire, England diff --git a/samples/bookshop-mtx/db/data/sap.capire.bookshop-Books.csv b/samples/bookshop-mtx/db/data/sap.capire.bookshop-Books.csv new file mode 100644 index 000000000..46d63fa5d --- /dev/null +++ b/samples/bookshop-mtx/db/data/sap.capire.bookshop-Books.csv @@ -0,0 +1,6 @@ +ID;title;descr;author_ID;stock;price;currency_code;genre_ID +aeeda49f-72f2-4880-be27-a513b2e53040;Wuthering Heights;"Wuthering Heights, Emily Brontë's only novel, was published in 1847 under the pseudonym ""Ellis Bell"". It was written between October 1845 and June 1846. Wuthering Heights and Anne Brontë's Agnes Grey were accepted by publisher Thomas Newby before the success of their sister Charlotte's novel Jane Eyre. After Emily's death, Charlotte edited the manuscript of Wuthering Heights and arranged for the edited version to be published as a posthumous second edition in 1850.";10fef92e-975f-4c41-8045-c58e5c27a040;12;11.11;GBP;11 +b0056977-4cf5-46a2-ab14-6409ee2e0df1;Jane Eyre;"Jane Eyre /ɛər/ (originally published as Jane Eyre: An Autobiography) is a novel by English writer Charlotte Brontë, published under the pen name ""Currer Bell"", on 16 October 1847, by Smith, Elder & Co. of London. The first American edition was published the following year by Harper & Brothers of New York. Primarily a bildungsroman, Jane Eyre follows the experiences of its eponymous heroine, including her growth to adulthood and her love for Mr. Rochester, the brooding master of Thornfield Hall. The novel revolutionised prose fiction in that the focus on Jane's moral and spiritual development is told through an intimate, first-person narrative, where actions and events are coloured by a psychological intensity. The book contains elements of social criticism, with a strong sense of Christian morality at its core and is considered by many to be ahead of its time because of Jane's individualistic character and how the novel approaches the topics of class, sexuality, religion and feminism.";d4585e0e-ab3b-4424-b2ac-f2bfa785f068;11;12.34;GBP;11 +c7641340-a9be-4673-8dad-785a2505f46e;The Raven;"""The Raven"" is a narrative poem by American writer Edgar Allan Poe. First published in January 1845, the poem is often noted for its musicality, stylized language, and supernatural atmosphere. It tells of a talking raven's mysterious visit to a distraught lover, tracing the man's slow fall into madness. The lover, often identified as being a student, is lamenting the loss of his love, Lenore. Sitting on a bust of Pallas, the raven seems to further distress the protagonist with its constant repetition of the word ""Nevermore"". The poem makes use of folk, mythological, religious, and classical references.";4cf60975-300d-4dbe-8598-57b02e62bae2;333;13.13;USD;16 +7756b725-cefc-43a2-a3c8-0c9104a349b8;Eleonora;"""Eleonora"" is a short story by Edgar Allan Poe, first published in 1842 in Philadelphia in the literary annual The Gift. It is often regarded as somewhat autobiographical and has a relatively ""happy"" ending.";4cf60975-300d-4dbe-8598-57b02e62bae2;555;14;USD;16 +a009c640-434a-4542-ac68-51b400c880ea;Catweazle;Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts.;df9fb9fa-f121-45b5-8be5-8ff7ad5219a2;22;150;JPY;13 diff --git a/samples/bookshop-mtx/db/data/sap.capire.bookshop-Books_texts.csv b/samples/bookshop-mtx/db/data/sap.capire.bookshop-Books_texts.csv new file mode 100644 index 000000000..3a3465b28 --- /dev/null +++ b/samples/bookshop-mtx/db/data/sap.capire.bookshop-Books_texts.csv @@ -0,0 +1,5 @@ +ID_texts;ID;locale;title;descr +52eee553-266d-4fdd-a5ca-909910e76ae4;aeeda49f-72f2-4880-be27-a513b2e53040;de;Sturmhöhe;Sturmhöhe (Originaltitel: Wuthering Heights) ist der einzige Roman der englischen Schriftstellerin Emily Brontë (1818–1848). Der 1847 unter dem Pseudonym Ellis Bell veröffentlichte Roman wurde vom viktorianischen Publikum weitgehend abgelehnt, heute gilt er als ein Klassiker der britischen Romanliteratur des 19. Jahrhunderts. +54e58142-f06e-49c1-a51d-138f86cea34e;aeeda49f-72f2-4880-be27-a513b2e53040;fr;Les Hauts de Hurlevent;Les Hauts de Hurlevent (titre original : Wuthering Heights), parfois orthographié Les Hauts de Hurle-Vent, est l'unique roman d'Emily Brontë, publié pour la première fois en 1847 sous le pseudonyme d’Ellis Bell. Loin d'être un récit moralisateur, Emily Brontë achève néanmoins le roman dans une atmosphère sereine, suggérant le triomphe de la paix et du Bien sur la vengeance et le Mal. +bbbf8a88-797d-4790-af1c-1cc857718ee0;b0056977-4cf5-46a2-ab14-6409ee2e0df1;de;Jane Eyre;Jane Eyre. Eine Autobiographie (Originaltitel: Jane Eyre. An Autobiography), erstmals erschienen im Jahr 1847 unter dem Pseudonym Currer Bell, ist der erste veröffentlichte Roman der britischen Autorin Charlotte Brontë und ein Klassiker der viktorianischen Romanliteratur des 19. Jahrhunderts. Der Roman erzählt in Form einer Ich-Erzählung die Lebensgeschichte von Jane Eyre (ausgesprochen /ˌdʒeɪn ˈɛə/), die nach einer schweren Kindheit eine Stelle als Gouvernante annimmt und sich in ihren Arbeitgeber verliebt, jedoch immer wieder um ihre Freiheit und Selbstbestimmung kämpfen muss. Als klein, dünn, blass, stets schlicht dunkel gekleidet und mit strengem Mittelscheitel beschrieben, gilt die Heldin des Romans Jane Eyre nicht zuletzt aufgrund der Kino- und Fernsehversionen der melodramatischen Romanvorlage als die bekannteste englische Gouvernante der Literaturgeschichte +a90d4378-1a3e-48e7-b60b-5670e78807e1;7756b725-cefc-43a2-a3c8-0c9104a349b8;de;Eleonora;“Eleonora” ist eine Erzählung von Edgar Allan Poe. Sie wurde 1841 erstveröffentlicht. In ihr geht es um das Paradox der Treue in der Treulosigkeit. diff --git a/samples/bookshop-mtx/db/data/sap.capire.bookshop-Genres.csv b/samples/bookshop-mtx/db/data/sap.capire.bookshop-Genres.csv new file mode 100644 index 000000000..1ea3793bb --- /dev/null +++ b/samples/bookshop-mtx/db/data/sap.capire.bookshop-Genres.csv @@ -0,0 +1,16 @@ +ID;parent_ID;name +10;;Fiction +11;10;Drama +12;10;Poetry +13;10;Fantasy +14;10;Science Fiction +15;10;Romance +16;10;Mystery +17;10;Thriller +18;10;Dystopia +19;10;Fairy Tale +20;;Non-Fiction +21;20;Biography +22;21;Autobiography +23;20;Essay +24;20;Speech diff --git a/samples/bookshop-mtx/db/package.json b/samples/bookshop-mtx/db/package.json new file mode 100644 index 000000000..3c1864b90 --- /dev/null +++ b/samples/bookshop-mtx/db/package.json @@ -0,0 +1,14 @@ +{ + "name": "deploy", + "dependencies": { + "hdb": "^2.0.0", + "@sap/hdi-deploy": "^5" + }, + "engines": { + "node": "^24.0.0" + }, + "scripts": { + "start": "node node_modules/@sap/hdi-deploy/deploy.js --use-hdb --parameter com.sap.hana.di.table/try_fast_table_migration=true", + "build": "npm i && npx cds build .. --for hana --production" + } +} diff --git a/samples/bookshop-mtx/db/schema.cds b/samples/bookshop-mtx/db/schema.cds new file mode 100644 index 000000000..1aedfbaca --- /dev/null +++ b/samples/bookshop-mtx/db/schema.cds @@ -0,0 +1,37 @@ +using { + Currency, + managed, + cuid, + sap.common.CodeList +} from '@sap/cds/common'; + +namespace sap.capire.bookshop; + +entity Books : managed, cuid { + @mandatory title : localized String(111); + descr : localized String(1111); + @mandatory author : Association to Authors; + genre : Association to Genres; + stock : Integer; + price : Decimal; + currency : Currency; + image : LargeBinary @Core.MediaType: 'image/png'; +} + +entity Authors : managed, cuid { + @mandatory name : String(111); + dateOfBirth : Date; + dateOfDeath : Date; + placeOfBirth : String; + placeOfDeath : String; + books : Association to many Books + on books.author = $self; +} + +/** Hierarchically organized Code List for Genres */ +entity Genres : CodeList { + key ID : Integer; + parent : Association to Genres; + children : Composition of many Genres + on children.parent = $self; +} diff --git a/samples/bookshop-mtx/db/undeploy.json b/samples/bookshop-mtx/db/undeploy.json new file mode 100644 index 000000000..aa14b9f1f --- /dev/null +++ b/samples/bookshop-mtx/db/undeploy.json @@ -0,0 +1,7 @@ +[ + "src/gen/**/*.hdbview", + "src/gen/**/*.hdbindex", + "src/gen/**/*.hdbconstraint", + "src/gen/**/*_drafts.hdbtable", + "src/gen/**/*.hdbcalculationview" +] diff --git a/samples/bookshop-mtx/mta.yaml b/samples/bookshop-mtx/mta.yaml new file mode 100644 index 000000000..fdf253a8f --- /dev/null +++ b/samples/bookshop-mtx/mta.yaml @@ -0,0 +1,151 @@ +_schema-version: 3.3.0 +ID: bookshop +version: 1.0.0-SNAPSHOT +description: "A simple CAP project." +parameters: + enable-parallel-deployments: true +modules: + - name: bookshop-srv + type: java + path: srv + parameters: + instances: 1 + buildpack: sap_java_buildpack_jakarta + properties: + SPRING_PROFILES_ACTIVE: cloud,sandbox + JBP_CONFIG_COMPONENTS: "jres: ['com.sap.xs.java.buildpack.jre.SAPMachineJRE']" + JBP_CONFIG_SAP_MACHINE_JRE: '{ version: 17.+ }' + CDS_MULTITENANCY_APPUI_TENANTSEPARATOR: "-" + build-parameters: + builder: custom + commands: + - mvn clean package -DskipTests=true --batch-mode + build-result: target/*-exec.jar + provides: + - name: srv-api # required by consumers of CAP services (e.g. approuter) + properties: + srv-url: ${default-url} + requires: + - name: bookshop-db + - name: mtx-api + properties: + CDS_MULTITENANCY_SIDECAR_URL: ~{mtx-url} + - name: app-api + properties: + CDS_MULTITENANCY_APPUI_URL: ~{url} + - name: bookshop-auth + - name: bookshop-registry + - name: bookshop-attachments + - name: bookshop-malware-scanner + + - name: bookshop-mtx + type: nodejs + path: mtx/sidecar + requires: + - name: bookshop-db + - name: bookshop-auth + build-parameters: + builder: custom + build-result: gen + commands: + - npm run build + requires: + - name: bookshop-srv + parameters: + instances: 1 + memory: 256M + disk-quota: 512M + provides: + - name: mtx-api + properties: + mtx-url: ${default-url} + + - name: bookshop + type: approuter.nodejs + path: app/router + parameters: + keep-existing-routes: true + disk-quota: 256M + memory: 256M + properties: + TENANT_HOST_PATTERN: "^(.*)-${default-uri}" + requires: + - name: srv-api + group: destinations + properties: + name: srv-api # must be used in xs-app.json as well + url: ~{srv-url} + forwardAuthToken: true + - name: mtx-api + group: destinations + properties: + name: mtx-api # must be used in xs-app.json as well + url: ~{mtx-url} + - name: bookshop-auth + provides: + - name: app-api + properties: + app-protocol: ${protocol} + app-uri: ${default-uri} + url: ${default-url} + +resources: + - name: bookshop-db + type: org.cloudfoundry.managed-service + parameters: + service: service-manager + service-plan: container + - name: bookshop-registry + type: org.cloudfoundry.managed-service + requires: + - name: srv-api + parameters: + service: saas-registry + service-plan: application + config: + xsappname: bookshop-${org}-${space} + appName: bookshop-${org}-${space} + displayName: bookshop + description: A simple CAP project. + category: 'Category' + appUrls: + getDependencies: ~{srv-api/srv-url}/mt/v1.0/subscriptions/dependencies + onSubscription: ~{srv-api/srv-url}/mt/v1.0/subscriptions/tenants/{tenantId} + onSubscriptionAsync: true + onUnSubscriptionAsync: true + onUpdateDependenciesAsync: true + callbackTimeoutMillis: 300000 # Increase if your deployments are taking longer than that + - name: bookshop-auth + type: org.cloudfoundry.managed-service + parameters: + service: xsuaa + service-plan: application + path: ./xs-security.json + config: + xsappname: bookshop-${org}-${space} + tenant-mode: shared + oauth2-configuration: + credential-types: + - "binding-secret" + - "x509" + redirect-uris: + - https://*~{app-api/app-uri}/** + role-collections: + - name: 'admin (bookshop ${org}-${space})' + description: 'generated' + role-template-references: + - '$XSAPPNAME.admin' + requires: + - name: app-api + - name: bookshop-attachments + type: org.cloudfoundry.managed-service + parameters: + service: objectstore + service-plan: standard + - name: bookshop-malware-scanner + type: org.cloudfoundry.managed-service + parameters: + service: malware-scanner + service-plan: clamav + config: + auth: mTLS diff --git a/samples/bookshop-mtx/mtx/sidecar/package.json b/samples/bookshop-mtx/mtx/sidecar/package.json new file mode 100644 index 000000000..ee933724d --- /dev/null +++ b/samples/bookshop-mtx/mtx/sidecar/package.json @@ -0,0 +1,26 @@ +{ + "name": "bookshop-mtx", + "dependencies": { + "@cap-js/hana": "^2", + "@sap/cds": "^9", + "@sap/cds-mtxs": "^3", + "@sap/xssec": "^4", + "express": "^4" + }, + "devDependencies": { + "@cap-js/sqlite": "^2" + }, + "engines": { + "node": ">=20" + }, + "scripts": { + "start": "cds-serve", + "build": "cds build ../.. --for mtx-sidecar --production && npm ci --prefix gen" + }, + "cds": { + "profiles": [ + "mtx-sidecar", + "java" + ] + } +} diff --git a/samples/bookshop-mtx/package.json b/samples/bookshop-mtx/package.json new file mode 100644 index 000000000..7fa281120 --- /dev/null +++ b/samples/bookshop-mtx/package.json @@ -0,0 +1,13 @@ +{ + "name": "bookshop-cds", + "version": "1.0.0", + "description": "Generated by cds-services-archetype", + "license": "ISC", + "repository": "", + "devDependencies": { + "@sap/cds-dk": "^9.3.2" + }, + "dependencies": { + "@sap/cds-mtxs": "^3.8.1" + } +} diff --git a/samples/bookshop-mtx/pom.xml b/samples/bookshop-mtx/pom.xml new file mode 100644 index 000000000..c09bda711 --- /dev/null +++ b/samples/bookshop-mtx/pom.xml @@ -0,0 +1,192 @@ + + + 4.0.0 + + customer + bookshop-parent + 1.0.0-SNAPSHOT + pom + + bookshop parent + + + + 17 + 4.6.1 + 3.5.7 + + UTF-8 + + + + srv + + + + + + + com.sap.cds + cds-services-bom + ${cds.services.version} + pom + import + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + com.sap.cds + cds-feature-attachments-oss + 1.4.0-SNAPSHOT + + + + + + + + + + com.sap.cds + cds-maven-plugin + ${cds.services.version} + + + cds.resolve + + resolve + + + + + + + + + + + maven-compiler-plugin + 3.14.1 + + ${jdk.version} + UTF-8 + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + true + + + + + + maven-surefire-plugin + 3.5.4 + + + + + org.codehaus.mojo + flatten-maven-plugin + 1.7.3 + + true + resolveCiFriendliesOnly + + + + flatten + process-resources + + flatten + + + + flatten.clean + clean + + clean + + + + + + + + maven-enforcer-plugin + 3.6.2 + + + Project Structure Checks + + enforce + + + + + 3.6.3 + + + ${jdk.version} + + + + true + + + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.14 + + + **/gen/** + **/generated/** + + + + + jacoco-initialize + + prepare-agent + + + + jacoco-report + test + + report + + + + + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + 4.0.0.4121 + + + + diff --git a/samples/bookshop-mtx/resources/admin-books.zip b/samples/bookshop-mtx/resources/admin-books.zip new file mode 100644 index 0000000000000000000000000000000000000000..291adfc4306eda8d7dcd2e2bad6c8fddc6136f03 GIT binary patch literal 4359 zcmZ{n2Q*ym+J;AqE~A7HJ<8~P^k_jCWeB21B$yCp^iFgM(FReXkLVIzL=B=Folhr9 z!ibWn(az+Y|NlPDI+?YeS$prb*R}3vulK#*`+-96@M!=5A|gQ8qagEd5$#b9001Bt zTXfj66On=o{r6Jk@`u0Fu-I06A8IqZQoF*2dLM(B2gR zA2(h=%z%LbgO+UH?C=<*nW?h(`{Km$Xj#od^46InA4x^Q-jJ$Rj)_D+J9;j3Z>lmN zuGSP^wIG6(U4eN!X2D;^$32ll19^TDn!lo%pTNNcD?n5x>={bj4@N6u+>oWGfh_oC z1cN~zkSrTv4c@VKCUQMGBR)Q7_OO>?gekoH#)66Hz#HbwCzI7dxo_1Xo^8>#O>5 z+7p{a(f%5;9NyQM|Q(dQ_eV4YfBu4R=f51m0T3N!sORTIK%@6-4zrhsBSK!p8+KYBzTB& z?%c+}@~Hi1Wcd}T1{bt1+SAihqjYww57BiJ<;1>TzhW8SUWVn30wkfw%Dg#i5ZJ}} zMyZK`7+;4%`Y?M2hZvBvwsr6i)rN7*dV|6d$)n}b(N-Y9?FCC%8j)Adx5PI55 zcnn0}n;tRC^P#at=45EBcK3+?mS>6Oj$No9e3afcG>lepoLNQ65){t9s0V$AwEb*k zWjn8ETfUbYKLM_)YL)gp)@Wha*VmIUlZHBXLw4YS~%Y> zaQiYuPx2(&!$wS0sLS*q8?i8QW z8!P!L8J>EWI=z?s`r6>g967Jip&l_7tT(aRXh{ZVS1#>dddjN24DtSP!X0HR{gUZb zDDJbcrZvvbf%vvDW8nwOn?iRsbL$TKHYk%ZmMuMV5ROImUl!lI8dY09jNUGdKR#nQ_B%(&vt7q}5 z_Gt-tl$1n9oAwOP8V}j$T$2wVv$LN|I4(O!oVY865R-+w15N_8Fk#Js-Mb*b&pxNyH8!Kbmg@kl(rl}Qd37Y zGXWU}si;viZh-%1d}UBV_s zH}N?$CJrxZF;fFa;5uCulyrm?om6?WLr+<6_-x^3cLu2F$56a(#%9lC?bs=QW^YyY zUS7+$*cfvey$#gHR9)~n)#2v03{e~7sJ&2*!H4#p6T++Ax`@+GspA6xeI)-m_WoXE z0!}VA4hSo2Y}aK*j=C#g;JJB#s`we;9$OGe9=PCsA9GS<$3Bp84=;PvL2gPys&==p zZJzg{sWPE_s)@j$$THV!9+ugauyV|j$?`qA$vPNWp?}?@*e#}X%|(d$r*83tCeqD! zIu6=h>DyqV2g`&_15uHX>7Y7)Hf?xoE}E|wVgpi`@F{A9@CiY>l4f(}2)rOK-NTb9 z?%Z^l?+z99@!&8?=DvmhaBL+mr~R=WYUPH>gVrWPsx;fWsk5O;`)|XpDkhVk(T_0& z1FAnmEN%R(r$O4C&XrIH)P!X{_1>6~Q z{b^NQIv)a@!mT^2O|1->Dx1|=5ZJqgdSK5qH~;JGteC&R8cCnZO%t;F1TN{mZ=2|(0$QI(zpI5V_(shvg@%g&VsEq z|8+vCZi?r92W;eiz0hYM+CWp%hmS$)A0^FOhhPdb5nKf5Y_FbFau;P1_&efAt5Q}n zv0z-gh+`78+cz?2xQb#(@Cq%^@EKDhX9{X6o++tENhT~vw?%M;@Q`GPdiS7ndMDj zzDpgT_k)CeyjnHNLgr>$AJ%YApKKf4{iyK?#1cvLYlqaFuMAQx5C45Lfc7 zGYyQyB=()T2}7CHT`BQh+{Nq5n81gW&65vchdRFB!P3`#ms@JpUv)L@(6QbAsqwk& zuo7w{`@MB~79)eSsC)KB@|Q);>4of8$uFq2nrG%) zD3P2{5>*xZCP2N4VSl3Ks__i7$9aojEmjg20JwSOocPChe%ozyywY#G4ZG-*^-g(h zR1)D8H)X7;1TvNzXCB#dc)7)e;o@tnys#K~ZB!mCEuYdaiI{ggO5`%$&W00(2@sG> zKdABi@+BLdx5vfOOaKgaKe&IBu)^IrIhd6m`NAv=@x2b*efid4Uj&R zTI9;ll)B&Eo5O#H88rZ|@J0416>_A_-c87Ju8jj$aN#e2XYjr|fEPBA!}|d~j&p*F zL8B>Gr%(D6(xv@p~ z+73uLS9^+l_Lu4@uW47iV>;8l)7`l7kV$Z}Vygw`KcAThJ9nj)1ejyL?B739?3I_R zfR&RI*3=R3q}YFF2u18kOes@X{hC71;uQDOCvBRTgK8YdwpIPak>|cgcTnuv<0yGBXLI{bL%e5S9vCY7->jw(K!BxQf z)1XQGck|Ujzc2qVb{ZNG0Px;?@3;GJ+SRG>GA-va?b3|+N7U8n?lOu4+s2hy?~jbD zk?=A@jNocO{3GIOKD&%qzl`|5wDw2L)s%D@Q+(~JlHZ&7kC?0R<1!|N@Dk=y82KaW zs-av)QDW2c|H?K0Q`!G-oXcyB6z~r&FUSi!n_t>S{}%KE-HTm21lZOA*msEmkyuxQ IQT)F97uYjmE&u=k literal 0 HcmV?d00001 diff --git a/samples/bookshop-mtx/resources/browse.zip b/samples/bookshop-mtx/resources/browse.zip new file mode 100644 index 0000000000000000000000000000000000000000..5c2d37ef02d5988425eee7814c987a6efa96e801 GIT binary patch literal 4490 zcmZ{o2{e>_`^P7{$!=11Etbeu86ui72!qF#HT#xzF!r6t&}e38MwTLDDI_#1vS+fB zog(|1vK3L*cY2@qUp)WQoO8{&?{n_+IoEaFzu))z{%(CZH4O*=U|;~m+eY71HD?+H z(f|Pa>;M2W008i`^Kx}|Li$O$ArW4+W|oK#U{FMNDO;?H$7}Hq5Gub2?vW5yx-z-e zN1{FEO*sq zjhH59d-~Fj=v{41eNCvmPV$vYhCo!3%^i$P&zFnaeX0t^E;r^pM|r|F=Ff?0uEZLo zUmE+ec@|1m^P&Hg?%eYr@KkOAR^1Hte4)cU^lj(Da3M>U2@RT+cuI~+#1*FNk4#{Dszm)o;yc|_1>lpy+3 zY3@t(p(glmZlJ{;pu%uN?K5+W(rqm_m6ixei}G>aQQ#xO#=*Lfg3_d=&ex2QPwWBx zoenz3r9iX$lGJn#%u3|D&RKo+LNNDc;nDv5x$$8%tM3!xvpa0w80!(ew2eAceGwfQ ze#@jP8y)68k^0o6D-# z?lSXQ=9nKfBU$fa%$$JHtpHJ9TYUF!#NDw`e@MD z$LG7woi6ExWsaSg?_XCK>dF-VvA)^9ens6~SWFZL9v&)qNhhv(o35!;8o##7V%A37 zqbu!&fIdu+O{sal$-K}fw?05}$GZZ-jWT?uMxK5A{+AENi3~*qn{;a^6x&aXC0$t+ zq*r3URSfYt2c?-~S^t_^N;eR}EsnV{bw9eakfj%aQ9BXkdFxu&{MH6gZW%sL0$d(^=uG_U~(oE(ZOW-0$$Ndb;U_|@j#W~8p6;n=*LkB}c-OBX5=@$Gy1%M8u7n+dql ztQ|jed@^lbLh14eiP~cAi;oZ1x@1kifF$-_X+@3RaCQT zjZ-?M_qA7WqqMPok%9fJL4QHRbR!zivzN=~3)zv`Ds2^R^IbX4iLc!8dIFM^efeIp zL>%lUtd4|1i%>ip-(~Yi{;cyAb!NL+t0@?|v!^!TblRLxzImC5RFsU3Fh5``$NEaL zcMIV^2G?e6*1zulbgA`UUA>)?*zQls_YoydQsP%vZ5*8rHC7iZ%R;6!cBh8S3K65t z$%WodkV?LM(&esX3~43o;CdiC$G6TUk*(_Hg#I4}EnnI$v?f1&mIvg;?-(zBE)b9C{QHO1!;H9ts)>_sP@i!$uN|t%fXU2}W?fu|4qRLw(5F;ReIZMe zsxe<%;7w3m*C)ZF6@(jh(7mQ)4m$R)3Mko2dD?j^zm;`=Bf~~JdGK}KJtc)@3qF%?cRhM$k_JFp&z8{L?!3*Ev>TfsHi=M7{~&&jVJ3FjA(yi|ZRz0wx8 zdSWUtZ$>tZBC3E*Wo-zCG z6}Qt2NS&g(SVKF#H{H@ltDV@?y%A0s@{YiN_UrHW)7NGXBU}j9nc?1EePdww^s@io;j)_JJ7en^ z84Hm#!G1EVRfcJu*vfS*>Bne#sla-mvny6ZmwqVwVotlt4!^Vqm!mVki5VOl`fQM? zZm16UYVrtN z3pi1Ob!)Q(SND=f?#Qg3i`23bRxNaNih;ZNATsjm?inEC>iqhAGZ9j}i=1}F_(zlR zYNvzCb=(4A%pyF31G!Ty&2p3I8u=oI2ufA zVIFJ$l=VEDJOj|IG#*zjnWyimD8q1F7cG933pXIU)P7!Uvf38jK<|z{`&3W&Jg9q> z$DZw#r;#nuCT&ui>*-XcP=Z4Cd~6SNQPnk4390OfiMdl~Hr2?%Xf=?K*QVzj z7EsW{ApjoQN5c`s(xCfq?=9MzE-$1&#qeW^N@_F+1r`eA)w=eZC8h}xt zRA1}aInAJv9{e*-;}P z>xZqJhGu^3n;CSV*A^d78Af+zv6_7RM0G>W@|_k)uej?A9P(F0&`kx-rQuuWkv8t^ zAb-a0RQyy3Un}IAhYOV`d!enW$BY~yfq{Rk_WK?jQXwuxE7BfCG+N|VIX6nR*yiv6 z_jYDye4E8|d7sPl(z(<3BIiAW5D5CVxGC8_=dxT@{|Z9Ol}%C|3f?@8oMl%9ig@*U zmE@9=H8@pz%v&Uf{iDTg=me)|CiPD3EpKJB4D>Awo zsSaF%!?g__yT*-l9MgGZZb53Wb=|l6=BSr#@_h{)5w!ibXT3V5xy`FzDlea9PPC@K z6k)_x;xWAvMb*ltc4N279)UaY&g!j*`1k0KuOi zNiLhHRZ~CyvL{18XI>sL?)M!pVzx?LVMIyN0!K_=22M{Cz4A6h1S9C!VmxxbNkJr) zzUMG-WGf~QF%C%D&2*Ie>gPVxPgfj_AZHey`*tRcCw3?%34XVV@#edQLZMcbgbrxI zx%;DPggl?}BsS6+nhEfOv&;5i8w8lvyV&B=6=2K>)CTBuMWEiy)RzJ*1TuSPH{;s z9ltI{TwM(G2t2?omljKlhFDifNjp9JQpu!Y>KkM~gt>G*^rX>f3xUZ}$;0v8h(?^~ zyzbVDw^QPc1Ll9G0t4mfmtGWclhWB=zvGmvAf%+7w>M=JAiVA+{xiV0!kX9>3tptK zNLlStH8kjgaGNiVd6MT0llvkmJHf!)n=yI|ANo!LfQ9n7H5rRFmn-3$hpK4af*BV5 zjHd@bbKrmgVxnl&CfHj&H>|6oT%#e3WGzYfF3uAZCd`cuo7R$jbH64HjN@nSwu1@ zKxKciC<4#GaeX*F!^|@s&vHsJUX=Bt4?jkAjrvb#UB+LMdF14K!wB0XM;WlcEZZT!_D9rFpL!V8PeJ{f(UceRN5oOfco=bq_E0uQ4dfqbM{V6< znjGDsg#Ksp{)jp1tqxr7cO6dT~vy6ZQimH8L`FZyrlfaW@ literal 0 HcmV?d00001 diff --git a/samples/bookshop-mtx/sonar-project.properties b/samples/bookshop-mtx/sonar-project.properties new file mode 100644 index 000000000..ea0101880 --- /dev/null +++ b/samples/bookshop-mtx/sonar-project.properties @@ -0,0 +1,23 @@ +# SonarQube configuration for CDS Feature Attachments - Bookshop Sample +sonar.projectKey=cds-feature-attachments-bookshop-sample +sonar.projectName=CDS Feature Attachments - Bookshop Sample +sonar.projectVersion=1.0.0-SNAPSHOT + +# Source and test directories +sonar.sources=srv/src/main/java +sonar.tests=srv/src/test/java + +# Java binaries +sonar.java.binaries=srv/target/classes + +# Code coverage +sonar.coverage.jacoco.xmlReportPaths=srv/target/site/jacoco/jacoco.xml + +# Exclusions +sonar.exclusions=**/gen/**,**/generated/**,**/target/** + +# Java version +sonar.java.source=17 + +# Quality gate +sonar.qualitygate.wait=true diff --git a/samples/bookshop-mtx/srv/admin-service.cds b/samples/bookshop-mtx/srv/admin-service.cds new file mode 100644 index 000000000..9ae8bbc10 --- /dev/null +++ b/samples/bookshop-mtx/srv/admin-service.cds @@ -0,0 +1,6 @@ +using {sap.capire.bookshop as my} from '../db/schema'; + +service AdminService @(requires: 'admin') { + entity Books as projection on my.Books; + entity Authors as projection on my.Authors; +} diff --git a/samples/bookshop-mtx/srv/attachments.cds b/samples/bookshop-mtx/srv/attachments.cds new file mode 100644 index 000000000..8603e44ba --- /dev/null +++ b/samples/bookshop-mtx/srv/attachments.cds @@ -0,0 +1,49 @@ +using {sap.capire.bookshop as my} from '../db/schema'; +using {sap.attachments.Attachments} from 'com.sap.cds/cds-feature-attachments'; + +// Extend Books entity to support file attachments (images, PDFs, documents) +// Each book can have multiple attachments via composition relationship +extend my.Books with { + attachments : Composition of many Attachments; + @UI.Hidden + sizeLimitedAttachments : Composition of many Attachments; + @UI.Hidden + mediaValidatedAttachments : Composition of many Attachments; +} + +annotate my.Books.sizeLimitedAttachments with { + content @Validation.Maximum: '5MB'; +} + +// Media type validation for attachments +annotate my.Books.mediaValidatedAttachments with { + content @Core.AcceptableMediaTypes: [ + 'image/jpeg', + 'image/png' + ]; +} + +// Add UI component for attachments table to the Browse Books App +using {CatalogService as service} from '../app/services'; + +annotate service.Books with @(UI.Facets: [{ + $Type : 'UI.ReferenceFacet', + ID : 'AttachmentsFacet', + Label : '{i18n>attachments}', + Target: 'attachments/@UI.LineItem' +}]); + +// Adding the UI Component (a table) to the Administrator App +using {AdminService as adminService} from '../app/services'; + +annotate adminService.Books with @(UI.Facets: [{ + $Type : 'UI.ReferenceFacet', + ID : 'AttachmentsFacet', + Label : '{i18n>attachments}', + Target: 'attachments/@UI.LineItem' +}]); + + +service nonDraft { + entity Books as projection on my.Books; +} diff --git a/samples/bookshop-mtx/srv/cat-service.cds b/samples/bookshop-mtx/srv/cat-service.cds new file mode 100644 index 000000000..1d2cbbab8 --- /dev/null +++ b/samples/bookshop-mtx/srv/cat-service.cds @@ -0,0 +1,34 @@ +using {sap.capire.bookshop as my} from '../db/schema'; + +service CatalogService { + + /** For displaying lists of Books */ + @readonly + entity ListOfBooks as + projection on Books + excluding { + descr + }; + + /** For display in details pages */ + @readonly + entity Books as + projection on my.Books { + *, + author.name as author + } + excluding { + createdBy, + modifiedBy + }; + + action submitOrder(book : Books:ID, quantity : Integer) returns { + stock : Integer + }; + + event OrderedBook : { + book : Books:ID; + quantity : Integer; + buyer : String + }; +} diff --git a/samples/bookshop-mtx/srv/pom.xml b/samples/bookshop-mtx/srv/pom.xml new file mode 100644 index 000000000..d29ab05fb --- /dev/null +++ b/samples/bookshop-mtx/srv/pom.xml @@ -0,0 +1,166 @@ + + 4.0.0 + + + bookshop-parent + customer + 1.0.0-SNAPSHOT + + + bookshop + jar + + bookshop + + + + + + com.sap.cds + cds-starter-spring-boot + + + + org.springframework.boot + spring-boot-devtools + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.sap.cds + cds-adapter-odata-v4 + runtime + + + + com.h2database + h2 + runtime + + + + org.xerial + sqlite-jdbc + runtime + + + + com.sap.cds + cds-starter-cloudfoundry + runtime + + + + org.springframework.boot + spring-boot-starter-security + + + + + com.sap.cds + cds-feature-attachments-oss + + + + + com.sap.cds + cds-feature-mt + runtime + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + + ${project.artifactId} + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + false + + + + repackage + + repackage + + + exec + + + + + + + + com.sap.cds + cds-maven-plugin + + + cds.clean + + clean + + + + + cds.install-node + + install-node + + + + + cds.resolve + + resolve + + + + + cds.build + + cds + + + + build --for java + deploy --to h2 --with-mocks --dry --out + "${project.basedir}/src/main/resources/schema-h2.sql" + + + + + + cds.generate + + generate + + + cds.gen + true + true + true + true + + + + + + + + \ No newline at end of file diff --git a/samples/bookshop-mtx/srv/src/main/java/customer/bookshop/Application.java b/samples/bookshop-mtx/srv/src/main/java/customer/bookshop/Application.java new file mode 100644 index 000000000..f395d210b --- /dev/null +++ b/samples/bookshop-mtx/srv/src/main/java/customer/bookshop/Application.java @@ -0,0 +1,13 @@ +package customer.bookshop; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/samples/bookshop-mtx/srv/src/main/java/customer/bookshop/handlers/CatalogServiceHandler.java b/samples/bookshop-mtx/srv/src/main/java/customer/bookshop/handlers/CatalogServiceHandler.java new file mode 100644 index 000000000..c480672e3 --- /dev/null +++ b/samples/bookshop-mtx/srv/src/main/java/customer/bookshop/handlers/CatalogServiceHandler.java @@ -0,0 +1,63 @@ +package customer.bookshop.handlers; + +import java.util.stream.Stream; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.sap.cds.ql.Select; +import com.sap.cds.ql.Update; +import com.sap.cds.services.cds.CqnService; +import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.handler.annotations.After; +import com.sap.cds.services.handler.annotations.On; +import com.sap.cds.services.handler.annotations.ServiceName; +import com.sap.cds.services.persistence.PersistenceService; + +import cds.gen.catalogservice.Books; +import cds.gen.catalogservice.Books_; +import cds.gen.catalogservice.CatalogService_; +import cds.gen.catalogservice.OrderedBook; +import cds.gen.catalogservice.OrderedBookContext; +import cds.gen.catalogservice.SubmitOrderContext; +import cds.gen.catalogservice.SubmitOrderContext.ReturnType; + +@Component +@ServiceName(CatalogService_.CDS_NAME) +public class CatalogServiceHandler implements EventHandler { + + @Autowired + private PersistenceService db; + + @On + public void submitOrder(SubmitOrderContext context) { + // decrease and update stock in database + db.run(Update.entity(Books_.class).byId(context.getBook()).set(b -> b.stock(), s -> s.minus(context.getQuantity()))); + + // read new stock from database + Books book = db.run(Select.from(Books_.class).where(b -> b.ID().eq(context.getBook()))).single(Books.class); + + // return new stock to client + ReturnType result = SubmitOrderContext.ReturnType.create(); + result.setStock(book.getStock()); + + OrderedBook orderedBook = OrderedBook.create(); + orderedBook.setBook(book.getId()); + orderedBook.setQuantity(context.getQuantity()); + orderedBook.setBuyer(context.getUserInfo().getName()); + + OrderedBookContext orderedBookEvent = OrderedBookContext.create(); + orderedBookEvent.setData(orderedBook); + context.getService().emit(orderedBookEvent); + + context.setResult(result); + } + + @After(event = CqnService.EVENT_READ) + public void discountBooks(Stream books) { + books.filter(b -> b.getTitle() != null && b.getStock() != null) + .filter(b -> b.getStock() > 200) + .forEach(b -> b.setTitle(b.getTitle() + " (discounted)")); + } + +} \ No newline at end of file diff --git a/samples/bookshop-mtx/srv/src/main/java/customer/bookshop/handlers/RestHandler.java b/samples/bookshop-mtx/srv/src/main/java/customer/bookshop/handlers/RestHandler.java new file mode 100644 index 000000000..d63ea146f --- /dev/null +++ b/samples/bookshop-mtx/srv/src/main/java/customer/bookshop/handlers/RestHandler.java @@ -0,0 +1,330 @@ +package customer.bookshop.handlers; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import com.sap.cds.ql.Delete; +import com.sap.cds.ql.Insert; +import com.sap.cds.ql.Select; +import com.sap.cds.ql.Update; +import com.sap.cds.services.cds.CqnService; +import com.sap.cds.services.persistence.PersistenceService; + +import cds.gen.catalogservice.BooksAttachments; +import cds.gen.catalogservice.BooksAttachments_; +import cds.gen.catalogservice.Books_; +import cds.gen.catalogservice.CatalogService_; + +/** + * REST Controller for managing book attachments + * Provides RESTful endpoints alongside the existing OData service + */ +@RestController +@RequestMapping("/api/v1/books/{bookId}/attachments") +public class RestHandler { + + @Autowired + private PersistenceService persistenceService; + + /** + * Get all attachments for a specific book + * GET /api/books/{bookId}/attachments + */ + @GetMapping + public ResponseEntity>> getAttachments(@PathVariable String bookId) { + try { + List attachments = persistenceService.run( + Select.from(BooksAttachments_.class) + .where(a -> a.up__ID().eq(bookId)) + ).listOf(BooksAttachments.class); + + List> response = attachments.stream().map(attachment -> { + Map map = new HashMap<>(); + map.put("id", attachment.getId()); + map.put("fileName", attachment.getFileName()); + map.put("mimeType", attachment.getMimeType()); + map.put("status", attachment.getStatus()); + map.put("note", attachment.getNote()); + return map; + }).toList(); + + return ResponseEntity.ok(response); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Get attachment metadata by ID + * GET /api/books/{bookId}/attachments/{attachmentId} + */ + @GetMapping(path = "/{attachmentsId}") + public ResponseEntity> getAttachment( + @PathVariable String bookId, + @PathVariable String attachmentsId) { + try { + Optional attachment = persistenceService.run( + Select.from(BooksAttachments_.class) + .where(a -> a.ID().eq(attachmentsId) + .and(a.up__ID().eq(bookId))) + ).first(BooksAttachments.class); + + return attachment.map(a -> { + Map map = new HashMap<>(); + map.put("id", a.getId()); + map.put("fileName", a.getFileName()); + map.put("mimeType", a.getMimeType()); + map.put("status", a.getStatus()); + map.put("note", a.getNote()); + return ResponseEntity.ok(map); + }).orElse(ResponseEntity.notFound().build()); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Download attachment content + * GET /api/books/{bookId}/attachments/{attachmentId}/content + */ + @GetMapping("/{attachmentsId}/content") + public ResponseEntity downloadAttachment( + @PathVariable String bookId, + @PathVariable String attachmentsId) { + try { + Optional attachmentOpt = persistenceService.run( + Select.from(BooksAttachments_.class) + .where(a -> a.ID().eq(attachmentsId) + .and(a.up__ID().eq(bookId))) + ).first(BooksAttachments.class); + + if (attachmentOpt.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + BooksAttachments attachment = attachmentOpt.get(); + + // Check if attachment is clean (security check) + if (!"Clean".equals(attachment.getStatus())) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .header("X-Error-Reason", "Attachment not scanned or infected") + .build(); + } + + InputStream content = attachment.getContent(); + if (content == null) { + return ResponseEntity.notFound().build(); + } + + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"" + attachment.getFileName() + "\""); + + if (attachment.getMimeType() != null) { + headers.add(HttpHeaders.CONTENT_TYPE, attachment.getMimeType()); + } else { + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE); + } + + return ResponseEntity.ok() + .headers(headers) + .body(new InputStreamResource(content)); + + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * Upload a new attachment + * POST /api/books/{bookId}/attachments + */ + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity> uploadAttachment( + @PathVariable String bookId, + @RequestParam("file") MultipartFile file, + @RequestParam(value = "note", required = false) String note) { + try { + // Validate book exists + boolean bookExists = persistenceService.run( + Select.from(Books_.class).where(b -> b.ID().eq(bookId)) + ).first().isPresent(); + + if (!bookExists) { + return ResponseEntity.badRequest() + .body(Map.of("error", "Book not found")); + } + + // Create attachment entity + BooksAttachments attachment = BooksAttachments.create(); + attachment.setUpId(bookId); + attachment.setFileName(file.getOriginalFilename()); + attachment.setMimeType(file.getContentType()); + attachment.setContent(file.getInputStream()); + attachment.setStatus("Clean"); // Will be updated by malware scanner + + if (note != null && !note.trim().isEmpty()) { + attachment.setNote(note); + } + + // Insert attachment + var result = persistenceService.run( + Insert.into(BooksAttachments_.class).entry(attachment) + ); + + // Get the created attachment with generated ID + List createdAttachments = result.listOf(BooksAttachments.class); + if (!createdAttachments.isEmpty()) { + BooksAttachments created = createdAttachments.get(0); + + Map response = new HashMap<>(); + response.put("id", created.getId()); + response.put("fileName", created.getFileName()); + response.put("mimeType", created.getMimeType()); + response.put("status", created.getStatus()); + response.put("note", created.getNote()); + + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "Failed to create attachment")); + + } catch (IOException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of("error", "Failed to read uploaded file: " + e.getMessage())); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "Failed to upload attachment: " + e.getMessage())); + } + } + + /** + * Update attachment metadata + * PATCH /api/books/{bookId}/attachments/{attachmentId} + */ + @PatchMapping("/{attachmentsId}") + public ResponseEntity> updateAttachment( + @PathVariable String bookId, + @PathVariable String attachmentsId, + @RequestBody Map updates) { + try { + // Verify attachment exists and belongs to the book + Optional existingOpt = persistenceService.run( + Select.from(BooksAttachments_.class) + .where(a -> a.ID().eq(attachmentsId) + .and(a.up__ID().eq(bookId))) + ).first(BooksAttachments.class); + + if (existingOpt.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + // Build update statement - create the attachment to update + BooksAttachments updateData = BooksAttachments.create(); + updateData.setId(attachmentsId); + + // Only allow updating certain fields + if (updates.containsKey("fileName")) { + updateData.setFileName((String) updates.get("fileName")); + } + if (updates.containsKey("note")) { + updateData.setNote((String) updates.get("note")); + } + + Update updateStmt = Update.entity(BooksAttachments_.class).entry(updateData); + + // Execute update + persistenceService.run(updateStmt); + + // Return updated attachment + BooksAttachments updated = persistenceService.run( + Select.from(BooksAttachments_.class) + .where(a -> a.ID().eq(attachmentsId)) + ).single(BooksAttachments.class); + + Map response = new HashMap<>(); + response.put("id", updated.getId()); + response.put("fileName", updated.getFileName()); + response.put("mimeType", updated.getMimeType()); + response.put("status", updated.getStatus()); + response.put("note", updated.getNote()); + + return ResponseEntity.ok(response); + + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "Failed to update attachment: " + e.getMessage())); + } + } + + /** + * Delete an attachment + * DELETE /api/books/{bookId}/attachments/{attachmentId} + */ + @DeleteMapping("/{attachmentsId}") + public ResponseEntity> deleteAttachment( + @PathVariable String bookId, + @PathVariable String attachmentsId) { + try { + // Verify attachment exists and belongs to the book + Optional existingOpt = persistenceService.run( + Select.from(BooksAttachments_.class) + .where(a -> a.ID().eq(attachmentsId) + .and(a.up__ID().eq(bookId))) + ).first(BooksAttachments.class); + + if (existingOpt.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + // Delete the attachment + persistenceService.run( + Delete.from(BooksAttachments_.class) + .where(a -> a.ID().eq(attachmentsId)) + ); + + return ResponseEntity.ok(Map.of("message", "Attachment deleted successfully")); + + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "Failed to delete attachment: " + e.getMessage())); + } + } + + /** + * Health check endpoint + * GET /api/books/{bookId}/attachments/health + */ + @GetMapping("/health") + public ResponseEntity> healthCheck(@PathVariable String bookId) { + Map health = new HashMap<>(); + health.put("status", "UP"); + health.put("service", "Bookshop Attachments REST API"); + health.put("bookId", bookId); + health.put("timestamp", System.currentTimeMillis()); + return ResponseEntity.ok(health); + } +} diff --git a/samples/bookshop-mtx/srv/src/main/resources/application.yaml b/samples/bookshop-mtx/srv/src/main/resources/application.yaml new file mode 100644 index 000000000..f655cc3d8 --- /dev/null +++ b/samples/bookshop-mtx/srv/src/main/resources/application.yaml @@ -0,0 +1,63 @@ + +--- +spring: + config: + activate: + on-profile: default + sql: + init: + platform: h2 +cds: + security: + mock: + users: + admin: + password: admin + roles: + - admin + user: + password: user + data-source: + auto-config: + enabled: false +--- +spring: + config: + activate: + on-profile: local-mtxs +cds: + multi-tenancy: + mtxs: + enabled: true + sidecar: + url: http://localhost:4005 + security: + mock: + users: + admin: + password: admin + tenant: t1 + roles: + - admin + user: + password: user + tenant: t1 + erin: + password: erin + tenant: t2 + roles: + - admin +--- +management: + endpoint: + health: + show-components: always + probes.enabled: true + endpoints: + web: + exposure: + include: health + health: + defaults.enabled: false + ping.enabled: true + db.enabled: true diff --git a/samples/bookshop-mtx/srv/src/test/java/customer/bookshop/ApplicationTest.java b/samples/bookshop-mtx/srv/src/test/java/customer/bookshop/ApplicationTest.java new file mode 100644 index 000000000..152543a5b --- /dev/null +++ b/samples/bookshop-mtx/srv/src/test/java/customer/bookshop/ApplicationTest.java @@ -0,0 +1,21 @@ +package customer.bookshop; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; + +@SpringBootTest +@AutoConfigureMockMvc +class ApplicationTest { + + @Autowired private ApplicationContext context; + + @Test + void checkApplicationContextCanBeLoaded() { + assertThat(context).isNotNull(); + } +} \ No newline at end of file diff --git a/samples/bookshop-mtx/srv/src/test/java/customer/bookshop/handlers/CatalogServiceHandlerTest.java b/samples/bookshop-mtx/srv/src/test/java/customer/bookshop/handlers/CatalogServiceHandlerTest.java new file mode 100644 index 000000000..e008b5ad6 --- /dev/null +++ b/samples/bookshop-mtx/srv/src/test/java/customer/bookshop/handlers/CatalogServiceHandlerTest.java @@ -0,0 +1,159 @@ +package customer.bookshop.handlers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.sap.cds.Result; +import com.sap.cds.ql.Select; +import com.sap.cds.ql.Update; +import com.sap.cds.services.persistence.PersistenceService; +import com.sap.cds.services.request.UserInfo; + +import cds.gen.catalogservice.Books; +import cds.gen.catalogservice.CatalogService; +import cds.gen.catalogservice.OrderedBookContext; +import cds.gen.catalogservice.SubmitOrderContext; + +class CatalogServiceHandlerTest { + + @Mock + private PersistenceService db; + + @InjectMocks + private CatalogServiceHandler handler; + + private Books book = Books.create(); + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + book.setTitle("title"); + } + + @Test + void testDiscount() { + book.setStock(500); + handler.discountBooks(Stream.of(book)); + assertEquals("title (discounted)", book.getTitle()); + } + + @Test + void testNoDiscount() { + book.setStock(100); + handler.discountBooks(Stream.of(book)); + assertEquals("title", book.getTitle()); + } + + @Test + void testNoStockAvailable() { + handler.discountBooks(Stream.of(book)); + assertEquals("title", book.getTitle()); + } + + @Test + void testDiscountWithNullTitle() { + book.setTitle(null); + book.setStock(500); + handler.discountBooks(Stream.of(book)); + // Should not throw exception and title should remain null + assertEquals(null, book.getTitle()); + } + + @Test + void testDiscountWithNullStock() { + book.setTitle("test"); + book.setStock(null); + handler.discountBooks(Stream.of(book)); + // Should not throw exception and title should remain unchanged + assertEquals("test", book.getTitle()); + } + + @Test + void testSubmitOrder() { + // Setup + String bookId = "aeeda49f-72f2-4880-be27-a513b2e53040"; + Integer quantity = 2; + Integer expectedNewStock = 9; + String userName = "testuser"; + + // Mock the context + SubmitOrderContext context = mock(SubmitOrderContext.class); + UserInfo userInfo = mock(UserInfo.class); + + // Import the generated service interface + CatalogService catalogService = mock(CatalogService.class); + + when(context.getBook()).thenReturn(bookId); + when(context.getQuantity()).thenReturn(quantity); + when(context.getUserInfo()).thenReturn(userInfo); + when(context.getService()).thenReturn(catalogService); + when(userInfo.getName()).thenReturn(userName); + + // Mock the book result after update + Books updatedBook = Books.create(); + updatedBook.setId(bookId); + updatedBook.setStock(expectedNewStock); + + Result mockResult = mock(Result.class); + when(mockResult.single(Books.class)).thenReturn(updatedBook); + when(db.run(any(Select.class))).thenReturn(mockResult); + + // Execute + handler.submitOrder(context); + + // Verify database operations + verify(db).run(any(Update.class)); + verify(db).run(any(Select.class)); + verify(context).setResult(any(SubmitOrderContext.ReturnType.class)); + verify(catalogService).emit(any(OrderedBookContext.class)); + } + + @Test + void testSubmitOrderWithZeroQuantity() { + // Setup + String bookId = "book-123"; + Integer quantity = 0; + Integer currentStock = 100; + String userName = "testuser"; + + // Mock the context + SubmitOrderContext context = mock(SubmitOrderContext.class); + UserInfo userInfo = mock(UserInfo.class); + + CatalogService catalogService = mock(CatalogService.class); + + when(context.getBook()).thenReturn(bookId); + when(context.getQuantity()).thenReturn(quantity); + when(context.getUserInfo()).thenReturn(userInfo); + when(context.getService()).thenReturn(catalogService); + when(userInfo.getName()).thenReturn(userName); + + // Mock the book result (stock should remain the same) + Books updatedBook = Books.create(); + updatedBook.setId(bookId); + updatedBook.setStock(currentStock); + + Result mockResult = mock(Result.class); + when(mockResult.single(Books.class)).thenReturn(updatedBook); + when(db.run(any(Select.class))).thenReturn(mockResult); + + // Execute + handler.submitOrder(context); + + // Verify database operations still happen + verify(db).run(any(Update.class)); + verify(db).run(any(Select.class)); + verify(context).setResult(any(SubmitOrderContext.ReturnType.class)); + } +} diff --git a/samples/bookshop-mtx/xs-security.json b/samples/bookshop-mtx/xs-security.json new file mode 100644 index 000000000..5a4b6112f --- /dev/null +++ b/samples/bookshop-mtx/xs-security.json @@ -0,0 +1,40 @@ +{ + "scopes": [ + { + "name": "$XSAPPNAME.admin", + "description": "admin" + }, + { + "name": "$XSAPPNAME.mtcallback", + "description": "Subscription via SaaS Registry", + "grant-as-authority-to-apps": [ + "$XSAPPNAME(application,sap-provisioning,tenant-onboarding)" + ] + }, + { + "name": "$XSAPPNAME.cds.ExtensionDeveloper", + "description": "Extend CAP applications via extension projects" + } + ], + "attributes": [], + "role-templates": [ + { + "name": "admin", + "description": "generated", + "scope-references": [ + "$XSAPPNAME.admin" + ], + "attribute-references": [] + }, + { + "name": "ExtensionDeveloper", + "description": "Extension development including UIFlex extensibility", + "scope-references": [ + "$XSAPPNAME.cds.ExtensionDeveloper" + ] + } + ], + "authorities": [ + "$XSAPPNAME.cds.ExtensionDeveloper" + ] +} From 14061cf107692fcfe6df326e3944b54f1de11f89 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 1 Apr 2026 15:15:17 +0200 Subject: [PATCH 20/39] Remove build artifacts and portal content from tracking Remove Makefile, app/portal, and UI resource zips from git and add them to .gitignore. --- samples/bookshop-mtx/.gitignore | 4 + .../bookshop-mtx/Makefile_20260401103121.mta | 54 ---------- .../portal/portal-site/CommonDataModel.json | 102 ------------------ .../portal-site/i18n/catalog.properties | 1 - .../portal-site/i18n/catalog_de.properties | 1 - .../portal/portal-site/i18n/group.properties | 1 - .../portal-site/i18n/group_de.properties | 1 - .../bookshop-mtx/resources/admin-books.zip | Bin 4359 -> 0 bytes samples/bookshop-mtx/resources/browse.zip | Bin 4490 -> 0 bytes 9 files changed, 4 insertions(+), 160 deletions(-) delete mode 100644 samples/bookshop-mtx/Makefile_20260401103121.mta delete mode 100644 samples/bookshop-mtx/app/portal/portal-site/CommonDataModel.json delete mode 100644 samples/bookshop-mtx/app/portal/portal-site/i18n/catalog.properties delete mode 100644 samples/bookshop-mtx/app/portal/portal-site/i18n/catalog_de.properties delete mode 100644 samples/bookshop-mtx/app/portal/portal-site/i18n/group.properties delete mode 100644 samples/bookshop-mtx/app/portal/portal-site/i18n/group_de.properties delete mode 100644 samples/bookshop-mtx/resources/admin-books.zip delete mode 100644 samples/bookshop-mtx/resources/browse.zip diff --git a/samples/bookshop-mtx/.gitignore b/samples/bookshop-mtx/.gitignore index 2ecc68df6..2acb766fd 100644 --- a/samples/bookshop-mtx/.gitignore +++ b/samples/bookshop-mtx/.gitignore @@ -30,5 +30,9 @@ hs_err* .idea .reloadtrigger +Makefile_*.mta +app/portal/ +resources/*.zip + # added by cds .cdsrc-private.json diff --git a/samples/bookshop-mtx/Makefile_20260401103121.mta b/samples/bookshop-mtx/Makefile_20260401103121.mta deleted file mode 100644 index e4a9e790c..000000000 --- a/samples/bookshop-mtx/Makefile_20260401103121.mta +++ /dev/null @@ -1,54 +0,0 @@ -# Generated with Cloud MTA Build Tool version 1.2.34 -version=0.0.1 -MBT=/Users/I569613/.nvm/versions/node/v22.20.0/lib/node_modules/mbt/unpacked_bin/mbt -ifndef p -$(error platform flag is expected. e.g. use make -f makefile.mta p=cf) -endif -target_provided=true -ifndef t -t="$(CURDIR)" -target_provided=false -endif -ifndef strict -strict=true -endif -ifndef mtar -mtar="*" -endif -modules := $(shell $(MBT) provide modules -d=dev) -modules := $(subst ],,$(subst [,,$(modules))) -# List of all the recipes to be executed during the build process -.PHONY: all pre_validate pre_build validate $(modules) post_build meta mtar cleanup -# Default target compile all -all: pre_validate pre_build validate $(modules) post_build meta mtar cleanup -# Validate mta.yaml -pre_validate: - @$(MBT) validate -r=${strict} -x="paths" -pre_build: pre_validate - @$(MBT) project build -p=pre - - -# Execute module build -define build_rule -$(1): validate - @$(MBT) module build -m=$(1) -p=${p} -t=${t} -endef - -$(foreach mod,$(modules),$(eval $(call build_rule,$(mod))))# Create META-INF folder with MANIFEST.MF & mtad.yaml -meta: $(modules) post_build - @$(MBT) gen meta -p=${p} -t=${t} - -post_build: $(modules) - @$(MBT) project build -p=post -t=${t} - -# Validate mta.yaml -validate: pre_build - @$(MBT) validate -r=${strict} - -# Pack as MTAR artifact -mtar: $(modules) meta - @$(MBT) gen mtar --mtar=${mtar} --target_provided=${target_provided} -t=${t} - -cleanup: mtar -# Remove tmp folder - @$(MBT) clean -t=${t} \ No newline at end of file diff --git a/samples/bookshop-mtx/app/portal/portal-site/CommonDataModel.json b/samples/bookshop-mtx/app/portal/portal-site/CommonDataModel.json deleted file mode 100644 index 627dacb54..000000000 --- a/samples/bookshop-mtx/app/portal/portal-site/CommonDataModel.json +++ /dev/null @@ -1,102 +0,0 @@ -{ - "_version": "3.0.0", - "identification": { - "title": "bookshop", - "id": "bookshop-flp", - "entityType": "bundle" - }, - "payload": { - "catalogs": [ - { - "_version": "3.0.0", - "identification": { - "id": "bookshop-catalog", - "title": "{{title}}", - "entityType": "catalog", - "i18n": "i18n/catalog.properties" - }, - "payload": { - "viz": [ - { - "appId": "bookshop.admin-books", - "vizId": "Books-manage" - }, - { - "appId": "bookshop.browse", - "vizId": "Books-display" - } - ] - } - } - ], - "groups": [ - { - "_version": "3.0.0", - "identification": { - "id": "bookshop-group", - "title": "{{title}}", - "entityType": "group", - "i18n": "i18n/group.properties" - }, - "payload": { - "viz": [ - { - "id": "bookshopadmin-books-001", - "appId": "bookshop.admin-books", - "vizId": "Books-manage" - }, - { - "id": "bookshopbrowse-001", - "appId": "bookshop.browse", - "vizId": "Books-display" - } - ] - } - } - ], - "sites": [ - { - "_version": "3.0.0", - "identification": { - "id": "bookshop-site", - "entityType": "site", - "title": "bookshop", - "description": "A simple CAP project." - }, - "payload": { - "config": { - "ushellConfig": { - "renderers": { - "fiori2": { - "componentData": { - "config": { - "applications": { - "Shell-home": {} - }, - "enableSearch": true, - "enablePersonalization": true, - "enableSetTheme": true, - "enableSetLanguage": true - } - } - } - } - } - }, - "groupsOrder": ["bookshop-group"], - "sap.cloud.portal": { - "config": { - "theme.id": "sap_horizon", - "theme.active": [ - "sap_horizon", - "sap_horizon_dark", - "sap_horizon_hcw", - "sap_horizon_hcb" - ] - } - } - } - } - ] - } -} diff --git a/samples/bookshop-mtx/app/portal/portal-site/i18n/catalog.properties b/samples/bookshop-mtx/app/portal/portal-site/i18n/catalog.properties deleted file mode 100644 index 3eb834376..000000000 --- a/samples/bookshop-mtx/app/portal/portal-site/i18n/catalog.properties +++ /dev/null @@ -1 +0,0 @@ -title=Application Title diff --git a/samples/bookshop-mtx/app/portal/portal-site/i18n/catalog_de.properties b/samples/bookshop-mtx/app/portal/portal-site/i18n/catalog_de.properties deleted file mode 100644 index e74142ab8..000000000 --- a/samples/bookshop-mtx/app/portal/portal-site/i18n/catalog_de.properties +++ /dev/null @@ -1 +0,0 @@ -title=Name der Anwendung diff --git a/samples/bookshop-mtx/app/portal/portal-site/i18n/group.properties b/samples/bookshop-mtx/app/portal/portal-site/i18n/group.properties deleted file mode 100644 index 3eb834376..000000000 --- a/samples/bookshop-mtx/app/portal/portal-site/i18n/group.properties +++ /dev/null @@ -1 +0,0 @@ -title=Application Title diff --git a/samples/bookshop-mtx/app/portal/portal-site/i18n/group_de.properties b/samples/bookshop-mtx/app/portal/portal-site/i18n/group_de.properties deleted file mode 100644 index e74142ab8..000000000 --- a/samples/bookshop-mtx/app/portal/portal-site/i18n/group_de.properties +++ /dev/null @@ -1 +0,0 @@ -title=Name der Anwendung diff --git a/samples/bookshop-mtx/resources/admin-books.zip b/samples/bookshop-mtx/resources/admin-books.zip deleted file mode 100644 index 291adfc4306eda8d7dcd2e2bad6c8fddc6136f03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4359 zcmZ{n2Q*ym+J;AqE~A7HJ<8~P^k_jCWeB21B$yCp^iFgM(FReXkLVIzL=B=Folhr9 z!ibWn(az+Y|NlPDI+?YeS$prb*R}3vulK#*`+-96@M!=5A|gQ8qagEd5$#b9001Bt zTXfj66On=o{r6Jk@`u0Fu-I06A8IqZQoF*2dLM(B2gR zA2(h=%z%LbgO+UH?C=<*nW?h(`{Km$Xj#od^46InA4x^Q-jJ$Rj)_D+J9;j3Z>lmN zuGSP^wIG6(U4eN!X2D;^$32ll19^TDn!lo%pTNNcD?n5x>={bj4@N6u+>oWGfh_oC z1cN~zkSrTv4c@VKCUQMGBR)Q7_OO>?gekoH#)66Hz#HbwCzI7dxo_1Xo^8>#O>5 z+7p{a(f%5;9NyQM|Q(dQ_eV4YfBu4R=f51m0T3N!sORTIK%@6-4zrhsBSK!p8+KYBzTB& z?%c+}@~Hi1Wcd}T1{bt1+SAihqjYww57BiJ<;1>TzhW8SUWVn30wkfw%Dg#i5ZJ}} zMyZK`7+;4%`Y?M2hZvBvwsr6i)rN7*dV|6d$)n}b(N-Y9?FCC%8j)Adx5PI55 zcnn0}n;tRC^P#at=45EBcK3+?mS>6Oj$No9e3afcG>lepoLNQ65){t9s0V$AwEb*k zWjn8ETfUbYKLM_)YL)gp)@Wha*VmIUlZHBXLw4YS~%Y> zaQiYuPx2(&!$wS0sLS*q8?i8QW z8!P!L8J>EWI=z?s`r6>g967Jip&l_7tT(aRXh{ZVS1#>dddjN24DtSP!X0HR{gUZb zDDJbcrZvvbf%vvDW8nwOn?iRsbL$TKHYk%ZmMuMV5ROImUl!lI8dY09jNUGdKR#nQ_B%(&vt7q}5 z_Gt-tl$1n9oAwOP8V}j$T$2wVv$LN|I4(O!oVY865R-+w15N_8Fk#Js-Mb*b&pxNyH8!Kbmg@kl(rl}Qd37Y zGXWU}si;viZh-%1d}UBV_s zH}N?$CJrxZF;fFa;5uCulyrm?om6?WLr+<6_-x^3cLu2F$56a(#%9lC?bs=QW^YyY zUS7+$*cfvey$#gHR9)~n)#2v03{e~7sJ&2*!H4#p6T++Ax`@+GspA6xeI)-m_WoXE z0!}VA4hSo2Y}aK*j=C#g;JJB#s`we;9$OGe9=PCsA9GS<$3Bp84=;PvL2gPys&==p zZJzg{sWPE_s)@j$$THV!9+ugauyV|j$?`qA$vPNWp?}?@*e#}X%|(d$r*83tCeqD! zIu6=h>DyqV2g`&_15uHX>7Y7)Hf?xoE}E|wVgpi`@F{A9@CiY>l4f(}2)rOK-NTb9 z?%Z^l?+z99@!&8?=DvmhaBL+mr~R=WYUPH>gVrWPsx;fWsk5O;`)|XpDkhVk(T_0& z1FAnmEN%R(r$O4C&XrIH)P!X{_1>6~Q z{b^NQIv)a@!mT^2O|1->Dx1|=5ZJqgdSK5qH~;JGteC&R8cCnZO%t;F1TN{mZ=2|(0$QI(zpI5V_(shvg@%g&VsEq z|8+vCZi?r92W;eiz0hYM+CWp%hmS$)A0^FOhhPdb5nKf5Y_FbFau;P1_&efAt5Q}n zv0z-gh+`78+cz?2xQb#(@Cq%^@EKDhX9{X6o++tENhT~vw?%M;@Q`GPdiS7ndMDj zzDpgT_k)CeyjnHNLgr>$AJ%YApKKf4{iyK?#1cvLYlqaFuMAQx5C45Lfc7 zGYyQyB=()T2}7CHT`BQh+{Nq5n81gW&65vchdRFB!P3`#ms@JpUv)L@(6QbAsqwk& zuo7w{`@MB~79)eSsC)KB@|Q);>4of8$uFq2nrG%) zD3P2{5>*xZCP2N4VSl3Ks__i7$9aojEmjg20JwSOocPChe%ozyywY#G4ZG-*^-g(h zR1)D8H)X7;1TvNzXCB#dc)7)e;o@tnys#K~ZB!mCEuYdaiI{ggO5`%$&W00(2@sG> zKdABi@+BLdx5vfOOaKgaKe&IBu)^IrIhd6m`NAv=@x2b*efid4Uj&R zTI9;ll)B&Eo5O#H88rZ|@J0416>_A_-c87Ju8jj$aN#e2XYjr|fEPBA!}|d~j&p*F zL8B>Gr%(D6(xv@p~ z+73uLS9^+l_Lu4@uW47iV>;8l)7`l7kV$Z}Vygw`KcAThJ9nj)1ejyL?B739?3I_R zfR&RI*3=R3q}YFF2u18kOes@X{hC71;uQDOCvBRTgK8YdwpIPak>|cgcTnuv<0yGBXLI{bL%e5S9vCY7->jw(K!BxQf z)1XQGck|Ujzc2qVb{ZNG0Px;?@3;GJ+SRG>GA-va?b3|+N7U8n?lOu4+s2hy?~jbD zk?=A@jNocO{3GIOKD&%qzl`|5wDw2L)s%D@Q+(~JlHZ&7kC?0R<1!|N@Dk=y82KaW zs-av)QDW2c|H?K0Q`!G-oXcyB6z~r&FUSi!n_t>S{}%KE-HTm21lZOA*msEmkyuxQ IQT)F97uYjmE&u=k diff --git a/samples/bookshop-mtx/resources/browse.zip b/samples/bookshop-mtx/resources/browse.zip deleted file mode 100644 index 5c2d37ef02d5988425eee7814c987a6efa96e801..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4490 zcmZ{o2{e>_`^P7{$!=11Etbeu86ui72!qF#HT#xzF!r6t&}e38MwTLDDI_#1vS+fB zog(|1vK3L*cY2@qUp)WQoO8{&?{n_+IoEaFzu))z{%(CZH4O*=U|;~m+eY71HD?+H z(f|Pa>;M2W008i`^Kx}|Li$O$ArW4+W|oK#U{FMNDO;?H$7}Hq5Gub2?vW5yx-z-e zN1{FEO*sq zjhH59d-~Fj=v{41eNCvmPV$vYhCo!3%^i$P&zFnaeX0t^E;r^pM|r|F=Ff?0uEZLo zUmE+ec@|1m^P&Hg?%eYr@KkOAR^1Hte4)cU^lj(Da3M>U2@RT+cuI~+#1*FNk4#{Dszm)o;yc|_1>lpy+3 zY3@t(p(glmZlJ{;pu%uN?K5+W(rqm_m6ixei}G>aQQ#xO#=*Lfg3_d=&ex2QPwWBx zoenz3r9iX$lGJn#%u3|D&RKo+LNNDc;nDv5x$$8%tM3!xvpa0w80!(ew2eAceGwfQ ze#@jP8y)68k^0o6D-# z?lSXQ=9nKfBU$fa%$$JHtpHJ9TYUF!#NDw`e@MD z$LG7woi6ExWsaSg?_XCK>dF-VvA)^9ens6~SWFZL9v&)qNhhv(o35!;8o##7V%A37 zqbu!&fIdu+O{sal$-K}fw?05}$GZZ-jWT?uMxK5A{+AENi3~*qn{;a^6x&aXC0$t+ zq*r3URSfYt2c?-~S^t_^N;eR}EsnV{bw9eakfj%aQ9BXkdFxu&{MH6gZW%sL0$d(^=uG_U~(oE(ZOW-0$$Ndb;U_|@j#W~8p6;n=*LkB}c-OBX5=@$Gy1%M8u7n+dql ztQ|jed@^lbLh14eiP~cAi;oZ1x@1kifF$-_X+@3RaCQT zjZ-?M_qA7WqqMPok%9fJL4QHRbR!zivzN=~3)zv`Ds2^R^IbX4iLc!8dIFM^efeIp zL>%lUtd4|1i%>ip-(~Yi{;cyAb!NL+t0@?|v!^!TblRLxzImC5RFsU3Fh5``$NEaL zcMIV^2G?e6*1zulbgA`UUA>)?*zQls_YoydQsP%vZ5*8rHC7iZ%R;6!cBh8S3K65t z$%WodkV?LM(&esX3~43o;CdiC$G6TUk*(_Hg#I4}EnnI$v?f1&mIvg;?-(zBE)b9C{QHO1!;H9ts)>_sP@i!$uN|t%fXU2}W?fu|4qRLw(5F;ReIZMe zsxe<%;7w3m*C)ZF6@(jh(7mQ)4m$R)3Mko2dD?j^zm;`=Bf~~JdGK}KJtc)@3qF%?cRhM$k_JFp&z8{L?!3*Ev>TfsHi=M7{~&&jVJ3FjA(yi|ZRz0wx8 zdSWUtZ$>tZBC3E*Wo-zCG z6}Qt2NS&g(SVKF#H{H@ltDV@?y%A0s@{YiN_UrHW)7NGXBU}j9nc?1EePdww^s@io;j)_JJ7en^ z84Hm#!G1EVRfcJu*vfS*>Bne#sla-mvny6ZmwqVwVotlt4!^Vqm!mVki5VOl`fQM? zZm16UYVrtN z3pi1Ob!)Q(SND=f?#Qg3i`23bRxNaNih;ZNATsjm?inEC>iqhAGZ9j}i=1}F_(zlR zYNvzCb=(4A%pyF31G!Ty&2p3I8u=oI2ufA zVIFJ$l=VEDJOj|IG#*zjnWyimD8q1F7cG933pXIU)P7!Uvf38jK<|z{`&3W&Jg9q> z$DZw#r;#nuCT&ui>*-XcP=Z4Cd~6SNQPnk4390OfiMdl~Hr2?%Xf=?K*QVzj z7EsW{ApjoQN5c`s(xCfq?=9MzE-$1&#qeW^N@_F+1r`eA)w=eZC8h}xt zRA1}aInAJv9{e*-;}P z>xZqJhGu^3n;CSV*A^d78Af+zv6_7RM0G>W@|_k)uej?A9P(F0&`kx-rQuuWkv8t^ zAb-a0RQyy3Un}IAhYOV`d!enW$BY~yfq{Rk_WK?jQXwuxE7BfCG+N|VIX6nR*yiv6 z_jYDye4E8|d7sPl(z(<3BIiAW5D5CVxGC8_=dxT@{|Z9Ol}%C|3f?@8oMl%9ig@*U zmE@9=H8@pz%v&Uf{iDTg=me)|CiPD3EpKJB4D>Awo zsSaF%!?g__yT*-l9MgGZZb53Wb=|l6=BSr#@_h{)5w!ibXT3V5xy`FzDlea9PPC@K z6k)_x;xWAvMb*ltc4N279)UaY&g!j*`1k0KuOi zNiLhHRZ~CyvL{18XI>sL?)M!pVzx?LVMIyN0!K_=22M{Cz4A6h1S9C!VmxxbNkJr) zzUMG-WGf~QF%C%D&2*Ie>gPVxPgfj_AZHey`*tRcCw3?%34XVV@#edQLZMcbgbrxI zx%;DPggl?}BsS6+nhEfOv&;5i8w8lvyV&B=6=2K>)CTBuMWEiy)RzJ*1TuSPH{;s z9ltI{TwM(G2t2?omljKlhFDifNjp9JQpu!Y>KkM~gt>G*^rX>f3xUZ}$;0v8h(?^~ zyzbVDw^QPc1Ll9G0t4mfmtGWclhWB=zvGmvAf%+7w>M=JAiVA+{xiV0!kX9>3tptK zNLlStH8kjgaGNiVd6MT0llvkmJHf!)n=yI|ANo!LfQ9n7H5rRFmn-3$hpK4af*BV5 zjHd@bbKrmgVxnl&CfHj&H>|6oT%#e3WGzYfF3uAZCd`cuo7R$jbH64HjN@nSwu1@ zKxKciC<4#GaeX*F!^|@s&vHsJUX=Bt4?jkAjrvb#UB+LMdF14K!wB0XM;WlcEZZT!_D9rFpL!V8PeJ{f(UceRN5oOfco=bq_E0uQ4dfqbM{V6< znjGDsg#Ksp{)jp1tqxr7cO6dT~vy6ZQimH8L`FZyrlfaW@ From ad74315892e53daa5506c308d098afcf5946c06b Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 1 Apr 2026 15:17:10 +0200 Subject: [PATCH 21/39] Remove undeploy.json from tracking --- samples/bookshop-mtx/.gitignore | 1 + samples/bookshop-mtx/db/undeploy.json | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 samples/bookshop-mtx/db/undeploy.json diff --git a/samples/bookshop-mtx/.gitignore b/samples/bookshop-mtx/.gitignore index 2acb766fd..cb4d9a685 100644 --- a/samples/bookshop-mtx/.gitignore +++ b/samples/bookshop-mtx/.gitignore @@ -33,6 +33,7 @@ hs_err* Makefile_*.mta app/portal/ resources/*.zip +undeploy.json # added by cds .cdsrc-private.json diff --git a/samples/bookshop-mtx/db/undeploy.json b/samples/bookshop-mtx/db/undeploy.json deleted file mode 100644 index aa14b9f1f..000000000 --- a/samples/bookshop-mtx/db/undeploy.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - "src/gen/**/*.hdbview", - "src/gen/**/*.hdbindex", - "src/gen/**/*.hdbconstraint", - "src/gen/**/*_drafts.hdbtable", - "src/gen/**/*.hdbcalculationview" -] From 6e3916044c9bc3fcfd46de6cd4ac34709f24b153 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 1 Apr 2026 16:04:05 +0200 Subject: [PATCH 22/39] Merge bookshop-mtx into bookshop with profile-based MT support Replace separate bookshop and bookshop-mtx samples with a single bookshop that defaults to single-tenant mode. Multi-tenancy is activated via profiles (with-mtx-sidecar for CDS, local-mtxs for Spring Boot). Remove tracked dist/ build artifacts. --- samples/bookshop-mtx/.cdsrc.json | 19 - samples/bookshop-mtx/.gitignore | 39 - samples/bookshop-mtx/README.md | 128 -- .../bookshop-mtx/app/_i18n/i18n.properties | 15 - .../bookshop-mtx/app/_i18n/i18n_de.properties | 15 - .../app/admin-books/dist/Component-dbg.js | 8 - .../app/admin-books/dist/Component-preload.js | 8 - .../admin-books/dist/Component-preload.js.map | 1 - .../app/admin-books/dist/Component.js | 2 - .../app/admin-books/dist/Component.js.map | 1 - .../app/admin-books/dist/admin-books.zip | Bin 4359 -> 0 bytes .../app/admin-books/dist/i18n/i18n.properties | 3 - .../admin-books/dist/i18n/i18n_de.properties | 3 - .../app/admin-books/dist/manifest.json | 158 -- .../app/admin-books/fiori-service.cds | 113 - .../app/admin-books/webapp/Component.js | 8 - .../admin-books/webapp/i18n/i18n.properties | 3 - .../webapp/i18n/i18n_de.properties | 3 - .../app/admin-books/webapp/manifest.json | 145 -- .../app/appconfig/fioriSandboxConfig.json | 95 - .../app/browse/dist/Component-dbg.js | 7 - .../app/browse/dist/Component-preload.js | 8 - .../app/browse/dist/Component-preload.js.map | 1 - .../bookshop-mtx/app/browse/dist/Component.js | 2 - .../app/browse/dist/Component.js.map | 1 - .../bookshop-mtx/app/browse/dist/browse.zip | Bin 4490 -> 0 bytes .../app/browse/dist/i18n/i18n.properties | 3 - .../app/browse/dist/i18n/i18n_de.properties | 3 - .../app/browse/dist/manifest.json | 150 -- .../bookshop-mtx/app/browse/fiori-service.cds | 51 - .../app/browse/webapp/Component.js | 7 - .../app/browse/webapp/i18n/i18n.properties | 3 - .../app/browse/webapp/i18n/i18n_de.properties | 3 - .../app/browse/webapp/manifest.json | 137 -- samples/bookshop-mtx/app/common.cds | 264 --- samples/bookshop-mtx/app/index.html | 32 - samples/bookshop-mtx/app/services.cds | 6 - .../db/data/sap.capire.bookshop-Authors.csv | 5 - .../db/data/sap.capire.bookshop-Books.csv | 6 - .../data/sap.capire.bookshop-Books_texts.csv | 5 - .../db/data/sap.capire.bookshop-Genres.csv | 16 - samples/bookshop-mtx/db/schema.cds | 37 - samples/bookshop-mtx/package.json | 13 - samples/bookshop-mtx/pom.xml | 192 -- samples/bookshop-mtx/sonar-project.properties | 23 - samples/bookshop-mtx/srv/admin-service.cds | 6 - samples/bookshop-mtx/srv/attachments.cds | 49 - samples/bookshop-mtx/srv/cat-service.cds | 34 - samples/bookshop-mtx/srv/pom.xml | 166 -- .../java/customer/bookshop/Application.java | 13 - .../handlers/CatalogServiceHandler.java | 63 - .../bookshop/handlers/RestHandler.java | 330 --- .../srv/src/main/resources/application.yaml | 63 - .../customer/bookshop/ApplicationTest.java | 21 - .../handlers/CatalogServiceHandlerTest.java | 159 -- samples/bookshop/.cdsrc.json | 16 + samples/bookshop/.gitignore | 6 + samples/bookshop/README.md | 29 + .../app/admin-books/package.json | 0 .../app/admin-books/ui5.yaml | 0 .../app/admin-books/xs-app.json | 0 .../app/browse/package.json | 0 .../app/browse/ui5.yaml | 0 .../app/browse/xs-app.json | 0 .../app/router/package.json | 0 .../app/router/xs-app.json | 0 .../db/package.json | 0 samples/{bookshop-mtx => bookshop}/mta.yaml | 0 .../mtx/sidecar/package.json | 0 samples/bookshop/package-lock.json | 1890 ++++++++++++++--- samples/bookshop/package.json | 3 + samples/bookshop/pom.xml | 4 +- samples/bookshop/srv/attachments.cds | 4 +- samples/bookshop/srv/pom.xml | 32 +- .../srv/src/main/resources/application.yaml | 43 +- .../xs-security.json | 0 76 files changed, 1665 insertions(+), 3008 deletions(-) delete mode 100644 samples/bookshop-mtx/.cdsrc.json delete mode 100644 samples/bookshop-mtx/.gitignore delete mode 100644 samples/bookshop-mtx/README.md delete mode 100644 samples/bookshop-mtx/app/_i18n/i18n.properties delete mode 100644 samples/bookshop-mtx/app/_i18n/i18n_de.properties delete mode 100644 samples/bookshop-mtx/app/admin-books/dist/Component-dbg.js delete mode 100644 samples/bookshop-mtx/app/admin-books/dist/Component-preload.js delete mode 100644 samples/bookshop-mtx/app/admin-books/dist/Component-preload.js.map delete mode 100644 samples/bookshop-mtx/app/admin-books/dist/Component.js delete mode 100644 samples/bookshop-mtx/app/admin-books/dist/Component.js.map delete mode 100644 samples/bookshop-mtx/app/admin-books/dist/admin-books.zip delete mode 100644 samples/bookshop-mtx/app/admin-books/dist/i18n/i18n.properties delete mode 100644 samples/bookshop-mtx/app/admin-books/dist/i18n/i18n_de.properties delete mode 100644 samples/bookshop-mtx/app/admin-books/dist/manifest.json delete mode 100644 samples/bookshop-mtx/app/admin-books/fiori-service.cds delete mode 100644 samples/bookshop-mtx/app/admin-books/webapp/Component.js delete mode 100644 samples/bookshop-mtx/app/admin-books/webapp/i18n/i18n.properties delete mode 100644 samples/bookshop-mtx/app/admin-books/webapp/i18n/i18n_de.properties delete mode 100644 samples/bookshop-mtx/app/admin-books/webapp/manifest.json delete mode 100644 samples/bookshop-mtx/app/appconfig/fioriSandboxConfig.json delete mode 100644 samples/bookshop-mtx/app/browse/dist/Component-dbg.js delete mode 100644 samples/bookshop-mtx/app/browse/dist/Component-preload.js delete mode 100644 samples/bookshop-mtx/app/browse/dist/Component-preload.js.map delete mode 100644 samples/bookshop-mtx/app/browse/dist/Component.js delete mode 100644 samples/bookshop-mtx/app/browse/dist/Component.js.map delete mode 100644 samples/bookshop-mtx/app/browse/dist/browse.zip delete mode 100644 samples/bookshop-mtx/app/browse/dist/i18n/i18n.properties delete mode 100644 samples/bookshop-mtx/app/browse/dist/i18n/i18n_de.properties delete mode 100644 samples/bookshop-mtx/app/browse/dist/manifest.json delete mode 100644 samples/bookshop-mtx/app/browse/fiori-service.cds delete mode 100644 samples/bookshop-mtx/app/browse/webapp/Component.js delete mode 100644 samples/bookshop-mtx/app/browse/webapp/i18n/i18n.properties delete mode 100644 samples/bookshop-mtx/app/browse/webapp/i18n/i18n_de.properties delete mode 100644 samples/bookshop-mtx/app/browse/webapp/manifest.json delete mode 100644 samples/bookshop-mtx/app/common.cds delete mode 100644 samples/bookshop-mtx/app/index.html delete mode 100644 samples/bookshop-mtx/app/services.cds delete mode 100644 samples/bookshop-mtx/db/data/sap.capire.bookshop-Authors.csv delete mode 100644 samples/bookshop-mtx/db/data/sap.capire.bookshop-Books.csv delete mode 100644 samples/bookshop-mtx/db/data/sap.capire.bookshop-Books_texts.csv delete mode 100644 samples/bookshop-mtx/db/data/sap.capire.bookshop-Genres.csv delete mode 100644 samples/bookshop-mtx/db/schema.cds delete mode 100644 samples/bookshop-mtx/package.json delete mode 100644 samples/bookshop-mtx/pom.xml delete mode 100644 samples/bookshop-mtx/sonar-project.properties delete mode 100644 samples/bookshop-mtx/srv/admin-service.cds delete mode 100644 samples/bookshop-mtx/srv/attachments.cds delete mode 100644 samples/bookshop-mtx/srv/cat-service.cds delete mode 100644 samples/bookshop-mtx/srv/pom.xml delete mode 100644 samples/bookshop-mtx/srv/src/main/java/customer/bookshop/Application.java delete mode 100644 samples/bookshop-mtx/srv/src/main/java/customer/bookshop/handlers/CatalogServiceHandler.java delete mode 100644 samples/bookshop-mtx/srv/src/main/java/customer/bookshop/handlers/RestHandler.java delete mode 100644 samples/bookshop-mtx/srv/src/main/resources/application.yaml delete mode 100644 samples/bookshop-mtx/srv/src/test/java/customer/bookshop/ApplicationTest.java delete mode 100644 samples/bookshop-mtx/srv/src/test/java/customer/bookshop/handlers/CatalogServiceHandlerTest.java rename samples/{bookshop-mtx => bookshop}/app/admin-books/package.json (100%) rename samples/{bookshop-mtx => bookshop}/app/admin-books/ui5.yaml (100%) rename samples/{bookshop-mtx => bookshop}/app/admin-books/xs-app.json (100%) rename samples/{bookshop-mtx => bookshop}/app/browse/package.json (100%) rename samples/{bookshop-mtx => bookshop}/app/browse/ui5.yaml (100%) rename samples/{bookshop-mtx => bookshop}/app/browse/xs-app.json (100%) rename samples/{bookshop-mtx => bookshop}/app/router/package.json (100%) rename samples/{bookshop-mtx => bookshop}/app/router/xs-app.json (100%) rename samples/{bookshop-mtx => bookshop}/db/package.json (100%) rename samples/{bookshop-mtx => bookshop}/mta.yaml (100%) rename samples/{bookshop-mtx => bookshop}/mtx/sidecar/package.json (100%) rename samples/{bookshop-mtx => bookshop}/xs-security.json (100%) diff --git a/samples/bookshop-mtx/.cdsrc.json b/samples/bookshop-mtx/.cdsrc.json deleted file mode 100644 index 7deba9f32..000000000 --- a/samples/bookshop-mtx/.cdsrc.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "profiles": [ - "with-mtx-sidecar", - "java" - ], - "requires": { - "[production]": { - "multitenancy": true, - "extensibility": true, - "toggles": true, - "auth": "xsuaa" - }, - "[with-mtx]": { - "multitenancy": true, - "extensibility": true, - "toggles": true - } - } -} diff --git a/samples/bookshop-mtx/.gitignore b/samples/bookshop-mtx/.gitignore deleted file mode 100644 index cb4d9a685..000000000 --- a/samples/bookshop-mtx/.gitignore +++ /dev/null @@ -1,39 +0,0 @@ -**/gen/ -**/edmx/ -*.db -*.sqlite -*.sqlite-wal -*.sqlite-shm -schema*.sql -default-env.json - -**/bin/ -**/target/ -.flattened-pom.xml -.classpath -.project -.settings - -**/node/ -**/node_modules/ - -**/.mta/ -*.mtar - -*.log* -gc_history* -hs_err* -*.tgz -*.iml - -.vscode -.idea -.reloadtrigger - -Makefile_*.mta -app/portal/ -resources/*.zip -undeploy.json - -# added by cds -.cdsrc-private.json diff --git a/samples/bookshop-mtx/README.md b/samples/bookshop-mtx/README.md deleted file mode 100644 index babcff672..000000000 --- a/samples/bookshop-mtx/README.md +++ /dev/null @@ -1,128 +0,0 @@ -# Bookshop Sample - Attachments Plugin - -This sample demonstrates how to use the `cds-feature-attachments` plugin in a CAP Java application. It extends the classic CAP bookshop sample to include file attachments for books. - -## What This Sample Demonstrates - -- Integration of the latest attachments plugin with CAP Java -- Extending existing entities with attachment capabilities -- UI integration with Fiori elements applications -- Basic attachment operations (upload, download, delete) - -## Prerequisites - -- Java 17 or higher -- Maven 3.6 or higher -- Node.js 18 or higher -- npm - -## Getting Started - -1. **Clone and navigate to the sample**: - ```bash - cd samples/bookshop - ``` - -2. **Install dependencies**: - ```bash - mvn clean compile - ``` - -3. **Run the application**: - ```bash - mvn spring-boot:run - ``` - -4. **Access the application**: - - Browse Books: http://localhost:8080/browse/index.html - - Admin Books: http://localhost:8080/admin-books/index.html - -## Using Attachments - -Once the application is running: - -1. Navigate to the Books app (browse or admin) -2. Select any book to open its details -3. Scroll down to find the "Attachments" section -4. Use the attachment controls to: - - Upload files by clicking the upload button - - View uploaded files in the attachment list - - Download files by clicking on them - - Delete files using the delete button - -## Implementation Details - -### Maven Configuration - -The attachments plugin is added to `srv/pom.xml`: - -```xml - - com.sap.cds - cds-feature-attachments - -``` - -The `cds-maven-plugin` includes the `resolve` goal to make CDS models from dependencies available: - -```xml - - com.sap.cds - cds-maven-plugin - - - cds.resolve - - resolve - - - - - -``` - -### CDS Model Extension - -The `srv/attachments.cds` file extends the Books entity with attachments: - -```cds -using { sap.capire.bookshop as my } from '../db/schema'; -using { sap.attachments.Attachments } from 'com.sap.cds/cds-feature-attachments'; - -extend my.Books with { - attachments: Composition of many Attachments; -} -``` - -### UI Integration - -The same file adds UI facets for both services to display attachments in the Fiori apps: - -```cds -using { CatalogService as service } from '../app/services'; -annotate service.Books with @( - UI.Facets: [ - { - $Type : 'UI.ReferenceFacet', - ID : 'AttachmentsFacet', - Label : '{i18n>attachments}', - Target : 'attachments/@UI.LineItem' - } - ] -); -``` - -## Storage Configuration - -This sample uses the default in-memory storage, which stores attachments directly in the H2 database. For production scenarios, consider using object store backends. - -## Advanced Configuration - -For advanced topics like object store integration, malware scanning, and security configuration, see the [main project documentation](../../README.md). - -## Troubleshooting - -- **Port conflicts**: If port 8080 is in use, specify a different port: `mvn spring-boot:run -Dspring-boot.run.arguments="--server.port=8081"` -- **Memory issues**: Increase JVM heap size: `export MAVEN_OPTS="-Xmx2g"` -- **File upload issues**: Check browser developer console for error messages -- **View loading issues**: If your view doesn’t load, try clearing the cache and local storage for `localhost` (and the relevant port) in your browser settings or try a private/incognito tab. diff --git a/samples/bookshop-mtx/app/_i18n/i18n.properties b/samples/bookshop-mtx/app/_i18n/i18n.properties deleted file mode 100644 index 7326bbb72..000000000 --- a/samples/bookshop-mtx/app/_i18n/i18n.properties +++ /dev/null @@ -1,15 +0,0 @@ -Books = Books -Book = Book -ID = ID -Title = Title -Author = Author -Authors = Authors -AuthorID = Author ID -AuthorName = Author Name -Name = Name -Age = Age -Stock = Stock -Order = Order -Orders = Orders -Price = Price -Genre = Genre \ No newline at end of file diff --git a/samples/bookshop-mtx/app/_i18n/i18n_de.properties b/samples/bookshop-mtx/app/_i18n/i18n_de.properties deleted file mode 100644 index cb712c12c..000000000 --- a/samples/bookshop-mtx/app/_i18n/i18n_de.properties +++ /dev/null @@ -1,15 +0,0 @@ -Books = Bücher -Book = Buch -ID = ID -Title = Titel -Author = Autor -Authors = Autoren -AuthorID = ID des Autors -AuthorName = Name des Autors -Name = Name -Age = Alter -Stock = Bestand -Order = Bestellung -Orders = Bestellungen -Price = Preis -Genre = Genre \ No newline at end of file diff --git a/samples/bookshop-mtx/app/admin-books/dist/Component-dbg.js b/samples/bookshop-mtx/app/admin-books/dist/Component-dbg.js deleted file mode 100644 index e98677ee9..000000000 --- a/samples/bookshop-mtx/app/admin-books/dist/Component-dbg.js +++ /dev/null @@ -1,8 +0,0 @@ -sap.ui.define(["sap/fe/core/AppComponent"], function (AppComponent) { - "use strict"; - return AppComponent.extend("books.Component", { - metadata: { manifest: "json" } - }); -}); - -/* eslint no-undef:0 */ diff --git a/samples/bookshop-mtx/app/admin-books/dist/Component-preload.js b/samples/bookshop-mtx/app/admin-books/dist/Component-preload.js deleted file mode 100644 index 5bacffae6..000000000 --- a/samples/bookshop-mtx/app/admin-books/dist/Component-preload.js +++ /dev/null @@ -1,8 +0,0 @@ -//@ui5-bundle bookshop/admin-books/Component-preload.js -sap.ui.predefine("bookshop/admin-books/Component", ["sap/fe/core/AppComponent"],function(e){"use strict";return e.extend("books.Component",{metadata:{manifest:"json"}})}); -sap.ui.require.preload({ - "bookshop/admin-books/i18n/i18n.properties":'appTitle=Manage Books\nappSubTitle=Manage bookshop inventory\nappDescription=Manage your bookshop inventory with ease.\n', - "bookshop/admin-books/i18n/i18n_de.properties":'appTitle=B\\u00fccher verwalten\nappSubTitle=Verwalten Sie den Bestand der Buchhandlungen\nappDescription=Verwalten Sie den Bestand Ihrer Buchhandlung ganz einfach.\n', - "bookshop/admin-books/manifest.json":'{"_version":"1.49.0","sap.app":{"applicationVersion":{"version":"1.0.0"},"id":"bookshop.admin-books","type":"application","title":"{{appTitle}}","description":"{{appDescription}}","i18n":{"bundleUrl":"i18n/i18n.properties","supportedLocales":["","de"]},"dataSources":{"AdminService":{"uri":"/odata/v4/AdminService/","type":"OData","settings":{"odataVersion":"4.0"}}},"crossNavigation":{"inbounds":{"intent-Books-manage":{"signature":{"parameters":{},"additionalParameters":"allowed"},"semanticObject":"Books","action":"manage"}}}},"sap.ui":{"technology":"UI5","fullWidth":false,"deviceTypes":{"desktop":true,"tablet":true,"phone":true}},"sap.ui5":{"dependencies":{"minUI5Version":"1.115.1","libs":{"sap.fe.templates":{}}},"models":{"i18n":{"type":"sap.ui.model.resource.ResourceModel","uri":"i18n/i18n.properties","settings":{"supportedLocales":["","de"]}},"":{"dataSource":"AdminService","settings":{"operationMode":"Server","autoExpandSelect":true,"earlyRequests":true,"groupProperties":{"default":{"submit":"Auto"}}}}},"routing":{"routes":[{"pattern":":?query:","name":"BooksList","target":"BooksList"},{"pattern":"Books({key}):?query:","name":"BooksDetails","target":"BooksDetails"},{"pattern":"Books({key}/author({key2}):?query:","name":"AuthorsDetails","target":"AuthorsDetails"}],"targets":{"BooksList":{"type":"Component","id":"BooksList","name":"sap.fe.templates.ListReport","options":{"settings":{"contextPath":"/Books","initialLoad":true,"navigation":{"Books":{"detail":{"route":"BooksDetails"}}}}}},"BooksDetails":{"type":"Component","id":"BooksDetailsList","name":"sap.fe.templates.ObjectPage","options":{"settings":{"contextPath":"/Books","editableHeaderContent":false,"navigation":{"Authors":{"detail":{"route":"AuthorsDetails"}}}}}},"AuthorsDetails":{"type":"Component","id":"AuthorsDetailsList","name":"sap.fe.templates.ObjectPage","options":{"settings":{"contextPath":"/Authors"}}}}},"contentDensities":{"compact":true,"cozy":true},"flexBundle":false},"sap.fiori":{"registrationIds":[],"archeType":"transactional"}}' -}); -//# sourceMappingURL=Component-preload.js.map diff --git a/samples/bookshop-mtx/app/admin-books/dist/Component-preload.js.map b/samples/bookshop-mtx/app/admin-books/dist/Component-preload.js.map deleted file mode 100644 index 9164d03e2..000000000 --- a/samples/bookshop-mtx/app/admin-books/dist/Component-preload.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"Component-preload.js","sections":[{"offset":{"line":1,"column":0},"map":{"version":3,"names":["sap","ui","define","AppComponent","extend","metadata","manifest"],"sources":["Component-dbg.js"],"mappings":"AAAAA,IAAIC,GAAGC,4CAAO,CAAC,4BAA6B,SAAUC,GACpD,aACA,OAAOA,EAAaC,OAAO,kBAAmB,CAC5CC,SAAU,CAAEC,SAAU,SAE1B","ignoreList":[],"sourceRoot":""}},{"offset":{"line":2,"column":0},"map":{"version":3,"names":[],"sources":["Component-preload.js?bundle-code-0"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.require.preload({\n"],"sourceRoot":""}}]} \ No newline at end of file diff --git a/samples/bookshop-mtx/app/admin-books/dist/Component.js b/samples/bookshop-mtx/app/admin-books/dist/Component.js deleted file mode 100644 index 60e1e4c86..000000000 --- a/samples/bookshop-mtx/app/admin-books/dist/Component.js +++ /dev/null @@ -1,2 +0,0 @@ -sap.ui.define(["sap/fe/core/AppComponent"],function(e){"use strict";return e.extend("books.Component",{metadata:{manifest:"json"}})}); -//# sourceMappingURL=Component.js.map \ No newline at end of file diff --git a/samples/bookshop-mtx/app/admin-books/dist/Component.js.map b/samples/bookshop-mtx/app/admin-books/dist/Component.js.map deleted file mode 100644 index f9544db1a..000000000 --- a/samples/bookshop-mtx/app/admin-books/dist/Component.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"Component.js","names":["sap","ui","define","AppComponent","extend","metadata","manifest"],"sources":["Component-dbg.js"],"mappings":"AAAAA,IAAIC,GAAGC,OAAO,CAAC,4BAA6B,SAAUC,GACpD,aACA,OAAOA,EAAaC,OAAO,kBAAmB,CAC5CC,SAAU,CAAEC,SAAU,SAE1B","ignoreList":[]} \ No newline at end of file diff --git a/samples/bookshop-mtx/app/admin-books/dist/admin-books.zip b/samples/bookshop-mtx/app/admin-books/dist/admin-books.zip deleted file mode 100644 index 291adfc4306eda8d7dcd2e2bad6c8fddc6136f03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4359 zcmZ{n2Q*ym+J;AqE~A7HJ<8~P^k_jCWeB21B$yCp^iFgM(FReXkLVIzL=B=Folhr9 z!ibWn(az+Y|NlPDI+?YeS$prb*R}3vulK#*`+-96@M!=5A|gQ8qagEd5$#b9001Bt zTXfj66On=o{r6Jk@`u0Fu-I06A8IqZQoF*2dLM(B2gR zA2(h=%z%LbgO+UH?C=<*nW?h(`{Km$Xj#od^46InA4x^Q-jJ$Rj)_D+J9;j3Z>lmN zuGSP^wIG6(U4eN!X2D;^$32ll19^TDn!lo%pTNNcD?n5x>={bj4@N6u+>oWGfh_oC z1cN~zkSrTv4c@VKCUQMGBR)Q7_OO>?gekoH#)66Hz#HbwCzI7dxo_1Xo^8>#O>5 z+7p{a(f%5;9NyQM|Q(dQ_eV4YfBu4R=f51m0T3N!sORTIK%@6-4zrhsBSK!p8+KYBzTB& z?%c+}@~Hi1Wcd}T1{bt1+SAihqjYww57BiJ<;1>TzhW8SUWVn30wkfw%Dg#i5ZJ}} zMyZK`7+;4%`Y?M2hZvBvwsr6i)rN7*dV|6d$)n}b(N-Y9?FCC%8j)Adx5PI55 zcnn0}n;tRC^P#at=45EBcK3+?mS>6Oj$No9e3afcG>lepoLNQ65){t9s0V$AwEb*k zWjn8ETfUbYKLM_)YL)gp)@Wha*VmIUlZHBXLw4YS~%Y> zaQiYuPx2(&!$wS0sLS*q8?i8QW z8!P!L8J>EWI=z?s`r6>g967Jip&l_7tT(aRXh{ZVS1#>dddjN24DtSP!X0HR{gUZb zDDJbcrZvvbf%vvDW8nwOn?iRsbL$TKHYk%ZmMuMV5ROImUl!lI8dY09jNUGdKR#nQ_B%(&vt7q}5 z_Gt-tl$1n9oAwOP8V}j$T$2wVv$LN|I4(O!oVY865R-+w15N_8Fk#Js-Mb*b&pxNyH8!Kbmg@kl(rl}Qd37Y zGXWU}si;viZh-%1d}UBV_s zH}N?$CJrxZF;fFa;5uCulyrm?om6?WLr+<6_-x^3cLu2F$56a(#%9lC?bs=QW^YyY zUS7+$*cfvey$#gHR9)~n)#2v03{e~7sJ&2*!H4#p6T++Ax`@+GspA6xeI)-m_WoXE z0!}VA4hSo2Y}aK*j=C#g;JJB#s`we;9$OGe9=PCsA9GS<$3Bp84=;PvL2gPys&==p zZJzg{sWPE_s)@j$$THV!9+ugauyV|j$?`qA$vPNWp?}?@*e#}X%|(d$r*83tCeqD! zIu6=h>DyqV2g`&_15uHX>7Y7)Hf?xoE}E|wVgpi`@F{A9@CiY>l4f(}2)rOK-NTb9 z?%Z^l?+z99@!&8?=DvmhaBL+mr~R=WYUPH>gVrWPsx;fWsk5O;`)|XpDkhVk(T_0& z1FAnmEN%R(r$O4C&XrIH)P!X{_1>6~Q z{b^NQIv)a@!mT^2O|1->Dx1|=5ZJqgdSK5qH~;JGteC&R8cCnZO%t;F1TN{mZ=2|(0$QI(zpI5V_(shvg@%g&VsEq z|8+vCZi?r92W;eiz0hYM+CWp%hmS$)A0^FOhhPdb5nKf5Y_FbFau;P1_&efAt5Q}n zv0z-gh+`78+cz?2xQb#(@Cq%^@EKDhX9{X6o++tENhT~vw?%M;@Q`GPdiS7ndMDj zzDpgT_k)CeyjnHNLgr>$AJ%YApKKf4{iyK?#1cvLYlqaFuMAQx5C45Lfc7 zGYyQyB=()T2}7CHT`BQh+{Nq5n81gW&65vchdRFB!P3`#ms@JpUv)L@(6QbAsqwk& zuo7w{`@MB~79)eSsC)KB@|Q);>4of8$uFq2nrG%) zD3P2{5>*xZCP2N4VSl3Ks__i7$9aojEmjg20JwSOocPChe%ozyywY#G4ZG-*^-g(h zR1)D8H)X7;1TvNzXCB#dc)7)e;o@tnys#K~ZB!mCEuYdaiI{ggO5`%$&W00(2@sG> zKdABi@+BLdx5vfOOaKgaKe&IBu)^IrIhd6m`NAv=@x2b*efid4Uj&R zTI9;ll)B&Eo5O#H88rZ|@J0416>_A_-c87Ju8jj$aN#e2XYjr|fEPBA!}|d~j&p*F zL8B>Gr%(D6(xv@p~ z+73uLS9^+l_Lu4@uW47iV>;8l)7`l7kV$Z}Vygw`KcAThJ9nj)1ejyL?B739?3I_R zfR&RI*3=R3q}YFF2u18kOes@X{hC71;uQDOCvBRTgK8YdwpIPak>|cgcTnuv<0yGBXLI{bL%e5S9vCY7->jw(K!BxQf z)1XQGck|Ujzc2qVb{ZNG0Px;?@3;GJ+SRG>GA-va?b3|+N7U8n?lOu4+s2hy?~jbD zk?=A@jNocO{3GIOKD&%qzl`|5wDw2L)s%D@Q+(~JlHZ&7kC?0R<1!|N@Dk=y82KaW zs-av)QDW2c|H?K0Q`!G-oXcyB6z~r&FUSi!n_t>S{}%KE-HTm21lZOA*msEmkyuxQ IQT)F97uYjmE&u=k diff --git a/samples/bookshop-mtx/app/admin-books/dist/i18n/i18n.properties b/samples/bookshop-mtx/app/admin-books/dist/i18n/i18n.properties deleted file mode 100644 index 9a23ee40a..000000000 --- a/samples/bookshop-mtx/app/admin-books/dist/i18n/i18n.properties +++ /dev/null @@ -1,3 +0,0 @@ -appTitle=Manage Books -appSubTitle=Manage bookshop inventory -appDescription=Manage your bookshop inventory with ease. diff --git a/samples/bookshop-mtx/app/admin-books/dist/i18n/i18n_de.properties b/samples/bookshop-mtx/app/admin-books/dist/i18n/i18n_de.properties deleted file mode 100644 index 27e8a53d7..000000000 --- a/samples/bookshop-mtx/app/admin-books/dist/i18n/i18n_de.properties +++ /dev/null @@ -1,3 +0,0 @@ -appTitle=B\u00fccher verwalten -appSubTitle=Verwalten Sie den Bestand der Buchhandlungen -appDescription=Verwalten Sie den Bestand Ihrer Buchhandlung ganz einfach. diff --git a/samples/bookshop-mtx/app/admin-books/dist/manifest.json b/samples/bookshop-mtx/app/admin-books/dist/manifest.json deleted file mode 100644 index 1edc63fa0..000000000 --- a/samples/bookshop-mtx/app/admin-books/dist/manifest.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "_version": "1.49.0", - "sap.app": { - "applicationVersion": { - "version": "1.0.0" - }, - "id": "bookshop.admin-books", - "type": "application", - "title": "{{appTitle}}", - "description": "{{appDescription}}", - "i18n": { - "bundleUrl": "i18n/i18n.properties", - "supportedLocales": [ - "", - "de" - ] - }, - "dataSources": { - "AdminService": { - "uri": "/odata/v4/AdminService/", - "type": "OData", - "settings": { - "odataVersion": "4.0" - } - } - }, - "crossNavigation": { - "inbounds": { - "intent-Books-manage": { - "signature": { - "parameters": {}, - "additionalParameters": "allowed" - }, - "semanticObject": "Books", - "action": "manage" - } - } - } - }, - "sap.ui": { - "technology": "UI5", - "fullWidth": false, - "deviceTypes": { - "desktop": true, - "tablet": true, - "phone": true - } - }, - "sap.ui5": { - "dependencies": { - "minUI5Version": "1.115.1", - "libs": { - "sap.fe.templates": {} - } - }, - "models": { - "i18n": { - "type": "sap.ui.model.resource.ResourceModel", - "uri": "i18n/i18n.properties", - "settings": { - "supportedLocales": [ - "", - "de" - ] - } - }, - "": { - "dataSource": "AdminService", - "settings": { - "operationMode": "Server", - "autoExpandSelect": true, - "earlyRequests": true, - "groupProperties": { - "default": { - "submit": "Auto" - } - } - } - } - }, - "routing": { - "routes": [ - { - "pattern": ":?query:", - "name": "BooksList", - "target": "BooksList" - }, - { - "pattern": "Books({key}):?query:", - "name": "BooksDetails", - "target": "BooksDetails" - }, - { - "pattern": "Books({key}/author({key2}):?query:", - "name": "AuthorsDetails", - "target": "AuthorsDetails" - } - ], - "targets": { - "BooksList": { - "type": "Component", - "id": "BooksList", - "name": "sap.fe.templates.ListReport", - "options": { - "settings": { - "contextPath": "/Books", - "initialLoad": true, - "navigation": { - "Books": { - "detail": { - "route": "BooksDetails" - } - } - } - } - } - }, - "BooksDetails": { - "type": "Component", - "id": "BooksDetailsList", - "name": "sap.fe.templates.ObjectPage", - "options": { - "settings": { - "contextPath": "/Books", - "editableHeaderContent": false, - "navigation": { - "Authors": { - "detail": { - "route": "AuthorsDetails" - } - } - } - } - } - }, - "AuthorsDetails": { - "type": "Component", - "id": "AuthorsDetailsList", - "name": "sap.fe.templates.ObjectPage", - "options": { - "settings": { - "contextPath": "/Authors" - } - } - } - } - }, - "contentDensities": { - "compact": true, - "cozy": true - }, - "flexBundle": false - }, - "sap.fiori": { - "registrationIds": [], - "archeType": "transactional" - } -} \ No newline at end of file diff --git a/samples/bookshop-mtx/app/admin-books/fiori-service.cds b/samples/bookshop-mtx/app/admin-books/fiori-service.cds deleted file mode 100644 index 36fa09086..000000000 --- a/samples/bookshop-mtx/app/admin-books/fiori-service.cds +++ /dev/null @@ -1,113 +0,0 @@ -using {AdminService} from '../../srv/admin-service.cds'; - -//////////////////////////////////////////////////////////////////////////// -// -// Books Object Page -// -annotate AdminService.Books with @(UI: { - HeaderInfo : { - TypeName : '{i18n>Book}', - TypeNamePlural: '{i18n>Books}', - Title : {Value: title}, - Description : {Value: author.name} - }, - Facets : [ - { - $Type : 'UI.ReferenceFacet', - Label : '{i18n>General}', - Target: '@UI.FieldGroup#General' - }, - { - $Type : 'UI.ReferenceFacet', - Label : '{i18n>Translations}', - Target: 'texts/@UI.LineItem' - }, - { - $Type : 'UI.ReferenceFacet', - Label : '{i18n>Details}', - Target: '@UI.FieldGroup#Details' - }, - { - $Type : 'UI.ReferenceFacet', - Label : '{i18n>Admin}', - Target: '@UI.FieldGroup#Admin' - } - ], - FieldGroup #General: {Data: [ - {Value: title}, - {Value: author_ID}, - {Value: genre_ID}, - {Value: descr} - ]}, - FieldGroup #Details: {Data: [ - {Value: stock}, - {Value: price} - ]}, - FieldGroup #Admin : {Data: [ - {Value: createdBy}, - {Value: createdAt}, - {Value: modifiedBy}, - {Value: modifiedAt} - ]} -}); - - -//////////////////////////////////////////////////////////// -// -// Draft for Localized Data -// -annotate sap.capire.bookshop.Books with @fiori.draft.enabled; -annotate AdminService.Books with @odata.draft.enabled; - -annotate AdminService.Books.texts with @(UI: { - Identification : [{Value: title}], - SelectionFields: [ - locale, - title - ], - LineItem : [ - { - Value: locale, - Label: 'Locale' - }, - { - Value: title, - Label: 'Title' - }, - { - Value: descr, - Label: 'Description' - } - ] -}); - -annotate AdminService.Books.texts with { - ID @UI.Hidden; - ID_texts @UI.Hidden; -}; - -// Add Value Help for Locales -annotate AdminService.Books.texts { - locale @( - ValueList.entity: 'Languages', - Common.ValueListWithFixedValues //show as drop down, not a dialog - ) -}; - -// In addition we need to expose Languages through AdminService as a target for ValueList -using {sap} from '@sap/cds/common'; - -extend service AdminService { - @readonly - entity Languages as projection on sap.common.Languages; -} - -// Workaround for Fiori popup for asking user to enter a new UUID on Create -annotate AdminService.Books with { - ID @Core.Computed; -} - -// Show Genre as drop down, not a dialog -annotate AdminService.Books with { - genre @Common.ValueListWithFixedValues; -} diff --git a/samples/bookshop-mtx/app/admin-books/webapp/Component.js b/samples/bookshop-mtx/app/admin-books/webapp/Component.js deleted file mode 100644 index e98677ee9..000000000 --- a/samples/bookshop-mtx/app/admin-books/webapp/Component.js +++ /dev/null @@ -1,8 +0,0 @@ -sap.ui.define(["sap/fe/core/AppComponent"], function (AppComponent) { - "use strict"; - return AppComponent.extend("books.Component", { - metadata: { manifest: "json" } - }); -}); - -/* eslint no-undef:0 */ diff --git a/samples/bookshop-mtx/app/admin-books/webapp/i18n/i18n.properties b/samples/bookshop-mtx/app/admin-books/webapp/i18n/i18n.properties deleted file mode 100644 index 9a23ee40a..000000000 --- a/samples/bookshop-mtx/app/admin-books/webapp/i18n/i18n.properties +++ /dev/null @@ -1,3 +0,0 @@ -appTitle=Manage Books -appSubTitle=Manage bookshop inventory -appDescription=Manage your bookshop inventory with ease. diff --git a/samples/bookshop-mtx/app/admin-books/webapp/i18n/i18n_de.properties b/samples/bookshop-mtx/app/admin-books/webapp/i18n/i18n_de.properties deleted file mode 100644 index 01d56a22c..000000000 --- a/samples/bookshop-mtx/app/admin-books/webapp/i18n/i18n_de.properties +++ /dev/null @@ -1,3 +0,0 @@ -appTitle=Bücher verwalten -appSubTitle=Verwalten Sie den Bestand der Buchhandlungen -appDescription=Verwalten Sie den Bestand Ihrer Buchhandlung ganz einfach. diff --git a/samples/bookshop-mtx/app/admin-books/webapp/manifest.json b/samples/bookshop-mtx/app/admin-books/webapp/manifest.json deleted file mode 100644 index 4bcc54ceb..000000000 --- a/samples/bookshop-mtx/app/admin-books/webapp/manifest.json +++ /dev/null @@ -1,145 +0,0 @@ -{ - "_version": "1.49.0", - "sap.app": { - "applicationVersion": { - "version": "1.0.0" - }, - "id": "bookshop.admin-books", - "type": "application", - "title": "{{appTitle}}", - "description": "{{appDescription}}", - "i18n": "i18n/i18n.properties", - "dataSources": { - "AdminService": { - "uri": "/odata/v4/AdminService/", - "type": "OData", - "settings": { - "odataVersion": "4.0" - } - } - }, - "crossNavigation": { - "inbounds": { - "intent-Books-manage": { - "signature": { - "parameters": {}, - "additionalParameters": "allowed" - }, - "semanticObject": "Books", - "action": "manage" - } - } - } - }, - "sap.ui": { - "technology": "UI5", - "fullWidth": false, - "deviceTypes": { - "desktop": true, - "tablet": true, - "phone": true - } - }, - "sap.ui5": { - "dependencies": { - "minUI5Version": "1.115.1", - "libs": { - "sap.fe.templates": {} - } - }, - "models": { - "i18n": { - "type": "sap.ui.model.resource.ResourceModel", - "uri": "i18n/i18n.properties" - }, - "": { - "dataSource": "AdminService", - "settings": { - "operationMode": "Server", - "autoExpandSelect": true, - "earlyRequests": true, - "groupProperties": { - "default": { - "submit": "Auto" - } - } - } - } - }, - "routing": { - "routes": [ - { - "pattern": ":?query:", - "name": "BooksList", - "target": "BooksList" - }, - { - "pattern": "Books({key}):?query:", - "name": "BooksDetails", - "target": "BooksDetails" - }, - { - "pattern": "Books({key}/author({key2}):?query:", - "name": "AuthorsDetails", - "target": "AuthorsDetails" - } - ], - "targets": { - "BooksList": { - "type": "Component", - "id": "BooksList", - "name": "sap.fe.templates.ListReport", - "options": { - "settings": { - "contextPath": "/Books", - "initialLoad": true, - "navigation": { - "Books": { - "detail": { - "route": "BooksDetails" - } - } - } - } - } - }, - "BooksDetails": { - "type": "Component", - "id": "BooksDetailsList", - "name": "sap.fe.templates.ObjectPage", - "options": { - "settings": { - "contextPath": "/Books", - "editableHeaderContent": false, - "navigation": { - "Authors": { - "detail": { - "route": "AuthorsDetails" - } - } - } - } - } - }, - "AuthorsDetails": { - "type": "Component", - "id": "AuthorsDetailsList", - "name": "sap.fe.templates.ObjectPage", - "options": { - "settings": { - "contextPath": "/Authors" - } - } - } - } - }, - "contentDensities": { - "compact": true, - "cozy": true - } - }, - "sap.fiori": { - "registrationIds": [], - "archeType": "transactional" - } -} diff --git a/samples/bookshop-mtx/app/appconfig/fioriSandboxConfig.json b/samples/bookshop-mtx/app/appconfig/fioriSandboxConfig.json deleted file mode 100644 index ff2ac499b..000000000 --- a/samples/bookshop-mtx/app/appconfig/fioriSandboxConfig.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "services": { - "LaunchPage": { - "adapter": { - "config": { - "catalogs": [], - "groups": [ - { - "id": "Bookshop", - "title": "Bookshop", - "isPreset": true, - "isVisible": true, - "isGroupLocked": false, - "tiles": [ - { - "id": "BrowseBooks", - "tileType": "sap.ushell.ui.tile.StaticTile", - "properties": { - "title": "Browse Books", - "targetURL": "#Books-display" - } - } - ] - }, - { - "id": "Administration", - "title": "Administration", - "isPreset": true, - "isVisible": true, - "isGroupLocked": false, - "tiles": [ - { - "id": "ManageBooks", - "tileType": "sap.ushell.ui.tile.StaticTile", - "properties": { - "title": "Manage Books", - "targetURL": "#Books-manage" - } - } - ] - } - ] - } - } - }, - "NavTargetResolution": { - "config": { - "enableClientSideTargetResolution": true - } - }, - "ClientSideTargetResolution": { - "adapter": { - "config": { - "inbounds": { - "BrowseBooks": { - "semanticObject": "Books", - "action": "display", - "title": "Browse Books", - "signature": { - "parameters": { - "Books.ID": { - "renameTo": "ID" - }, - "Authors.books.ID": { - "renameTo": "ID" - } - }, - "additionalParameters": "ignored" - }, - "resolutionResult": { - "applicationType": "SAPUI5", - "additionalInformation": "SAPUI5.Component=bookshop", - "url": "browse/webapp" - } - }, - "ManageBooks": { - "semanticObject": "Books", - "action": "manage", - "title": "Manage Books", - "signature": { - "parameters": {}, - "additionalParameters": "allowed" - }, - "resolutionResult": { - "applicationType": "SAPUI5", - "additionalInformation": "SAPUI5.Component=books", - "url": "admin-books/webapp" - } - } - } - } - } - } - } -} diff --git a/samples/bookshop-mtx/app/browse/dist/Component-dbg.js b/samples/bookshop-mtx/app/browse/dist/Component-dbg.js deleted file mode 100644 index 4020679f8..000000000 --- a/samples/bookshop-mtx/app/browse/dist/Component-dbg.js +++ /dev/null @@ -1,7 +0,0 @@ -sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) { - "use strict"; - return AppComponent.extend("bookshop.Component", { - metadata: { manifest: "json" } - }); -}); -/* eslint no-undef:0 */ diff --git a/samples/bookshop-mtx/app/browse/dist/Component-preload.js b/samples/bookshop-mtx/app/browse/dist/Component-preload.js deleted file mode 100644 index 2553610fb..000000000 --- a/samples/bookshop-mtx/app/browse/dist/Component-preload.js +++ /dev/null @@ -1,8 +0,0 @@ -//@ui5-bundle bookshop/browse/Component-preload.js -sap.ui.predefine("bookshop/browse/Component", ["sap/fe/core/AppComponent"],function(e){"use strict";return e.extend("bookshop.Component",{metadata:{manifest:"json"}})}); -sap.ui.require.preload({ - "bookshop/browse/i18n/i18n.properties":'appTitle=Browse Books\nappSubTitle=Find all your favorite books\nappDescription=This application lets you find the next books you want to read.\n', - "bookshop/browse/i18n/i18n_de.properties":'appTitle=B\\u00fccher anschauen\nappSubTitle=Finden sie ihre n\\u00e4chste Lekt\\u00fcre\nappDescription=Finden Sie die nachsten B\\u00fccher, die Sie lesen m\\u00f6chten.\n', - "bookshop/browse/manifest.json":'{"_version":"1.49.0","sap.app":{"id":"bookshop.browse","applicationVersion":{"version":"1.0.0"},"type":"application","title":"{{appTitle}}","description":"{{appDescription}}","i18n":{"bundleUrl":"i18n/i18n.properties","supportedLocales":["","de"]},"dataSources":{"CatalogService":{"uri":"/odata/v4/CatalogService/","type":"OData","settings":{"odataVersion":"4.0"}}},"crossNavigation":{"inbounds":{"intent1":{"signature":{"parameters":{"Books.ID":{"renameTo":"ID"},"Authors.books.ID":{"renameTo":"ID"}},"additionalParameters":"ignored"},"semanticObject":"Books","action":"display","title":"{{appTitle}}","subTitle":"{{appSubTitle}}","icon":"sap-icon://course-book","indicatorDataSource":{"dataSource":"CatalogService","path":"Books/$count","refresh":1800}}}}},"sap.ui":{"technology":"UI5","fullWidth":false,"deviceTypes":{"desktop":true,"tablet":true,"phone":true}},"sap.ui5":{"dependencies":{"minUI5Version":"1.115.1","libs":{"sap.fe.templates":{}}},"models":{"i18n":{"type":"sap.ui.model.resource.ResourceModel","uri":"i18n/i18n.properties","settings":{"supportedLocales":["","de"]}},"":{"dataSource":"CatalogService","settings":{"operationMode":"Server","autoExpandSelect":true,"earlyRequests":true,"groupProperties":{"default":{"submit":"Auto"}}}}},"routing":{"routes":[{"pattern":":?query:","name":"BooksList","target":"BooksList"},{"pattern":"Books({key}):?query:","name":"BooksDetails","target":"BooksDetails"}],"targets":{"BooksList":{"type":"Component","id":"BooksList","name":"sap.fe.templates.ListReport","options":{"settings":{"contextPath":"/Books","initialLoad":true,"navigation":{"Books":{"detail":{"route":"BooksDetails"}}}}}},"BooksDetails":{"type":"Component","id":"BooksDetailsList","name":"sap.fe.templates.ObjectPage","options":{"settings":{"contextPath":"/Books"}}}}},"contentDensities":{"compact":true,"cozy":true},"flexBundle":false},"sap.fiori":{"registrationIds":[],"archeType":"transactional"}}' -}); -//# sourceMappingURL=Component-preload.js.map diff --git a/samples/bookshop-mtx/app/browse/dist/Component-preload.js.map b/samples/bookshop-mtx/app/browse/dist/Component-preload.js.map deleted file mode 100644 index 03c8337d2..000000000 --- a/samples/bookshop-mtx/app/browse/dist/Component-preload.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"Component-preload.js","sections":[{"offset":{"line":1,"column":0},"map":{"version":3,"names":["sap","ui","define","AppComponent","extend","metadata","manifest"],"sources":["Component-dbg.js"],"mappings":"AAAAA,IAAIC,GAAGC,uCAAO,CAAC,4BAA6B,SAASC,GACpD,aACA,OAAOA,EAAaC,OAAO,qBAAsB,CAChDC,SAAU,CAAEC,SAAU,SAExB","ignoreList":[],"sourceRoot":""}},{"offset":{"line":2,"column":0},"map":{"version":3,"names":[],"sources":["Component-preload.js?bundle-code-0"],"mappings":"AAAA;AACA","sourcesContent":["sap.ui.require.preload({\n"],"sourceRoot":""}}]} \ No newline at end of file diff --git a/samples/bookshop-mtx/app/browse/dist/Component.js b/samples/bookshop-mtx/app/browse/dist/Component.js deleted file mode 100644 index 1005152b5..000000000 --- a/samples/bookshop-mtx/app/browse/dist/Component.js +++ /dev/null @@ -1,2 +0,0 @@ -sap.ui.define(["sap/fe/core/AppComponent"],function(e){"use strict";return e.extend("bookshop.Component",{metadata:{manifest:"json"}})}); -//# sourceMappingURL=Component.js.map \ No newline at end of file diff --git a/samples/bookshop-mtx/app/browse/dist/Component.js.map b/samples/bookshop-mtx/app/browse/dist/Component.js.map deleted file mode 100644 index 94e303bb9..000000000 --- a/samples/bookshop-mtx/app/browse/dist/Component.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"Component.js","names":["sap","ui","define","AppComponent","extend","metadata","manifest"],"sources":["Component-dbg.js"],"mappings":"AAAAA,IAAIC,GAAGC,OAAO,CAAC,4BAA6B,SAASC,GACpD,aACA,OAAOA,EAAaC,OAAO,qBAAsB,CAChDC,SAAU,CAAEC,SAAU,SAExB","ignoreList":[]} \ No newline at end of file diff --git a/samples/bookshop-mtx/app/browse/dist/browse.zip b/samples/bookshop-mtx/app/browse/dist/browse.zip deleted file mode 100644 index 5c2d37ef02d5988425eee7814c987a6efa96e801..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4490 zcmZ{o2{e>_`^P7{$!=11Etbeu86ui72!qF#HT#xzF!r6t&}e38MwTLDDI_#1vS+fB zog(|1vK3L*cY2@qUp)WQoO8{&?{n_+IoEaFzu))z{%(CZH4O*=U|;~m+eY71HD?+H z(f|Pa>;M2W008i`^Kx}|Li$O$ArW4+W|oK#U{FMNDO;?H$7}Hq5Gub2?vW5yx-z-e zN1{FEO*sq zjhH59d-~Fj=v{41eNCvmPV$vYhCo!3%^i$P&zFnaeX0t^E;r^pM|r|F=Ff?0uEZLo zUmE+ec@|1m^P&Hg?%eYr@KkOAR^1Hte4)cU^lj(Da3M>U2@RT+cuI~+#1*FNk4#{Dszm)o;yc|_1>lpy+3 zY3@t(p(glmZlJ{;pu%uN?K5+W(rqm_m6ixei}G>aQQ#xO#=*Lfg3_d=&ex2QPwWBx zoenz3r9iX$lGJn#%u3|D&RKo+LNNDc;nDv5x$$8%tM3!xvpa0w80!(ew2eAceGwfQ ze#@jP8y)68k^0o6D-# z?lSXQ=9nKfBU$fa%$$JHtpHJ9TYUF!#NDw`e@MD z$LG7woi6ExWsaSg?_XCK>dF-VvA)^9ens6~SWFZL9v&)qNhhv(o35!;8o##7V%A37 zqbu!&fIdu+O{sal$-K}fw?05}$GZZ-jWT?uMxK5A{+AENi3~*qn{;a^6x&aXC0$t+ zq*r3URSfYt2c?-~S^t_^N;eR}EsnV{bw9eakfj%aQ9BXkdFxu&{MH6gZW%sL0$d(^=uG_U~(oE(ZOW-0$$Ndb;U_|@j#W~8p6;n=*LkB}c-OBX5=@$Gy1%M8u7n+dql ztQ|jed@^lbLh14eiP~cAi;oZ1x@1kifF$-_X+@3RaCQT zjZ-?M_qA7WqqMPok%9fJL4QHRbR!zivzN=~3)zv`Ds2^R^IbX4iLc!8dIFM^efeIp zL>%lUtd4|1i%>ip-(~Yi{;cyAb!NL+t0@?|v!^!TblRLxzImC5RFsU3Fh5``$NEaL zcMIV^2G?e6*1zulbgA`UUA>)?*zQls_YoydQsP%vZ5*8rHC7iZ%R;6!cBh8S3K65t z$%WodkV?LM(&esX3~43o;CdiC$G6TUk*(_Hg#I4}EnnI$v?f1&mIvg;?-(zBE)b9C{QHO1!;H9ts)>_sP@i!$uN|t%fXU2}W?fu|4qRLw(5F;ReIZMe zsxe<%;7w3m*C)ZF6@(jh(7mQ)4m$R)3Mko2dD?j^zm;`=Bf~~JdGK}KJtc)@3qF%?cRhM$k_JFp&z8{L?!3*Ev>TfsHi=M7{~&&jVJ3FjA(yi|ZRz0wx8 zdSWUtZ$>tZBC3E*Wo-zCG z6}Qt2NS&g(SVKF#H{H@ltDV@?y%A0s@{YiN_UrHW)7NGXBU}j9nc?1EePdww^s@io;j)_JJ7en^ z84Hm#!G1EVRfcJu*vfS*>Bne#sla-mvny6ZmwqVwVotlt4!^Vqm!mVki5VOl`fQM? zZm16UYVrtN z3pi1Ob!)Q(SND=f?#Qg3i`23bRxNaNih;ZNATsjm?inEC>iqhAGZ9j}i=1}F_(zlR zYNvzCb=(4A%pyF31G!Ty&2p3I8u=oI2ufA zVIFJ$l=VEDJOj|IG#*zjnWyimD8q1F7cG933pXIU)P7!Uvf38jK<|z{`&3W&Jg9q> z$DZw#r;#nuCT&ui>*-XcP=Z4Cd~6SNQPnk4390OfiMdl~Hr2?%Xf=?K*QVzj z7EsW{ApjoQN5c`s(xCfq?=9MzE-$1&#qeW^N@_F+1r`eA)w=eZC8h}xt zRA1}aInAJv9{e*-;}P z>xZqJhGu^3n;CSV*A^d78Af+zv6_7RM0G>W@|_k)uej?A9P(F0&`kx-rQuuWkv8t^ zAb-a0RQyy3Un}IAhYOV`d!enW$BY~yfq{Rk_WK?jQXwuxE7BfCG+N|VIX6nR*yiv6 z_jYDye4E8|d7sPl(z(<3BIiAW5D5CVxGC8_=dxT@{|Z9Ol}%C|3f?@8oMl%9ig@*U zmE@9=H8@pz%v&Uf{iDTg=me)|CiPD3EpKJB4D>Awo zsSaF%!?g__yT*-l9MgGZZb53Wb=|l6=BSr#@_h{)5w!ibXT3V5xy`FzDlea9PPC@K z6k)_x;xWAvMb*ltc4N279)UaY&g!j*`1k0KuOi zNiLhHRZ~CyvL{18XI>sL?)M!pVzx?LVMIyN0!K_=22M{Cz4A6h1S9C!VmxxbNkJr) zzUMG-WGf~QF%C%D&2*Ie>gPVxPgfj_AZHey`*tRcCw3?%34XVV@#edQLZMcbgbrxI zx%;DPggl?}BsS6+nhEfOv&;5i8w8lvyV&B=6=2K>)CTBuMWEiy)RzJ*1TuSPH{;s z9ltI{TwM(G2t2?omljKlhFDifNjp9JQpu!Y>KkM~gt>G*^rX>f3xUZ}$;0v8h(?^~ zyzbVDw^QPc1Ll9G0t4mfmtGWclhWB=zvGmvAf%+7w>M=JAiVA+{xiV0!kX9>3tptK zNLlStH8kjgaGNiVd6MT0llvkmJHf!)n=yI|ANo!LfQ9n7H5rRFmn-3$hpK4af*BV5 zjHd@bbKrmgVxnl&CfHj&H>|6oT%#e3WGzYfF3uAZCd`cuo7R$jbH64HjN@nSwu1@ zKxKciC<4#GaeX*F!^|@s&vHsJUX=Bt4?jkAjrvb#UB+LMdF14K!wB0XM;WlcEZZT!_D9rFpL!V8PeJ{f(UceRN5oOfco=bq_E0uQ4dfqbM{V6< znjGDsg#Ksp{)jp1tqxr7cO6dT~vy6ZQimH8L`FZyrlfaW@ diff --git a/samples/bookshop-mtx/app/browse/dist/i18n/i18n.properties b/samples/bookshop-mtx/app/browse/dist/i18n/i18n.properties deleted file mode 100644 index 21436e8e4..000000000 --- a/samples/bookshop-mtx/app/browse/dist/i18n/i18n.properties +++ /dev/null @@ -1,3 +0,0 @@ -appTitle=Browse Books -appSubTitle=Find all your favorite books -appDescription=This application lets you find the next books you want to read. diff --git a/samples/bookshop-mtx/app/browse/dist/i18n/i18n_de.properties b/samples/bookshop-mtx/app/browse/dist/i18n/i18n_de.properties deleted file mode 100644 index b0d5d250a..000000000 --- a/samples/bookshop-mtx/app/browse/dist/i18n/i18n_de.properties +++ /dev/null @@ -1,3 +0,0 @@ -appTitle=B\u00fccher anschauen -appSubTitle=Finden sie ihre n\u00e4chste Lekt\u00fcre -appDescription=Finden Sie die nachsten B\u00fccher, die Sie lesen m\u00f6chten. diff --git a/samples/bookshop-mtx/app/browse/dist/manifest.json b/samples/bookshop-mtx/app/browse/dist/manifest.json deleted file mode 100644 index ca3e63b13..000000000 --- a/samples/bookshop-mtx/app/browse/dist/manifest.json +++ /dev/null @@ -1,150 +0,0 @@ -{ - "_version": "1.49.0", - "sap.app": { - "id": "bookshop.browse", - "applicationVersion": { - "version": "1.0.0" - }, - "type": "application", - "title": "{{appTitle}}", - "description": "{{appDescription}}", - "i18n": { - "bundleUrl": "i18n/i18n.properties", - "supportedLocales": [ - "", - "de" - ] - }, - "dataSources": { - "CatalogService": { - "uri": "/odata/v4/CatalogService/", - "type": "OData", - "settings": { - "odataVersion": "4.0" - } - } - }, - "crossNavigation": { - "inbounds": { - "intent1": { - "signature": { - "parameters": { - "Books.ID": { - "renameTo": "ID" - }, - "Authors.books.ID": { - "renameTo": "ID" - } - }, - "additionalParameters": "ignored" - }, - "semanticObject": "Books", - "action": "display", - "title": "{{appTitle}}", - "subTitle": "{{appSubTitle}}", - "icon": "sap-icon://course-book", - "indicatorDataSource": { - "dataSource": "CatalogService", - "path": "Books/$count", - "refresh": 1800 - } - } - } - } - }, - "sap.ui": { - "technology": "UI5", - "fullWidth": false, - "deviceTypes": { - "desktop": true, - "tablet": true, - "phone": true - } - }, - "sap.ui5": { - "dependencies": { - "minUI5Version": "1.115.1", - "libs": { - "sap.fe.templates": {} - } - }, - "models": { - "i18n": { - "type": "sap.ui.model.resource.ResourceModel", - "uri": "i18n/i18n.properties", - "settings": { - "supportedLocales": [ - "", - "de" - ] - } - }, - "": { - "dataSource": "CatalogService", - "settings": { - "operationMode": "Server", - "autoExpandSelect": true, - "earlyRequests": true, - "groupProperties": { - "default": { - "submit": "Auto" - } - } - } - } - }, - "routing": { - "routes": [ - { - "pattern": ":?query:", - "name": "BooksList", - "target": "BooksList" - }, - { - "pattern": "Books({key}):?query:", - "name": "BooksDetails", - "target": "BooksDetails" - } - ], - "targets": { - "BooksList": { - "type": "Component", - "id": "BooksList", - "name": "sap.fe.templates.ListReport", - "options": { - "settings": { - "contextPath": "/Books", - "initialLoad": true, - "navigation": { - "Books": { - "detail": { - "route": "BooksDetails" - } - } - } - } - } - }, - "BooksDetails": { - "type": "Component", - "id": "BooksDetailsList", - "name": "sap.fe.templates.ObjectPage", - "options": { - "settings": { - "contextPath": "/Books" - } - } - } - } - }, - "contentDensities": { - "compact": true, - "cozy": true - }, - "flexBundle": false - }, - "sap.fiori": { - "registrationIds": [], - "archeType": "transactional" - } -} \ No newline at end of file diff --git a/samples/bookshop-mtx/app/browse/fiori-service.cds b/samples/bookshop-mtx/app/browse/fiori-service.cds deleted file mode 100644 index b49a94f18..000000000 --- a/samples/bookshop-mtx/app/browse/fiori-service.cds +++ /dev/null @@ -1,51 +0,0 @@ -using {CatalogService} from '../../srv/cat-service.cds'; - -//////////////////////////////////////////////////////////////////////////// -// -// Books Object Page -// -annotate CatalogService.Books with @(UI: { - HeaderInfo : { - TypeName : '{i18n>Book}', - TypeNamePlural: '{i18n>Books}', - Title : {Value: title}, - Description : {Value: author} - }, - HeaderFacets : [{ - $Type : 'UI.ReferenceFacet', - Label : '{i18n>Description}', - Target: '@UI.FieldGroup#Descr' - }], - Facets : [{ - $Type : 'UI.ReferenceFacet', - Label : '{i18n>Details}', - Target: '@UI.FieldGroup#Price' - }], - FieldGroup #Descr: {Data: [{Value: descr}]}, - FieldGroup #Price: {Data: [{Value: price}]} -}); - -//////////////////////////////////////////////////////////////////////////// -// -// Books List Page -// -annotate CatalogService.Books with @(UI: { - SelectionFields: [ - ID, - price, - currency_code - ], - LineItem : [ - { - Value: ID, - Label: '{i18n>Title}' - }, - { - Value: author, - Label: '{i18n>Author}' - }, - {Value: genre.name}, - {Value: price}, - {Value: currency.symbol} - ] -}); diff --git a/samples/bookshop-mtx/app/browse/webapp/Component.js b/samples/bookshop-mtx/app/browse/webapp/Component.js deleted file mode 100644 index 4020679f8..000000000 --- a/samples/bookshop-mtx/app/browse/webapp/Component.js +++ /dev/null @@ -1,7 +0,0 @@ -sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) { - "use strict"; - return AppComponent.extend("bookshop.Component", { - metadata: { manifest: "json" } - }); -}); -/* eslint no-undef:0 */ diff --git a/samples/bookshop-mtx/app/browse/webapp/i18n/i18n.properties b/samples/bookshop-mtx/app/browse/webapp/i18n/i18n.properties deleted file mode 100644 index 21436e8e4..000000000 --- a/samples/bookshop-mtx/app/browse/webapp/i18n/i18n.properties +++ /dev/null @@ -1,3 +0,0 @@ -appTitle=Browse Books -appSubTitle=Find all your favorite books -appDescription=This application lets you find the next books you want to read. diff --git a/samples/bookshop-mtx/app/browse/webapp/i18n/i18n_de.properties b/samples/bookshop-mtx/app/browse/webapp/i18n/i18n_de.properties deleted file mode 100644 index ea86c3f29..000000000 --- a/samples/bookshop-mtx/app/browse/webapp/i18n/i18n_de.properties +++ /dev/null @@ -1,3 +0,0 @@ -appTitle=Bücher anschauen -appSubTitle=Finden sie ihre nächste Lektüre -appDescription=Finden Sie die nachsten Bücher, die Sie lesen möchten. diff --git a/samples/bookshop-mtx/app/browse/webapp/manifest.json b/samples/bookshop-mtx/app/browse/webapp/manifest.json deleted file mode 100644 index cd4b1c3cf..000000000 --- a/samples/bookshop-mtx/app/browse/webapp/manifest.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "_version": "1.49.0", - "sap.app": { - "id": "bookshop.browse", - "applicationVersion": { - "version": "1.0.0" - }, - "type": "application", - "title": "{{appTitle}}", - "description": "{{appDescription}}", - "i18n": "i18n/i18n.properties", - "dataSources": { - "CatalogService": { - "uri": "/odata/v4/CatalogService/", - "type": "OData", - "settings": { - "odataVersion": "4.0" - } - } - }, - "crossNavigation": { - "inbounds": { - "intent1": { - "signature": { - "parameters": { - "Books.ID": { - "renameTo": "ID" - }, - "Authors.books.ID": { - "renameTo": "ID" - } - }, - "additionalParameters": "ignored" - }, - "semanticObject": "Books", - "action": "display", - "title": "{{appTitle}}", - "subTitle": "{{appSubTitle}}", - "icon": "sap-icon://course-book", - "indicatorDataSource": { - "dataSource": "CatalogService", - "path": "Books/$count", - "refresh": 1800 - } - } - } - } - }, - "sap.ui": { - "technology": "UI5", - "fullWidth": false, - "deviceTypes": { - "desktop": true, - "tablet": true, - "phone": true - } - }, - "sap.ui5": { - "dependencies": { - "minUI5Version": "1.115.1", - "libs": { - "sap.fe.templates": {} - } - }, - "models": { - "i18n": { - "type": "sap.ui.model.resource.ResourceModel", - "uri": "i18n/i18n.properties" - }, - "": { - "dataSource": "CatalogService", - "settings": { - "operationMode": "Server", - "autoExpandSelect": true, - "earlyRequests": true, - "groupProperties": { - "default": { - "submit": "Auto" - } - } - } - } - }, - "routing": { - "routes": [ - { - "pattern": ":?query:", - "name": "BooksList", - "target": "BooksList" - }, - { - "pattern": "Books({key}):?query:", - "name": "BooksDetails", - "target": "BooksDetails" - } - ], - "targets": { - "BooksList": { - "type": "Component", - "id": "BooksList", - "name": "sap.fe.templates.ListReport", - "options": { - "settings": { - "contextPath": "/Books", - "initialLoad": true, - "navigation": { - "Books": { - "detail": { - "route": "BooksDetails" - } - } - } - } - } - }, - "BooksDetails": { - "type": "Component", - "id": "BooksDetailsList", - "name": "sap.fe.templates.ObjectPage", - "options": { - "settings": { - "contextPath": "/Books" - } - } - } - } - }, - "contentDensities": { - "compact": true, - "cozy": true - } - }, - "sap.fiori": { - "registrationIds": [], - "archeType": "transactional" - } -} diff --git a/samples/bookshop-mtx/app/common.cds b/samples/bookshop-mtx/app/common.cds deleted file mode 100644 index 69627beb5..000000000 --- a/samples/bookshop-mtx/app/common.cds +++ /dev/null @@ -1,264 +0,0 @@ -/* - Common Annotations shared by all apps -*/ - -using {sap.capire.bookshop as my} from '../db/schema'; -using { - sap.common, - sap.common.Currencies -} from '@sap/cds/common'; - -//////////////////////////////////////////////////////////////////////////// -// -// Books Lists -// -annotate my.Books with @( - Common.SemanticKey: [ID], - UI : { - Identification : [{Value: title}], - SelectionFields: [ - ID, - author_ID, - price, - currency_code - ], - LineItem : [ - { - Value: ID, - Label: '{i18n>Title}' - }, - { - Value: author.ID, - Label: '{i18n>Author}' - }, - {Value: genre.name}, - {Value: stock}, - {Value: price}, - {Value: currency.symbol} - ] - } -) { - ID @Common : { - SemanticObject : 'Books', - Text : title, - TextArrangement: #TextOnly - }; - author @ValueList.entity: 'Authors'; -}; - -annotate Currencies with { - symbol @Common.Label: '{i18n>Currency}'; -} - -//////////////////////////////////////////////////////////////////////////// -// -// Books Elements -// -annotate my.Books with { - ID @title: '{i18n>ID}'; - title @title: '{i18n>Title}'; - genre @title: '{i18n>Genre}' @Common : { - Text : genre.name, - TextArrangement: #TextOnly - }; - author @title: '{i18n>Author}' @Common : { - Text : author.name, - TextArrangement: #TextOnly - }; - price @title: '{i18n>Price}' @Measures.ISOCurrency: currency_code; - stock @title: '{i18n>Stock}'; - descr @title: '{i18n>Description}' @UI.MultiLineText; - image @title: '{i18n>Image}'; -} - -//////////////////////////////////////////////////////////////////////////// -// -// Genres List -// -annotate my.Genres with @( - Common.SemanticKey: [name], - UI : { - SelectionFields: [name], - LineItem : [ - {Value: name}, - { - Value: parent.name, - Label: 'Main Genre' - } - ] - } -); - -annotate my.Genres with { - ID @Common.Text: name @Common.TextArrangement: #TextOnly; -} - -//////////////////////////////////////////////////////////////////////////// -// -// Genre Details -// -annotate my.Genres with @(UI: { - Identification: [{Value: name}], - HeaderInfo : { - TypeName : '{i18n>Genre}', - TypeNamePlural: '{i18n>Genres}', - Title : {Value: name}, - Description : {Value: ID} - }, - Facets : [{ - $Type : 'UI.ReferenceFacet', - Label : '{i18n>SubGenres}', - Target: 'children/@UI.LineItem' - }] -}); - -//////////////////////////////////////////////////////////////////////////// -// -// Genres Elements -// -annotate my.Genres with { - ID @title: '{i18n>ID}'; - name @title: '{i18n>Genre}'; -} - -//////////////////////////////////////////////////////////////////////////// -// -// Authors List -// -annotate my.Authors with @( - Common.SemanticKey: [ID], - UI : { - Identification : [{Value: name}], - SelectionFields: [name], - LineItem : [ - {Value: ID}, - {Value: dateOfBirth}, - {Value: dateOfDeath}, - {Value: placeOfBirth}, - {Value: placeOfDeath} - ] - } -) { - ID @Common: { - SemanticObject : 'Authors', - Text : name, - TextArrangement: #TextOnly - }; -}; - -//////////////////////////////////////////////////////////////////////////// -// -// Author Details -// -annotate my.Authors with @(UI: { - HeaderInfo: { - TypeName : '{i18n>Author}', - TypeNamePlural: '{i18n>Authors}', - Title : {Value: name}, - Description : {Value: dateOfBirth} - }, - Facets : [{ - $Type : 'UI.ReferenceFacet', - Target: 'books/@UI.LineItem' - }] -}); - - -//////////////////////////////////////////////////////////////////////////// -// -// Authors Elements -// -annotate my.Authors with { - ID @title: '{i18n>ID}'; - name @title: '{i18n>Name}'; - dateOfBirth @title: '{i18n>DateOfBirth}'; - dateOfDeath @title: '{i18n>DateOfDeath}'; - placeOfBirth @title: '{i18n>PlaceOfBirth}'; - placeOfDeath @title: '{i18n>PlaceOfDeath}'; -} - -//////////////////////////////////////////////////////////////////////////// -// -// Languages List -// -annotate common.Languages with @( - Common.SemanticKey: [code], - Identification : [{Value: code}], - UI : { - SelectionFields: [ - name, - descr - ], - LineItem : [ - {Value: code}, - {Value: name} - ] - } -); - -//////////////////////////////////////////////////////////////////////////// -// -// Language Details -// -annotate common.Languages with @(UI: { - HeaderInfo : { - TypeName : '{i18n>Language}', - TypeNamePlural: '{i18n>Languages}', - Title : {Value: name}, - Description : {Value: descr} - }, - Facets : [{ - $Type : 'UI.ReferenceFacet', - Label : '{i18n>Details}', - Target: '@UI.FieldGroup#Details' - }], - FieldGroup #Details: {Data: [ - {Value: code}, - {Value: name}, - {Value: descr} - ]} -}); - -//////////////////////////////////////////////////////////////////////////// -// -// Currencies List -// -annotate common.Currencies with @( - Common.SemanticKey: [code], - Identification : [{Value: code}], - UI : { - SelectionFields: [ - name, - descr - ], - LineItem : [ - {Value: descr}, - {Value: symbol}, - {Value: code} - ] - } -); - -//////////////////////////////////////////////////////////////////////////// -// -// Currency Details -// -annotate common.Currencies with @(UI: { - HeaderInfo : { - TypeName : '{i18n>Currency}', - TypeNamePlural: '{i18n>Currencies}', - Title : {Value: descr}, - Description : {Value: code} - }, - Facets : [{ - $Type : 'UI.ReferenceFacet', - Label : '{i18n>Details}', - Target: '@UI.FieldGroup#Details' - }], - FieldGroup #Details: {Data: [ - {Value: name}, - {Value: symbol}, - {Value: code}, - {Value: descr} - ]} -}); diff --git a/samples/bookshop-mtx/app/index.html b/samples/bookshop-mtx/app/index.html deleted file mode 100644 index 70f631507..000000000 --- a/samples/bookshop-mtx/app/index.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - Bookshop - - - - - - - - - - diff --git a/samples/bookshop-mtx/app/services.cds b/samples/bookshop-mtx/app/services.cds deleted file mode 100644 index 87e7b310f..000000000 --- a/samples/bookshop-mtx/app/services.cds +++ /dev/null @@ -1,6 +0,0 @@ -/* - This model controls what gets served to Fiori frontends... -*/ -using from './common'; -using from './browse/fiori-service'; -using from './admin-books/fiori-service'; diff --git a/samples/bookshop-mtx/db/data/sap.capire.bookshop-Authors.csv b/samples/bookshop-mtx/db/data/sap.capire.bookshop-Authors.csv deleted file mode 100644 index 5272ee157..000000000 --- a/samples/bookshop-mtx/db/data/sap.capire.bookshop-Authors.csv +++ /dev/null @@ -1,5 +0,0 @@ -ID;name;dateOfBirth;placeOfBirth;dateOfDeath;placeOfDeath -10fef92e-975f-4c41-8045-c58e5c27a040;Emily Brontë;1818-07-30;Thornton, Yorkshire;1848-12-19;Haworth, Yorkshire -d4585e0e-ab3b-4424-b2ac-f2bfa785f068;Charlotte Brontë;1818-04-21;Thornton, Yorkshire;1855-03-31;Haworth, Yorkshire -4cf60975-300d-4dbe-8598-57b02e62bae2;Edgar Allen Poe;1809-01-19;Boston, Massachusetts;1849-10-07;Baltimore, Maryland -df9fb9fa-f121-45b5-8be5-8ff7ad5219a2;Richard Carpenter;1929-08-14;King’s Lynn, Norfolk;2012-02-26;Hertfordshire, England diff --git a/samples/bookshop-mtx/db/data/sap.capire.bookshop-Books.csv b/samples/bookshop-mtx/db/data/sap.capire.bookshop-Books.csv deleted file mode 100644 index 46d63fa5d..000000000 --- a/samples/bookshop-mtx/db/data/sap.capire.bookshop-Books.csv +++ /dev/null @@ -1,6 +0,0 @@ -ID;title;descr;author_ID;stock;price;currency_code;genre_ID -aeeda49f-72f2-4880-be27-a513b2e53040;Wuthering Heights;"Wuthering Heights, Emily Brontë's only novel, was published in 1847 under the pseudonym ""Ellis Bell"". It was written between October 1845 and June 1846. Wuthering Heights and Anne Brontë's Agnes Grey were accepted by publisher Thomas Newby before the success of their sister Charlotte's novel Jane Eyre. After Emily's death, Charlotte edited the manuscript of Wuthering Heights and arranged for the edited version to be published as a posthumous second edition in 1850.";10fef92e-975f-4c41-8045-c58e5c27a040;12;11.11;GBP;11 -b0056977-4cf5-46a2-ab14-6409ee2e0df1;Jane Eyre;"Jane Eyre /ɛər/ (originally published as Jane Eyre: An Autobiography) is a novel by English writer Charlotte Brontë, published under the pen name ""Currer Bell"", on 16 October 1847, by Smith, Elder & Co. of London. The first American edition was published the following year by Harper & Brothers of New York. Primarily a bildungsroman, Jane Eyre follows the experiences of its eponymous heroine, including her growth to adulthood and her love for Mr. Rochester, the brooding master of Thornfield Hall. The novel revolutionised prose fiction in that the focus on Jane's moral and spiritual development is told through an intimate, first-person narrative, where actions and events are coloured by a psychological intensity. The book contains elements of social criticism, with a strong sense of Christian morality at its core and is considered by many to be ahead of its time because of Jane's individualistic character and how the novel approaches the topics of class, sexuality, religion and feminism.";d4585e0e-ab3b-4424-b2ac-f2bfa785f068;11;12.34;GBP;11 -c7641340-a9be-4673-8dad-785a2505f46e;The Raven;"""The Raven"" is a narrative poem by American writer Edgar Allan Poe. First published in January 1845, the poem is often noted for its musicality, stylized language, and supernatural atmosphere. It tells of a talking raven's mysterious visit to a distraught lover, tracing the man's slow fall into madness. The lover, often identified as being a student, is lamenting the loss of his love, Lenore. Sitting on a bust of Pallas, the raven seems to further distress the protagonist with its constant repetition of the word ""Nevermore"". The poem makes use of folk, mythological, religious, and classical references.";4cf60975-300d-4dbe-8598-57b02e62bae2;333;13.13;USD;16 -7756b725-cefc-43a2-a3c8-0c9104a349b8;Eleonora;"""Eleonora"" is a short story by Edgar Allan Poe, first published in 1842 in Philadelphia in the literary annual The Gift. It is often regarded as somewhat autobiographical and has a relatively ""happy"" ending.";4cf60975-300d-4dbe-8598-57b02e62bae2;555;14;USD;16 -a009c640-434a-4542-ac68-51b400c880ea;Catweazle;Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts.;df9fb9fa-f121-45b5-8be5-8ff7ad5219a2;22;150;JPY;13 diff --git a/samples/bookshop-mtx/db/data/sap.capire.bookshop-Books_texts.csv b/samples/bookshop-mtx/db/data/sap.capire.bookshop-Books_texts.csv deleted file mode 100644 index 3a3465b28..000000000 --- a/samples/bookshop-mtx/db/data/sap.capire.bookshop-Books_texts.csv +++ /dev/null @@ -1,5 +0,0 @@ -ID_texts;ID;locale;title;descr -52eee553-266d-4fdd-a5ca-909910e76ae4;aeeda49f-72f2-4880-be27-a513b2e53040;de;Sturmhöhe;Sturmhöhe (Originaltitel: Wuthering Heights) ist der einzige Roman der englischen Schriftstellerin Emily Brontë (1818–1848). Der 1847 unter dem Pseudonym Ellis Bell veröffentlichte Roman wurde vom viktorianischen Publikum weitgehend abgelehnt, heute gilt er als ein Klassiker der britischen Romanliteratur des 19. Jahrhunderts. -54e58142-f06e-49c1-a51d-138f86cea34e;aeeda49f-72f2-4880-be27-a513b2e53040;fr;Les Hauts de Hurlevent;Les Hauts de Hurlevent (titre original : Wuthering Heights), parfois orthographié Les Hauts de Hurle-Vent, est l'unique roman d'Emily Brontë, publié pour la première fois en 1847 sous le pseudonyme d’Ellis Bell. Loin d'être un récit moralisateur, Emily Brontë achève néanmoins le roman dans une atmosphère sereine, suggérant le triomphe de la paix et du Bien sur la vengeance et le Mal. -bbbf8a88-797d-4790-af1c-1cc857718ee0;b0056977-4cf5-46a2-ab14-6409ee2e0df1;de;Jane Eyre;Jane Eyre. Eine Autobiographie (Originaltitel: Jane Eyre. An Autobiography), erstmals erschienen im Jahr 1847 unter dem Pseudonym Currer Bell, ist der erste veröffentlichte Roman der britischen Autorin Charlotte Brontë und ein Klassiker der viktorianischen Romanliteratur des 19. Jahrhunderts. Der Roman erzählt in Form einer Ich-Erzählung die Lebensgeschichte von Jane Eyre (ausgesprochen /ˌdʒeɪn ˈɛə/), die nach einer schweren Kindheit eine Stelle als Gouvernante annimmt und sich in ihren Arbeitgeber verliebt, jedoch immer wieder um ihre Freiheit und Selbstbestimmung kämpfen muss. Als klein, dünn, blass, stets schlicht dunkel gekleidet und mit strengem Mittelscheitel beschrieben, gilt die Heldin des Romans Jane Eyre nicht zuletzt aufgrund der Kino- und Fernsehversionen der melodramatischen Romanvorlage als die bekannteste englische Gouvernante der Literaturgeschichte -a90d4378-1a3e-48e7-b60b-5670e78807e1;7756b725-cefc-43a2-a3c8-0c9104a349b8;de;Eleonora;“Eleonora” ist eine Erzählung von Edgar Allan Poe. Sie wurde 1841 erstveröffentlicht. In ihr geht es um das Paradox der Treue in der Treulosigkeit. diff --git a/samples/bookshop-mtx/db/data/sap.capire.bookshop-Genres.csv b/samples/bookshop-mtx/db/data/sap.capire.bookshop-Genres.csv deleted file mode 100644 index 1ea3793bb..000000000 --- a/samples/bookshop-mtx/db/data/sap.capire.bookshop-Genres.csv +++ /dev/null @@ -1,16 +0,0 @@ -ID;parent_ID;name -10;;Fiction -11;10;Drama -12;10;Poetry -13;10;Fantasy -14;10;Science Fiction -15;10;Romance -16;10;Mystery -17;10;Thriller -18;10;Dystopia -19;10;Fairy Tale -20;;Non-Fiction -21;20;Biography -22;21;Autobiography -23;20;Essay -24;20;Speech diff --git a/samples/bookshop-mtx/db/schema.cds b/samples/bookshop-mtx/db/schema.cds deleted file mode 100644 index 1aedfbaca..000000000 --- a/samples/bookshop-mtx/db/schema.cds +++ /dev/null @@ -1,37 +0,0 @@ -using { - Currency, - managed, - cuid, - sap.common.CodeList -} from '@sap/cds/common'; - -namespace sap.capire.bookshop; - -entity Books : managed, cuid { - @mandatory title : localized String(111); - descr : localized String(1111); - @mandatory author : Association to Authors; - genre : Association to Genres; - stock : Integer; - price : Decimal; - currency : Currency; - image : LargeBinary @Core.MediaType: 'image/png'; -} - -entity Authors : managed, cuid { - @mandatory name : String(111); - dateOfBirth : Date; - dateOfDeath : Date; - placeOfBirth : String; - placeOfDeath : String; - books : Association to many Books - on books.author = $self; -} - -/** Hierarchically organized Code List for Genres */ -entity Genres : CodeList { - key ID : Integer; - parent : Association to Genres; - children : Composition of many Genres - on children.parent = $self; -} diff --git a/samples/bookshop-mtx/package.json b/samples/bookshop-mtx/package.json deleted file mode 100644 index 7fa281120..000000000 --- a/samples/bookshop-mtx/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "bookshop-cds", - "version": "1.0.0", - "description": "Generated by cds-services-archetype", - "license": "ISC", - "repository": "", - "devDependencies": { - "@sap/cds-dk": "^9.3.2" - }, - "dependencies": { - "@sap/cds-mtxs": "^3.8.1" - } -} diff --git a/samples/bookshop-mtx/pom.xml b/samples/bookshop-mtx/pom.xml deleted file mode 100644 index c09bda711..000000000 --- a/samples/bookshop-mtx/pom.xml +++ /dev/null @@ -1,192 +0,0 @@ - - - 4.0.0 - - customer - bookshop-parent - 1.0.0-SNAPSHOT - pom - - bookshop parent - - - - 17 - 4.6.1 - 3.5.7 - - UTF-8 - - - - srv - - - - - - - com.sap.cds - cds-services-bom - ${cds.services.version} - pom - import - - - - - org.springframework.boot - spring-boot-dependencies - ${spring.boot.version} - pom - import - - - - - com.sap.cds - cds-feature-attachments-oss - 1.4.0-SNAPSHOT - - - - - - - - - - com.sap.cds - cds-maven-plugin - ${cds.services.version} - - - cds.resolve - - resolve - - - - - - - - - - - maven-compiler-plugin - 3.14.1 - - ${jdk.version} - UTF-8 - - - - - - org.springframework.boot - spring-boot-maven-plugin - ${spring.boot.version} - - true - - - - - - maven-surefire-plugin - 3.5.4 - - - - - org.codehaus.mojo - flatten-maven-plugin - 1.7.3 - - true - resolveCiFriendliesOnly - - - - flatten - process-resources - - flatten - - - - flatten.clean - clean - - clean - - - - - - - - maven-enforcer-plugin - 3.6.2 - - - Project Structure Checks - - enforce - - - - - 3.6.3 - - - ${jdk.version} - - - - true - - - - - - - - org.jacoco - jacoco-maven-plugin - 0.8.14 - - - **/gen/** - **/generated/** - - - - - jacoco-initialize - - prepare-agent - - - - jacoco-report - test - - report - - - - - - - - org.sonarsource.scanner.maven - sonar-maven-plugin - 4.0.0.4121 - - - - diff --git a/samples/bookshop-mtx/sonar-project.properties b/samples/bookshop-mtx/sonar-project.properties deleted file mode 100644 index ea0101880..000000000 --- a/samples/bookshop-mtx/sonar-project.properties +++ /dev/null @@ -1,23 +0,0 @@ -# SonarQube configuration for CDS Feature Attachments - Bookshop Sample -sonar.projectKey=cds-feature-attachments-bookshop-sample -sonar.projectName=CDS Feature Attachments - Bookshop Sample -sonar.projectVersion=1.0.0-SNAPSHOT - -# Source and test directories -sonar.sources=srv/src/main/java -sonar.tests=srv/src/test/java - -# Java binaries -sonar.java.binaries=srv/target/classes - -# Code coverage -sonar.coverage.jacoco.xmlReportPaths=srv/target/site/jacoco/jacoco.xml - -# Exclusions -sonar.exclusions=**/gen/**,**/generated/**,**/target/** - -# Java version -sonar.java.source=17 - -# Quality gate -sonar.qualitygate.wait=true diff --git a/samples/bookshop-mtx/srv/admin-service.cds b/samples/bookshop-mtx/srv/admin-service.cds deleted file mode 100644 index 9ae8bbc10..000000000 --- a/samples/bookshop-mtx/srv/admin-service.cds +++ /dev/null @@ -1,6 +0,0 @@ -using {sap.capire.bookshop as my} from '../db/schema'; - -service AdminService @(requires: 'admin') { - entity Books as projection on my.Books; - entity Authors as projection on my.Authors; -} diff --git a/samples/bookshop-mtx/srv/attachments.cds b/samples/bookshop-mtx/srv/attachments.cds deleted file mode 100644 index 8603e44ba..000000000 --- a/samples/bookshop-mtx/srv/attachments.cds +++ /dev/null @@ -1,49 +0,0 @@ -using {sap.capire.bookshop as my} from '../db/schema'; -using {sap.attachments.Attachments} from 'com.sap.cds/cds-feature-attachments'; - -// Extend Books entity to support file attachments (images, PDFs, documents) -// Each book can have multiple attachments via composition relationship -extend my.Books with { - attachments : Composition of many Attachments; - @UI.Hidden - sizeLimitedAttachments : Composition of many Attachments; - @UI.Hidden - mediaValidatedAttachments : Composition of many Attachments; -} - -annotate my.Books.sizeLimitedAttachments with { - content @Validation.Maximum: '5MB'; -} - -// Media type validation for attachments -annotate my.Books.mediaValidatedAttachments with { - content @Core.AcceptableMediaTypes: [ - 'image/jpeg', - 'image/png' - ]; -} - -// Add UI component for attachments table to the Browse Books App -using {CatalogService as service} from '../app/services'; - -annotate service.Books with @(UI.Facets: [{ - $Type : 'UI.ReferenceFacet', - ID : 'AttachmentsFacet', - Label : '{i18n>attachments}', - Target: 'attachments/@UI.LineItem' -}]); - -// Adding the UI Component (a table) to the Administrator App -using {AdminService as adminService} from '../app/services'; - -annotate adminService.Books with @(UI.Facets: [{ - $Type : 'UI.ReferenceFacet', - ID : 'AttachmentsFacet', - Label : '{i18n>attachments}', - Target: 'attachments/@UI.LineItem' -}]); - - -service nonDraft { - entity Books as projection on my.Books; -} diff --git a/samples/bookshop-mtx/srv/cat-service.cds b/samples/bookshop-mtx/srv/cat-service.cds deleted file mode 100644 index 1d2cbbab8..000000000 --- a/samples/bookshop-mtx/srv/cat-service.cds +++ /dev/null @@ -1,34 +0,0 @@ -using {sap.capire.bookshop as my} from '../db/schema'; - -service CatalogService { - - /** For displaying lists of Books */ - @readonly - entity ListOfBooks as - projection on Books - excluding { - descr - }; - - /** For display in details pages */ - @readonly - entity Books as - projection on my.Books { - *, - author.name as author - } - excluding { - createdBy, - modifiedBy - }; - - action submitOrder(book : Books:ID, quantity : Integer) returns { - stock : Integer - }; - - event OrderedBook : { - book : Books:ID; - quantity : Integer; - buyer : String - }; -} diff --git a/samples/bookshop-mtx/srv/pom.xml b/samples/bookshop-mtx/srv/pom.xml deleted file mode 100644 index d29ab05fb..000000000 --- a/samples/bookshop-mtx/srv/pom.xml +++ /dev/null @@ -1,166 +0,0 @@ - - 4.0.0 - - - bookshop-parent - customer - 1.0.0-SNAPSHOT - - - bookshop - jar - - bookshop - - - - - - com.sap.cds - cds-starter-spring-boot - - - - org.springframework.boot - spring-boot-devtools - true - - - - org.springframework.boot - spring-boot-starter-test - test - - - - com.sap.cds - cds-adapter-odata-v4 - runtime - - - - com.h2database - h2 - runtime - - - - org.xerial - sqlite-jdbc - runtime - - - - com.sap.cds - cds-starter-cloudfoundry - runtime - - - - org.springframework.boot - spring-boot-starter-security - - - - - com.sap.cds - cds-feature-attachments-oss - - - - - com.sap.cds - cds-feature-mt - runtime - - - - org.springframework.boot - spring-boot-starter-actuator - - - - - - ${project.artifactId} - - - - org.springframework.boot - spring-boot-maven-plugin - ${spring.boot.version} - - false - - - - repackage - - repackage - - - exec - - - - - - - - com.sap.cds - cds-maven-plugin - - - cds.clean - - clean - - - - - cds.install-node - - install-node - - - - - cds.resolve - - resolve - - - - - cds.build - - cds - - - - build --for java - deploy --to h2 --with-mocks --dry --out - "${project.basedir}/src/main/resources/schema-h2.sql" - - - - - - cds.generate - - generate - - - cds.gen - true - true - true - true - - - - - - - - \ No newline at end of file diff --git a/samples/bookshop-mtx/srv/src/main/java/customer/bookshop/Application.java b/samples/bookshop-mtx/srv/src/main/java/customer/bookshop/Application.java deleted file mode 100644 index f395d210b..000000000 --- a/samples/bookshop-mtx/srv/src/main/java/customer/bookshop/Application.java +++ /dev/null @@ -1,13 +0,0 @@ -package customer.bookshop; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class Application { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - -} diff --git a/samples/bookshop-mtx/srv/src/main/java/customer/bookshop/handlers/CatalogServiceHandler.java b/samples/bookshop-mtx/srv/src/main/java/customer/bookshop/handlers/CatalogServiceHandler.java deleted file mode 100644 index c480672e3..000000000 --- a/samples/bookshop-mtx/srv/src/main/java/customer/bookshop/handlers/CatalogServiceHandler.java +++ /dev/null @@ -1,63 +0,0 @@ -package customer.bookshop.handlers; - -import java.util.stream.Stream; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import com.sap.cds.ql.Select; -import com.sap.cds.ql.Update; -import com.sap.cds.services.cds.CqnService; -import com.sap.cds.services.handler.EventHandler; -import com.sap.cds.services.handler.annotations.After; -import com.sap.cds.services.handler.annotations.On; -import com.sap.cds.services.handler.annotations.ServiceName; -import com.sap.cds.services.persistence.PersistenceService; - -import cds.gen.catalogservice.Books; -import cds.gen.catalogservice.Books_; -import cds.gen.catalogservice.CatalogService_; -import cds.gen.catalogservice.OrderedBook; -import cds.gen.catalogservice.OrderedBookContext; -import cds.gen.catalogservice.SubmitOrderContext; -import cds.gen.catalogservice.SubmitOrderContext.ReturnType; - -@Component -@ServiceName(CatalogService_.CDS_NAME) -public class CatalogServiceHandler implements EventHandler { - - @Autowired - private PersistenceService db; - - @On - public void submitOrder(SubmitOrderContext context) { - // decrease and update stock in database - db.run(Update.entity(Books_.class).byId(context.getBook()).set(b -> b.stock(), s -> s.minus(context.getQuantity()))); - - // read new stock from database - Books book = db.run(Select.from(Books_.class).where(b -> b.ID().eq(context.getBook()))).single(Books.class); - - // return new stock to client - ReturnType result = SubmitOrderContext.ReturnType.create(); - result.setStock(book.getStock()); - - OrderedBook orderedBook = OrderedBook.create(); - orderedBook.setBook(book.getId()); - orderedBook.setQuantity(context.getQuantity()); - orderedBook.setBuyer(context.getUserInfo().getName()); - - OrderedBookContext orderedBookEvent = OrderedBookContext.create(); - orderedBookEvent.setData(orderedBook); - context.getService().emit(orderedBookEvent); - - context.setResult(result); - } - - @After(event = CqnService.EVENT_READ) - public void discountBooks(Stream books) { - books.filter(b -> b.getTitle() != null && b.getStock() != null) - .filter(b -> b.getStock() > 200) - .forEach(b -> b.setTitle(b.getTitle() + " (discounted)")); - } - -} \ No newline at end of file diff --git a/samples/bookshop-mtx/srv/src/main/java/customer/bookshop/handlers/RestHandler.java b/samples/bookshop-mtx/srv/src/main/java/customer/bookshop/handlers/RestHandler.java deleted file mode 100644 index d63ea146f..000000000 --- a/samples/bookshop-mtx/srv/src/main/java/customer/bookshop/handlers/RestHandler.java +++ /dev/null @@ -1,330 +0,0 @@ -package customer.bookshop.handlers; - -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.InputStreamResource; -import org.springframework.core.io.Resource; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; - -import com.sap.cds.ql.Delete; -import com.sap.cds.ql.Insert; -import com.sap.cds.ql.Select; -import com.sap.cds.ql.Update; -import com.sap.cds.services.cds.CqnService; -import com.sap.cds.services.persistence.PersistenceService; - -import cds.gen.catalogservice.BooksAttachments; -import cds.gen.catalogservice.BooksAttachments_; -import cds.gen.catalogservice.Books_; -import cds.gen.catalogservice.CatalogService_; - -/** - * REST Controller for managing book attachments - * Provides RESTful endpoints alongside the existing OData service - */ -@RestController -@RequestMapping("/api/v1/books/{bookId}/attachments") -public class RestHandler { - - @Autowired - private PersistenceService persistenceService; - - /** - * Get all attachments for a specific book - * GET /api/books/{bookId}/attachments - */ - @GetMapping - public ResponseEntity>> getAttachments(@PathVariable String bookId) { - try { - List attachments = persistenceService.run( - Select.from(BooksAttachments_.class) - .where(a -> a.up__ID().eq(bookId)) - ).listOf(BooksAttachments.class); - - List> response = attachments.stream().map(attachment -> { - Map map = new HashMap<>(); - map.put("id", attachment.getId()); - map.put("fileName", attachment.getFileName()); - map.put("mimeType", attachment.getMimeType()); - map.put("status", attachment.getStatus()); - map.put("note", attachment.getNote()); - return map; - }).toList(); - - return ResponseEntity.ok(response); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - } - - /** - * Get attachment metadata by ID - * GET /api/books/{bookId}/attachments/{attachmentId} - */ - @GetMapping(path = "/{attachmentsId}") - public ResponseEntity> getAttachment( - @PathVariable String bookId, - @PathVariable String attachmentsId) { - try { - Optional attachment = persistenceService.run( - Select.from(BooksAttachments_.class) - .where(a -> a.ID().eq(attachmentsId) - .and(a.up__ID().eq(bookId))) - ).first(BooksAttachments.class); - - return attachment.map(a -> { - Map map = new HashMap<>(); - map.put("id", a.getId()); - map.put("fileName", a.getFileName()); - map.put("mimeType", a.getMimeType()); - map.put("status", a.getStatus()); - map.put("note", a.getNote()); - return ResponseEntity.ok(map); - }).orElse(ResponseEntity.notFound().build()); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - } - - /** - * Download attachment content - * GET /api/books/{bookId}/attachments/{attachmentId}/content - */ - @GetMapping("/{attachmentsId}/content") - public ResponseEntity downloadAttachment( - @PathVariable String bookId, - @PathVariable String attachmentsId) { - try { - Optional attachmentOpt = persistenceService.run( - Select.from(BooksAttachments_.class) - .where(a -> a.ID().eq(attachmentsId) - .and(a.up__ID().eq(bookId))) - ).first(BooksAttachments.class); - - if (attachmentOpt.isEmpty()) { - return ResponseEntity.notFound().build(); - } - - BooksAttachments attachment = attachmentOpt.get(); - - // Check if attachment is clean (security check) - if (!"Clean".equals(attachment.getStatus())) { - return ResponseEntity.status(HttpStatus.FORBIDDEN) - .header("X-Error-Reason", "Attachment not scanned or infected") - .build(); - } - - InputStream content = attachment.getContent(); - if (content == null) { - return ResponseEntity.notFound().build(); - } - - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.CONTENT_DISPOSITION, - "attachment; filename=\"" + attachment.getFileName() + "\""); - - if (attachment.getMimeType() != null) { - headers.add(HttpHeaders.CONTENT_TYPE, attachment.getMimeType()); - } else { - headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE); - } - - return ResponseEntity.ok() - .headers(headers) - .body(new InputStreamResource(content)); - - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - } - - /** - * Upload a new attachment - * POST /api/books/{bookId}/attachments - */ - @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity> uploadAttachment( - @PathVariable String bookId, - @RequestParam("file") MultipartFile file, - @RequestParam(value = "note", required = false) String note) { - try { - // Validate book exists - boolean bookExists = persistenceService.run( - Select.from(Books_.class).where(b -> b.ID().eq(bookId)) - ).first().isPresent(); - - if (!bookExists) { - return ResponseEntity.badRequest() - .body(Map.of("error", "Book not found")); - } - - // Create attachment entity - BooksAttachments attachment = BooksAttachments.create(); - attachment.setUpId(bookId); - attachment.setFileName(file.getOriginalFilename()); - attachment.setMimeType(file.getContentType()); - attachment.setContent(file.getInputStream()); - attachment.setStatus("Clean"); // Will be updated by malware scanner - - if (note != null && !note.trim().isEmpty()) { - attachment.setNote(note); - } - - // Insert attachment - var result = persistenceService.run( - Insert.into(BooksAttachments_.class).entry(attachment) - ); - - // Get the created attachment with generated ID - List createdAttachments = result.listOf(BooksAttachments.class); - if (!createdAttachments.isEmpty()) { - BooksAttachments created = createdAttachments.get(0); - - Map response = new HashMap<>(); - response.put("id", created.getId()); - response.put("fileName", created.getFileName()); - response.put("mimeType", created.getMimeType()); - response.put("status", created.getStatus()); - response.put("note", created.getNote()); - - return ResponseEntity.status(HttpStatus.CREATED).body(response); - } - - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(Map.of("error", "Failed to create attachment")); - - } catch (IOException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(Map.of("error", "Failed to read uploaded file: " + e.getMessage())); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(Map.of("error", "Failed to upload attachment: " + e.getMessage())); - } - } - - /** - * Update attachment metadata - * PATCH /api/books/{bookId}/attachments/{attachmentId} - */ - @PatchMapping("/{attachmentsId}") - public ResponseEntity> updateAttachment( - @PathVariable String bookId, - @PathVariable String attachmentsId, - @RequestBody Map updates) { - try { - // Verify attachment exists and belongs to the book - Optional existingOpt = persistenceService.run( - Select.from(BooksAttachments_.class) - .where(a -> a.ID().eq(attachmentsId) - .and(a.up__ID().eq(bookId))) - ).first(BooksAttachments.class); - - if (existingOpt.isEmpty()) { - return ResponseEntity.notFound().build(); - } - - // Build update statement - create the attachment to update - BooksAttachments updateData = BooksAttachments.create(); - updateData.setId(attachmentsId); - - // Only allow updating certain fields - if (updates.containsKey("fileName")) { - updateData.setFileName((String) updates.get("fileName")); - } - if (updates.containsKey("note")) { - updateData.setNote((String) updates.get("note")); - } - - Update updateStmt = Update.entity(BooksAttachments_.class).entry(updateData); - - // Execute update - persistenceService.run(updateStmt); - - // Return updated attachment - BooksAttachments updated = persistenceService.run( - Select.from(BooksAttachments_.class) - .where(a -> a.ID().eq(attachmentsId)) - ).single(BooksAttachments.class); - - Map response = new HashMap<>(); - response.put("id", updated.getId()); - response.put("fileName", updated.getFileName()); - response.put("mimeType", updated.getMimeType()); - response.put("status", updated.getStatus()); - response.put("note", updated.getNote()); - - return ResponseEntity.ok(response); - - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(Map.of("error", "Failed to update attachment: " + e.getMessage())); - } - } - - /** - * Delete an attachment - * DELETE /api/books/{bookId}/attachments/{attachmentId} - */ - @DeleteMapping("/{attachmentsId}") - public ResponseEntity> deleteAttachment( - @PathVariable String bookId, - @PathVariable String attachmentsId) { - try { - // Verify attachment exists and belongs to the book - Optional existingOpt = persistenceService.run( - Select.from(BooksAttachments_.class) - .where(a -> a.ID().eq(attachmentsId) - .and(a.up__ID().eq(bookId))) - ).first(BooksAttachments.class); - - if (existingOpt.isEmpty()) { - return ResponseEntity.notFound().build(); - } - - // Delete the attachment - persistenceService.run( - Delete.from(BooksAttachments_.class) - .where(a -> a.ID().eq(attachmentsId)) - ); - - return ResponseEntity.ok(Map.of("message", "Attachment deleted successfully")); - - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(Map.of("error", "Failed to delete attachment: " + e.getMessage())); - } - } - - /** - * Health check endpoint - * GET /api/books/{bookId}/attachments/health - */ - @GetMapping("/health") - public ResponseEntity> healthCheck(@PathVariable String bookId) { - Map health = new HashMap<>(); - health.put("status", "UP"); - health.put("service", "Bookshop Attachments REST API"); - health.put("bookId", bookId); - health.put("timestamp", System.currentTimeMillis()); - return ResponseEntity.ok(health); - } -} diff --git a/samples/bookshop-mtx/srv/src/main/resources/application.yaml b/samples/bookshop-mtx/srv/src/main/resources/application.yaml deleted file mode 100644 index f655cc3d8..000000000 --- a/samples/bookshop-mtx/srv/src/main/resources/application.yaml +++ /dev/null @@ -1,63 +0,0 @@ - ---- -spring: - config: - activate: - on-profile: default - sql: - init: - platform: h2 -cds: - security: - mock: - users: - admin: - password: admin - roles: - - admin - user: - password: user - data-source: - auto-config: - enabled: false ---- -spring: - config: - activate: - on-profile: local-mtxs -cds: - multi-tenancy: - mtxs: - enabled: true - sidecar: - url: http://localhost:4005 - security: - mock: - users: - admin: - password: admin - tenant: t1 - roles: - - admin - user: - password: user - tenant: t1 - erin: - password: erin - tenant: t2 - roles: - - admin ---- -management: - endpoint: - health: - show-components: always - probes.enabled: true - endpoints: - web: - exposure: - include: health - health: - defaults.enabled: false - ping.enabled: true - db.enabled: true diff --git a/samples/bookshop-mtx/srv/src/test/java/customer/bookshop/ApplicationTest.java b/samples/bookshop-mtx/srv/src/test/java/customer/bookshop/ApplicationTest.java deleted file mode 100644 index 152543a5b..000000000 --- a/samples/bookshop-mtx/srv/src/test/java/customer/bookshop/ApplicationTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package customer.bookshop; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.ApplicationContext; - -@SpringBootTest -@AutoConfigureMockMvc -class ApplicationTest { - - @Autowired private ApplicationContext context; - - @Test - void checkApplicationContextCanBeLoaded() { - assertThat(context).isNotNull(); - } -} \ No newline at end of file diff --git a/samples/bookshop-mtx/srv/src/test/java/customer/bookshop/handlers/CatalogServiceHandlerTest.java b/samples/bookshop-mtx/srv/src/test/java/customer/bookshop/handlers/CatalogServiceHandlerTest.java deleted file mode 100644 index e008b5ad6..000000000 --- a/samples/bookshop-mtx/srv/src/test/java/customer/bookshop/handlers/CatalogServiceHandlerTest.java +++ /dev/null @@ -1,159 +0,0 @@ -package customer.bookshop.handlers; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.stream.Stream; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import com.sap.cds.Result; -import com.sap.cds.ql.Select; -import com.sap.cds.ql.Update; -import com.sap.cds.services.persistence.PersistenceService; -import com.sap.cds.services.request.UserInfo; - -import cds.gen.catalogservice.Books; -import cds.gen.catalogservice.CatalogService; -import cds.gen.catalogservice.OrderedBookContext; -import cds.gen.catalogservice.SubmitOrderContext; - -class CatalogServiceHandlerTest { - - @Mock - private PersistenceService db; - - @InjectMocks - private CatalogServiceHandler handler; - - private Books book = Books.create(); - - @BeforeEach - public void setUp() { - MockitoAnnotations.openMocks(this); - book.setTitle("title"); - } - - @Test - void testDiscount() { - book.setStock(500); - handler.discountBooks(Stream.of(book)); - assertEquals("title (discounted)", book.getTitle()); - } - - @Test - void testNoDiscount() { - book.setStock(100); - handler.discountBooks(Stream.of(book)); - assertEquals("title", book.getTitle()); - } - - @Test - void testNoStockAvailable() { - handler.discountBooks(Stream.of(book)); - assertEquals("title", book.getTitle()); - } - - @Test - void testDiscountWithNullTitle() { - book.setTitle(null); - book.setStock(500); - handler.discountBooks(Stream.of(book)); - // Should not throw exception and title should remain null - assertEquals(null, book.getTitle()); - } - - @Test - void testDiscountWithNullStock() { - book.setTitle("test"); - book.setStock(null); - handler.discountBooks(Stream.of(book)); - // Should not throw exception and title should remain unchanged - assertEquals("test", book.getTitle()); - } - - @Test - void testSubmitOrder() { - // Setup - String bookId = "aeeda49f-72f2-4880-be27-a513b2e53040"; - Integer quantity = 2; - Integer expectedNewStock = 9; - String userName = "testuser"; - - // Mock the context - SubmitOrderContext context = mock(SubmitOrderContext.class); - UserInfo userInfo = mock(UserInfo.class); - - // Import the generated service interface - CatalogService catalogService = mock(CatalogService.class); - - when(context.getBook()).thenReturn(bookId); - when(context.getQuantity()).thenReturn(quantity); - when(context.getUserInfo()).thenReturn(userInfo); - when(context.getService()).thenReturn(catalogService); - when(userInfo.getName()).thenReturn(userName); - - // Mock the book result after update - Books updatedBook = Books.create(); - updatedBook.setId(bookId); - updatedBook.setStock(expectedNewStock); - - Result mockResult = mock(Result.class); - when(mockResult.single(Books.class)).thenReturn(updatedBook); - when(db.run(any(Select.class))).thenReturn(mockResult); - - // Execute - handler.submitOrder(context); - - // Verify database operations - verify(db).run(any(Update.class)); - verify(db).run(any(Select.class)); - verify(context).setResult(any(SubmitOrderContext.ReturnType.class)); - verify(catalogService).emit(any(OrderedBookContext.class)); - } - - @Test - void testSubmitOrderWithZeroQuantity() { - // Setup - String bookId = "book-123"; - Integer quantity = 0; - Integer currentStock = 100; - String userName = "testuser"; - - // Mock the context - SubmitOrderContext context = mock(SubmitOrderContext.class); - UserInfo userInfo = mock(UserInfo.class); - - CatalogService catalogService = mock(CatalogService.class); - - when(context.getBook()).thenReturn(bookId); - when(context.getQuantity()).thenReturn(quantity); - when(context.getUserInfo()).thenReturn(userInfo); - when(context.getService()).thenReturn(catalogService); - when(userInfo.getName()).thenReturn(userName); - - // Mock the book result (stock should remain the same) - Books updatedBook = Books.create(); - updatedBook.setId(bookId); - updatedBook.setStock(currentStock); - - Result mockResult = mock(Result.class); - when(mockResult.single(Books.class)).thenReturn(updatedBook); - when(db.run(any(Select.class))).thenReturn(mockResult); - - // Execute - handler.submitOrder(context); - - // Verify database operations still happen - verify(db).run(any(Update.class)); - verify(db).run(any(Select.class)); - verify(context).setResult(any(SubmitOrderContext.ReturnType.class)); - } -} diff --git a/samples/bookshop/.cdsrc.json b/samples/bookshop/.cdsrc.json index 2c63c0851..d4facf2b9 100644 --- a/samples/bookshop/.cdsrc.json +++ b/samples/bookshop/.cdsrc.json @@ -1,2 +1,18 @@ { + "profiles": [ + "java" + ], + "requires": { + "[production]": { + "multitenancy": true, + "extensibility": true, + "toggles": true, + "auth": "xsuaa" + }, + "[with-mtx]": { + "multitenancy": true, + "extensibility": true, + "toggles": true + } + } } diff --git a/samples/bookshop/.gitignore b/samples/bookshop/.gitignore index 2ecc68df6..47a678e3f 100644 --- a/samples/bookshop/.gitignore +++ b/samples/bookshop/.gitignore @@ -30,5 +30,11 @@ hs_err* .idea .reloadtrigger +Makefile_*.mta +app/portal/ +resources/*.zip +undeploy.json +**/dist/ + # added by cds .cdsrc-private.json diff --git a/samples/bookshop/README.md b/samples/bookshop/README.md index babcff672..7fe10868d 100644 --- a/samples/bookshop/README.md +++ b/samples/bookshop/README.md @@ -120,6 +120,35 @@ This sample uses the default in-memory storage, which stores attachments directl For advanced topics like object store integration, malware scanning, and security configuration, see the [main project documentation](../../README.md). +## Multi-Tenancy + +This sample also supports multi-tenant mode via profiles. + +### Local MTX + +Run the MTX sidecar and the Java app in separate terminals: + +```bash +# Terminal 1: Start the sidecar +cd mtx/sidecar +npm install +cds watch + +# Terminal 2: Start the Java app with MTX profiles +cds watch --profile with-mtx-sidecar +# or: mvn spring-boot:run -Dspring-boot.run.profiles=local-mtxs +``` + +Mock users with tenant assignments: `admin`/`admin` (tenant t1), `erin`/`erin` (tenant t2). + +### Cloud Foundry Deployment + +```bash +cds add xsuaa,attachments # if not already done +cds build --production +cf deploy gen/mta.tar +``` + ## Troubleshooting - **Port conflicts**: If port 8080 is in use, specify a different port: `mvn spring-boot:run -Dspring-boot.run.arguments="--server.port=8081"` diff --git a/samples/bookshop-mtx/app/admin-books/package.json b/samples/bookshop/app/admin-books/package.json similarity index 100% rename from samples/bookshop-mtx/app/admin-books/package.json rename to samples/bookshop/app/admin-books/package.json diff --git a/samples/bookshop-mtx/app/admin-books/ui5.yaml b/samples/bookshop/app/admin-books/ui5.yaml similarity index 100% rename from samples/bookshop-mtx/app/admin-books/ui5.yaml rename to samples/bookshop/app/admin-books/ui5.yaml diff --git a/samples/bookshop-mtx/app/admin-books/xs-app.json b/samples/bookshop/app/admin-books/xs-app.json similarity index 100% rename from samples/bookshop-mtx/app/admin-books/xs-app.json rename to samples/bookshop/app/admin-books/xs-app.json diff --git a/samples/bookshop-mtx/app/browse/package.json b/samples/bookshop/app/browse/package.json similarity index 100% rename from samples/bookshop-mtx/app/browse/package.json rename to samples/bookshop/app/browse/package.json diff --git a/samples/bookshop-mtx/app/browse/ui5.yaml b/samples/bookshop/app/browse/ui5.yaml similarity index 100% rename from samples/bookshop-mtx/app/browse/ui5.yaml rename to samples/bookshop/app/browse/ui5.yaml diff --git a/samples/bookshop-mtx/app/browse/xs-app.json b/samples/bookshop/app/browse/xs-app.json similarity index 100% rename from samples/bookshop-mtx/app/browse/xs-app.json rename to samples/bookshop/app/browse/xs-app.json diff --git a/samples/bookshop-mtx/app/router/package.json b/samples/bookshop/app/router/package.json similarity index 100% rename from samples/bookshop-mtx/app/router/package.json rename to samples/bookshop/app/router/package.json diff --git a/samples/bookshop-mtx/app/router/xs-app.json b/samples/bookshop/app/router/xs-app.json similarity index 100% rename from samples/bookshop-mtx/app/router/xs-app.json rename to samples/bookshop/app/router/xs-app.json diff --git a/samples/bookshop-mtx/db/package.json b/samples/bookshop/db/package.json similarity index 100% rename from samples/bookshop-mtx/db/package.json rename to samples/bookshop/db/package.json diff --git a/samples/bookshop-mtx/mta.yaml b/samples/bookshop/mta.yaml similarity index 100% rename from samples/bookshop-mtx/mta.yaml rename to samples/bookshop/mta.yaml diff --git a/samples/bookshop-mtx/mtx/sidecar/package.json b/samples/bookshop/mtx/sidecar/package.json similarity index 100% rename from samples/bookshop-mtx/mtx/sidecar/package.json rename to samples/bookshop/mtx/sidecar/package.json diff --git a/samples/bookshop/package-lock.json b/samples/bookshop/package-lock.json index 2527e0311..8cdc31e8d 100644 --- a/samples/bookshop/package-lock.json +++ b/samples/bookshop/package-lock.json @@ -8,14 +8,80 @@ "name": "bookshop-cds", "version": "1.0.0", "license": "ISC", + "dependencies": { + "@sap/cds-mtxs": "^3.8.1" + }, "devDependencies": { "@sap/cds-dk": "^9.3.2" } }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "license": "MIT", + "peer": true, + "engines": { + "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": { + "version": "9.8.4", + "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-9.8.4.tgz", + "integrity": "sha512-1cnFdz4Ex4LV15dztDsSLDiilVdJ2mfTqr5A3TGsRd/6cM31SnHhuZ/Ra0FEErodokNS2pwasGZmSfgHVDW8KA==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@sap/cds-compiler": "^6.4", + "@sap/cds-fiori": "^2", + "express": "^4.22.1 || ^5", + "js-yaml": "^4.1.1" + }, + "bin": { + "cds-deploy": "bin/deploy.js", + "cds-serve": "bin/serve.js" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@eslint/js": "^9 || ^10", + "tar": "^7.5.6" + }, + "peerDependenciesMeta": { + "tar": { + "optional": true + } + } + }, + "node_modules/@sap/cds-compiler": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-6.8.0.tgz", + "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", + "license": "SEE LICENSE IN LICENSE", + "bin": { + "cdsc": "bin/cdsc.js", + "cdshi": "bin/cdshi.js", + "cdsse": "bin/cdsse.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@sap/cds-dk": { - "version": "9.4.3", - "resolved": "https://registry.npmjs.org/@sap/cds-dk/-/cds-dk-9.4.3.tgz", - "integrity": "sha512-kVz08dhBF7Zms1disoXUoEIrR/ctJkZd7gky1I/sww4fwl832elW6ZxTs85HLn+LeF0Gr3/HX+jJoqRy+3GYNg==", + "version": "9.8.2", + "resolved": "https://registry.npmjs.org/@sap/cds-dk/-/cds-dk-9.8.2.tgz", + "integrity": "sha512-PGynoQpOCD8Nsr0EEzsEHWg8szYBLVtVd6byxvaaAPYBkNAj2WA+kin6gWaDTiyG3kJa0BSGPk4PCbA2xDoE9A==", "dev": true, "hasShrinkwrap": true, "license": "SEE LICENSE IN LICENSE", @@ -26,11 +92,10 @@ "@sap/cds-mtxs": ">=2", "@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" @@ -54,8 +119,8 @@ } }, "node_modules/@sap/cds-dk/node_modules/@cap-js/db-service": { - "version": "2.6.0", - "integrity": "sha512-t72/FcAYFbPdx+5iV+lVKcwF2MLOx8II3jJdlC1dX/KXQORoS3wDFwWbakP0f/eharE5hfa7KMFJqrSMtDigbQ==", + "version": "2.9.0", + "integrity": "sha512-WCXhoqezaF6A5I2l0MNZeHKXXtHRNEq7Rp0R89/uccOHQIx0DuU0U9NuJJPV/1G5RGk2QKQ9VBo/KYn+MZuuNQ==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -63,12 +128,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": { @@ -79,41 +144,56 @@ } }, "node_modules/@sap/cds-dk/node_modules/@cap-js/sqlite": { - "version": "2.0.4", - "integrity": "sha512-QPVkycLJG6EubtjrPeiK4dTI1zPH/nabvhiYnTeg2AbeQ8mbazm5pjmcLrzOOKF/5bGS8KQo2J+49fU5LPRR3A==", + "version": "2.2.0", + "integrity": "sha512-FPj+uVU/14vtGUl2P/Q8y7XhZbsLgrCav2O5PjHPXnupegjby4sMJkgVNxVHnkyKPFgO/W8uEsq9r5TU9VPx8w==", "dev": true, "license": "Apache-2.0", "optional": true, "dependencies": { - "@cap-js/db-service": "^2.6.0", + "@cap-js/db-service": "^2.9.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==", + "version": "10.0.1", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", "dev": 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.8.3", + "integrity": "sha512-up/kNVCZK8bOTyye1QN2BjMpH+IgcdyhT3U9mS3TqkpmjNE2JXq8Qz1iNAN+nYcXRWJ1XAUNpgebLfuh/49MGQ==", "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", + "js-yaml": "^4.1.1" }, "bin": { "cds-deploy": "bin/deploy.js", @@ -123,22 +203,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.6", - "integrity": "sha512-auAjRh9t0KKj4LiGAr/fxikZRIngx9YXVHTJWf0LeaGv0ZpYOi6iWbSnU1XRB2e6hsf+Ou1w5oTOHooC5sZfog==", + "version": "6.8.0", + "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", "dev": true, "license": "SEE LICENSE IN LICENSE", "bin": { @@ -151,18 +227,17 @@ } }, "node_modules/@sap/cds-dk/node_modules/@sap/cds-fiori": { - "version": "2.1.1", - "integrity": "sha512-X+4v4LBAT8HIt0zr28/kJNS15nlNlcM97vAMW+agLrmK134nyBiMwUMcp8BMhxlG9B2PykrnAKH56D9O3tfoBg==", + "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.3", - "integrity": "sha512-vgABFr7huaKWGx2fWHeGom5bVgsQKD7/gqkC7aQ/7yC9hdZdrx0mz4iZ0ASHUZ5PZWp2FWLD+eaJ9sXKUGHgpA==", + "version": "3.8.1", + "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", "dev": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { @@ -201,13 +276,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", @@ -230,27 +305,27 @@ } }, "node_modules/@sap/cds-dk/node_modules/@sap/xsenv": { - "version": "5.6.1", - "integrity": "sha512-4pDpsYLNJsLUBWtTSG+TJ8ul5iY0dWDyJgTy2H/WZGZww9CSPLP/39x+syDDTjkggsmZAlo9t7y9TiXMmtAunw==", + "version": "6.1.0", + "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", "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" @@ -262,12 +337,6 @@ "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==", @@ -290,13 +359,13 @@ "license": "MIT" }, "node_modules/@sap/cds-dk/node_modules/axios": { - "version": "1.13.1", - "integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==", + "version": "1.13.6", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "dev": true, "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -322,8 +391,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.8.0", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -333,7 +402,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": { @@ -359,44 +428,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==", @@ -501,15 +555,16 @@ } }, "node_modules/@sap/cds-dk/node_modules/content-disposition": { - "version": "0.5.4", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "version": "1.0.1", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", "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": { @@ -522,8 +577,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": { @@ -531,10 +586,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", @@ -543,8 +601,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": { @@ -603,16 +661,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==", @@ -745,66 +793,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==", @@ -834,38 +864,26 @@ } }, "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": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "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==", @@ -887,8 +905,8 @@ } }, "node_modules/@sap/cds-dk/node_modules/form-data": { - "version": "4.0.4", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, "license": "MIT", "dependencies": { @@ -902,6 +920,27 @@ "node": ">= 6" } }, + "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@sap/cds-dk/node_modules/forwarded": { "version": "0.2.0", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", @@ -912,12 +951,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": { @@ -1063,43 +1102,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": ">= 18" + }, + "optionalDependencies": { + "lz4-wasm-nodejs": "0.9.2" + } + }, + "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.0.0" }, "engines": { - "node": ">= 0.12" + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/@sap/cds-dk/node_modules/http-errors": { - "version": "2.0.0", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "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.1", - "toidentifier": "1.0.1" + "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.4.24", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.7.2", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "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/ieee754": { @@ -1154,9 +1220,15 @@ "node": ">=0.12.0" } }, + "node_modules/@sap/cds-dk/node_modules/is-promise": { + "version": "4.0.0", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@sap/cds-dk/node_modules/js-yaml": { - "version": "4.1.0", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -1172,6 +1244,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==", @@ -1182,32 +1261,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==", @@ -1222,8 +1295,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": { @@ -1233,21 +1306,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": { @@ -1255,15 +1316,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": { @@ -1318,8 +1383,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": { @@ -1333,8 +1398,8 @@ "license": "MIT" }, "node_modules/@sap/cds-dk/node_modules/node-abi": { - "version": "3.79.0", - "integrity": "sha512-Pr/5KdBQGG8TirdkS0qN3B+f3eo8zTOfZQWAxHoJqopMz2/uvRnG+S4fWu/6AZxKei2CP2p/psdQ5HFC2Ap5BA==", + "version": "3.89.0", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", "dev": true, "license": "MIT", "optional": true, @@ -1357,15 +1422,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==", @@ -1395,7 +1451,6 @@ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", - "optional": true, "dependencies": { "wrappy": "1" } @@ -1410,10 +1465,14 @@ } }, "node_modules/@sap/cds-dk/node_modules/path-to-regexp": { - "version": "0.1.12", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "8.3.0", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "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", @@ -1427,6 +1486,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, @@ -1471,8 +1531,8 @@ "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, @@ -1482,12 +1542,12 @@ } }, "node_modules/@sap/cds-dk/node_modules/qs": { - "version": "6.13.0", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.15.0", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -1506,18 +1566,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": { @@ -1551,6 +1611,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==", @@ -1569,7 +1645,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", @@ -1578,14 +1655,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, @@ -1597,66 +1677,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": { @@ -1794,8 +1856,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": { @@ -1887,13 +1949,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" @@ -1928,15 +1991,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==", @@ -1970,12 +2024,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": { @@ -2007,8 +2060,8 @@ } }, "node_modules/@sap/cds-dk/node_modules/yaml": { - "version": "2.8.1", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "version": "2.8.3", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", "bin": { @@ -2016,7 +2069,1142 @@ }, "engines": { "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/@sap/cds-fiori": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sap/cds-fiori/-/cds-fiori-2.3.0.tgz", + "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", + "license": "SEE LICENSE IN LICENSE", + "peerDependencies": { + "@sap/cds": ">=8" + } + }, + "node_modules/@sap/cds-mtxs": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@sap/cds-mtxs/-/cds-mtxs-3.8.1.tgz", + "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@sap/hdi-deploy": "^5" + }, + "bin": { + "cds-mtx": "bin/cds-mtx.js", + "cds-mtx-migrate": "bin/cds-mtx-migrate.js" + }, + "peerDependencies": { + "@sap/cds": "^9" + } + }, + "node_modules/@sap/hdi": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@sap/hdi/-/hdi-4.8.0.tgz", + "integrity": "sha512-tkJmY2ffm6mt4/LFwRBihlQkMxNAXa3ngvRe2N/6+qLIsUNdrH/M03S5mkygXq56K+KoVVZYuradajCusMWwsw==", + "license": "See LICENSE file", + "dependencies": { + "async": "^3.2.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@sap/hana-client": "^2 >= 2.5", + "hdb": "^2 || ^0" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + }, + "hdb": { + "optional": true + } + } + }, + "node_modules/@sap/hdi-deploy": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@sap/hdi-deploy/-/hdi-deploy-5.6.1.tgz", + "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", + "license": "See LICENSE file", + "dependencies": { + "@sap/hdi": "^4.8.0", + "@sap/xsenv": "^6.0.0", + "async": "^3.2.6", + "dotenv": "^16.4.5", + "handlebars": "^4.7.8", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=18.x" + }, + "peerDependencies": { + "@sap/hana-client": "^2 >= 2.6", + "hdb": "^2 || ^0" + }, + "peerDependenciesMeta": { + "@sap/hana-client": { + "optional": true + }, + "hdb": { + "optional": true + } + } + }, + "node_modules/@sap/xsenv": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@sap/xsenv/-/xsenv-6.1.0.tgz", + "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", + "license": "SEE LICENSE IN LICENSE file", + "dependencies": { + "debug": "4.4.3", + "node-cache": "^5.1.2", + "verror": "1.10.1" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "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": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "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/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "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": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "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/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "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/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "license": "MIT", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.1.tgz", + "integrity": "sha512-fvU78fIjZ+SBM9YwCknCvKOUKkLVqtWDVctl0s7xIqfmfb38t2TT4ZU2gHm+Z8xGwgW+QWEU3oQSAzIbo89Ggw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "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": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" } } } diff --git a/samples/bookshop/package.json b/samples/bookshop/package.json index 546722325..7fa281120 100644 --- a/samples/bookshop/package.json +++ b/samples/bookshop/package.json @@ -6,5 +6,8 @@ "repository": "", "devDependencies": { "@sap/cds-dk": "^9.3.2" + }, + "dependencies": { + "@sap/cds-mtxs": "^3.8.1" } } diff --git a/samples/bookshop/pom.xml b/samples/bookshop/pom.xml index ce37d9cf3..c09bda711 100644 --- a/samples/bookshop/pom.xml +++ b/samples/bookshop/pom.xml @@ -47,8 +47,8 @@ com.sap.cds - cds-feature-attachments - 1.3.3 + cds-feature-attachments-oss + 1.4.0-SNAPSHOT diff --git a/samples/bookshop/srv/attachments.cds b/samples/bookshop/srv/attachments.cds index 5a54d43f0..8603e44ba 100644 --- a/samples/bookshop/srv/attachments.cds +++ b/samples/bookshop/srv/attachments.cds @@ -9,8 +9,6 @@ extend my.Books with { sizeLimitedAttachments : Composition of many Attachments; @UI.Hidden mediaValidatedAttachments : Composition of many Attachments; - @UI.Hidden - anotherMediaValidatedAttachments : Composition of many Attachments; } annotate my.Books.sizeLimitedAttachments with { @@ -42,7 +40,7 @@ annotate adminService.Books with @(UI.Facets: [{ $Type : 'UI.ReferenceFacet', ID : 'AttachmentsFacet', Label : '{i18n>attachments}', - Target: 'mediaValidatedAttachments/@UI.LineItem' + Target: 'attachments/@UI.LineItem' }]); diff --git a/samples/bookshop/srv/pom.xml b/samples/bookshop/srv/pom.xml index 5cb9299fe..4d51762b6 100644 --- a/samples/bookshop/srv/pom.xml +++ b/samples/bookshop/srv/pom.xml @@ -1,7 +1,4 @@ - - + 4.0.0 @@ -47,6 +44,18 @@ runtime + + org.xerial + sqlite-jdbc + runtime + + + + com.sap.cds + cds-starter-cloudfoundry + runtime + + org.springframework.boot spring-boot-starter-security @@ -55,8 +64,21 @@ com.sap.cds - cds-feature-attachments + cds-feature-attachments-oss + + + + com.sap.cds + cds-feature-mt + runtime + + + + org.springframework.boot + spring-boot-starter-actuator + + diff --git a/samples/bookshop/srv/src/main/resources/application.yaml b/samples/bookshop/srv/src/main/resources/application.yaml index bed4c390f..f655cc3d8 100644 --- a/samples/bookshop/srv/src/main/resources/application.yaml +++ b/samples/bookshop/srv/src/main/resources/application.yaml @@ -19,4 +19,45 @@ cds: password: user data-source: auto-config: - enabled: false \ No newline at end of file + enabled: false +--- +spring: + config: + activate: + on-profile: local-mtxs +cds: + multi-tenancy: + mtxs: + enabled: true + sidecar: + url: http://localhost:4005 + security: + mock: + users: + admin: + password: admin + tenant: t1 + roles: + - admin + user: + password: user + tenant: t1 + erin: + password: erin + tenant: t2 + roles: + - admin +--- +management: + endpoint: + health: + show-components: always + probes.enabled: true + endpoints: + web: + exposure: + include: health + health: + defaults.enabled: false + ping.enabled: true + db.enabled: true diff --git a/samples/bookshop-mtx/xs-security.json b/samples/bookshop/xs-security.json similarity index 100% rename from samples/bookshop-mtx/xs-security.json rename to samples/bookshop/xs-security.json From 931c59128643c32f347bacbbdb97cae0ff19303d Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 1 Apr 2026 17:09:39 +0200 Subject: [PATCH 23/39] Remove dead code in getTenant: unreachable "default" fallback Since getTenant is only called from buildObjectKey when multitenancyEnabled is true, the null-tenant case always throws. The ternary fallback to "default" was unreachable dead code. Simplify to throw-if-null and return tenant directly. --- .../attachments/oss/handler/OSSAttachmentsServiceHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java index 892238344..52c960bd4 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java @@ -212,10 +212,10 @@ private String buildObjectKey(EventContext context, String contentId) { private String getTenant(EventContext context) { String tenant = context.getUserInfo().getTenant(); - if (tenant == null && multitenancyEnabled) { + if (tenant == null) { throw new ServiceException("Tenant ID is required for multitenant attachment operations"); } - return tenant != null ? tenant : "default"; + return tenant; } static void validateTenantId(String tenantId) { From 1d1d22987a53743c4ed47c5dd171ef78963ee656 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 1 Apr 2026 17:09:57 +0200 Subject: [PATCH 24/39] Replace generic catch(Exception) with catch(ExecutionException) in AzureClient After catching InterruptedException and RuntimeException, only ExecutionException (from deleteBatch's Future.get()) can reach the final catch block. Make this explicit for clarity. --- .../sap/cds/feature/attachments/oss/client/AzureClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java index d83c64ee1..63f6afaa5 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AzureClient.java @@ -119,10 +119,10 @@ public Future deleteContentByPrefix(String prefix) { Thread.currentThread().interrupt(); throw new ObjectStoreServiceException( "Interrupted while deleting objects by prefix from the Azure Object Store", e); - } catch (RuntimeException e) { + } catch (ExecutionException e) { throw new ObjectStoreServiceException( "Failed to delete objects by prefix from the Azure Object Store", e); - } catch (Exception e) { + } catch (RuntimeException e) { throw new ObjectStoreServiceException( "Failed to delete objects by prefix from the Azure Object Store", e); } From 55dff5f3f2ce128f7e1924d348145dedf541ff27 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 1 Apr 2026 17:10:45 +0200 Subject: [PATCH 25/39] Propagate partial S3 delete failures instead of silently logging Collect failed keys across all pagination pages and throw ObjectStoreServiceException after the loop completes. This ensures all pages are processed before reporting failure, and the caller (TenantCleanupHandler) knows cleanup was incomplete. --- .../attachments/oss/client/AWSClient.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java index ec2ed5e7b..392e5f593 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java @@ -6,6 +6,7 @@ import com.sap.cds.feature.attachments.oss.handler.ObjectStoreServiceException; import com.sap.cloud.environment.servicebinding.api.ServiceBinding; import java.io.InputStream; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -151,6 +152,7 @@ public Future deleteContentByPrefix(String prefix) { return executor.submit( () -> { try { + List allFailedKeys = new ArrayList<>(); ListObjectsV2Request listReq = ListObjectsV2Request.builder().bucket(this.bucketName).prefix(prefix).build(); ListObjectsV2Response listResp; @@ -168,15 +170,27 @@ public Future deleteContentByPrefix(String prefix) { .build(); DeleteObjectsResponse deleteResp = s3Client.deleteObjects(deleteReq); if (deleteResp.hasErrors() && !deleteResp.errors().isEmpty()) { + List failedKeys = + deleteResp.errors().stream().map(S3Error::key).toList(); logger.warn( "Failed to delete {} objects during prefix cleanup: {}", - deleteResp.errors().size(), - deleteResp.errors().stream().map(S3Error::key).toList()); + failedKeys.size(), + failedKeys); + allFailedKeys.addAll(failedKeys); } } listReq = listReq.toBuilder().continuationToken(listResp.nextContinuationToken()).build(); } while (listResp.isTruncated()); + if (!allFailedKeys.isEmpty()) { + throw new ObjectStoreServiceException( + "Partial failure during prefix cleanup: " + + allFailedKeys.size() + + " objects could not be deleted: " + + allFailedKeys); + } + } catch (ObjectStoreServiceException e) { + throw e; } catch (RuntimeException e) { throw new ObjectStoreServiceException( "Failed to delete objects by prefix from the AWS Object Store", e); From 8756a9f679c4bcd516e76495291a5862589f40fa Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 1 Apr 2026 17:11:57 +0200 Subject: [PATCH 26/39] Add post-integration-test execution to stop MTX sidecar The sidecar was started asynchronously at pre-integration-test but never stopped, leaving an orphaned node process that causes port conflicts on repeated runs. Kill the process on port 4005 at post-integration-test. --- integration-tests/mtx-local/srv/pom.xml | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/integration-tests/mtx-local/srv/pom.xml b/integration-tests/mtx-local/srv/pom.xml index 5d9601fca..fc511bbf4 100644 --- a/integration-tests/mtx-local/srv/pom.xml +++ b/integration-tests/mtx-local/srv/pom.xml @@ -224,15 +224,10 @@ exec-maven-plugin 3.5.0 - ${cds.npm.executable} ${cds.node.directory}${path.separator}${env.PATH} ${skipTests} - ${sidecar.dir} - true - true - run start @@ -241,6 +236,24 @@ exec pre-integration-test + + ${cds.npm.executable} + ${sidecar.dir} + true + true + run start + + + + stop-sidecar + + exec + + post-integration-test + + sh + -c "lsof -ti :4005 | xargs kill 2>/dev/null || true" + From 7f0c9c0d77f0328cc6a322da677494799f3b86af Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 1 Apr 2026 17:12:39 +0200 Subject: [PATCH 27/39] Use unique titles per test run for test isolation Use UUID-based document titles so tests are independent of execution order and prior data. This prevents cross-test interference when running within the same Spring context. --- .../MultiTenantAttachmentIsolationTest.java | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java index d19bf5ed5..f234f4fde 100644 --- a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java +++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.UUID; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -33,13 +34,15 @@ class MultiTenantAttachmentIsolationTest { @Test void createDocumentInTenant1_notVisibleInTenant2() throws Exception { + String uniqueTitle = "Only-in-T1-" + UUID.randomUUID(); + // Create a document in tenant-1 client .perform( post(DOCUMENTS_URL) .with(httpBasic("user-in-tenant-1", "")) .contentType(MediaType.APPLICATION_JSON) - .content("{ \"title\": \"Only in tenant-1\" }")) + .content("{ \"title\": \"" + uniqueTitle + "\" }")) .andExpect(status().isCreated()); // Read documents in tenant-2 — should NOT see the tenant-1 document @@ -53,18 +56,21 @@ void createDocumentInTenant1_notVisibleInTenant2() throws Exception { JsonNode values = objectMapper.readTree(response).path("value"); values.forEach( - node -> assertThat(node.get("title").asText("")).isNotEqualTo("Only in tenant-1")); + node -> assertThat(node.get("title").asText("")).isNotEqualTo(uniqueTitle)); } @Test void createDocumentsInBothTenants_eachSeeOnlyOwn() throws Exception { + String titleT1 = "Doc-T1-" + UUID.randomUUID(); + String titleT2 = "Doc-T2-" + UUID.randomUUID(); + // Create in tenant-1 client .perform( post(DOCUMENTS_URL) .with(httpBasic("user-in-tenant-1", "")) .contentType(MediaType.APPLICATION_JSON) - .content("{ \"title\": \"Doc-T1\" }")) + .content("{ \"title\": \"" + titleT1 + "\" }")) .andExpect(status().isCreated()); // Create in tenant-2 @@ -73,10 +79,10 @@ void createDocumentsInBothTenants_eachSeeOnlyOwn() throws Exception { post(DOCUMENTS_URL) .with(httpBasic("user-in-tenant-2", "")) .contentType(MediaType.APPLICATION_JSON) - .content("{ \"title\": \"Doc-T2\" }")) + .content("{ \"title\": \"" + titleT2 + "\" }")) .andExpect(status().isCreated()); - // Read from tenant-1 — should see Doc-T1 but not Doc-T2 + // Read from tenant-1 — should see titleT1 but not titleT2 String response1 = client .perform(get(DOCUMENTS_URL).with(httpBasic("user-in-tenant-1", ""))) @@ -88,14 +94,14 @@ void createDocumentsInBothTenants_eachSeeOnlyOwn() throws Exception { JsonNode values1 = objectMapper.readTree(response1).path("value"); boolean foundT1 = false; for (JsonNode node : values1) { - assertThat(node.get("title").asText("")).isNotEqualTo("Doc-T2"); - if ("Doc-T1".equals(node.get("title").asText(""))) { + assertThat(node.get("title").asText("")).isNotEqualTo(titleT2); + if (titleT1.equals(node.get("title").asText(""))) { foundT1 = true; } } assertThat(foundT1).isTrue(); - // Read from tenant-2 — should see Doc-T2 but not Doc-T1 + // Read from tenant-2 — should see titleT2 but not titleT1 String response2 = client .perform(get(DOCUMENTS_URL).with(httpBasic("user-in-tenant-2", ""))) @@ -107,8 +113,8 @@ void createDocumentsInBothTenants_eachSeeOnlyOwn() throws Exception { JsonNode values2 = objectMapper.readTree(response2).path("value"); boolean foundT2 = false; for (JsonNode node : values2) { - assertThat(node.get("title").asText("")).isNotEqualTo("Doc-T1"); - if ("Doc-T2".equals(node.get("title").asText(""))) { + assertThat(node.get("title").asText("")).isNotEqualTo(titleT1); + if (titleT2.equals(node.get("title").asText(""))) { foundT2 = true; } } From b008e3cfdeadd325bd2365fbea52c0fa35a5df5b Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 1 Apr 2026 17:13:01 +0200 Subject: [PATCH 28/39] Replace Apache Commons Base64 with java.util.Base64 Use the standard library Base64 encoder with explicit UTF-8 charset instead of the Apache Commons variant with platform default encoding. --- .../mt/utils/SubscriptionEndpointClient.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java index e78fe0b05..38a6179b7 100644 --- a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java +++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/utils/SubscriptionEndpointClient.java @@ -8,7 +8,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.codec.binary.Base64; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; @@ -20,7 +21,7 @@ public class SubscriptionEndpointClient { private final ObjectMapper objectMapper; private final MockMvc client; private final String credentials = - "Basic " + new String(Base64.encodeBase64("privileged:".getBytes())); + "Basic " + Base64.getEncoder().encodeToString("privileged:".getBytes(StandardCharsets.UTF_8)); public SubscriptionEndpointClient(ObjectMapper objectMapper, MockMvc client) { this.objectMapper = objectMapper; From fbcbc9f3f3c30ba17dd06d62a0d9de7c6afccd5b Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 1 Apr 2026 17:15:00 +0200 Subject: [PATCH 29/39] formatting --- integration-tests/mtx-local/srv/pom.xml | 2 +- .../mt/MultiTenantAttachmentIsolationTest.java | 3 +-- .../com/sap/cds/feature/attachments/oss/client/AWSClient.java | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/integration-tests/mtx-local/srv/pom.xml b/integration-tests/mtx-local/srv/pom.xml index fc511bbf4..330674cf2 100644 --- a/integration-tests/mtx-local/srv/pom.xml +++ b/integration-tests/mtx-local/srv/pom.xml @@ -252,7 +252,7 @@ post-integration-test sh - -c "lsof -ti :4005 | xargs kill 2>/dev/null || true" + -c "lsof -ti :4005 | xargs kill 2>/dev/null || true" diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java index f234f4fde..25d2d5339 100644 --- a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java +++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/attachments/integrationtests/mt/MultiTenantAttachmentIsolationTest.java @@ -55,8 +55,7 @@ void createDocumentInTenant1_notVisibleInTenant2() throws Exception { .getContentAsString(); JsonNode values = objectMapper.readTree(response).path("value"); - values.forEach( - node -> assertThat(node.get("title").asText("")).isNotEqualTo(uniqueTitle)); + values.forEach(node -> assertThat(node.get("title").asText("")).isNotEqualTo(uniqueTitle)); } @Test diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java index 392e5f593..9a7d592e8 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/AWSClient.java @@ -170,8 +170,7 @@ public Future deleteContentByPrefix(String prefix) { .build(); DeleteObjectsResponse deleteResp = s3Client.deleteObjects(deleteReq); if (deleteResp.hasErrors() && !deleteResp.errors().isEmpty()) { - List failedKeys = - deleteResp.errors().stream().map(S3Error::key).toList(); + List failedKeys = deleteResp.errors().stream().map(S3Error::key).toList(); logger.warn( "Failed to delete {} objects during prefix cleanup: {}", failedKeys.size(), From b95b6f583627ca99d244f534233d899f27b235b4 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 7 Apr 2026 17:22:33 +0200 Subject: [PATCH 30/39] Add Javadoc to OSClient, OSSAttachmentsServiceHandler, and TenantCleanupHandler --- .../attachments/oss/client/OSClient.java | 27 +++++++++++++++++++ .../handler/OSSAttachmentsServiceHandler.java | 19 +++++++++++++ .../oss/handler/TenantCleanupHandler.java | 4 +++ 3 files changed, 50 insertions(+) diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClient.java index e89e5ca98..63f5a0081 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClient.java @@ -10,12 +10,39 @@ /** The {@link OSClient} is the connection to the object store service. */ public interface OSClient { + /** + * Uploads content to the object store. + * + * @param content the input stream of the file content + * @param completeFileName the object key under which to store the content + * @param contentType the MIME type of the content + * @return a {@link Future} that completes when the upload finishes + */ Future uploadContent(InputStream content, String completeFileName, String contentType); + /** + * Deletes a single object from the object store. + * + * @param completeFileName the object key to delete + * @return a {@link Future} that completes when the deletion finishes + */ Future deleteContent(String completeFileName); + /** + * Reads the content of an object from the object store. + * + * @param completeFileName the object key to read + * @return a {@link Future} containing the content as an {@link InputStream} + */ Future readContent(String completeFileName); + /** + * Deletes all objects whose keys start with the given prefix. Used for tenant cleanup in shared + * multitenancy mode. + * + * @param prefix the key prefix to match (e.g. {@code "tenantId/"}) + * @return a {@link Future} that completes when all matching objects have been deleted + */ default Future deleteContentByPrefix(String prefix) { return CompletableFuture.failedFuture( new UnsupportedOperationException("deleteContentByPrefix not supported by this client")); diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java index 52c960bd4..17e871d1f 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java @@ -196,10 +196,21 @@ void readAttachment(AttachmentReadEventContext context) { } } + /** + * Returns the underlying {@link OSClient} instance. Intended for use by {@link + * TenantCleanupHandler} wiring in {@link + * com.sap.cds.feature.attachments.oss.configuration.Registration}. + * + * @return the object store client + */ public OSClient getOsClient() { return osClient; } + /** + * Builds the object key for storage operations. In shared multitenancy mode, the key is prefixed + * with the tenant ID ({@code tenantId/contentId}). Otherwise, the raw content ID is used. + */ private String buildObjectKey(EventContext context, String contentId) { if (multitenancyEnabled && "shared".equals(objectStoreKind)) { String tenant = getTenant(context); @@ -218,6 +229,14 @@ private String getTenant(EventContext context) { return tenant; } + /** + * Validates that the tenant ID is safe for use in object key construction. Rejects null, empty, + * or values containing path separators ({@code /}, {@code \}, {@code ..}) to prevent path + * traversal attacks. + * + * @param tenantId the tenant ID to validate + * @throws ServiceException if the tenant ID is invalid + */ static void validateTenantId(String tenantId) { if (tenantId == null || tenantId.isEmpty() diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java index b35d7f9e4..96fb0a230 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/TenantCleanupHandler.java @@ -12,6 +12,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * Event handler that cleans up a tenant's attachment data from the shared object store when the + * tenant unsubscribes. Registered only in shared multitenancy mode. + */ @ServiceName(DeploymentService.DEFAULT_NAME) public class TenantCleanupHandler implements EventHandler { From edf8fabe09aba8cc311ef09ae4ea0a95cf94807e Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 7 Apr 2026 17:22:43 +0200 Subject: [PATCH 31/39] Use batch deletion in GoogleClient.deleteContentByPrefix Replace one-by-one storage.delete() calls with a single storage.delete(List) call to avoid the N+1 problem. --- .../attachments/oss/client/GoogleClient.java | 19 ++++++++++++------- .../oss/client/GoogleClientTest.java | 4 ++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java index ece8e91cb..a4c4bfa86 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/GoogleClient.java @@ -19,7 +19,9 @@ import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; +import java.util.ArrayList; import java.util.Base64; +import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import org.slf4j.Logger; @@ -151,14 +153,17 @@ public Future deleteContentByPrefix(String prefix) { bucketName, Storage.BlobListOption.prefix(prefix), Storage.BlobListOption.versions(true)); + List blobIds = new ArrayList<>(); for (Blob blob : blobs.iterateAll()) { - boolean deleted = - storage.delete(BlobId.of(bucketName, blob.getName(), blob.getGeneration())); - if (!deleted) { - logger.warn( - "Failed to delete blob {} (generation {}) during prefix cleanup", - blob.getName(), - blob.getGeneration()); + blobIds.add(BlobId.of(bucketName, blob.getName(), blob.getGeneration())); + } + if (!blobIds.isEmpty()) { + List results = storage.delete(blobIds); + for (int i = 0; i < results.size(); i++) { + if (!results.get(i)) { + logger.warn( + "Failed to delete blob {} during prefix cleanup", blobIds.get(i).getName()); + } } } } catch (RuntimeException e) { diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java index c61af0a4f..e959bef4e 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/GoogleClientTest.java @@ -189,11 +189,11 @@ void testDeleteContentByPrefix() throws InterruptedException, ExecutionException Iterator blobIterator = java.util.List.of(mockBlob1, mockBlob2).iterator(); when(mockPage.iterateAll()).thenReturn(() -> blobIterator); when(mockStorage.list(anyString(), any(), any())).thenReturn(mockPage); - when(mockStorage.delete(any(BlobId.class))).thenReturn(true); + when(mockStorage.delete(anyList())).thenReturn(java.util.List.of(true, true)); googleClient.deleteContentByPrefix("prefix/").get(); - verify(mockStorage, times(2)).delete(any(BlobId.class)); + verify(mockStorage).delete(anyList()); } @Test From 692c104b704a49f55ad0238fe13cfe8b5b85c068 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 7 Apr 2026 17:22:52 +0200 Subject: [PATCH 32/39] Replace unbounded thread pool with fixed pool of daemon threads Use newFixedThreadPool(16) with daemon threads instead of newCachedThreadPool() to bound thread creation. --- .../attachments/oss/configuration/Registration.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java index 2276dd9c0..72b055954 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java @@ -28,7 +28,14 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { boolean multitenancyEnabled = isMultitenancyEnabled(env); String objectStoreKind = getObjectStoreKind(env); - ExecutorService executor = Executors.newCachedThreadPool(); + ExecutorService executor = + Executors.newFixedThreadPool( + 16, + r -> { + Thread t = new Thread(r, "attachment-oss-tasks"); + t.setDaemon(true); + return t; + }); Runtime.getRuntime() .addShutdownHook( new Thread( From b2050c15b8cfc764393fc7b9f09d0c6b609d1f0c Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 7 Apr 2026 17:25:42 +0200 Subject: [PATCH 33/39] Remove BTP deployment artifacts from bookshop sample MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove mta.yaml, xs-security.json, and approuter — the sample should focus on demonstrating attachments usage, not deployment. --- samples/bookshop/app/router/package.json | 9 -- samples/bookshop/app/router/xs-app.json | 16 --- samples/bookshop/mta.yaml | 151 ----------------------- samples/bookshop/xs-security.json | 40 ------ 4 files changed, 216 deletions(-) delete mode 100644 samples/bookshop/app/router/package.json delete mode 100644 samples/bookshop/app/router/xs-app.json delete mode 100644 samples/bookshop/mta.yaml delete mode 100644 samples/bookshop/xs-security.json diff --git a/samples/bookshop/app/router/package.json b/samples/bookshop/app/router/package.json deleted file mode 100644 index 92b6a2821..000000000 --- a/samples/bookshop/app/router/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "approuter", - "dependencies": { - "@sap/approuter": "^20.0.0" - }, - "scripts": { - "start": "node node_modules/@sap/approuter/approuter.js" - } -} diff --git a/samples/bookshop/app/router/xs-app.json b/samples/bookshop/app/router/xs-app.json deleted file mode 100644 index 819cdeac6..000000000 --- a/samples/bookshop/app/router/xs-app.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "welcomeFile": "/cp.portal", - "routes": [ - { - "source": "^/-/cds/.*", - "destination": "mtx-api", - "authenticationType": "none" - }, - { - "source": "^/(.*)$", - "target": "$1", - "destination": "srv-api", - "csrfProtection": true - } - ] -} diff --git a/samples/bookshop/mta.yaml b/samples/bookshop/mta.yaml deleted file mode 100644 index fdf253a8f..000000000 --- a/samples/bookshop/mta.yaml +++ /dev/null @@ -1,151 +0,0 @@ -_schema-version: 3.3.0 -ID: bookshop -version: 1.0.0-SNAPSHOT -description: "A simple CAP project." -parameters: - enable-parallel-deployments: true -modules: - - name: bookshop-srv - type: java - path: srv - parameters: - instances: 1 - buildpack: sap_java_buildpack_jakarta - properties: - SPRING_PROFILES_ACTIVE: cloud,sandbox - JBP_CONFIG_COMPONENTS: "jres: ['com.sap.xs.java.buildpack.jre.SAPMachineJRE']" - JBP_CONFIG_SAP_MACHINE_JRE: '{ version: 17.+ }' - CDS_MULTITENANCY_APPUI_TENANTSEPARATOR: "-" - build-parameters: - builder: custom - commands: - - mvn clean package -DskipTests=true --batch-mode - build-result: target/*-exec.jar - provides: - - name: srv-api # required by consumers of CAP services (e.g. approuter) - properties: - srv-url: ${default-url} - requires: - - name: bookshop-db - - name: mtx-api - properties: - CDS_MULTITENANCY_SIDECAR_URL: ~{mtx-url} - - name: app-api - properties: - CDS_MULTITENANCY_APPUI_URL: ~{url} - - name: bookshop-auth - - name: bookshop-registry - - name: bookshop-attachments - - name: bookshop-malware-scanner - - - name: bookshop-mtx - type: nodejs - path: mtx/sidecar - requires: - - name: bookshop-db - - name: bookshop-auth - build-parameters: - builder: custom - build-result: gen - commands: - - npm run build - requires: - - name: bookshop-srv - parameters: - instances: 1 - memory: 256M - disk-quota: 512M - provides: - - name: mtx-api - properties: - mtx-url: ${default-url} - - - name: bookshop - type: approuter.nodejs - path: app/router - parameters: - keep-existing-routes: true - disk-quota: 256M - memory: 256M - properties: - TENANT_HOST_PATTERN: "^(.*)-${default-uri}" - requires: - - name: srv-api - group: destinations - properties: - name: srv-api # must be used in xs-app.json as well - url: ~{srv-url} - forwardAuthToken: true - - name: mtx-api - group: destinations - properties: - name: mtx-api # must be used in xs-app.json as well - url: ~{mtx-url} - - name: bookshop-auth - provides: - - name: app-api - properties: - app-protocol: ${protocol} - app-uri: ${default-uri} - url: ${default-url} - -resources: - - name: bookshop-db - type: org.cloudfoundry.managed-service - parameters: - service: service-manager - service-plan: container - - name: bookshop-registry - type: org.cloudfoundry.managed-service - requires: - - name: srv-api - parameters: - service: saas-registry - service-plan: application - config: - xsappname: bookshop-${org}-${space} - appName: bookshop-${org}-${space} - displayName: bookshop - description: A simple CAP project. - category: 'Category' - appUrls: - getDependencies: ~{srv-api/srv-url}/mt/v1.0/subscriptions/dependencies - onSubscription: ~{srv-api/srv-url}/mt/v1.0/subscriptions/tenants/{tenantId} - onSubscriptionAsync: true - onUnSubscriptionAsync: true - onUpdateDependenciesAsync: true - callbackTimeoutMillis: 300000 # Increase if your deployments are taking longer than that - - name: bookshop-auth - type: org.cloudfoundry.managed-service - parameters: - service: xsuaa - service-plan: application - path: ./xs-security.json - config: - xsappname: bookshop-${org}-${space} - tenant-mode: shared - oauth2-configuration: - credential-types: - - "binding-secret" - - "x509" - redirect-uris: - - https://*~{app-api/app-uri}/** - role-collections: - - name: 'admin (bookshop ${org}-${space})' - description: 'generated' - role-template-references: - - '$XSAPPNAME.admin' - requires: - - name: app-api - - name: bookshop-attachments - type: org.cloudfoundry.managed-service - parameters: - service: objectstore - service-plan: standard - - name: bookshop-malware-scanner - type: org.cloudfoundry.managed-service - parameters: - service: malware-scanner - service-plan: clamav - config: - auth: mTLS diff --git a/samples/bookshop/xs-security.json b/samples/bookshop/xs-security.json deleted file mode 100644 index 5a4b6112f..000000000 --- a/samples/bookshop/xs-security.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "scopes": [ - { - "name": "$XSAPPNAME.admin", - "description": "admin" - }, - { - "name": "$XSAPPNAME.mtcallback", - "description": "Subscription via SaaS Registry", - "grant-as-authority-to-apps": [ - "$XSAPPNAME(application,sap-provisioning,tenant-onboarding)" - ] - }, - { - "name": "$XSAPPNAME.cds.ExtensionDeveloper", - "description": "Extend CAP applications via extension projects" - } - ], - "attributes": [], - "role-templates": [ - { - "name": "admin", - "description": "generated", - "scope-references": [ - "$XSAPPNAME.admin" - ], - "attribute-references": [] - }, - { - "name": "ExtensionDeveloper", - "description": "Extension development including UIFlex extensibility", - "scope-references": [ - "$XSAPPNAME.cds.ExtensionDeveloper" - ] - } - ], - "authorities": [ - "$XSAPPNAME.cds.ExtensionDeveloper" - ] -} From 48132ffec42ff22b7da93ef48c16b089f3fed3ef Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 7 Apr 2026 17:34:58 +0200 Subject: [PATCH 34/39] Extract OSClientFactory to decouple client creation from handler Move AWS/Azure/GCP detection logic from OSSAttachmentsServiceHandler constructor into a dedicated OSClientFactory.create() method. The handler now accepts an OSClient directly, and Registration creates the client via the factory and passes it to both the handler and TenantCleanupHandler. This removes the getOsClient() getter. --- .../oss/client/OSClientFactory.java | 80 +++++++++++++++++++ .../oss/configuration/Registration.java | 8 +- .../handler/OSSAttachmentsServiceHandler.java | 80 ++----------------- .../attachments/oss/client/AWSClientTest.java | 9 +-- .../OSSAttachmentsServiceHandlerTest.java | 41 ++++------ ...OSSAttachmentsServiceHandlerTestUtils.java | 4 +- 6 files changed, 112 insertions(+), 110 deletions(-) create mode 100644 storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClientFactory.java diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClientFactory.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClientFactory.java new file mode 100644 index 000000000..7cfb26f59 --- /dev/null +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClientFactory.java @@ -0,0 +1,80 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.oss.client; + +import com.sap.cds.feature.attachments.oss.handler.ObjectStoreServiceException; +import com.sap.cloud.environment.servicebinding.api.ServiceBinding; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.concurrent.ExecutorService; +import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Factory for creating {@link OSClient} instances from object store service bindings. Detects the + * storage backend (AWS S3, Azure Blob Storage, Google Cloud Storage) based on the credentials in the + * binding. + */ +public final class OSClientFactory { + + private static final Logger logger = LoggerFactory.getLogger(OSClientFactory.class); + + private OSClientFactory() {} + + /** + * Creates an {@link OSClient} for the given service binding. + * + *
    + *
  • For AWS, the binding must contain a "host" with "aws", "s3", or "amazon". + *
  • For Azure, the binding must contain a "container_uri" with "azure" or "windows". + *
  • For Google, the binding must contain a valid "base64EncodedPrivateKeyData" containing + * "google" or "gcp". + *
+ * + * @param binding the {@link ServiceBinding} containing credentials for the object store service + * @param executor the {@link ExecutorService} for async operations + * @return the appropriate {@link OSClient} implementation + * @throws ObjectStoreServiceException if no valid object store service binding is found + */ + public static OSClient create(ServiceBinding binding, ExecutorService executor) { + final String host = (String) binding.getCredentials().get("host"); // AWS + final String containerUri = (String) binding.getCredentials().get("container_uri"); // Azure + final String base64EncodedPrivateKeyData = + (String) binding.getCredentials().get("base64EncodedPrivateKeyData"); // GCP + + if (host != null && Stream.of("aws", "s3", "amazon").anyMatch(host::contains)) { + logger.info("Detected AWS S3 object store from binding: {}", binding); + return new AWSClient(binding, executor); + } else if (containerUri != null + && Stream.of("azure", "windows").anyMatch(containerUri::contains)) { + logger.info("Detected Azure Blob Storage from binding: {}", binding); + return new AzureClient(binding, executor); + } else if (base64EncodedPrivateKeyData != null) { + String decoded; + try { + decoded = + new String( + Base64.getDecoder().decode(base64EncodedPrivateKeyData), StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + throw new ObjectStoreServiceException( + "No valid base64EncodedPrivateKeyData found in Google service binding: %s" + .formatted(binding), + e); + } + if (Stream.of("google", "gcp").anyMatch(decoded::contains)) { + logger.info("Detected Google Cloud Storage from binding: {}", binding); + return new GoogleClient(binding, executor); + } else { + throw new ObjectStoreServiceException( + "No valid Google service binding found in binding: %s".formatted(binding)); + } + } else { + throw new ObjectStoreServiceException( + "No valid object store service found in binding: %s. Please ensure you have a valid AWS" + + " S3, Azure Blob Storage, or Google Cloud Storage service binding." + .formatted(binding)); + } + } +} diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java index 72b055954..51a2f1a5e 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java @@ -3,6 +3,8 @@ */ package com.sap.cds.feature.attachments.oss.configuration; +import com.sap.cds.feature.attachments.oss.client.OSClient; +import com.sap.cds.feature.attachments.oss.client.OSClientFactory; import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandler; import com.sap.cds.feature.attachments.oss.handler.TenantCleanupHandler; import com.sap.cds.services.environment.CdsEnvironment; @@ -50,13 +52,13 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { Thread.currentThread().interrupt(); } })); + OSClient osClient = OSClientFactory.create(bindingOpt.get(), executor); OSSAttachmentsServiceHandler handler = - new OSSAttachmentsServiceHandler( - bindingOpt.get(), executor, multitenancyEnabled, objectStoreKind); + new OSSAttachmentsServiceHandler(osClient, multitenancyEnabled, objectStoreKind); configurer.eventHandler(handler); if (multitenancyEnabled && "shared".equals(objectStoreKind)) { - configurer.eventHandler(new TenantCleanupHandler(handler.getOsClient())); + configurer.eventHandler(new TenantCleanupHandler(osClient)); logger.info( "Registered OSS Attachments Service Handler with shared multitenancy mode and tenant cleanup."); } else { diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java index 17e871d1f..cdc4e6bb3 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandler.java @@ -6,9 +6,6 @@ import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.StatusCode; -import com.sap.cds.feature.attachments.oss.client.AWSClient; -import com.sap.cds.feature.attachments.oss.client.AzureClient; -import com.sap.cds.feature.attachments.oss.client.GoogleClient; import com.sap.cds.feature.attachments.oss.client.OSClient; import com.sap.cds.feature.attachments.service.AttachmentService; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; @@ -20,14 +17,9 @@ 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.cloud.environment.servicebinding.api.ServiceBinding; import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.Base64; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; -import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,69 +36,20 @@ public class OSSAttachmentsServiceHandler implements EventHandler { private final String objectStoreKind; /** - * Creates a new OSSAttachmentsServiceHandler using the provided {@link ServiceBinding}. + * Creates a new OSSAttachmentsServiceHandler with the given {@link OSClient}. * - *

The handler will automatically detect the storage backend (AWS S3, Azure Blob Storage, - * Google Cloud Storage) based on the credentials in the service binding. If no valid binding is - * found, an {@link ObjectStoreServiceException} is thrown. + *

Use {@link com.sap.cds.feature.attachments.oss.client.OSClientFactory#create + * OSClientFactory.create()} to obtain an {@link OSClient} from a service binding. * - *

    - *
  • For AWS, the binding must contain a "host" with "aws", "s3", or "amazon". - *
  • For Azure, the binding must contain a "container_uri" with "azure" or "windows". - *
  • For Google, the binding must contain a valid "base64EncodedPrivateKeyData" containing - * "google" or "gcp". - *
- * - * @param binding the {@link ServiceBinding} containing credentials for the object store service - * @param executor the {@link ExecutorService} for async operations + * @param osClient the object store client for storage operations * @param multitenancyEnabled whether multitenancy is enabled * @param objectStoreKind the object store kind (e.g. "shared") - * @throws ObjectStoreServiceException if no valid object store service binding is found */ public OSSAttachmentsServiceHandler( - ServiceBinding binding, - ExecutorService executor, - boolean multitenancyEnabled, - String objectStoreKind) { + OSClient osClient, boolean multitenancyEnabled, String objectStoreKind) { + this.osClient = osClient; this.multitenancyEnabled = multitenancyEnabled; this.objectStoreKind = objectStoreKind; - final String host = (String) binding.getCredentials().get("host"); // AWS - final String containerUri = (String) binding.getCredentials().get("container_uri"); // Azure - final String base64EncodedPrivateKeyData = - (String) binding.getCredentials().get("base64EncodedPrivateKeyData"); // GCP - - // Check the service binding credentials to determine which client to use. - if (host != null && Stream.of("aws", "s3", "amazon").anyMatch(host::contains)) { - this.osClient = new AWSClient(binding, executor); - } else if (containerUri != null - && Stream.of("azure", "windows").anyMatch(containerUri::contains)) { - this.osClient = new AzureClient(binding, executor); - } else if (base64EncodedPrivateKeyData != null) { - String decoded = ""; - try { - decoded = - new String( - Base64.getDecoder().decode(base64EncodedPrivateKeyData), StandardCharsets.UTF_8); - } catch (IllegalArgumentException e) { - throw new ObjectStoreServiceException( - "No valid base64EncodedPrivateKeyData found in Google service binding: %s" - .formatted(binding), - e); - } - // Redeclaring is needed here to make the variable effectively final for the - // lambda expression - final String dec = decoded; - if (Stream.of("google", "gcp").anyMatch(dec::contains)) { - this.osClient = new GoogleClient(binding, executor); - } else { - throw new ObjectStoreServiceException( - "No valid Google service binding found in binding: %s".formatted(binding)); - } - } else { - throw new ObjectStoreServiceException( - "No valid object store service found in binding: %s. Please ensure you have a valid AWS S3, Azure Blob Storage, or Google Cloud Storage service binding." - .formatted(binding)); - } } @On @@ -196,17 +139,6 @@ void readAttachment(AttachmentReadEventContext context) { } } - /** - * Returns the underlying {@link OSClient} instance. Intended for use by {@link - * TenantCleanupHandler} wiring in {@link - * com.sap.cds.feature.attachments.oss.configuration.Registration}. - * - * @return the object store client - */ - public OSClient getOsClient() { - return osClient; - } - /** * Builds the object key for storage operations. In shared multitenancy mode, the key is prefixed * with the tenant ID ({@code tenantId/contentId}). Otherwise, the raw content ID is used. diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java index 2710e2d8f..df25d4fa2 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/client/AWSClientTest.java @@ -14,8 +14,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandler; -import com.sap.cds.feature.attachments.oss.handler.OSSAttachmentsServiceHandlerTestUtils; import com.sap.cds.feature.attachments.oss.handler.ObjectStoreServiceException; import com.sap.cloud.environment.servicebinding.api.ServiceBinding; import java.io.ByteArrayInputStream; @@ -49,11 +47,8 @@ class AWSClientTest { ExecutorService executor = Executors.newCachedThreadPool(); @Test - void testConstructorWithAwsBindingUsesAwsClient() - throws NoSuchFieldException, IllegalAccessException { - OSSAttachmentsServiceHandler handler = - new OSSAttachmentsServiceHandler(getDummyBinding(), executor, false, null); - OSClient client = OSSAttachmentsServiceHandlerTestUtils.getOsClient(handler); + void testFactoryWithAwsBindingCreatesAwsClient() { + OSClient client = OSClientFactory.create(getDummyBinding(), executor); assertInstanceOf(AWSClient.class, client); } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index 36fb3f75c..a6417994c 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -16,6 +16,7 @@ import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.StatusCode; import com.sap.cds.feature.attachments.oss.client.OSClient; +import com.sap.cds.feature.attachments.oss.client.OSClientFactory; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; @@ -59,13 +60,6 @@ private static ServiceBinding createAwsBinding() { return binding; } - private static void injectOsClient(OSSAttachmentsServiceHandler handler, OSClient client) - throws NoSuchFieldException, IllegalAccessException { - var field = OSSAttachmentsServiceHandler.class.getDeclaredField("osClient"); - field.setAccessible(true); - field.set(handler, client); - } - private static CdsEntity stubEntity(String name) { CdsEntity entity = mock(CdsEntity.class); when(entity.getQualifiedName()).thenReturn(name); @@ -95,10 +89,10 @@ private static UserInfo userInfoWithTenant(String tenant) { } @Nested - class ConstructorTests { + class FactoryTests { @Test - void testConstructorHandlesInvalidBase64EncodedPrivateKeyData() { + void testFactoryHandlesInvalidBase64EncodedPrivateKeyData() { ServiceBinding binding = mock(ServiceBinding.class); HashMap creds = new HashMap<>(); creds.put("base64EncodedPrivateKeyData", "not-a-valid-base64-string"); @@ -106,11 +100,11 @@ void testConstructorHandlesInvalidBase64EncodedPrivateKeyData() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + () -> OSClientFactory.create(binding, executor)); } @Test - void testConstructorHandlesValidBase64ButNoGoogleOrGcp() { + void testFactoryHandlesValidBase64ButNoGoogleOrGcp() { String plain = "this is just a dummy string without keywords"; String base64 = Base64.getEncoder().encodeToString(plain.getBytes(StandardCharsets.UTF_8)); @@ -121,11 +115,11 @@ void testConstructorHandlesValidBase64ButNoGoogleOrGcp() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + () -> OSClientFactory.create(binding, executor)); } @Test - void testConstructorHandlesInValidBase64() { + void testFactoryHandlesInValidBase64() { ServiceBinding binding = mock(ServiceBinding.class); HashMap creds = new HashMap<>(); creds.put("base64EncodedPrivateKeyData", "this is just a dummy string without keywords"); @@ -133,11 +127,11 @@ void testConstructorHandlesInValidBase64() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + () -> OSClientFactory.create(binding, executor)); } @Test - void testConstructorHandlesNoValidObjectStoreService() { + void testFactoryHandlesNoValidObjectStoreService() { ServiceBinding binding = mock(ServiceBinding.class); HashMap creds = new HashMap<>(); creds.put("someOtherField", "someValue"); @@ -145,7 +139,7 @@ void testConstructorHandlesNoValidObjectStoreService() { assertThrows( ObjectStoreServiceException.class, - () -> new OSSAttachmentsServiceHandler(binding, executor, false, null)); + () -> OSClientFactory.create(binding, executor)); } } @@ -153,10 +147,9 @@ void testConstructorHandlesNoValidObjectStoreService() { class SingleTenantOperations { @BeforeEach - void setup() throws NoSuchFieldException, IllegalAccessException { - handler = new OSSAttachmentsServiceHandler(createAwsBinding(), executor, false, null); + void setup() { mockOsClient = mock(OSClient.class); - injectOsClient(handler, mockOsClient); + handler = new OSSAttachmentsServiceHandler(mockOsClient, false, null); } @Test @@ -233,10 +226,9 @@ void testMarkAttachmentAsDeletedDeletesContent() { class ExceptionHandling { @BeforeEach - void setup() throws NoSuchFieldException, IllegalAccessException { - handler = new OSSAttachmentsServiceHandler(createAwsBinding(), executor, false, null); + void setup() { mockOsClient = mock(OSClient.class); - injectOsClient(handler, mockOsClient); + handler = new OSSAttachmentsServiceHandler(mockOsClient, false, null); } @Test @@ -343,10 +335,9 @@ private AttachmentReadEventContext createContextForReadException(Exception excep class MultitenancyTests { @BeforeEach - void setup() throws NoSuchFieldException, IllegalAccessException { - handler = new OSSAttachmentsServiceHandler(createAwsBinding(), executor, true, "shared"); + void setup() { mockOsClient = mock(OSClient.class); - injectOsClient(handler, mockOsClient); + handler = new OSSAttachmentsServiceHandler(mockOsClient, true, "shared"); } @Test diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java index f96178dd3..f07a9c591 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java @@ -15,6 +15,7 @@ import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; import com.sap.cds.feature.attachments.oss.client.OSClient; +import com.sap.cds.feature.attachments.oss.client.OSClientFactory; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; @@ -39,8 +40,9 @@ public static void testCreateReadDeleteAttachmentFlow( String testFileName = "testFileName-" + System.currentTimeMillis() + ".txt"; String testFileContent = "test"; + OSClient osClient = OSClientFactory.create(binding, executor); OSSAttachmentsServiceHandler handler = - new OSSAttachmentsServiceHandler(binding, executor, false, null); + new OSSAttachmentsServiceHandler(osClient, false, null); // Create an AttachmentCreateEventContext with mocked data - to upload a test attachment MediaData createMediaData = mock(MediaData.class); From 740cc1af358ecf8c270b10df98b3bc6dda5a3bca Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Tue, 7 Apr 2026 17:51:50 +0200 Subject: [PATCH 35/39] spotless --- .../attachments/oss/client/OSClientFactory.java | 4 ++-- .../handler/OSSAttachmentsServiceHandlerTest.java | 12 ++++-------- .../OSSAttachmentsServiceHandlerTestUtils.java | 3 +-- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClientFactory.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClientFactory.java index 7cfb26f59..e4db97bda 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClientFactory.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/client/OSClientFactory.java @@ -14,8 +14,8 @@ /** * Factory for creating {@link OSClient} instances from object store service bindings. Detects the - * storage backend (AWS S3, Azure Blob Storage, Google Cloud Storage) based on the credentials in the - * binding. + * storage backend (AWS S3, Azure Blob Storage, Google Cloud Storage) based on the credentials in + * the binding. */ public final class OSClientFactory { diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java index a6417994c..91bc98926 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTest.java @@ -99,8 +99,7 @@ void testFactoryHandlesInvalidBase64EncodedPrivateKeyData() { when(binding.getCredentials()).thenReturn(creds); assertThrows( - ObjectStoreServiceException.class, - () -> OSClientFactory.create(binding, executor)); + ObjectStoreServiceException.class, () -> OSClientFactory.create(binding, executor)); } @Test @@ -114,8 +113,7 @@ void testFactoryHandlesValidBase64ButNoGoogleOrGcp() { when(binding.getCredentials()).thenReturn(creds); assertThrows( - ObjectStoreServiceException.class, - () -> OSClientFactory.create(binding, executor)); + ObjectStoreServiceException.class, () -> OSClientFactory.create(binding, executor)); } @Test @@ -126,8 +124,7 @@ void testFactoryHandlesInValidBase64() { when(binding.getCredentials()).thenReturn(creds); assertThrows( - ObjectStoreServiceException.class, - () -> OSClientFactory.create(binding, executor)); + ObjectStoreServiceException.class, () -> OSClientFactory.create(binding, executor)); } @Test @@ -138,8 +135,7 @@ void testFactoryHandlesNoValidObjectStoreService() { when(binding.getCredentials()).thenReturn(creds); assertThrows( - ObjectStoreServiceException.class, - () -> OSClientFactory.create(binding, executor)); + ObjectStoreServiceException.class, () -> OSClientFactory.create(binding, executor)); } } diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java index f07a9c591..e0fbc6a00 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/handler/OSSAttachmentsServiceHandlerTestUtils.java @@ -41,8 +41,7 @@ public static void testCreateReadDeleteAttachmentFlow( String testFileContent = "test"; OSClient osClient = OSClientFactory.create(binding, executor); - OSSAttachmentsServiceHandler handler = - new OSSAttachmentsServiceHandler(osClient, false, null); + OSSAttachmentsServiceHandler handler = new OSSAttachmentsServiceHandler(osClient, false, null); // Create an AttachmentCreateEventContext with mocked data - to upload a test attachment MediaData createMediaData = mock(MediaData.class); From fcc6916676a80c43bbb040a4e085530aa5012266 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 8 Apr 2026 11:06:21 +0200 Subject: [PATCH 36/39] implement suggestion --- .../feature/attachments/oss/configuration/Registration.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java index 51a2f1a5e..3fbca05e2 100644 --- a/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java +++ b/storage-targets/cds-feature-attachments-oss/src/main/java/com/sap/cds/feature/attachments/oss/configuration/Registration.java @@ -30,9 +30,13 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { boolean multitenancyEnabled = isMultitenancyEnabled(env); String objectStoreKind = getObjectStoreKind(env); + // Fixed thread pool for background I/O operations (upload, download, delete). + // Default 16 is tuned for I/O-bound cloud storage calls, not CPU-bound work. + int threadPoolSize = + env.getProperty("cds.attachments.objectStore.threadPoolSize", Integer.class, 16); ExecutorService executor = Executors.newFixedThreadPool( - 16, + threadPoolSize, r -> { Thread t = new Thread(r, "attachment-oss-tasks"); t.setDaemon(true); From 0b29833c355311fa809def90e6e1adf864d26877 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 8 Apr 2026 11:24:19 +0200 Subject: [PATCH 37/39] align with tests --- .../feature/attachments/oss/configuration/RegistrationTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java index 951740ea3..3987e1a72 100644 --- a/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java +++ b/storage-targets/cds-feature-attachments-oss/src/test/java/com/sap/cds/feature/attachments/oss/configuration/RegistrationTest.java @@ -50,6 +50,8 @@ void setup() { environment = mock(CdsEnvironment.class); when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); when(cdsRuntime.getEnvironment()).thenReturn(environment); + when(environment.getProperty("cds.attachments.objectStore.threadPoolSize", Integer.class, 16)) + .thenReturn(16); awsBinding = createAwsBinding(); } From 0e0a79003581e420ad434adc49928422fd7b289f Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 8 Apr 2026 14:12:19 +0200 Subject: [PATCH 38/39] Remove samples/bookshop/package-lock.json from tracking --- samples/bookshop/package-lock.json | 3210 ---------------------------- 1 file changed, 3210 deletions(-) delete mode 100644 samples/bookshop/package-lock.json diff --git a/samples/bookshop/package-lock.json b/samples/bookshop/package-lock.json deleted file mode 100644 index 8cdc31e8d..000000000 --- a/samples/bookshop/package-lock.json +++ /dev/null @@ -1,3210 +0,0 @@ -{ - "name": "bookshop-cds", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "bookshop-cds", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@sap/cds-mtxs": "^3.8.1" - }, - "devDependencies": { - "@sap/cds-dk": "^9.3.2" - } - }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "license": "MIT", - "peer": true, - "engines": { - "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": { - "version": "9.8.4", - "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-9.8.4.tgz", - "integrity": "sha512-1cnFdz4Ex4LV15dztDsSLDiilVdJ2mfTqr5A3TGsRd/6cM31SnHhuZ/Ra0FEErodokNS2pwasGZmSfgHVDW8KA==", - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/cds-compiler": "^6.4", - "@sap/cds-fiori": "^2", - "express": "^4.22.1 || ^5", - "js-yaml": "^4.1.1" - }, - "bin": { - "cds-deploy": "bin/deploy.js", - "cds-serve": "bin/serve.js" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@eslint/js": "^9 || ^10", - "tar": "^7.5.6" - }, - "peerDependenciesMeta": { - "tar": { - "optional": true - } - } - }, - "node_modules/@sap/cds-compiler": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-6.8.0.tgz", - "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", - "license": "SEE LICENSE IN LICENSE", - "bin": { - "cdsc": "bin/cdsc.js", - "cdshi": "bin/cdshi.js", - "cdsse": "bin/cdsse.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@sap/cds-dk": { - "version": "9.8.2", - "resolved": "https://registry.npmjs.org/@sap/cds-dk/-/cds-dk-9.8.2.tgz", - "integrity": "sha512-PGynoQpOCD8Nsr0EEzsEHWg8szYBLVtVd6byxvaaAPYBkNAj2WA+kin6gWaDTiyG3kJa0BSGPk4PCbA2xDoE9A==", - "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/hdi-deploy": "^5", - "axios": "^1", - "express": "^4.22.1 || ^5", - "hdb": "^2.0.0", - "livereload-js": "^4.0.1", - "mustache": "^4.0.1", - "ws": "^8.4.2", - "xml-js": "^1.6.11", - "yaml": "^2" - }, - "bin": { - "cds": "bin/cds.js", - "cds-ts": "bin/cds-ts.js", - "cds-tsx": "bin/cds-tsx.js" - }, - "optionalDependencies": { - "@cap-js/sqlite": ">=1" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/asyncapi": { - "version": "1.0.3", - "integrity": "sha512-vZSWKAe+3qfvZDXV5SSFiObGWmqyS9MDyEADb5PLVT8kzO39qGaSDPv/GzI/gwvRfCayGAjU4ThiBKrFA7Gclg==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "peerDependencies": { - "@sap/cds": ">=7.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/db-service": { - "version": "2.9.0", - "integrity": "sha512-WCXhoqezaF6A5I2l0MNZeHKXXtHRNEq7Rp0R89/uccOHQIx0DuU0U9NuJJPV/1G5RGk2QKQ9VBo/KYn+MZuuNQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "generic-pool": "^3.9.0" - }, - "peerDependencies": { - "@sap/cds": ">=9.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/openapi": { - "version": "1.4.0", - "integrity": "sha512-/LRSwn4SDxAi3qKwl09zoOhEVGaPGlYOPz/0S3UBnaMJVvaLyPiKbbaOtOnrrgulUX5OXt+ujPIQznOsbTzuAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "pluralize": "^8.0.0" - }, - "peerDependencies": { - "@sap/cds": ">=7.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/@cap-js/sqlite": { - "version": "2.2.0", - "integrity": "sha512-FPj+uVU/14vtGUl2P/Q8y7XhZbsLgrCav2O5PjHPXnupegjby4sMJkgVNxVHnkyKPFgO/W8uEsq9r5TU9VPx8w==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@cap-js/db-service": "^2.9.0", - "better-sqlite3": "^12.0.0" - }, - "peerDependencies": { - "@sap/cds": ">=9.8", - "sql.js": "^1.13.0" - }, - "peerDependenciesMeta": { - "sql.js": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@eslint/js": { - "version": "10.0.1", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "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.8.3", - "integrity": "sha512-up/kNVCZK8bOTyye1QN2BjMpH+IgcdyhT3U9mS3TqkpmjNE2JXq8Qz1iNAN+nYcXRWJ1XAUNpgebLfuh/49MGQ==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/cds-compiler": "^6.4", - "@sap/cds-fiori": "^2", - "express": "^4.22.1 || ^5", - "js-yaml": "^4.1.1" - }, - "bin": { - "cds-deploy": "bin/deploy.js", - "cds-serve": "bin/serve.js" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@eslint/js": "^9 || ^10", - "tar": "^7.5.6" - }, - "peerDependenciesMeta": { - "tar": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/cds-compiler": { - "version": "6.8.0", - "integrity": "sha512-yRoTZcH8DFiP4PcEuIHe42YAaNt51V736+6RRr+U4nbO91sz36L8be2djcE8n760t8IbqLxzZ/UkATiL0fjRmA==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "bin": { - "cdsc": "bin/cdsc.js", - "cdshi": "bin/cdshi.js", - "cdsse": "bin/cdsse.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/cds-fiori": { - "version": "2.3.0", - "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "peerDependencies": { - "@sap/cds": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/cds-mtxs": { - "version": "3.8.1", - "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", - "dev": true, - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/hdi-deploy": "^5" - }, - "bin": { - "cds-mtx": "bin/cds-mtx.js", - "cds-mtx-migrate": "bin/cds-mtx-migrate.js" - }, - "peerDependencies": { - "@sap/cds": "^9" - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/hdi": { - "version": "4.8.0", - "integrity": "sha512-tkJmY2ffm6mt4/LFwRBihlQkMxNAXa3ngvRe2N/6+qLIsUNdrH/M03S5mkygXq56K+KoVVZYuradajCusMWwsw==", - "dev": true, - "license": "See LICENSE file", - "dependencies": { - "async": "^3.2.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.5", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/hdi-deploy": { - "version": "5.6.1", - "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", - "dev": true, - "license": "See LICENSE file", - "dependencies": { - "@sap/hdi": "^4.8.0", - "@sap/xsenv": "^6.0.0", - "async": "^3.2.6", - "dotenv": "^16.4.5", - "handlebars": "^4.7.8", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=18.x" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.6", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/@sap/xsenv": { - "version": "6.1.0", - "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", - "dev": true, - "license": "SEE LICENSE IN LICENSE file", - "dependencies": { - "debug": "4.4.3", - "node-cache": "^5.1.2", - "verror": "1.10.1" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/accepts": { - "version": "2.0.0", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, - "license": "MIT", - "dependencies": { - "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/assert-plus": { - "version": "1.0.0", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/async": { - "version": "3.2.6", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "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.13.6", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/base64-js": { - "version": "1.5.1", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "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", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/better-sqlite3": { - "version": "12.8.0", - "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - }, - "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x" - } - }, - "node_modules/@sap/cds-dk/node_modules/bindings": { - "version": "1.5.0", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/bl": { - "version": "4.1.0", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/body-parser": { - "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": "^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": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/braces": { - "version": "3.0.3", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/buffer": { - "version": "5.7.1", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "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", - "optional": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/@sap/cds-dk/node_modules/bytes": { - "version": "3.1.2", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/call-bound": { - "version": "1.0.4", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "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/@sap/cds-dk/node_modules/chownr": { - "version": "1.1.4", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/clone": { - "version": "2.1.2", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "dev": true, - "license": "MIT", - "engines": { - "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": "1.0.1", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/content-type": { - "version": "1.0.5", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/cookie": { - "version": "0.7.2", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/cookie-signature": { - "version": "1.2.2", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/core-util-is": { - "version": "1.0.2", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/debug": { - "version": "4.4.3", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/decompress-response": { - "version": "6.0.0", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sap/cds-dk/node_modules/deep-extend": { - "version": "0.6.0", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/detect-libc": { - "version": "2.1.2", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/dotenv": { - "version": "16.6.1", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/@sap/cds-dk/node_modules/dunder-proto": { - "version": "1.0.1", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/ee-first": { - "version": "1.1.1", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/encodeurl": { - "version": "2.0.0", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/end-of-stream": { - "version": "1.4.5", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/es-define-property": { - "version": "1.0.1", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/es-errors": { - "version": "1.3.0", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/es-object-atoms": { - "version": "1.1.1", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/etag": { - "version": "1.8.1", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/expand-template": { - "version": "2.0.3", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "license": "(MIT OR WTFPL)", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sap/cds-dk/node_modules/express": { - "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/extsprintf": { - "version": "1.4.1", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/file-uri-to-path": { - "version": "1.0.0", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/fill-range": { - "version": "7.1.1", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sap/cds-dk/node_modules/finalhandler": { - "version": "2.1.1", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "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": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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.5", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "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" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/forwarded": { - "version": "0.2.0", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/fresh": { - "version": "2.0.0", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/fs-constants": { - "version": "1.0.0", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/function-bind": { - "version": "1.1.2", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/generic-pool": { - "version": "3.9.0", - "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/@sap/cds-dk/node_modules/get-intrinsic": { - "version": "1.3.0", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "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/@sap/cds-dk/node_modules/get-proto": { - "version": "1.0.1", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/github-from-package": { - "version": "0.0.0", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/gopd": { - "version": "1.2.0", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/handlebars": { - "version": "4.7.8", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/has-symbols": { - "version": "1.1.0", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/hdb": { - "version": "2.27.1", - "integrity": "sha512-xYL/W+fq2TyGHyzm8muolQnw8tdh4+2NQ8mQP2FpLSuhfJ8l0jQNSUZoAXic7NfMEan1Jvf8V1L4blwkgTc6+A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "iconv-lite": "0.7.0" - }, - "engines": { - "node": ">= 18" - }, - "optionalDependencies": { - "lz4-wasm-nodejs": "0.9.2" - } - }, - "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.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": { - "version": "1.2.1", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "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": "BSD-3-Clause", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/inherits": { - "version": "2.0.4", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@sap/cds-dk/node_modules/ini": { - "version": "1.3.8", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/ipaddr.js": { - "version": "1.9.1", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/@sap/cds-dk/node_modules/is-number": { - "version": "7.0.0", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/is-promise": { - "version": "4.0.0", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/js-yaml": { - "version": "4.1.1", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@sap/cds-dk/node_modules/livereload-js": { - "version": "4.0.2", - "integrity": "sha512-Fy7VwgQNiOkynYyNBTo3v9hQUhcW5pFAheJN148+DTgpShjsy/22pLHKKwDK5v0kOsZsJBK+6q1PMgLvRmrwFQ==", - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/media-typer": { - "version": "1.1.0", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/merge-descriptors": { - "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/micromatch": { - "version": "4.0.8", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@sap/cds-dk/node_modules/mime-db": { - "version": "1.54.0", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/mime-types": { - "version": "3.0.2", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/mimic-response": { - "version": "3.1.0", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sap/cds-dk/node_modules/minimist": { - "version": "1.2.8", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/mkdirp-classic": { - "version": "0.5.3", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/ms": { - "version": "2.1.3", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/mustache": { - "version": "4.2.0", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", - "dev": true, - "license": "MIT", - "bin": { - "mustache": "bin/mustache" - } - }, - "node_modules/@sap/cds-dk/node_modules/napi-build-utils": { - "version": "2.0.0", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/negotiator": { - "version": "1.0.0", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/neo-async": { - "version": "2.6.2", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/node-abi": { - "version": "3.89.0", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@sap/cds-dk/node_modules/node-cache": { - "version": "5.1.2", - "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "2.x" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/object-inspect": { - "version": "1.13.4", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/on-finished": { - "version": "2.4.1", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/once": { - "version": "1.4.0", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/@sap/cds-dk/node_modules/parseurl": { - "version": "1.3.3", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/path-to-regexp": { - "version": "8.3.0", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/pluralize": { - "version": "8.0.0", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "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, - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@sap/cds-dk/node_modules/proxy-addr": { - "version": "2.0.7", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "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.4", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/@sap/cds-dk/node_modules/qs": { - "version": "6.15.0", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/range-parser": { - "version": "1.2.1", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/raw-body": { - "version": "3.0.2", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/@sap/cds-dk/node_modules/rc": { - "version": "1.2.8", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "optional": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/@sap/cds-dk/node_modules/readable-stream": { - "version": "3.6.2", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "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==", - "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", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/safer-buffer": { - "version": "2.1.2", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/sax": { - "version": "1.6.0", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/semver": { - "version": "7.7.4", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@sap/cds-dk/node_modules/send": { - "version": "1.2.1", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "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": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/serve-static": { - "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": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@sap/cds-dk/node_modules/setprototypeof": { - "version": "1.2.0", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, - "license": "ISC" - }, - "node_modules/@sap/cds-dk/node_modules/side-channel": { - "version": "1.1.0", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "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/@sap/cds-dk/node_modules/side-channel-list": { - "version": "1.0.0", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@sap/cds-dk/node_modules/side-channel-map": { - "version": "1.0.1", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "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/@sap/cds-dk/node_modules/side-channel-weakmap": { - "version": "1.0.2", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "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/@sap/cds-dk/node_modules/simple-concat": { - "version": "1.0.1", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "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", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/simple-get": { - "version": "4.0.1", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "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", - "optional": true, - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/source-map": { - "version": "0.6.1", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/statuses": { - "version": "2.0.2", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/string_decoder": { - "version": "1.3.0", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/strip-json-comments": { - "version": "2.0.1", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/tar-fs": { - "version": "2.1.4", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/@sap/cds-dk/node_modules/tar-stream": { - "version": "2.2.0", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sap/cds-dk/node_modules/to-regex-range": { - "version": "5.0.1", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/toidentifier": { - "version": "1.0.1", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/tunnel-agent": { - "version": "0.6.0", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@sap/cds-dk/node_modules/type-is": { - "version": "2.0.1", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@sap/cds-dk/node_modules/uglify-js": { - "version": "3.19.3", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/unpipe": { - "version": "1.0.0", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/util-deprecate": { - "version": "1.0.2", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@sap/cds-dk/node_modules/vary": { - "version": "1.1.2", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@sap/cds-dk/node_modules/verror": { - "version": "1.10.1", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/@sap/cds-dk/node_modules/wordwrap": { - "version": "1.0.0", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sap/cds-dk/node_modules/wrappy": { - "version": "1.0.2", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@sap/cds-dk/node_modules/ws": { - "version": "8.20.0", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@sap/cds-dk/node_modules/xml-js": { - "version": "1.6.11", - "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "sax": "^1.2.4" - }, - "bin": { - "xml-js": "bin/cli.js" - } - }, - "node_modules/@sap/cds-dk/node_modules/yaml": { - "version": "2.8.3", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/@sap/cds-fiori": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sap/cds-fiori/-/cds-fiori-2.3.0.tgz", - "integrity": "sha512-6oWov+DSpFrSTgxXR0dZhak6aZ/IVRZvaHERMi0EgSTzIJdlvZlpw3Kf18ePMcTrRrtEXwD4RIjKt8pbs0g2Hg==", - "license": "SEE LICENSE IN LICENSE", - "peerDependencies": { - "@sap/cds": ">=8" - } - }, - "node_modules/@sap/cds-mtxs": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@sap/cds-mtxs/-/cds-mtxs-3.8.1.tgz", - "integrity": "sha512-FUm1of08WsZ1O3eur7OGj7ZYp9Gy3RuYEGBZK+zosSPyJ3QrBhBzKwYvbh2DJdbv3P/xuk46NGg5yz914qZxrw==", - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "@sap/hdi-deploy": "^5" - }, - "bin": { - "cds-mtx": "bin/cds-mtx.js", - "cds-mtx-migrate": "bin/cds-mtx-migrate.js" - }, - "peerDependencies": { - "@sap/cds": "^9" - } - }, - "node_modules/@sap/hdi": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@sap/hdi/-/hdi-4.8.0.tgz", - "integrity": "sha512-tkJmY2ffm6mt4/LFwRBihlQkMxNAXa3ngvRe2N/6+qLIsUNdrH/M03S5mkygXq56K+KoVVZYuradajCusMWwsw==", - "license": "See LICENSE file", - "dependencies": { - "async": "^3.2.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.5", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/hdi-deploy": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@sap/hdi-deploy/-/hdi-deploy-5.6.1.tgz", - "integrity": "sha512-+qQ7qwG8lko303L5yRj2dud/nDAVuVblV/mmzJT44oPbF0Nry18eD2LUS23hFeuxjRa7rYK5YKQ8ffGgWxVrYQ==", - "license": "See LICENSE file", - "dependencies": { - "@sap/hdi": "^4.8.0", - "@sap/xsenv": "^6.0.0", - "async": "^3.2.6", - "dotenv": "^16.4.5", - "handlebars": "^4.7.8", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=18.x" - }, - "peerDependencies": { - "@sap/hana-client": "^2 >= 2.6", - "hdb": "^2 || ^0" - }, - "peerDependenciesMeta": { - "@sap/hana-client": { - "optional": true - }, - "hdb": { - "optional": true - } - } - }, - "node_modules/@sap/xsenv": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@sap/xsenv/-/xsenv-6.1.0.tgz", - "integrity": "sha512-vlW4Zad3uiDqHtnYdQ0TsEIH8VIO4HmPGDowfBL5dIcHPmeKDISEQ9ibeHL5FkceqvYcXJEQAVZ5/hsHDqlXZg==", - "license": "SEE LICENSE IN LICENSE file", - "dependencies": { - "debug": "4.4.3", - "node-cache": "^5.1.2", - "verror": "1.10.1" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || ^24.0.0" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "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": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "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/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "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": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/handlebars": { - "version": "4.7.9", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "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/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "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/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" - }, - "node_modules/node-cache": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", - "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", - "license": "MIT", - "dependencies": { - "clone": "2.x" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.1.tgz", - "integrity": "sha512-fvU78fIjZ+SBM9YwCknCvKOUKkLVqtWDVctl0s7xIqfmfb38t2TT4ZU2gHm+Z8xGwgW+QWEU3oQSAzIbo89Ggw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "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/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "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": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "license": "MIT" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - } - } -} From 97cc9e03b2c2d01de6e99166ba2bf418c3ed7564 Mon Sep 17 00:00:00 2001 From: Marvin Lindner Date: Wed, 8 Apr 2026 14:16:14 +0200 Subject: [PATCH 39/39] revert --- samples/bookshop/pom.xml | 4 ++-- samples/bookshop/srv/pom.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/bookshop/pom.xml b/samples/bookshop/pom.xml index c09bda711..a4fe276bc 100644 --- a/samples/bookshop/pom.xml +++ b/samples/bookshop/pom.xml @@ -47,8 +47,8 @@ com.sap.cds - cds-feature-attachments-oss - 1.4.0-SNAPSHOT + cds-feature-attachments + 1.4.1 diff --git a/samples/bookshop/srv/pom.xml b/samples/bookshop/srv/pom.xml index 4d51762b6..d480ddd60 100644 --- a/samples/bookshop/srv/pom.xml +++ b/samples/bookshop/srv/pom.xml @@ -64,7 +64,7 @@ com.sap.cds - cds-feature-attachments-oss + cds-feature-attachments @@ -173,4 +173,4 @@
-
\ No newline at end of file +