diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/AICoreService.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/AICoreService.java deleted file mode 100644 index 8a2e24c..0000000 --- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/AICoreService.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * © 2026 SAP SE or an SAP affiliate company and cds-ai contributors. - */ -package com.sap.cds.feature.aicore.api; - -import com.sap.cds.services.cds.RemoteService; -import com.sap.cloud.sdk.services.openapi.apache.apiclient.ApiClient; - -/** - * CAP service contract for SAP AI Core integration. - * - *

The service exposes resource-group, configuration and deployment lifecycle as CDS entities - * (see {@link #RESOURCE_GROUPS}, {@link #DEPLOYMENTS}, {@link #CONFIGURATIONS}) and additionally - * provides programmatic helpers to: - * - *

- * - *

The implementation is tenant-aware: it reads the current tenant from the {@code - * RequestContext}. Callers do not need to pass tenant identifiers explicitly. - */ -public interface AICoreService extends RemoteService { - - /** Default service name under which an instance is registered in the service catalog. */ - String DEFAULT_NAME = "AICoreService$Default"; - - /** Qualified name of the {@code resourceGroups} entity exposed by this service. */ - String RESOURCE_GROUPS = "AICore.resourceGroups"; - - /** Qualified name of the {@code deployments} entity exposed by this service. */ - String DEPLOYMENTS = "AICore.deployments"; - - /** Qualified name of the {@code configurations} entity exposed by this service. */ - String CONFIGURATIONS = "AICore.configurations"; - - /** - * Returns the AI Core resource group ID associated with the current tenant. - * - *

When multi-tenancy is disabled the configured default resource group is returned. When - * enabled, the resource group is looked up by the {@code ext.ai.sap.com/CDS_TENANT_ID} label and - * created on first call if it does not exist. - * - * @return the AI Core resource group ID for the current tenant - */ - String resourceGroup(); - - /** - * Returns the AI Core resource group ID associated with the given tenant. - * - *

This variant is used during subscribe/unsubscribe flows where the tenant ID is explicitly - * available from the context rather than the current request. - * - * @param tenantId the CDS tenant identifier - * @return the AI Core resource group ID for the specified tenant - */ - String resourceGroupForTenant(String tenantId); - - /** - * Returns the deployment ID for the given model spec inside the given resource group. - * - *

Looks up an existing RUNNING/PENDING deployment that matches the spec, otherwise creates a - * configuration (if missing) and a new deployment, then polls until the deployment reaches - * RUNNING. Results are cached per {@code (resourceGroupId, configurationName)} pair. - * - * @param resourceGroupId the AI Core resource group to operate in - * @param spec the deployment specification (scenario, executable, configuration name and - * existing-match predicate) - * @return the deployment ID - */ - String deploymentId(String resourceGroupId, ModelDeploymentSpec spec); - - /** - * Returns an {@link ApiClient} preconfigured with the inference destination for the given - * deployment, suitable for constructing foundation-model SDK clients. - * - * @param resourceGroupId the AI Core resource group containing the deployment - * @param deploymentId the deployment ID returned by {@link #deploymentId(String, - * ModelDeploymentSpec)} - * @return a configured {@link ApiClient} pointing at the deployment's inference endpoint - */ - ApiClient inferenceClient(String resourceGroupId, String deploymentId); -} diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/DeploymentIdContext.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/DeploymentIdContext.java index 4fa04d9..04857c0 100644 --- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/DeploymentIdContext.java +++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/DeploymentIdContext.java @@ -9,9 +9,9 @@ /** * Typed {@link EventContext} for the {@code deploymentId} event. * - *

Emitted by {@link AICoreService#deploymentId(String, ModelDeploymentSpec)} to resolve (or - * create) a deployment matching the given spec inside the given resource group. The ON handler - * performs cache lookup, retry, configuration creation, deployment creation and polling. + *

Emitted on the AI Core service to resolve (or create) a deployment matching the given spec + * inside the given resource group. The ON handler performs cache lookup, retry, configuration + * creation, deployment creation and polling. */ @EventName(DeploymentIdContext.EVENT) public interface DeploymentIdContext extends EventContext { diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/InferenceClientContext.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/InferenceClientContext.java index 1b3095e..8ed8710 100644 --- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/InferenceClientContext.java +++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/InferenceClientContext.java @@ -10,8 +10,8 @@ /** * Typed {@link EventContext} for the {@code inferenceClient} event. * - *

Emitted by {@link AICoreService#inferenceClient(String, String)} to build an {@link ApiClient} - * preconfigured with the inference destination for the given deployment. + *

Emitted on the AI Core service to build an {@link ApiClient} preconfigured with the inference + * destination for the given deployment. */ @EventName(InferenceClientContext.EVENT) public interface InferenceClientContext extends EventContext { diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/ModelDeploymentSpec.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/ModelDeploymentSpec.java index a51d1b6..72b29f3 100644 --- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/ModelDeploymentSpec.java +++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/ModelDeploymentSpec.java @@ -9,8 +9,8 @@ import java.util.function.Predicate; /** - * Describes a target AI Core deployment used by {@link AICoreService#deploymentId(String, - * ModelDeploymentSpec)} to look up or create a deployment inside a resource group. + * Describes a target AI Core deployment used by the {@code deploymentId} event to look up or create + * a deployment inside a resource group. * *

The spec carries the AI Core scenario/executable identification, the human-readable * configuration name (used as a stable key for caching and idempotent reuse), the parameter diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/ResourceGroupContext.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/ResourceGroupContext.java index 19a2a82..9d08328 100644 --- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/ResourceGroupContext.java +++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/api/ResourceGroupContext.java @@ -9,9 +9,9 @@ /** * Typed {@link EventContext} for the {@code resourceGroup} event. * - *

Emitted by {@link AICoreService#resourceGroup()} to resolve the AI Core resource group ID for - * the current tenant. In multi-tenancy mode, the resource group is created on-demand if it does not - * exist. In single-tenancy mode, the configured default resource group is returned. + *

Emitted on the AI Core service to resolve the AI Core resource group ID for the current + * tenant. In multi-tenancy mode, the resource group is created on-demand if it does not exist. In + * single-tenancy mode, the configured default resource group is returned. * *

If {@link #getTenantId()} is non-null, the handler uses the explicit tenant ID. Otherwise, the * current tenant is read from the {@code RequestContext}. diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/AICoreServiceConfiguration.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/AICoreServiceConfiguration.java index 202ed8d..8ad1bda 100644 --- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/AICoreServiceConfiguration.java +++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/AICoreServiceConfiguration.java @@ -7,7 +7,6 @@ import com.sap.ai.sdk.core.client.ConfigurationApi; import com.sap.ai.sdk.core.client.DeploymentApi; import com.sap.ai.sdk.core.client.ResourceGroupApi; -import com.sap.cds.feature.aicore.api.AICoreService; import com.sap.cds.feature.aicore.core.handler.AICoreApiHandler; import com.sap.cds.feature.aicore.core.handler.AICoreSetupHandler; import com.sap.cds.feature.aicore.core.handler.ActionHandler; @@ -17,7 +16,9 @@ import com.sap.cds.feature.aicore.core.handler.MockAICoreSetupHandler; import com.sap.cds.feature.aicore.core.handler.MockEntityHandler; import com.sap.cds.feature.aicore.core.handler.ResourceGroupHandler; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; import com.sap.cds.services.environment.CdsProperties; +import com.sap.cds.services.environment.CdsProperties.Remote.RemoteServiceConfig; import com.sap.cds.services.mt.DeploymentService; import com.sap.cds.services.runtime.CdsRuntime; import com.sap.cds.services.runtime.CdsRuntimeConfiguration; @@ -27,13 +28,16 @@ import org.slf4j.LoggerFactory; /** - * {@link CdsRuntimeConfiguration} that wires the {@code AICore} service and its event handlers into - * the CAP Java runtime. + * {@link CdsRuntimeConfiguration} that wires the {@code AICore} remote service and its event + * handlers into the CAP Java runtime. * - *

Detects the presence of an SAP AI Core service binding (either a regular service binding or - * the {@code AICORE_SERVICE_KEY} environment variable used for hybrid local testing) and registers - * the appropriate handlers. Picked up automatically through {@code ServiceLoader}; applications do - * not need to instantiate this class directly. + *

In the {@link #environment} phase, a {@link RemoteServiceConfig} entry for "AICore" is + * injected into the runtime properties so the framework's {@code RemoteServiceConfiguration} + * auto-creates the service instance from the CDS model. This follows the same pattern used by + * {@code cds-feature-notifications}. + * + *

In the {@link #eventHandlers} phase, production or mock handlers are registered depending on + * whether an AI Core service binding is present. */ public class AICoreServiceConfiguration implements CdsRuntimeConfiguration { @@ -43,34 +47,23 @@ public class AICoreServiceConfiguration implements CdsRuntimeConfiguration { private AICoreClients clients; private DeploymentResolver resolver; - private static boolean hasAICoreModel(CdsRuntime runtime) { - return runtime.getCdsModel().findService("AICore").isPresent(); - } - - private static boolean hasAICoreBinding(CdsRuntime runtime) { - return runtime - .getEnvironment() - .getServiceBindings() - .filter(b -> ServiceBindingUtils.matches(b, "aicore")) - .findFirst() - .isPresent(); - } - /** - * Detects multi-tenancy by checking the standard CAP Java {@code cds.multiTenancy.sidecar.url} - * property or the presence of a {@link DeploymentService} in the service catalog. This aligns - * with the standard CAP Java convention — no custom property flag is needed. + * Injects a {@link RemoteServiceConfig} for "AICore" into the runtime properties. This runs + * before all {@code services()} methods, ensuring the framework's {@code + * RemoteServiceConfiguration} will auto-create a {@code RemoteServiceImpl} for the AICore CDS + * service definition. */ - private static boolean detectMultiTenancy(CdsRuntime runtime) { - CdsProperties props = runtime.getEnvironment().getCdsProperties(); - String sidecarUrl = props.getMultiTenancy().getSidecar().getUrl(); - if (sidecarUrl != null && !sidecarUrl.isBlank()) { - return true; - } - return runtime - .getServiceCatalog() - .getService(DeploymentService.class, DeploymentService.DEFAULT_NAME) - != null; + @Override + public void environment(CdsRuntimeConfigurer configurer) { + RemoteServiceConfig remoteConfig = new RemoteServiceConfig(AICore_.CDS_NAME); + remoteConfig.setModel(AICore_.CDS_NAME); + configurer + .getCdsRuntime() + .getEnvironment() + .getCdsProperties() + .getRemote() + .getServices() + .putIfAbsent(AICore_.CDS_NAME, remoteConfig); } @Override @@ -78,7 +71,7 @@ public void services(CdsRuntimeConfigurer configurer) { CdsRuntime runtime = configurer.getCdsRuntime(); if (!hasAICoreModel(runtime)) { - logger.debug("AICore CDS model not found in runtime model — skipping service registration."); + logger.debug("AICore CDS model not found in runtime model - skipping handler setup."); return; } @@ -96,23 +89,19 @@ public void services(CdsRuntimeConfigurer configurer) { this.clients = new AICoreClients(deploymentApi, configurationApi, resourceGroupApi, sdkService); this.resolver = new DeploymentResolver(config, deploymentApi, resourceGroupApi); - logger.info("Registered AICoreService backed by AI Core binding."); + logger.info("AI Core binding detected - production handlers will be registered."); } else { - logger.info( - "Registered AICoreService (no AI Core binding found — mock handlers will be used)."); + logger.info("No AI Core binding found - mock handlers will be registered."); } - - configurer.service(new AICoreServiceImpl(AICoreService.DEFAULT_NAME, runtime)); } @Override public void eventHandlers(CdsRuntimeConfigurer configurer) { if (config == null) { - return; // No AICore model — services() skipped registration + return; // No AICore model - services() skipped } if (clients != null) { - // Production path: real AI Core binding configurer.eventHandler(new AICoreApiHandler(config, clients, resolver)); configurer.eventHandler(new ResourceGroupHandler(config, clients, resolver)); configurer.eventHandler(new DeploymentHandler(config, clients, resolver)); @@ -125,7 +114,6 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { logger.debug("Registered AI Core setup handler for MTX subscribe/unsubscribe."); } } else { - // Mock path: no AI Core binding MockAICoreApiHandler mockApiHandler = new MockAICoreApiHandler(config); configurer.eventHandler(new MockEntityHandler()); configurer.eventHandler(mockApiHandler); @@ -137,4 +125,29 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { } } } + + private static boolean hasAICoreModel(CdsRuntime runtime) { + return runtime.getCdsModel().findService(AICore_.CDS_NAME).isPresent(); + } + + private static boolean hasAICoreBinding(CdsRuntime runtime) { + return runtime + .getEnvironment() + .getServiceBindings() + .filter(b -> ServiceBindingUtils.matches(b, "aicore")) + .findFirst() + .isPresent(); + } + + private static boolean detectMultiTenancy(CdsRuntime runtime) { + CdsProperties props = runtime.getEnvironment().getCdsProperties(); + String sidecarUrl = props.getMultiTenancy().getSidecar().getUrl(); + if (sidecarUrl != null && !sidecarUrl.isBlank()) { + return true; + } + return runtime + .getServiceCatalog() + .getService(DeploymentService.class, DeploymentService.DEFAULT_NAME) + != null; + } } diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/AICoreServiceImpl.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/AICoreServiceImpl.java deleted file mode 100644 index b1c226c..0000000 --- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/AICoreServiceImpl.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * © 2026 SAP SE or an SAP affiliate company and cds-ai contributors. - */ -package com.sap.cds.feature.aicore.core; - -import com.sap.cds.feature.aicore.api.AICoreService; -import com.sap.cds.feature.aicore.api.DeploymentIdContext; -import com.sap.cds.feature.aicore.api.InferenceClientContext; -import com.sap.cds.feature.aicore.api.ModelDeploymentSpec; -import com.sap.cds.feature.aicore.api.ResourceGroupContext; -import com.sap.cds.services.impl.cds.AbstractCdsDefinedService; -import com.sap.cds.services.runtime.CdsRuntime; -import com.sap.cloud.sdk.services.openapi.apache.apiclient.ApiClient; - -/** - * Production implementation of {@link AICoreService}. - * - *

This class is a pure delegation layer: each API method creates a typed {@link - * com.sap.cds.services.EventContext EventContext}, emits it via the CAP event mechanism, and - * returns the handler's result. All business logic (caching, locking, API calls) lives in the - * registered ON handlers which receive their dependencies via constructor injection. - * - *

Implementation note: This class extends {@code AbstractCdsDefinedService} - * from the CAP Java runtime's internal {@code impl} package. This is necessary because the public - * API ({@code ServiceDelegator}) does not provide CQN execution capabilities or CDS model binding. - * The semi-public {@code AbstractCqnService} (from {@code cds-services-utils}) provides CQN but not - * {@code getDefinition()}. Until a public API alternative exists, this coupling is accepted and - * version-compatibility is verified through integration tests against the CAP Java runtime. - */ -public class AICoreServiceImpl extends AbstractCdsDefinedService implements AICoreService { - - private static final String CDS_DEFINITION_NAME = "AICore"; - - public AICoreServiceImpl(String name, CdsRuntime runtime) { - super(name, CDS_DEFINITION_NAME, runtime); - } - - @Override - public String resourceGroup() { - ResourceGroupContext ctx = ResourceGroupContext.create(); - emit(ctx); - return ctx.getResult(); - } - - @Override - public String resourceGroupForTenant(String tenantId) { - ResourceGroupContext ctx = ResourceGroupContext.create(); - ctx.setTenantId(tenantId); - emit(ctx); - return ctx.getResult(); - } - - @Override - public String deploymentId(String resourceGroupId, ModelDeploymentSpec spec) { - DeploymentIdContext ctx = DeploymentIdContext.create(); - ctx.setResourceGroupId(resourceGroupId); - ctx.setSpec(spec); - emit(ctx); - return ctx.getResult(); - } - - @Override - public ApiClient inferenceClient(String resourceGroupId, String deploymentId) { - InferenceClientContext ctx = InferenceClientContext.create(); - ctx.setResourceGroupId(resourceGroupId); - ctx.setDeploymentId(deploymentId); - emit(ctx); - return ctx.getResult(); - } -} diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/AICoreApiHandler.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/AICoreApiHandler.java index 3ecf8e7..2ab2ee7 100644 --- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/AICoreApiHandler.java +++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/AICoreApiHandler.java @@ -10,7 +10,6 @@ import com.sap.ai.sdk.core.model.AiDeploymentList; import com.sap.ai.sdk.core.model.AiDeploymentResponseWithDetails; import com.sap.ai.sdk.core.model.AiDeploymentStatus; -import com.sap.cds.feature.aicore.api.AICoreService; import com.sap.cds.feature.aicore.api.DeploymentIdContext; import com.sap.cds.feature.aicore.api.InferenceClientContext; import com.sap.cds.feature.aicore.api.ModelDeploymentSpec; @@ -18,6 +17,7 @@ import com.sap.cds.feature.aicore.core.AICoreClients; import com.sap.cds.feature.aicore.core.AICoreConfig; import com.sap.cds.feature.aicore.core.DeploymentResolver; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; import com.sap.cds.services.ErrorStatuses; import com.sap.cds.services.ServiceException; import com.sap.cds.services.handler.EventHandler; @@ -32,13 +32,13 @@ import org.slf4j.LoggerFactory; /** - * ON handler for the {@link AICoreService} API events ({@code resourceGroup}, {@code deploymentId}, + * ON handler for the AI Core service API events ({@code resourceGroup}, {@code deploymentId}, * {@code inferenceClient}). * *

Contains the business logic for deployment discovery/creation and inference client * construction. Resource-group resolution is delegated to {@link DeploymentResolver}. */ -@ServiceName(AICoreService.DEFAULT_NAME) +@ServiceName(AICore_.CDS_NAME) public class AICoreApiHandler implements EventHandler { private static final Logger logger = LoggerFactory.getLogger(AICoreApiHandler.class); diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ActionHandler.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ActionHandler.java index 2c9d1e7..f2ea8ef 100644 --- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ActionHandler.java +++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ActionHandler.java @@ -5,10 +5,10 @@ import com.sap.ai.sdk.core.model.AiDeploymentModificationRequest; import com.sap.ai.sdk.core.model.AiDeploymentTargetStatus; -import com.sap.cds.feature.aicore.api.AICoreService; import com.sap.cds.feature.aicore.core.AICoreClients; import com.sap.cds.feature.aicore.core.AICoreConfig; import com.sap.cds.feature.aicore.core.DeploymentResolver; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; import com.sap.cds.feature.aicore.generated.cds4j.aicore.Deployments; import com.sap.cds.feature.aicore.generated.cds4j.aicore.DeploymentsStopContext; import com.sap.cds.feature.aicore.generated.cds4j.aicore.Deployments_; @@ -19,7 +19,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@ServiceName(AICoreService.DEFAULT_NAME) +@ServiceName(AICore_.CDS_NAME) public class ActionHandler extends AbstractCrudHandler { private static final Logger logger = LoggerFactory.getLogger(ActionHandler.class); diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ConfigurationHandler.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ConfigurationHandler.java index bf4e1d8..01a9f4b 100644 --- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ConfigurationHandler.java +++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ConfigurationHandler.java @@ -7,10 +7,10 @@ import com.sap.ai.sdk.core.model.AiConfigurationBaseData; import com.sap.ai.sdk.core.model.AiConfigurationList; import com.sap.ai.sdk.core.model.AiParameterArgumentBinding; -import com.sap.cds.feature.aicore.api.AICoreService; import com.sap.cds.feature.aicore.core.AICoreClients; import com.sap.cds.feature.aicore.core.AICoreConfig; import com.sap.cds.feature.aicore.core.DeploymentResolver; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; import com.sap.cds.feature.aicore.generated.cds4j.aicore.ArtifactArgumentBinding; import com.sap.cds.feature.aicore.generated.cds4j.aicore.Configurations; import com.sap.cds.feature.aicore.generated.cds4j.aicore.Configurations_; @@ -32,7 +32,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@ServiceName(AICoreService.DEFAULT_NAME) +@ServiceName(AICore_.CDS_NAME) public class ConfigurationHandler extends AbstractCrudHandler { private static final Logger logger = LoggerFactory.getLogger(ConfigurationHandler.class); diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/DeploymentHandler.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/DeploymentHandler.java index 4657bc4..c16f166 100644 --- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/DeploymentHandler.java +++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/DeploymentHandler.java @@ -9,10 +9,10 @@ import com.sap.ai.sdk.core.model.AiDeploymentModificationRequest; import com.sap.ai.sdk.core.model.AiDeploymentResponseWithDetails; import com.sap.ai.sdk.core.model.AiDeploymentTargetStatus; -import com.sap.cds.feature.aicore.api.AICoreService; import com.sap.cds.feature.aicore.core.AICoreClients; import com.sap.cds.feature.aicore.core.AICoreConfig; import com.sap.cds.feature.aicore.core.DeploymentResolver; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; import com.sap.cds.feature.aicore.generated.cds4j.aicore.Deployments; import com.sap.cds.feature.aicore.generated.cds4j.aicore.Deployments_; import com.sap.cds.feature.aicore.generated.cds4j.aicore.ResourceGroups; @@ -36,7 +36,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@ServiceName(AICoreService.DEFAULT_NAME) +@ServiceName(AICore_.CDS_NAME) public class DeploymentHandler extends AbstractCrudHandler { private static final Logger logger = LoggerFactory.getLogger(DeploymentHandler.class); diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/MockAICoreApiHandler.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/MockAICoreApiHandler.java index f5155a2..b8916c1 100644 --- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/MockAICoreApiHandler.java +++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/MockAICoreApiHandler.java @@ -3,12 +3,12 @@ */ package com.sap.cds.feature.aicore.core.handler; -import com.sap.cds.feature.aicore.api.AICoreService; import com.sap.cds.feature.aicore.api.DeploymentIdContext; import com.sap.cds.feature.aicore.api.InferenceClientContext; import com.sap.cds.feature.aicore.api.ModelDeploymentSpec; import com.sap.cds.feature.aicore.api.ResourceGroupContext; import com.sap.cds.feature.aicore.core.AICoreConfig; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; import com.sap.cds.services.ErrorStatuses; import com.sap.cds.services.ServiceException; import com.sap.cds.services.handler.EventHandler; @@ -20,10 +20,10 @@ import org.slf4j.LoggerFactory; /** - * Mock ON handler for the {@link AICoreService} API events when no AI Core binding is available. - * Uses in-memory maps instead of real API calls. + * Mock ON handler for the AI Core service API events when no AI Core binding is available. Uses + * in-memory maps instead of real API calls. */ -@ServiceName(AICoreService.DEFAULT_NAME) +@ServiceName(AICore_.CDS_NAME) public class MockAICoreApiHandler implements EventHandler { private static final Logger logger = LoggerFactory.getLogger(MockAICoreApiHandler.class); diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/MockEntityHandler.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/MockEntityHandler.java index 2ac76ab..a8ea0c9 100644 --- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/MockEntityHandler.java +++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/MockEntityHandler.java @@ -4,7 +4,10 @@ package com.sap.cds.feature.aicore.core.handler; import com.sap.cds.CdsData; -import com.sap.cds.feature.aicore.api.AICoreService; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.Configurations_; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.Deployments_; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.ResourceGroups_; import com.sap.cds.ql.cqn.AnalysisResult; import com.sap.cds.ql.cqn.CqnAnalyzer; import com.sap.cds.ql.cqn.CqnDelete; @@ -25,7 +28,7 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -@ServiceName(AICoreService.DEFAULT_NAME) +@ServiceName(AICore_.CDS_NAME) public class MockEntityHandler implements EventHandler { private final Map> resourceGroups = new ConcurrentHashMap<>(); @@ -34,7 +37,7 @@ public class MockEntityHandler implements EventHandler { // --- Resource Groups --- - @On(entity = AICoreService.RESOURCE_GROUPS) + @On(entity = ResourceGroups_.CDS_NAME) public void readResourceGroups(CdsReadEventContext context) { CqnSelect select = context.getCqn(); CdsModel model = context.getModel(); @@ -61,7 +64,7 @@ public void readResourceGroups(CdsReadEventContext context) { } } - @On(entity = AICoreService.RESOURCE_GROUPS) + @On(entity = ResourceGroups_.CDS_NAME) public void createResourceGroups(CdsCreateEventContext context) { CqnInsert insert = context.getCqn(); List> results = new ArrayList<>(); @@ -76,7 +79,7 @@ public void createResourceGroups(CdsCreateEventContext context) { context.setResult(results); } - @On(entity = AICoreService.RESOURCE_GROUPS) + @On(entity = ResourceGroups_.CDS_NAME) public void updateResourceGroups(CdsUpdateEventContext context) { CqnUpdate update = context.getCqn(); CdsModel model = context.getModel(); @@ -92,7 +95,7 @@ public void updateResourceGroups(CdsUpdateEventContext context) { context.setResult(List.of(CdsData.create(existing))); } - @On(entity = AICoreService.RESOURCE_GROUPS) + @On(entity = ResourceGroups_.CDS_NAME) public void deleteResourceGroups(CdsDeleteEventContext context) { CqnDelete delete = context.getCqn(); CdsModel model = context.getModel(); @@ -105,7 +108,7 @@ public void deleteResourceGroups(CdsDeleteEventContext context) { // --- Deployments --- - @On(entity = AICoreService.DEPLOYMENTS) + @On(entity = Deployments_.CDS_NAME) public void readDeployments(CdsReadEventContext context) { CqnSelect select = context.getCqn(); CdsModel model = context.getModel(); @@ -121,7 +124,7 @@ public void readDeployments(CdsReadEventContext context) { } } - @On(entity = AICoreService.DEPLOYMENTS) + @On(entity = Deployments_.CDS_NAME) public void createDeployments(CdsCreateEventContext context) { CqnInsert insert = context.getCqn(); List> results = new ArrayList<>(); @@ -136,7 +139,7 @@ public void createDeployments(CdsCreateEventContext context) { context.setResult(results); } - @On(entity = AICoreService.DEPLOYMENTS) + @On(entity = Deployments_.CDS_NAME) public void updateDeployments(CdsUpdateEventContext context) { CqnUpdate update = context.getCqn(); CdsModel model = context.getModel(); @@ -152,7 +155,7 @@ public void updateDeployments(CdsUpdateEventContext context) { context.setResult(List.of(CdsData.create(existing))); } - @On(entity = AICoreService.DEPLOYMENTS) + @On(entity = Deployments_.CDS_NAME) public void deleteDeployments(CdsDeleteEventContext context) { CqnDelete delete = context.getCqn(); CdsModel model = context.getModel(); @@ -165,7 +168,7 @@ public void deleteDeployments(CdsDeleteEventContext context) { // --- Configurations --- - @On(entity = AICoreService.CONFIGURATIONS) + @On(entity = Configurations_.CDS_NAME) public void readConfigurations(CdsReadEventContext context) { CqnSelect select = context.getCqn(); CdsModel model = context.getModel(); @@ -181,7 +184,7 @@ public void readConfigurations(CdsReadEventContext context) { } } - @On(entity = AICoreService.CONFIGURATIONS) + @On(entity = Configurations_.CDS_NAME) public void createConfigurations(CdsCreateEventContext context) { CqnInsert insert = context.getCqn(); List> results = new ArrayList<>(); diff --git a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ResourceGroupHandler.java b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ResourceGroupHandler.java index 9471662..567f292 100644 --- a/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ResourceGroupHandler.java +++ b/cds-feature-ai-core/src/main/java/com/sap/cds/feature/aicore/core/handler/ResourceGroupHandler.java @@ -9,10 +9,10 @@ import com.sap.ai.sdk.core.model.BckndResourceGroupPatchRequest; import com.sap.ai.sdk.core.model.BckndResourceGroupsPostRequest; import com.sap.cds.CdsData; -import com.sap.cds.feature.aicore.api.AICoreService; import com.sap.cds.feature.aicore.core.AICoreClients; import com.sap.cds.feature.aicore.core.AICoreConfig; import com.sap.cds.feature.aicore.core.DeploymentResolver; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; import com.sap.cds.feature.aicore.generated.cds4j.aicore.ResourceGroups; import com.sap.cds.feature.aicore.generated.cds4j.aicore.ResourceGroups_; import com.sap.cds.ql.cqn.AnalysisResult; @@ -36,7 +36,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@ServiceName(AICoreService.DEFAULT_NAME) +@ServiceName(AICore_.CDS_NAME) public class ResourceGroupHandler extends AbstractCrudHandler { private static final Logger logger = LoggerFactory.getLogger(ResourceGroupHandler.class); diff --git a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceConfigurationTest.java b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceConfigurationTest.java index c6612a6..fcea483 100644 --- a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceConfigurationTest.java +++ b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceConfigurationTest.java @@ -5,7 +5,8 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.sap.cds.feature.aicore.api.AICoreService; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; +import com.sap.cds.services.cds.RemoteService; import com.sap.cds.services.environment.CdsProperties; import com.sap.cds.services.impl.environment.SimplePropertiesProvider; import com.sap.cds.services.runtime.CdsRuntime; @@ -17,8 +18,8 @@ * CDS model. This verifies the full service registration and handler wiring lifecycle without heavy * Mockito mocks. * - *

Since the test runtime has no service bindings, the configuration always registers an {@link - * AICoreServiceImpl} with mock handlers regardless of environment variables. + *

Since the test runtime has no service bindings, the configuration always registers mock + * handlers regardless of environment variables. */ class AICoreServiceConfigurationTest { @@ -26,15 +27,16 @@ class AICoreServiceConfigurationTest { void noBinding_noMultiTenancy_registersService() { CdsRuntime runtime = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(new CdsProperties())) + .environmentConfigurations() .cdsModel("edmx/csn.json") .serviceConfigurations() .eventHandlerConfigurations() .complete(); - AICoreService service = - runtime.getServiceCatalog().getService(AICoreService.class, AICoreService.DEFAULT_NAME); + RemoteService service = + runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); - assertThat(service).isNotNull().isInstanceOf(AICoreServiceImpl.class); + assertThat(service).isNotNull(); } @Test @@ -48,15 +50,16 @@ void noBinding_withSidecarUrl_registersService() { CdsRuntime runtime = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props)) + .environmentConfigurations() .cdsModel("edmx/csn.json") .serviceConfigurations() .eventHandlerConfigurations() .complete(); - AICoreService service = - runtime.getServiceCatalog().getService(AICoreService.class, AICoreService.DEFAULT_NAME); + RemoteService service = + runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); - assertThat(service).isNotNull().isInstanceOf(AICoreServiceImpl.class); + assertThat(service).isNotNull(); } @Test @@ -67,8 +70,8 @@ void noModel_skipsServiceRegistration() { .eventHandlerConfigurations() .complete(); - AICoreService service = - runtime.getServiceCatalog().getService(AICoreService.class, AICoreService.DEFAULT_NAME); + RemoteService service = + runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); assertThat(service).isNull(); } diff --git a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceImplDeploymentIdTest.java b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceImplDeploymentIdTest.java index 9c66431..e43f64b 100644 --- a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceImplDeploymentIdTest.java +++ b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/AICoreServiceImplDeploymentIdTest.java @@ -23,16 +23,19 @@ import com.sap.ai.sdk.core.model.AiDeploymentList; import com.sap.ai.sdk.core.model.AiDeploymentResponseWithDetails; import com.sap.ai.sdk.core.model.AiDeploymentStatus; -import com.sap.cds.feature.aicore.api.AICoreService; +import com.sap.cds.feature.aicore.api.DeploymentIdContext; import com.sap.cds.feature.aicore.api.ModelDeploymentSpec; +import com.sap.cds.feature.aicore.api.ResourceGroupContext; import com.sap.cds.feature.aicore.core.handler.AICoreApiHandler; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; +import com.sap.cds.services.cds.RemoteService; import com.sap.cds.services.environment.CdsProperties; +import com.sap.cds.services.environment.CdsProperties.Remote.RemoteServiceConfig; import com.sap.cds.services.impl.environment.SimplePropertiesProvider; import com.sap.cds.services.runtime.CdsRuntime; import com.sap.cds.services.runtime.CdsRuntimeConfigurer; import com.sap.cloud.sdk.services.openapi.apache.core.OpenApiRequestException; import java.lang.reflect.Field; -import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.jupiter.api.BeforeEach; @@ -53,7 +56,7 @@ class AICoreServiceImplDeploymentIdTest { private DeploymentApi deploymentApi; private ConfigurationApi configurationApi; private ResourceGroupApi resourceGroupApi; - private AICoreServiceImpl service; + private RemoteService service; private DeploymentResolver resolver; private final ModelDeploymentSpec spec = @@ -64,16 +67,19 @@ private String cacheKey() { } /** - * Creates an {@link AICoreServiceImpl} properly registered with a CDS runtime and the {@link + * Creates a {@link RemoteService} properly registered with a CDS runtime and the {@link * AICoreApiHandler} so that {@code emit()} dispatches to the handler. */ - private AICoreServiceImpl createService(boolean multiTenancy) { - TestPropertiesProvider props = new TestPropertiesProvider(); - props.setProperty("cds.ai.core.maxRetries", 1); - props.setProperty("cds.ai.core.initialDelayMs", 1L); - - CdsRuntimeConfigurer configurer = CdsRuntimeConfigurer.create(props); + private RemoteService createService(boolean multiTenancy) { + CdsProperties props = new CdsProperties(); + RemoteServiceConfig rsConfig = new RemoteServiceConfig(AICore_.CDS_NAME); + rsConfig.setModel(AICore_.CDS_NAME); + props.getRemote().getServices().put(AICore_.CDS_NAME, rsConfig); + + CdsRuntimeConfigurer configurer = + CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props)); configurer.cdsModel("edmx/csn.json"); + configurer.serviceConfigurations(); CdsRuntime runtime = configurer.getCdsRuntime(); AICoreConfig config = new AICoreConfig("default", "cds-", 1, 1L, multiTenancy); @@ -82,11 +88,10 @@ private AICoreServiceImpl createService(boolean multiTenancy) { deploymentApi, configurationApi, resourceGroupApi, mock(AiCoreService.class)); resolver = new DeploymentResolver(config, deploymentApi, resourceGroupApi); - AICoreServiceImpl svc = new AICoreServiceImpl(AICoreService.DEFAULT_NAME, runtime); - configurer.service(svc); configurer.eventHandler(new AICoreApiHandler(config, clients, resolver)); configurer.complete(); - return svc; + + return runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); } @BeforeEach @@ -105,7 +110,7 @@ void cacheHit_runningDeployment_returnsCachedIdWithoutQuery() throws Exception { when(running.getStatus()).thenReturn(AiDeploymentStatus.RUNNING); when(deploymentApi.get(RG, DEPLOYMENT_ID)).thenReturn(running); - String result = service.deploymentId(RG, spec); + String result = emitDeploymentId(service, RG, spec); assertThat(result).isEqualTo(DEPLOYMENT_ID); verify(deploymentApi).get(RG, DEPLOYMENT_ID); @@ -130,7 +135,7 @@ void cacheStale_404OnGet_invalidatesAndReturnsExistingFromQuery() throws Excepti when(deploymentApi.query(eq(RG), any(), any(), eq(SCENARIO), any(), any(), any(), any())) .thenReturn(list); - String result = service.deploymentId(RG, spec); + String result = emitDeploymentId(service, RG, spec); assertThat(result).isEqualTo(otherDeployment); assertThat(getDeploymentCache(resolver)).containsEntry(cacheKey(), otherDeployment); @@ -144,7 +149,7 @@ void cacheStale_5xxOnGet_propagatesAndPreservesCacheEntry() throws Exception { OpenApiRequestException serverError = new OpenApiRequestException("boom").statusCode(503); when(deploymentApi.get(RG, "still-valid-id")).thenThrow(serverError); - assertThatThrownBy(() -> service.deploymentId(RG, spec)).rootCause().isSameAs(serverError); + assertThatThrownBy(() -> emitDeploymentId(service, RG, spec)).rootCause().isSameAs(serverError); assertThat(getDeploymentCache(resolver)).containsEntry(cacheKey(), "still-valid-id"); verify(deploymentApi, never()).query(any(), any(), any(), any(), any(), any(), any(), any()); @@ -162,7 +167,7 @@ void noCache_existingMatchingDeployment_isReusedAndCached() throws Exception { when(deploymentApi.query(eq(RG), any(), any(), eq(SCENARIO), any(), any(), any(), any())) .thenReturn(list); - String result = service.deploymentId(RG, spec); + String result = emitDeploymentId(service, RG, spec); assertThat(result).isEqualTo(DEPLOYMENT_ID); assertThat(getDeploymentCache(resolver)).containsEntry(cacheKey(), DEPLOYMENT_ID); @@ -185,8 +190,8 @@ void secondCallUsesCachedResult_singleQueryToApi() { when(running.getStatus()).thenReturn(AiDeploymentStatus.RUNNING); when(deploymentApi.get(RG, DEPLOYMENT_ID)).thenReturn(running); - String first = service.deploymentId(RG, spec); - String second = service.deploymentId(RG, spec); + String first = emitDeploymentId(service, RG, spec); + String second = emitDeploymentId(service, RG, spec); assertThat(first).isEqualTo(DEPLOYMENT_ID); assertThat(second).isEqualTo(DEPLOYMENT_ID); @@ -197,15 +202,15 @@ void secondCallUsesCachedResult_singleQueryToApi() { @Test void resourceGroupForTenant_nullTenantId_returnsDefault() { - AICoreServiceImpl mtService = createService(true); + RemoteService mtService = createService(true); - String result = mtService.resourceGroupForTenant(null); + String result = emitResourceGroup(mtService, null); assertThat(result).isEqualTo("default"); } @Test void resourceGroupForTenant_multiTenancyDisabled_returnsDefault() { - String result = service.resourceGroupForTenant("any-tenant"); + String result = emitResourceGroup(service, "any-tenant"); assertThat(result).isEqualTo("default"); } @@ -232,7 +237,7 @@ void noCacheNoExistingDeployment_createsNewDeploymentWhenConfigExists() throws E when(runningPoll.getStatus()).thenReturn(AiDeploymentStatus.RUNNING); when(deploymentApi.get(RG, DEPLOYMENT_ID)).thenReturn(runningPoll); - String result = service.deploymentId(RG, spec); + String result = emitDeploymentId(service, RG, spec); assertThat(result).isEqualTo(DEPLOYMENT_ID); assertThat(getDeploymentCache(resolver)).containsEntry(cacheKey(), DEPLOYMENT_ID); @@ -244,6 +249,21 @@ void noCacheNoExistingDeployment_createsNewDeploymentWhenConfigExists() throws E // Helpers // ────────────────────────────────────────────────────────────────────────── + private static String emitDeploymentId(RemoteService svc, String rg, ModelDeploymentSpec spec) { + DeploymentIdContext ctx = DeploymentIdContext.create(); + ctx.setResourceGroupId(rg); + ctx.setSpec(spec); + svc.emit(ctx); + return ctx.getResult(); + } + + private static String emitResourceGroup(RemoteService svc, String tenantId) { + ResourceGroupContext ctx = ResourceGroupContext.create(); + ctx.setTenantId(tenantId); + svc.emit(ctx); + return ctx.getResult(); + } + @SuppressWarnings("unchecked") private static void putInDeploymentCache(DeploymentResolver resolver, String key, String value) throws Exception { @@ -260,27 +280,4 @@ private static Map getDeploymentCache(DeploymentResolver resolve field.setAccessible(true); return ((com.github.benmanes.caffeine.cache.Cache) field.get(resolver)).asMap(); } - - /** Properties provider that allows overriding specific keys for test configuration. */ - private static class TestPropertiesProvider extends SimplePropertiesProvider { - private final Map properties = new HashMap<>(); - - TestPropertiesProvider() { - super(new CdsProperties()); - } - - void setProperty(String key, Object value) { - properties.put(key, value); - } - - @Override - @SuppressWarnings("unchecked") - public T getProperty(String key, Class asClazz, T defaultValue) { - Object value = properties.get(key); - if (value != null && asClazz.isInstance(value)) { - return (T) value; - } - return defaultValue; - } - } } diff --git a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/MockAICoreServiceImplTest.java b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/MockAICoreServiceImplTest.java index c555b58..00919b7 100644 --- a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/MockAICoreServiceImplTest.java +++ b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/MockAICoreServiceImplTest.java @@ -5,9 +5,12 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.sap.cds.feature.aicore.api.AICoreService; +import com.sap.cds.feature.aicore.api.DeploymentIdContext; import com.sap.cds.feature.aicore.api.ModelDeploymentSpec; +import com.sap.cds.feature.aicore.api.ResourceGroupContext; import com.sap.cds.feature.aicore.core.handler.MockAICoreApiHandler; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; +import com.sap.cds.services.cds.RemoteService; import com.sap.cds.services.environment.CdsProperties; import com.sap.cds.services.impl.environment.SimplePropertiesProvider; import com.sap.cds.services.runtime.CdsRuntime; @@ -21,7 +24,7 @@ */ class MockAICoreServiceImplTest { - private AICoreService createMockService(boolean multiTenancy) { + private RemoteService createMockService(boolean multiTenancy) { CdsProperties props = new CdsProperties(); if (multiTenancy) { CdsProperties.MultiTenancy mt = new CdsProperties.MultiTenancy(); @@ -33,55 +36,84 @@ private AICoreService createMockService(boolean multiTenancy) { CdsRuntime runtime = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props)) + .environmentConfigurations() .cdsModel("edmx/csn.json") .serviceConfigurations() .eventHandlerConfigurations() .complete(); - return runtime.getServiceCatalog().getService(AICoreService.class, AICoreService.DEFAULT_NAME); + return runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); } @Test void noMultiTenancy_resourceGroupReturnsDefault() { - AICoreService service = createMockService(false); - assertThat(service.resourceGroup()).isEqualTo("default"); + RemoteService service = createMockService(false); + ResourceGroupContext rgCtx = ResourceGroupContext.create(); + service.emit(rgCtx); + assertThat(rgCtx.getResult()).isEqualTo("default"); } @Test void noMultiTenancy_resourceGroupForTenant_returnsDefault() { - AICoreService service = createMockService(false); - assertThat(service.resourceGroupForTenant("any-tenant")).isEqualTo("default"); + RemoteService service = createMockService(false); + ResourceGroupContext rgCtx = ResourceGroupContext.create(); + rgCtx.setTenantId("any-tenant"); + service.emit(rgCtx); + assertThat(rgCtx.getResult()).isEqualTo("default"); } @Test void multiTenancy_resourceGroupForTenant_returnsPrefixed() { - AICoreService service = createMockService(true); - String rg = service.resourceGroupForTenant("my-tenant"); - assertThat(rg).isEqualTo("cds-my-tenant"); + RemoteService service = createMockService(true); + ResourceGroupContext rgCtx = ResourceGroupContext.create(); + rgCtx.setTenantId("my-tenant"); + service.emit(rgCtx); + assertThat(rgCtx.getResult()).isEqualTo("cds-my-tenant"); } @Test void multiTenancy_resourceGroupForTenant_cachesResult() { - AICoreService service = createMockService(true); - String first = service.resourceGroupForTenant("t1"); - String second = service.resourceGroupForTenant("t1"); + RemoteService service = createMockService(true); + ResourceGroupContext rgCtx1 = ResourceGroupContext.create(); + rgCtx1.setTenantId("t1"); + service.emit(rgCtx1); + String first = rgCtx1.getResult(); + + ResourceGroupContext rgCtx2 = ResourceGroupContext.create(); + rgCtx2.setTenantId("t1"); + service.emit(rgCtx2); + String second = rgCtx2.getResult(); assertThat(first).isEqualTo(second); } @Test void deploymentId_returnsMockId() { - AICoreService service = createMockService(false); + RemoteService service = createMockService(false); var spec = new ModelDeploymentSpec("scenario", "exec", "cfg1", List.of(), d -> true); - String id = service.deploymentId("default", spec); + DeploymentIdContext depCtx = DeploymentIdContext.create(); + depCtx.setResourceGroupId("default"); + depCtx.setSpec(spec); + service.emit(depCtx); + String id = depCtx.getResult(); assertThat(id).startsWith("mock-deployment-"); } @Test void deploymentId_cachesSameResult() { - AICoreService service = createMockService(false); + RemoteService service = createMockService(false); var spec = new ModelDeploymentSpec("scenario", "exec", "cfg1", List.of(), d -> true); - String first = service.deploymentId("default", spec); - String second = service.deploymentId("default", spec); + + DeploymentIdContext depCtx1 = DeploymentIdContext.create(); + depCtx1.setResourceGroupId("default"); + depCtx1.setSpec(spec); + service.emit(depCtx1); + String first = depCtx1.getResult(); + + DeploymentIdContext depCtx2 = DeploymentIdContext.create(); + depCtx2.setResourceGroupId("default"); + depCtx2.setSpec(spec); + service.emit(depCtx2); + String second = depCtx2.getResult(); assertThat(first).isEqualTo(second); } } diff --git a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/ConfigurationHandlerTest.java b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/ConfigurationHandlerTest.java index 7fdb3e2..03d360a 100644 --- a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/ConfigurationHandlerTest.java +++ b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/ConfigurationHandlerTest.java @@ -20,14 +20,14 @@ import com.sap.ai.sdk.core.model.AiConfigurationCreationResponse; import com.sap.ai.sdk.core.model.AiConfigurationList; import com.sap.cds.Result; -import com.sap.cds.feature.aicore.api.AICoreService; import com.sap.cds.feature.aicore.core.AICoreClients; import com.sap.cds.feature.aicore.core.AICoreConfig; -import com.sap.cds.feature.aicore.core.AICoreServiceImpl; import com.sap.cds.feature.aicore.core.DeploymentResolver; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.Configurations_; import com.sap.cds.ql.Insert; import com.sap.cds.ql.Select; -import com.sap.cds.services.environment.CdsProperties; +import com.sap.cds.services.cds.RemoteService; import com.sap.cds.services.impl.environment.SimplePropertiesProvider; import com.sap.cds.services.request.RequestContext; import com.sap.cds.services.runtime.CdsRuntime; @@ -47,7 +47,7 @@ class ConfigurationHandlerTest { private static CdsRuntime runtime; - private static AICoreServiceImpl service; + private static RemoteService service; private static ConfigurationApi configurationApi; private static ResourceGroupApi resourceGroupApi; @@ -57,8 +57,11 @@ static void bootRuntime() { resourceGroupApi = mock(ResourceGroupApi.class); DeploymentApi deploymentApi = mock(DeploymentApi.class); - var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(new CdsProperties())); + var props = HandlerTestUtils.aicoreProperties(); + + var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props)); configurer.cdsModel("edmx/csn.json"); + configurer.serviceConfigurations(); runtime = configurer.getCdsRuntime(); AICoreConfig config = new AICoreConfig("default", "cds-", 10, 300, false); @@ -67,11 +70,11 @@ static void bootRuntime() { deploymentApi, configurationApi, resourceGroupApi, mock(AiCoreService.class)); DeploymentResolver resolver = new DeploymentResolver(config, deploymentApi, resourceGroupApi); - service = new AICoreServiceImpl(AICoreService.DEFAULT_NAME, runtime); - configurer.service(service); configurer.eventHandler(new AICoreApiHandler(config, clients, resolver)); configurer.eventHandler(new ConfigurationHandler(config, clients, resolver)); configurer.complete(); + + service = runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); } @BeforeEach @@ -99,7 +102,7 @@ void onRead_returnsConfigurationsForResourceGroup() { (Function) ctx -> service.run( - Select.from("AICore.configurations") + Select.from(Configurations_.CDS_NAME) .where(c -> c.get("resourceGroup_resourceGroupId").eq("default")))); verify(configurationApi).query(eq("default"), any(), any(), any(), any(), any(), any(), any()); @@ -122,7 +125,7 @@ void onRead_nullResources_returnsEmptyList() { (Function) ctx -> service.run( - Select.from("AICore.configurations") + Select.from(Configurations_.CDS_NAME) .where(c -> c.get("resourceGroup_resourceGroupId").eq("default")))); assertThat(result.list()).isEmpty(); @@ -142,7 +145,7 @@ void onCreate_createsConfiguration() { (Function) ctx -> service.run( - Insert.into("AICore.configurations") + Insert.into(Configurations_.CDS_NAME) .entry( Map.of( "name", "test-config", diff --git a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/DeploymentHandlerTest.java b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/DeploymentHandlerTest.java index 0e47302..c4fad6c 100644 --- a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/DeploymentHandlerTest.java +++ b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/DeploymentHandlerTest.java @@ -21,16 +21,16 @@ import com.sap.ai.sdk.core.model.AiDeploymentModificationRequest; import com.sap.ai.sdk.core.model.AiExecutionStatus; import com.sap.cds.Result; -import com.sap.cds.feature.aicore.api.AICoreService; import com.sap.cds.feature.aicore.core.AICoreClients; import com.sap.cds.feature.aicore.core.AICoreConfig; -import com.sap.cds.feature.aicore.core.AICoreServiceImpl; import com.sap.cds.feature.aicore.core.DeploymentResolver; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.Deployments_; import com.sap.cds.ql.Insert; import com.sap.cds.ql.Update; import com.sap.cds.services.ErrorStatuses; import com.sap.cds.services.ServiceException; -import com.sap.cds.services.environment.CdsProperties; +import com.sap.cds.services.cds.RemoteService; import com.sap.cds.services.impl.environment.SimplePropertiesProvider; import com.sap.cds.services.request.RequestContext; import com.sap.cds.services.runtime.CdsRuntime; @@ -50,7 +50,7 @@ class DeploymentHandlerTest { private static CdsRuntime runtime; - private static AICoreServiceImpl service; + private static RemoteService service; private static DeploymentApi deploymentApi; private static ResourceGroupApi resourceGroupApi; private static ConfigurationApi configurationApi; @@ -61,8 +61,11 @@ static void bootRuntime() { resourceGroupApi = mock(ResourceGroupApi.class); configurationApi = mock(ConfigurationApi.class); - var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(new CdsProperties())); + var props = HandlerTestUtils.aicoreProperties(); + + var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props)); configurer.cdsModel("edmx/csn.json"); + configurer.serviceConfigurations(); runtime = configurer.getCdsRuntime(); AICoreConfig config = new AICoreConfig("default", "cds-", 10, 300, false); @@ -71,11 +74,11 @@ static void bootRuntime() { deploymentApi, configurationApi, resourceGroupApi, mock(AiCoreService.class)); DeploymentResolver resolver = new DeploymentResolver(config, deploymentApi, resourceGroupApi); - service = new AICoreServiceImpl(AICoreService.DEFAULT_NAME, runtime); - configurer.service(service); configurer.eventHandler(new AICoreApiHandler(config, clients, resolver)); configurer.eventHandler(new DeploymentHandler(config, clients, resolver)); configurer.complete(); + + service = runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); } @BeforeEach @@ -98,7 +101,7 @@ void onCreate_createsDeploymentWithConfigurationId() { (Function) ctx -> service.run( - Insert.into("AICore.deployments") + Insert.into(Deployments_.CDS_NAME) .entry( Map.of( "configurationId", "cfg-1", @@ -125,7 +128,7 @@ void onCreate_withTtl_setsTtlOnRequest() { (Function) ctx -> service.run( - Insert.into("AICore.deployments") + Insert.into(Deployments_.CDS_NAME) .entry( Map.of( "configurationId", "cfg-2", @@ -146,7 +149,7 @@ void onUpdate_withTargetStatus_callsModifyWithTargetStatus() { (Function) ctx -> service.run( - Update.entity("AICore.deployments") + Update.entity(Deployments_.CDS_NAME) .where(d -> d.get("id").eq("dep-123")) .data("targetStatus", "STOPPED"))); @@ -164,7 +167,7 @@ void onUpdate_withConfigurationId_callsModifyWithConfigurationId() { (Function) ctx -> service.run( - Update.entity("AICore.deployments") + Update.entity(Deployments_.CDS_NAME) .where(d -> d.get("id").eq("dep-789")) .data("configurationId", "config-456"))); @@ -184,7 +187,7 @@ void onUpdate_withoutTargetStatusOrConfigurationId_throwsBadRequest() { (Function) ctx -> service.run( - Update.entity("AICore.deployments") + Update.entity(Deployments_.CDS_NAME) .where(d -> d.get("id").eq("dep-x")) .data("ttl", "1d")))) .isInstanceOfSatisfying( diff --git a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/HandlerTestUtils.java b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/HandlerTestUtils.java new file mode 100644 index 0000000..a8ac72c --- /dev/null +++ b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/HandlerTestUtils.java @@ -0,0 +1,23 @@ +/* + * © 2026 SAP SE or an SAP affiliate company and cds-ai contributors. + */ +package com.sap.cds.feature.aicore.core.handler; + +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; +import com.sap.cds.services.environment.CdsProperties; +import com.sap.cds.services.environment.CdsProperties.Remote.RemoteServiceConfig; + +/** Shared test utilities for handler tests that boot a CDS runtime with the AICore model. */ +final class HandlerTestUtils { + + private HandlerTestUtils() {} + + /** Creates CdsProperties with the AICore RemoteService configured. */ + static CdsProperties aicoreProperties() { + CdsProperties props = new CdsProperties(); + RemoteServiceConfig rsConfig = new RemoteServiceConfig(AICore_.CDS_NAME); + rsConfig.setModel(AICore_.CDS_NAME); + props.getRemote().getServices().put(AICore_.CDS_NAME, rsConfig); + return props; + } +} diff --git a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/ResourceGroupHandlerTest.java b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/ResourceGroupHandlerTest.java index 4dfc443..abe4335 100644 --- a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/ResourceGroupHandlerTest.java +++ b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/ResourceGroupHandlerTest.java @@ -22,15 +22,15 @@ import com.sap.ai.sdk.core.model.BckndResourceGroupPatchRequest; import com.sap.ai.sdk.core.model.BckndResourceGroupsPostRequest; import com.sap.cds.Result; -import com.sap.cds.feature.aicore.api.AICoreService; import com.sap.cds.feature.aicore.core.AICoreClients; import com.sap.cds.feature.aicore.core.AICoreConfig; -import com.sap.cds.feature.aicore.core.AICoreServiceImpl; import com.sap.cds.feature.aicore.core.DeploymentResolver; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.ResourceGroups_; import com.sap.cds.ql.Insert; import com.sap.cds.ql.Select; import com.sap.cds.ql.Update; -import com.sap.cds.services.environment.CdsProperties; +import com.sap.cds.services.cds.RemoteService; import com.sap.cds.services.impl.environment.SimplePropertiesProvider; import com.sap.cds.services.request.RequestContext; import com.sap.cds.services.runtime.CdsRuntime; @@ -51,7 +51,7 @@ class ResourceGroupHandlerTest { private static CdsRuntime runtime; - private static AICoreServiceImpl service; + private static RemoteService service; private static ResourceGroupApi resourceGroupApi; @BeforeAll @@ -60,8 +60,11 @@ static void bootRuntime() { DeploymentApi deploymentApi = mock(DeploymentApi.class); ConfigurationApi configurationApi = mock(ConfigurationApi.class); - var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(new CdsProperties())); + var props = HandlerTestUtils.aicoreProperties(); + + var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props)); configurer.cdsModel("edmx/csn.json"); + configurer.serviceConfigurations(); runtime = configurer.getCdsRuntime(); AICoreConfig config = new AICoreConfig("default", "cds-", 10, 300, false); @@ -70,11 +73,11 @@ static void bootRuntime() { deploymentApi, configurationApi, resourceGroupApi, mock(AiCoreService.class)); DeploymentResolver resolver = new DeploymentResolver(config, deploymentApi, resourceGroupApi); - service = new AICoreServiceImpl(AICoreService.DEFAULT_NAME, runtime); - configurer.service(service); configurer.eventHandler(new AICoreApiHandler(config, clients, resolver)); configurer.eventHandler(new ResourceGroupHandler(config, clients, resolver)); configurer.complete(); + + service = runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); } @BeforeEach @@ -97,7 +100,7 @@ void onRead_returnsAllResourceGroups() { .requestContext() .run( (Function) - ctx -> service.run(Select.from("AICore.resourceGroups"))); + ctx -> service.run(Select.from(ResourceGroups_.CDS_NAME))); verify(resourceGroupApi).getAll(any(), any(), any(), any(), any(), any(), any()); assertThat(result.list()).hasSize(1); @@ -112,7 +115,7 @@ void onCreate_createsResourceGroup() { (Function) ctx -> service.run( - Insert.into("AICore.resourceGroups") + Insert.into(ResourceGroups_.CDS_NAME) .entry(Map.of("resourceGroupId", "rg-new")))); ArgumentCaptor captor = @@ -129,7 +132,7 @@ void onCreate_withTenantId_setsTenantLabel() { (Function) ctx -> service.run( - Insert.into("AICore.resourceGroups") + Insert.into(ResourceGroups_.CDS_NAME) .entry( Map.of( "resourceGroupId", "rg-tenant", @@ -156,7 +159,7 @@ void onUpdate_withLabels_callsPatchWithLabels() { (Function) ctx -> service.run( - Update.entity("AICore.resourceGroups") + Update.entity(ResourceGroups_.CDS_NAME) .where(d -> d.get("resourceGroupId").eq("rg-upd")) .data("labels", List.of(Map.of("key", "env", "value", "staging"))))); @@ -181,7 +184,7 @@ void onUpdate_withoutLabels_callsPatchWithoutLabels() { (Function) ctx -> service.run( - Update.entity("AICore.resourceGroups") + Update.entity(ResourceGroups_.CDS_NAME) .where(d -> d.get("resourceGroupId").eq("rg-nolabel")) .data("statusMessage", "updated"))); @@ -199,7 +202,7 @@ void onUpdate_withoutLabels_callsPatchWithoutLabels() { class MultiTenancyTests { private static CdsRuntime mtRuntime; - private static AICoreServiceImpl mtService; + private static RemoteService mtService; private static ResourceGroupApi mtResourceGroupApi; @BeforeAll @@ -208,9 +211,11 @@ static void bootMtRuntime() { DeploymentApi deploymentApi = mock(DeploymentApi.class); ConfigurationApi configurationApi = mock(ConfigurationApi.class); - var configurer = - CdsRuntimeConfigurer.create(new SimplePropertiesProvider(new CdsProperties())); + var props = HandlerTestUtils.aicoreProperties(); + + var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props)); configurer.cdsModel("edmx/csn.json"); + configurer.serviceConfigurations(); mtRuntime = configurer.getCdsRuntime(); AICoreConfig config = new AICoreConfig("default", "cds-", 10, 300, true); @@ -220,11 +225,11 @@ static void bootMtRuntime() { DeploymentResolver resolver = new DeploymentResolver(config, deploymentApi, mtResourceGroupApi); - mtService = new AICoreServiceImpl(AICoreService.DEFAULT_NAME, mtRuntime); - configurer.service(mtService); configurer.eventHandler(new AICoreApiHandler(config, clients, resolver)); configurer.eventHandler(new ResourceGroupHandler(config, clients, resolver)); configurer.complete(); + + mtService = mtRuntime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); } @BeforeEach @@ -251,7 +256,7 @@ void readAll_multiTenancy_nonProviderUser_restrictsByCurrentTenant() { .setIsAuthenticated(true)) .run( (Function) - ctx -> mtService.run(Select.from("AICore.resourceGroups"))); + ctx -> mtService.run(Select.from(ResourceGroups_.CDS_NAME))); ArgumentCaptor> selectorCaptor = ArgumentCaptor.forClass(List.class); verify(mtResourceGroupApi) diff --git a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/TenantScopingTest.java b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/TenantScopingTest.java index 078779c..a79326f 100644 --- a/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/TenantScopingTest.java +++ b/cds-feature-ai-core/src/test/java/com/sap/cds/feature/aicore/core/handler/TenantScopingTest.java @@ -18,14 +18,14 @@ import com.sap.ai.sdk.core.model.BckndResourceGroup; import com.sap.ai.sdk.core.model.BckndResourceGroupLabel; import com.sap.cds.Result; -import com.sap.cds.feature.aicore.api.AICoreService; import com.sap.cds.feature.aicore.core.AICoreClients; import com.sap.cds.feature.aicore.core.AICoreConfig; -import com.sap.cds.feature.aicore.core.AICoreServiceImpl; import com.sap.cds.feature.aicore.core.DeploymentResolver; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.Deployments_; import com.sap.cds.ql.Select; import com.sap.cds.services.ServiceException; -import com.sap.cds.services.environment.CdsProperties; +import com.sap.cds.services.cds.RemoteService; import com.sap.cds.services.impl.environment.SimplePropertiesProvider; import com.sap.cds.services.request.RequestContext; import com.sap.cds.services.runtime.CdsRuntime; @@ -44,7 +44,7 @@ class TenantScopingTest { private static CdsRuntime runtime; - private static AICoreServiceImpl service; + private static RemoteService service; private static DeploymentApi deploymentApi; private static ResourceGroupApi resourceGroupApi; @@ -54,8 +54,11 @@ static void bootRuntime() { resourceGroupApi = mock(ResourceGroupApi.class); ConfigurationApi configurationApi = mock(ConfigurationApi.class); - var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(new CdsProperties())); + var props = HandlerTestUtils.aicoreProperties(); + + var configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(props)); configurer.cdsModel("edmx/csn.json"); + configurer.serviceConfigurations(); runtime = configurer.getCdsRuntime(); AICoreConfig config = new AICoreConfig("default", "cds-", 10, 300, true); @@ -64,11 +67,11 @@ static void bootRuntime() { deploymentApi, configurationApi, resourceGroupApi, mock(AiCoreService.class)); DeploymentResolver resolver = new DeploymentResolver(config, deploymentApi, resourceGroupApi); - service = new AICoreServiceImpl(AICoreService.DEFAULT_NAME, runtime); - configurer.service(service); configurer.eventHandler(new AICoreApiHandler(config, clients, resolver)); configurer.eventHandler(new DeploymentHandler(config, clients, resolver)); configurer.complete(); + + service = runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); } @BeforeEach @@ -96,7 +99,7 @@ void matchingTenant_allowsAccess() { (Function) ctx -> service.run( - Select.from("AICore.deployments") + Select.from(Deployments_.CDS_NAME) .where( d -> d.get("resourceGroup_resourceGroupId") @@ -123,7 +126,7 @@ void nonMatchingTenant_throws404() { (Function) ctx -> service.run( - Select.from("AICore.deployments") + Select.from(Deployments_.CDS_NAME) .where( d -> d.get("resourceGroup_resourceGroupId") @@ -147,7 +150,7 @@ void providerUser_bypassesTenantCheck() { (Function) ctx -> service.run( - Select.from("AICore.deployments") + Select.from(Deployments_.CDS_NAME) .where( d -> d.get("resourceGroup_resourceGroupId") @@ -175,7 +178,7 @@ void nullTenantUser_bypassesTenantCheck() { (Function) ctx -> service.run( - Select.from("AICore.deployments") + Select.from(Deployments_.CDS_NAME) .where( d -> d.get("resourceGroup_resourceGroupId") @@ -204,7 +207,7 @@ void noLabelsOnResourceGroup_throws404() { (Function) ctx -> service.run( - Select.from("AICore.deployments") + Select.from(Deployments_.CDS_NAME) .where( d -> d.get("resourceGroup_resourceGroupId") @@ -234,7 +237,7 @@ void emptyLabelsOnResourceGroup_throws404() { (Function) ctx -> service.run( - Select.from("AICore.deployments") + Select.from(Deployments_.CDS_NAME) .where( d -> d.get("resourceGroup_resourceGroupId") diff --git a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RecommendationConfiguration.java b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RecommendationConfiguration.java index 529c7cf..773526e 100644 --- a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RecommendationConfiguration.java +++ b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RecommendationConfiguration.java @@ -3,12 +3,16 @@ */ package com.sap.cds.feature.recommendation; -import com.sap.cds.feature.aicore.api.AICoreService; +import com.sap.cds.feature.aicore.api.DeploymentIdContext; +import com.sap.cds.feature.aicore.api.InferenceClientContext; +import com.sap.cds.feature.aicore.api.ResourceGroupContext; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; import com.sap.cds.feature.recommendation.api.RecommendationClient; import com.sap.cds.feature.recommendation.api.RecommendationClientResolver; import com.sap.cds.feature.recommendation.api.RptInferenceClient; import com.sap.cds.feature.recommendation.api.RptModelSpec; import com.sap.cds.services.ServiceCatalog; +import com.sap.cds.services.cds.RemoteService; import com.sap.cds.services.persistence.PersistenceService; import com.sap.cds.services.runtime.CdsRuntime; import com.sap.cds.services.runtime.CdsRuntimeConfiguration; @@ -27,8 +31,7 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { CdsRuntime runtime = configurer.getCdsRuntime(); ServiceCatalog serviceCatalog = runtime.getServiceCatalog(); - AICoreService aiCoreService = - serviceCatalog.getService(AICoreService.class, AICoreService.DEFAULT_NAME); + RemoteService aiCoreService = serviceCatalog.getService(RemoteService.class, AICore_.CDS_NAME); if (aiCoreService == null) { logger.info("No AICoreService found, skipping Fiori recommendation handler registration."); @@ -47,7 +50,7 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { boolean hasBind = hasAICoreBinding(runtime); // The real resolver is a lambda resolved at prediction time. That's necessary because // resource group and deployment ID are tenant-specific and are only available at - // prediction time from the request context. AICoreService is captured in the closure. + // prediction time from the request context. The RemoteService is captured in the closure. RecommendationClientResolver> clientResolver = hasBind ? keyNames -> resolveRptClient(aiCoreService, keyNames) @@ -68,9 +71,33 @@ private static boolean hasAICoreBinding(CdsRuntime runtime) { } private static RecommendationClient resolveRptClient( - AICoreService service, List keyNames) { - String resourceGroup = service.resourceGroup(); - String deploymentId = service.deploymentId(resourceGroup, RptModelSpec.rpt1()); - return new RptInferenceClient(service.inferenceClient(resourceGroup, deploymentId), keyNames); + RemoteService service, List keyNames) { + ResourceGroupContext rgCtx = ResourceGroupContext.create(); + service.emit(rgCtx); + String resourceGroup = rgCtx.getResult(); + if (resourceGroup == null) { + throw new IllegalStateException("Failed to resolve resource group from AI Core service"); + } + + DeploymentIdContext depCtx = DeploymentIdContext.create(); + depCtx.setResourceGroupId(resourceGroup); + depCtx.setSpec(RptModelSpec.rpt1()); + service.emit(depCtx); + String deploymentId = depCtx.getResult(); + if (deploymentId == null) { + throw new IllegalStateException( + "Failed to resolve deployment ID for resource group: " + resourceGroup); + } + + InferenceClientContext infCtx = InferenceClientContext.create(); + infCtx.setResourceGroupId(resourceGroup); + infCtx.setDeploymentId(deploymentId); + service.emit(infCtx); + if (infCtx.getResult() == null) { + throw new IllegalStateException( + "Failed to create inference client for deployment: " + deploymentId); + } + + return new RptInferenceClient(infCtx.getResult(), keyNames); } } diff --git a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RecommendationContextBuilder.java b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RecommendationContextBuilder.java index 049d6e5..d0c105c 100644 --- a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RecommendationContextBuilder.java +++ b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RecommendationContextBuilder.java @@ -124,10 +124,11 @@ CdsData buildPredictRow(CdsData row) { } Set allowed = new HashSet<>(contextColumns); allowed.addAll(keyNames); - Map predictRow = - allowed.stream() - .filter(row::containsKey) - .collect(HashMap::new, (m, col) -> m.put(col, row.get(col)), HashMap::putAll); + Map predictRow = new HashMap<>(); + allowed.forEach( + col -> { + if (row.containsKey(col)) predictRow.put(col, row.get(col)); + }); return CdsData.create(predictRow); } diff --git a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/api/RptInferenceClient.java b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/api/RptInferenceClient.java index c8058e6..c3e3c43 100644 --- a/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/api/RptInferenceClient.java +++ b/cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/api/RptInferenceClient.java @@ -33,10 +33,19 @@ *

Example usage: * *

{@code
- * AICoreService service = ...;
- * String rg = service.resourceGroup();
- * String deploymentId = service.deploymentId(rg, RptModelSpec.rpt1());
- * RptInferenceClient client = new RptInferenceClient(service.inferenceClient(rg, deploymentId), keyNames);
+ * RemoteService service = runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME);
+ * ResourceGroupContext rgCtx = ResourceGroupContext.create();
+ * service.emit(rgCtx);
+ * String rg = rgCtx.getResult();
+ * DeploymentIdContext depCtx = DeploymentIdContext.create();
+ * depCtx.setResourceGroupId(rg);
+ * depCtx.setSpec(RptModelSpec.rpt1());
+ * service.emit(depCtx);
+ * InferenceClientContext infCtx = InferenceClientContext.create();
+ * infCtx.setResourceGroupId(rg);
+ * infCtx.setDeploymentId(depCtx.getResult());
+ * service.emit(infCtx);
+ * RptInferenceClient client = new RptInferenceClient(infCtx.getResult(), keyNames);
  * List predictions = client.predict(predictionRow, contextRows, List.of("targetColumn"));
  * }
*/ @@ -124,7 +133,7 @@ private static PredictRequestPayload buildRequest( // RPT-1 requires exactly one string-typed index column per row to identify predictions. // When the entity key is composite or non-string, then the index column is - // RptIndexColumns.SYNTHETIC_INDEX_COLUMN and we need to compute the sytheticKey for all rows + // RptIndexColumns.SYNTHETIC_INDEX_COLUMN and we need to compute the syntheticKey for all rows // before sending them to RPT-1. boolean syntheticKeyNeeded = RptIndexColumns.SYNTHETIC_INDEX_COLUMN.equals(indexColumn); var sdkRows = diff --git a/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/FioriRecommendationHandlerTest.java b/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/FioriRecommendationHandlerTest.java index c21c823..b25faa9 100644 --- a/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/FioriRecommendationHandlerTest.java +++ b/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/FioriRecommendationHandlerTest.java @@ -31,11 +31,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; -import org.mockito.junit.jupiter.MockitoExtension; -@ExtendWith(MockitoExtension.class) class FioriRecommendationHandlerTest { private static CdsRuntime runtime; @@ -61,7 +58,7 @@ void setup() { reset(db); when(db.getName()).thenReturn(PersistenceService.DEFAULT_NAME); predictionClient = randomPickClient(); - cut = new FioriRecommendationHandler(keyNames -> predictionClient, db); + cut = new FioriRecommendationHandler((keyNames) -> predictionClient, db); } // ── tests ────────────────────────────────────────────────────────────────── diff --git a/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/RecommendationConfigurationTest.java b/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/RecommendationConfigurationTest.java index 8b3ebce..ee9ee52 100644 --- a/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/RecommendationConfigurationTest.java +++ b/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/RecommendationConfigurationTest.java @@ -5,8 +5,9 @@ import static org.mockito.Mockito.*; -import com.sap.cds.feature.aicore.api.AICoreService; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; import com.sap.cds.services.ServiceCatalog; +import com.sap.cds.services.cds.RemoteService; import com.sap.cds.services.environment.CdsEnvironment; import com.sap.cds.services.persistence.PersistenceService; import com.sap.cds.services.runtime.CdsRuntime; @@ -24,7 +25,7 @@ class RecommendationConfigurationTest { @Mock private CdsRuntime runtime; @Mock private ServiceCatalog serviceCatalog; @Mock private CdsEnvironment environment; - @Mock private AICoreService aiCoreService; + @Mock private RemoteService aiCoreService; @Mock private PersistenceService persistenceService; @Test @@ -33,7 +34,7 @@ void aiCoreServiceFound_registersHandler() { when(runtime.getServiceCatalog()).thenReturn(serviceCatalog); when(runtime.getEnvironment()).thenReturn(environment); when(environment.getServiceBindings()).thenReturn(Stream.empty()); - when(serviceCatalog.getService(AICoreService.class, AICoreService.DEFAULT_NAME)) + when(serviceCatalog.getService(RemoteService.class, AICore_.CDS_NAME)) .thenReturn(aiCoreService); when(serviceCatalog.getService(PersistenceService.class, PersistenceService.DEFAULT_NAME)) .thenReturn(persistenceService); @@ -47,8 +48,7 @@ void aiCoreServiceFound_registersHandler() { void aiCoreServiceNull_doesNotRegisterHandler() { when(configurer.getCdsRuntime()).thenReturn(runtime); when(runtime.getServiceCatalog()).thenReturn(serviceCatalog); - when(serviceCatalog.getService(AICoreService.class, AICoreService.DEFAULT_NAME)) - .thenReturn(null); + when(serviceCatalog.getService(RemoteService.class, AICore_.CDS_NAME)).thenReturn(null); new RecommendationConfiguration().eventHandlers(configurer); diff --git a/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/api/RptInferenceClientTest.java b/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/api/RptInferenceClientTest.java index 8abce82..54586bb 100644 --- a/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/api/RptInferenceClientTest.java +++ b/cds-feature-recommendations/src/test/java/com/sap/cds/feature/recommendation/api/RptInferenceClientTest.java @@ -5,12 +5,40 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.sap.cds.CdsData; +import com.sap.cds.feature.recommendation.RptIndexColumns; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; class RptInferenceClientTest { + @Test + void resolveIndexColumn_singleStringKey_usesItDirectly() { + CdsData row = CdsData.create(Map.of("isbn", "978-3-16")); + assertThat(RptIndexColumns.resolveIndexColumn(List.of("isbn"), row)).isEqualTo("isbn"); + } + + @Test + void resolveIndexColumn_singleUuidKey_usesItDirectly() { + CdsData row = CdsData.create(Map.of("ID", "a009c640-434a-4542-ac68-51b400c880ec")); + assertThat(RptIndexColumns.resolveIndexColumn(List.of("ID"), row)).isEqualTo("ID"); + } + + @Test + void resolveIndexColumn_singleIntegerKey_usesSyntheticColumn() { + CdsData row = CdsData.create(Map.of("order_ID", 42)); + assertThat(RptIndexColumns.resolveIndexColumn(List.of("order_ID"), row)) + .isEqualTo("SAP_RECOMMENDATIONS_ID"); + } + + @Test + void resolveIndexColumn_compositeKey_usesSyntheticColumn() { + CdsData row = CdsData.create(Map.of("order_ID", 1, "item_no", 10)); + assertThat(RptIndexColumns.resolveIndexColumn(List.of("order_ID", "item_no"), row)) + .isEqualTo("SAP_RECOMMENDATIONS_ID"); + } + @Test void computeSyntheticKey_singleKey() { String key = RptInferenceClient.computeSyntheticKey(Map.of("ID", "abc"), List.of("ID")); diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/MtxLifecycleTest.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/MtxLifecycleTest.java index 6e88c40..30e5a4d 100644 --- a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/MtxLifecycleTest.java +++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/MtxLifecycleTest.java @@ -7,8 +7,10 @@ import static org.assertj.core.api.Assertions.assertThatCode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.sap.cds.feature.aicore.api.AICoreService; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; +import com.sap.cds.feature.aicore.api.ResourceGroupContext; import com.sap.cds.feature.aicore.itest.mt.utils.SubscriptionEndpointClient; +import com.sap.cds.services.cds.RemoteService; import com.sap.cds.services.runtime.CdsRuntime; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -57,19 +59,22 @@ void unsubscribe_isIdempotent() throws Exception { @Test void subscribeUnsubscribe_repeatedTwice_completesCleanly() throws Exception { - AICoreService service = getService(); + RemoteService service = getService(); for (int i = 0; i < 2; i++) { subscriptionEndpointClient.subscribeTenant(TENANT); // After subscribe, the service should resolve a resource group for this tenant - String rg = service.resourceGroupForTenant(TENANT); + ResourceGroupContext rgCtx = ResourceGroupContext.create(); + rgCtx.setTenantId(TENANT); + service.emit(rgCtx); + String rg = rgCtx.getResult(); assertThat(rg).isNotNull().isNotBlank(); subscriptionEndpointClient.unsubscribeTenant(TENANT); } } - private AICoreService getService() { - return runtime.getServiceCatalog().getService(AICoreService.class, AICoreService.DEFAULT_NAME); + private RemoteService getService() { + return runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); } } diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/SubscribeUnsubscribeTest.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/SubscribeUnsubscribeTest.java index eb1b9d2..185d387 100644 --- a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/SubscribeUnsubscribeTest.java +++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/SubscribeUnsubscribeTest.java @@ -9,8 +9,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; -import com.sap.cds.feature.aicore.api.AICoreService; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; +import com.sap.cds.feature.aicore.api.ResourceGroupContext; import com.sap.cds.feature.aicore.itest.mt.utils.SubscriptionEndpointClient; +import com.sap.cds.services.cds.RemoteService; import com.sap.cds.services.runtime.CdsRuntime; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -50,12 +52,15 @@ void subscribeTenant_thenServiceIsReachable() throws Exception { @Test void subscribeTenant_createsResourceGroup() throws Exception { - AICoreService service = getService(); + RemoteService service = getService(); subscriptionEndpointClient.subscribeTenant("tenant-3"); // After subscription, the service should be able to resolve a resource group for the tenant - String resourceGroup = service.resourceGroupForTenant("tenant-3"); + ResourceGroupContext rgCtx = ResourceGroupContext.create(); + rgCtx.setTenantId("tenant-3"); + service.emit(rgCtx); + String resourceGroup = rgCtx.getResult(); assertThat(resourceGroup).isNotNull().isNotBlank(); } @@ -82,7 +87,7 @@ void tearDown() { } } - private AICoreService getService() { - return runtime.getServiceCatalog().getService(AICoreService.class, AICoreService.DEFAULT_NAME); + private RemoteService getService() { + return runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); } } diff --git a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/TenantIsolationTest.java b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/TenantIsolationTest.java index 7aa06b8..d30830f 100644 --- a/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/TenantIsolationTest.java +++ b/integration-tests/mtx-local/srv/src/test/java/com/sap/cds/feature/aicore/itest/mt/TenantIsolationTest.java @@ -6,9 +6,11 @@ import static org.assertj.core.api.Assertions.assertThat; import com.fasterxml.jackson.databind.ObjectMapper; -import com.sap.cds.feature.aicore.api.AICoreService; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; +import com.sap.cds.feature.aicore.api.ResourceGroupContext; import com.sap.cds.feature.aicore.core.AICoreConfig; import com.sap.cds.feature.aicore.itest.mt.utils.SubscriptionEndpointClient; +import com.sap.cds.services.cds.RemoteService; import com.sap.cds.services.environment.CdsProperties; import com.sap.cds.services.runtime.CdsRuntime; import org.junit.jupiter.api.AfterEach; @@ -44,13 +46,20 @@ void multiTenancyEnabled() { @Test void differentTenants_getDifferentResourceGroups() throws Exception { - AICoreService service = getService(); + RemoteService service = getService(); subscriptionEndpointClient.subscribeTenant("tenant-1"); subscriptionEndpointClient.subscribeTenant("tenant-2"); - String rg1 = service.resourceGroupForTenant("tenant-1"); - String rg2 = service.resourceGroupForTenant("tenant-2"); + ResourceGroupContext rgCtx1 = ResourceGroupContext.create(); + rgCtx1.setTenantId("tenant-1"); + service.emit(rgCtx1); + String rg1 = rgCtx1.getResult(); + + ResourceGroupContext rgCtx2 = ResourceGroupContext.create(); + rgCtx2.setTenantId("tenant-2"); + service.emit(rgCtx2); + String rg2 = rgCtx2.getResult(); assertThat(rg1).isNotNull(); assertThat(rg2).isNotNull(); @@ -60,16 +69,19 @@ void differentTenants_getDifferentResourceGroups() throws Exception { @Test void resourceGroupPrefix_applied() throws Exception { AICoreConfig config = getConfig(); - AICoreService service = getService(); + RemoteService service = getService(); subscriptionEndpointClient.subscribeTenant("tenant-1"); - String rg = service.resourceGroupForTenant("tenant-1"); + ResourceGroupContext rgCtx = ResourceGroupContext.create(); + rgCtx.setTenantId("tenant-1"); + service.emit(rgCtx); + String rg = rgCtx.getResult(); assertThat(rg).startsWith(config.resourceGroupPrefix()); } - private AICoreService getService() { - return runtime.getServiceCatalog().getService(AICoreService.class, AICoreService.DEFAULT_NAME); + private RemoteService getService() { + return runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); } private AICoreConfig getConfig() { diff --git a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/AICoreServiceTest.java b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/AICoreServiceTest.java index ce0c89b..795d6f8 100644 --- a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/AICoreServiceTest.java +++ b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/AICoreServiceTest.java @@ -5,9 +5,11 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.sap.cds.feature.aicore.api.AICoreService; +import com.sap.cds.feature.aicore.api.DeploymentIdContext; +import com.sap.cds.feature.aicore.api.ResourceGroupContext; import com.sap.cds.feature.aicore.core.AICoreConfig; import com.sap.cds.feature.recommendation.api.RptModelSpec; +import com.sap.cds.services.cds.RemoteService; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -23,15 +25,18 @@ void prepareDeployment() { @Test void service_isRegistered() { assertThat(getAICoreService()).isNotNull(); - assertThat(getAICoreService()).isInstanceOf(AICoreService.class); + assertThat(getAICoreService()).isInstanceOf(RemoteService.class); } @Test void resourceGroupForTenant_singleTenancy_returnsDefault() { AICoreConfig config = getAICoreConfig(); - AICoreService service = getAICoreService(); + RemoteService service = getAICoreService(); if (!config.multiTenancyEnabled()) { - String result = service.resourceGroupForTenant("any-tenant"); + ResourceGroupContext rgCtx = ResourceGroupContext.create(); + rgCtx.setTenantId("any-tenant"); + service.emit(rgCtx); + String result = rgCtx.getResult(); assertThat(result).isEqualTo(config.defaultResourceGroup()); } } @@ -39,29 +44,43 @@ void resourceGroupForTenant_singleTenancy_returnsDefault() { @Test void resourceGroupForTenant_multiTenancy_createsOrFindsGroup() { AICoreConfig config = getAICoreConfig(); - AICoreService service = getAICoreService(); + RemoteService service = getAICoreService(); if (config.multiTenancyEnabled()) { String tenantId = "itest-svc-tenant-" + System.currentTimeMillis(); - String resourceGroupId = service.resourceGroupForTenant(tenantId); + ResourceGroupContext rgCtx = ResourceGroupContext.create(); + rgCtx.setTenantId(tenantId); + service.emit(rgCtx); + String resourceGroupId = rgCtx.getResult(); assertThat(resourceGroupId).startsWith(config.resourceGroupPrefix()); assertThat(resourceGroupId).contains(tenantId); // Second call should return cached value - String cached = service.resourceGroupForTenant(tenantId); + ResourceGroupContext rgCtx2 = ResourceGroupContext.create(); + rgCtx2.setTenantId(tenantId); + service.emit(rgCtx2); + String cached = rgCtx2.getResult(); assertThat(cached).isEqualTo(resourceGroupId); } } @Test void deploymentId_returnsDeploymentId() { - AICoreService service = getAICoreService(); + RemoteService service = getAICoreService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); - String deploymentId = service.deploymentId(resourceGroup, RptModelSpec.rpt1()); + DeploymentIdContext depCtx = DeploymentIdContext.create(); + depCtx.setResourceGroupId(resourceGroup); + depCtx.setSpec(RptModelSpec.rpt1()); + service.emit(depCtx); + String deploymentId = depCtx.getResult(); assertThat(deploymentId).isNotNull().isNotBlank(); // Second call should use cache - String cached = service.deploymentId(resourceGroup, RptModelSpec.rpt1()); + DeploymentIdContext depCtx2 = DeploymentIdContext.create(); + depCtx2.setResourceGroupId(resourceGroup); + depCtx2.setSpec(RptModelSpec.rpt1()); + service.emit(depCtx2); + String cached = depCtx2.getResult(); assertThat(cached).isEqualTo(deploymentId); } diff --git a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ActionTest.java b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ActionTest.java index 0e7bf34..42ae5d3 100644 --- a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ActionTest.java +++ b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ActionTest.java @@ -9,12 +9,14 @@ import com.sap.cds.Result; import com.sap.cds.Row; -import com.sap.cds.feature.aicore.api.AICoreService; +import com.sap.cds.feature.aicore.api.DeploymentIdContext; +import com.sap.cds.feature.aicore.api.ResourceGroupContext; import com.sap.cds.feature.aicore.core.AICoreConfig; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.Deployments_; import com.sap.cds.feature.recommendation.api.RptModelSpec; import com.sap.cds.ql.Select; import com.sap.cds.ql.Update; -import com.sap.cds.services.cds.CqnService; +import com.sap.cds.services.cds.RemoteService; import java.util.Map; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; @@ -28,45 +30,64 @@ class ActionTest extends BaseIntegrationTest { @BeforeAll void ensureResourceGroupReady() { - ensureResourceGroupProvisioned(getAICoreCqnService(), getAICoreConfig().defaultResourceGroup()); + ensureResourceGroupProvisioned(getAICoreService(), getAICoreConfig().defaultResourceGroup()); } @Test void resourceGroupForTenant_singleTenancy_returnsDefault() { AICoreConfig config = getAICoreConfig(); - AICoreService service = getAICoreService(); + RemoteService service = getAICoreService(); assumeFalse(config.multiTenancyEnabled(), "Multi-tenancy is enabled"); - String result = service.resourceGroupForTenant("any-tenant-id"); + ResourceGroupContext rgCtx = ResourceGroupContext.create(); + rgCtx.setTenantId("any-tenant-id"); + service.emit(rgCtx); + String result = rgCtx.getResult(); assertThat(result).isEqualTo(config.defaultResourceGroup()); } @Test void resourceGroupForTenant_multiTenancy_createsGroup() { AICoreConfig config = getAICoreConfig(); - AICoreService service = getAICoreService(); + RemoteService service = getAICoreService(); assumeTrue(config.multiTenancyEnabled(), "Multi-tenancy is not enabled"); String tenantId = "itest-action-tenant-" + System.currentTimeMillis(); - String resourceGroupId = service.resourceGroupForTenant(tenantId); + ResourceGroupContext rgCtx = ResourceGroupContext.create(); + rgCtx.setTenantId(tenantId); + service.emit(rgCtx); + String resourceGroupId = rgCtx.getResult(); assertThat(resourceGroupId).startsWith(config.resourceGroupPrefix()); assertThat(resourceGroupId).contains(tenantId); } @Test void deploymentId_returnsValidDeployment() { - AICoreService service = getAICoreService(); + RemoteService service = getAICoreService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); - String deploymentId = service.deploymentId(resourceGroup, RptModelSpec.rpt1()); + DeploymentIdContext depCtx = DeploymentIdContext.create(); + depCtx.setResourceGroupId(resourceGroup); + depCtx.setSpec(RptModelSpec.rpt1()); + service.emit(depCtx); + String deploymentId = depCtx.getResult(); assertThat(deploymentId).isNotNull().isNotBlank(); } @Test void deploymentId_cachedOnSecondCall() { - AICoreService service = getAICoreService(); + RemoteService service = getAICoreService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); - String first = service.deploymentId(resourceGroup, RptModelSpec.rpt1()); - String second = service.deploymentId(resourceGroup, RptModelSpec.rpt1()); + DeploymentIdContext depCtx1 = DeploymentIdContext.create(); + depCtx1.setResourceGroupId(resourceGroup); + depCtx1.setSpec(RptModelSpec.rpt1()); + service.emit(depCtx1); + String first = depCtx1.getResult(); + + DeploymentIdContext depCtx2 = DeploymentIdContext.create(); + depCtx2.setResourceGroupId(resourceGroup); + depCtx2.setSpec(RptModelSpec.rpt1()); + service.emit(depCtx2); + String second = depCtx2.getResult(); assertThat(second).isEqualTo(first); } @@ -75,12 +96,12 @@ void deploymentId_cachedOnSecondCall() { + "re-enable once test creates its own isolated deployment") @Test void stop_deployment_changesTargetStatus() { - CqnService service = getAICoreCqnService(); + RemoteService service = getAICoreService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); Result deployments = service.run( - Select.from("AICore.deployments") + Select.from(Deployments_.CDS_NAME) .where(d -> d.get("resourceGroup_resourceGroupId").eq(resourceGroup))); String deploymentId = null; @@ -96,14 +117,14 @@ void stop_deployment_changesTargetStatus() { final String targetId = deploymentId; service.run( - Update.entity("AICore.deployments") + Update.entity(Deployments_.CDS_NAME) .where(d -> d.get("id").eq(targetId)) .data( Map.of("targetStatus", "STOPPED", "resourceGroup_resourceGroupId", resourceGroup))); Result readResult = service.run( - Select.from("AICore.deployments") + Select.from(Deployments_.CDS_NAME) .where( d -> d.get("id") diff --git a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/BaseIntegrationTest.java b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/BaseIntegrationTest.java index 06a09af..ea356aa 100644 --- a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/BaseIntegrationTest.java +++ b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/BaseIntegrationTest.java @@ -5,12 +5,14 @@ import com.sap.cds.Result; import com.sap.cds.Row; -import com.sap.cds.feature.aicore.api.AICoreService; +import com.sap.cds.feature.aicore.api.DeploymentIdContext; import com.sap.cds.feature.aicore.core.AICoreConfig; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.ResourceGroups_; import com.sap.cds.feature.recommendation.api.RptModelSpec; import com.sap.cds.ql.Insert; import com.sap.cds.ql.Select; -import com.sap.cds.services.cds.CqnService; +import com.sap.cds.services.cds.RemoteService; import com.sap.cds.services.environment.CdsProperties; import com.sap.cds.services.runtime.CdsRuntime; import java.util.List; @@ -39,8 +41,8 @@ public abstract class BaseIntegrationTest { @Autowired protected CdsRuntime runtime; - protected AICoreService getAICoreService() { - return runtime.getServiceCatalog().getService(AICoreService.class, AICoreService.DEFAULT_NAME); + protected RemoteService getAICoreService() { + return runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); } protected AICoreConfig getAICoreConfig() { @@ -50,25 +52,26 @@ protected AICoreConfig getAICoreConfig() { return AICoreConfig.from(runtime.getEnvironment(), mt); } - protected CqnService getAICoreCqnService() { - return (CqnService) getAICoreService(); - } - protected String ensureRptDeploymentReady() { String resourceGroup = getAICoreConfig().defaultResourceGroup(); return CACHED_DEPLOYMENT_IDS.computeIfAbsent( resourceGroup, rg -> { - ensureResourceGroupProvisioned(getAICoreCqnService(), rg); - return getAICoreService().deploymentId(rg, RptModelSpec.rpt1()); + ensureResourceGroupProvisioned(getAICoreService(), rg); + RemoteService service = getAICoreService(); + DeploymentIdContext depCtx = DeploymentIdContext.create(); + depCtx.setResourceGroupId(rg); + depCtx.setSpec(RptModelSpec.rpt1()); + service.emit(depCtx); + return depCtx.getResult(); }); } - protected void ensureResourceGroupProvisioned(CqnService service, String resourceGroup) { + protected void ensureResourceGroupProvisioned(RemoteService service, String resourceGroup) { if (!resourceGroupExists(service, resourceGroup)) { logger.info("Creating resource group {} with itest owner label", resourceGroup); service.run( - Insert.into("AICore.resourceGroups") + Insert.into(ResourceGroups_.CDS_NAME) .entry( Map.of( "resourceGroupId", @@ -79,8 +82,8 @@ protected void ensureResourceGroupProvisioned(CqnService service, String resourc waitForResourceGroupProvisioned(service, resourceGroup); } - private boolean resourceGroupExists(CqnService service, String resourceGroup) { - Result all = service.run(Select.from("AICore.resourceGroups")); + private boolean resourceGroupExists(RemoteService service, String resourceGroup) { + Result all = service.run(Select.from(ResourceGroups_.CDS_NAME)); for (Row row : all) { if (resourceGroup.equals(row.get("resourceGroupId"))) { return true; @@ -89,9 +92,9 @@ private boolean resourceGroupExists(CqnService service, String resourceGroup) { return false; } - private void waitForResourceGroupProvisioned(CqnService service, String resourceGroup) { + private void waitForResourceGroupProvisioned(RemoteService service, String resourceGroup) { for (int i = 0; i < 30; i++) { - Result all = service.run(Select.from("AICore.resourceGroups")); + Result all = service.run(Select.from(ResourceGroups_.CDS_NAME)); for (Row row : all) { if (resourceGroup.equals(row.get("resourceGroupId"))) { String status = (String) row.get("status"); diff --git a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ConfigurationTest.java b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ConfigurationTest.java index 2b1e5b5..db4a823 100644 --- a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ConfigurationTest.java +++ b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ConfigurationTest.java @@ -7,9 +7,10 @@ import com.sap.cds.Result; import com.sap.cds.Row; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.Configurations_; import com.sap.cds.ql.Insert; import com.sap.cds.ql.Select; -import com.sap.cds.services.cds.CqnService; +import com.sap.cds.services.cds.RemoteService; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; @@ -18,11 +19,11 @@ class ConfigurationTest extends BaseIntegrationTest { @Test void readAll_returnsConfigurations() { - CqnService service = getAICoreCqnService(); + RemoteService service = getAICoreService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); Result result = service.run( - Select.from("AICore.configurations") + Select.from(Configurations_.CDS_NAME) .where(c -> c.get("resourceGroup_resourceGroupId").eq(resourceGroup))); assertThat(result.list()).isNotNull(); @@ -30,11 +31,11 @@ void readAll_returnsConfigurations() { @Test void readAll_filterByScenario() { - CqnService service = getAICoreCqnService(); + RemoteService service = getAICoreService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); Result result = service.run( - Select.from("AICore.configurations") + Select.from(Configurations_.CDS_NAME) .where( c -> c.get("scenarioId") @@ -46,13 +47,13 @@ void readAll_filterByScenario() { @Test void create_andReadById() { - CqnService service = getAICoreCqnService(); + RemoteService service = getAICoreService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); String configName = "itest-config-" + System.currentTimeMillis(); Result created = service.run( - Insert.into("AICore.configurations") + Insert.into(Configurations_.CDS_NAME) .entry( Map.of( "name", @@ -75,7 +76,7 @@ void create_andReadById() { // Read back by ID Result readResult = service.run( - Select.from("AICore.configurations") + Select.from(Configurations_.CDS_NAME) .where( c -> c.get("id") @@ -91,13 +92,13 @@ void create_andReadById() { @Test void create_withParameterBindings_mapsCorrectly() { - CqnService service = getAICoreCqnService(); + RemoteService service = getAICoreService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); String configName = "itest-params-" + System.currentTimeMillis(); Result created = service.run( - Insert.into("AICore.configurations") + Insert.into(Configurations_.CDS_NAME) .entry( Map.of( "name", @@ -117,7 +118,7 @@ void create_withParameterBindings_mapsCorrectly() { Result readResult = service.run( - Select.from("AICore.configurations") + Select.from(Configurations_.CDS_NAME) .where( c -> c.get("id") diff --git a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/DeploymentTest.java b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/DeploymentTest.java index d73fd50..e4fbba8 100644 --- a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/DeploymentTest.java +++ b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/DeploymentTest.java @@ -8,9 +8,10 @@ import com.sap.cds.Result; import com.sap.cds.Row; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.Deployments_; import com.sap.cds.ql.Select; import com.sap.cds.ql.Update; -import com.sap.cds.services.cds.CqnService; +import com.sap.cds.services.cds.RemoteService; import java.util.Map; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -19,11 +20,11 @@ class DeploymentTest extends BaseIntegrationTest { @Test void readAll_returnsDeployments() { - CqnService service = getAICoreCqnService(); + RemoteService service = getAICoreService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); Result result = service.run( - Select.from("AICore.deployments") + Select.from(Deployments_.CDS_NAME) .where(d -> d.get("resourceGroup_resourceGroupId").eq(resourceGroup))); assertThat(result.list()).isNotNull(); @@ -31,11 +32,11 @@ void readAll_returnsDeployments() { @Test void readSingle_returnsDeploymentDetails() { - CqnService service = getAICoreCqnService(); + RemoteService service = getAICoreService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); Result all = service.run( - Select.from("AICore.deployments") + Select.from(Deployments_.CDS_NAME) .where(d -> d.get("resourceGroup_resourceGroupId").eq(resourceGroup))); assumeFalse(all.list().isEmpty(), "No deployments available"); @@ -43,7 +44,7 @@ void readSingle_returnsDeploymentDetails() { String id = (String) all.list().get(0).get("id"); Result single = service.run( - Select.from("AICore.deployments") + Select.from(Deployments_.CDS_NAME) .where( d -> d.get("id") @@ -62,12 +63,12 @@ void readSingle_returnsDeploymentDetails() { + "re-enable once test creates its own isolated deployment") @Test void update_targetStatus_stopsRunningDeployment() { - CqnService service = getAICoreCqnService(); + RemoteService service = getAICoreService(); String resourceGroup = getAICoreConfig().defaultResourceGroup(); Result deployments = service.run( - Select.from("AICore.deployments") + Select.from(Deployments_.CDS_NAME) .where(d -> d.get("resourceGroup_resourceGroupId").eq(resourceGroup))); String deploymentId = null; @@ -83,14 +84,14 @@ void update_targetStatus_stopsRunningDeployment() { final String targetId = deploymentId; service.run( - Update.entity("AICore.deployments") + Update.entity(Deployments_.CDS_NAME) .where(d -> d.get("id").eq(targetId)) .data( Map.of("targetStatus", "STOPPED", "resourceGroup_resourceGroupId", resourceGroup))); Result readResult = service.run( - Select.from("AICore.deployments") + Select.from(Deployments_.CDS_NAME) .where( d -> d.get("id") diff --git a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/MultiTenancyTest.java b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/MultiTenancyTest.java index 2716d06..9d88af9 100644 --- a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/MultiTenancyTest.java +++ b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/MultiTenancyTest.java @@ -7,8 +7,9 @@ import static org.junit.jupiter.api.Assumptions.assumeFalse; import static org.junit.jupiter.api.Assumptions.assumeTrue; -import com.sap.cds.feature.aicore.api.AICoreService; +import com.sap.cds.feature.aicore.api.ResourceGroupContext; import com.sap.cds.feature.aicore.core.AICoreConfig; +import com.sap.cds.services.cds.RemoteService; import org.junit.jupiter.api.Test; class MultiTenancyTest extends BaseIntegrationTest { @@ -16,13 +17,20 @@ class MultiTenancyTest extends BaseIntegrationTest { @Test void differentTenants_getDifferentResourceGroups() { AICoreConfig config = getAICoreConfig(); - AICoreService service = getAICoreService(); + RemoteService service = getAICoreService(); assumeTrue(config.multiTenancyEnabled(), "Multi-tenancy is not enabled"); String tenantA = "itest-mt-a-" + System.currentTimeMillis(); String tenantB = "itest-mt-b-" + System.currentTimeMillis(); - String rgA = service.resourceGroupForTenant(tenantA); - String rgB = service.resourceGroupForTenant(tenantB); + ResourceGroupContext rgCtxA = ResourceGroupContext.create(); + rgCtxA.setTenantId(tenantA); + service.emit(rgCtxA); + String rgA = rgCtxA.getResult(); + + ResourceGroupContext rgCtxB = ResourceGroupContext.create(); + rgCtxB.setTenantId(tenantB); + service.emit(rgCtxB); + String rgB = rgCtxB.getResult(); assertThat(rgA).isNotEqualTo(rgB); assertThat(rgA).contains(tenantA); @@ -32,21 +40,32 @@ void differentTenants_getDifferentResourceGroups() { @Test void resourceGroupPrefix_appliedCorrectly() { AICoreConfig config = getAICoreConfig(); - AICoreService service = getAICoreService(); + RemoteService service = getAICoreService(); assumeTrue(config.multiTenancyEnabled(), "Multi-tenancy is not enabled"); String tenantA = "itest-prefix-" + System.currentTimeMillis(); - String rg = service.resourceGroupForTenant(tenantA); + ResourceGroupContext rgCtx = ResourceGroupContext.create(); + rgCtx.setTenantId(tenantA); + service.emit(rgCtx); + String rg = rgCtx.getResult(); assertThat(rg).startsWith(config.resourceGroupPrefix()); } @Test void singleTenancy_alwaysReturnsDefault() { AICoreConfig config = getAICoreConfig(); - AICoreService service = getAICoreService(); + RemoteService service = getAICoreService(); assumeFalse(config.multiTenancyEnabled(), "Multi-tenancy is enabled"); - String rg1 = service.resourceGroupForTenant("tenant-x"); - String rg2 = service.resourceGroupForTenant("tenant-y"); + + ResourceGroupContext rgCtx1 = ResourceGroupContext.create(); + rgCtx1.setTenantId("tenant-x"); + service.emit(rgCtx1); + String rg1 = rgCtx1.getResult(); + + ResourceGroupContext rgCtx2 = ResourceGroupContext.create(); + rgCtx2.setTenantId("tenant-y"); + service.emit(rgCtx2); + String rg2 = rgCtx2.getResult(); assertThat(rg1).isEqualTo(config.defaultResourceGroup()); assertThat(rg2).isEqualTo(config.defaultResourceGroup()); diff --git a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ResourceGroupTest.java b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ResourceGroupTest.java index e6ca394..fea4ff2 100644 --- a/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ResourceGroupTest.java +++ b/integration-tests/spring/src/test/java/com/sap/cds/feature/aicore/itest/ResourceGroupTest.java @@ -8,10 +8,11 @@ import com.sap.cds.Result; import com.sap.cds.Row; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.ResourceGroups_; import com.sap.cds.ql.Delete; import com.sap.cds.ql.Insert; import com.sap.cds.ql.Select; -import com.sap.cds.services.cds.CqnService; +import com.sap.cds.services.cds.RemoteService; import java.util.List; import java.util.Map; import org.junit.jupiter.api.AfterEach; @@ -30,10 +31,10 @@ class ResourceGroupTest extends BaseIntegrationTest { void cleanup() { if (createdResourceGroupId != null) { try { - CqnService service = getAICoreCqnService(); + RemoteService service = getAICoreService(); waitForResourceGroupProvisioned(service, createdResourceGroupId); service.run( - Delete.from("AICore.resourceGroups") + Delete.from(ResourceGroups_.CDS_NAME) .where(r -> r.get("resourceGroupId").eq(createdResourceGroupId))); } catch (Exception ignored) { } @@ -44,15 +45,15 @@ void cleanup() { @Test void create_andRead_resourceGroup() { createdResourceGroupId = TEST_RG_PREFIX + System.currentTimeMillis(); - CqnService service = getAICoreCqnService(); + RemoteService service = getAICoreService(); service.run( - Insert.into("AICore.resourceGroups") + Insert.into(ResourceGroups_.CDS_NAME) .entry(Map.of("resourceGroupId", createdResourceGroupId))); Result result = service.run( - Select.from("AICore.resourceGroups") + Select.from(ResourceGroups_.CDS_NAME) .where(r -> r.get("resourceGroupId").eq(createdResourceGroupId))); assertThat(result.list()).hasSize(1); @@ -65,15 +66,15 @@ void create_andRead_resourceGroup() { void create_withTenantLabel_andFilterByTenant() { String tenantId = "itest-tenant-" + System.currentTimeMillis(); createdResourceGroupId = TEST_RG_PREFIX + tenantId; - CqnService service = getAICoreCqnService(); + RemoteService service = getAICoreService(); service.run( - Insert.into("AICore.resourceGroups") + Insert.into(ResourceGroups_.CDS_NAME) .entry(Map.of("resourceGroupId", createdResourceGroupId, "tenantId", tenantId))); Result result = service.run( - Select.from("AICore.resourceGroups").where(r -> r.get("tenantId").eq(tenantId))); + Select.from(ResourceGroups_.CDS_NAME).where(r -> r.get("tenantId").eq(tenantId))); assertThat(result.list()).isNotEmpty(); Row row = result.first().orElseThrow(); @@ -82,18 +83,18 @@ void create_withTenantLabel_andFilterByTenant() { @Test void readAll_returnsResourceGroups() { - CqnService service = getAICoreCqnService(); - Result result = service.run(Select.from("AICore.resourceGroups")); + RemoteService service = getAICoreService(); + Result result = service.run(Select.from(ResourceGroups_.CDS_NAME)); assertThat(result.list()).isNotNull(); } @Test void create_withLabels() { createdResourceGroupId = TEST_RG_PREFIX + "labels-" + System.currentTimeMillis(); - CqnService service = getAICoreCqnService(); + RemoteService service = getAICoreService(); service.run( - Insert.into("AICore.resourceGroups") + Insert.into(ResourceGroups_.CDS_NAME) .entry( Map.of( "resourceGroupId", @@ -106,7 +107,7 @@ void create_withLabels() { Result result = service.run( - Select.from("AICore.resourceGroups") + Select.from(ResourceGroups_.CDS_NAME) .where(r -> r.get("resourceGroupId").eq(createdResourceGroupId))); assertThat(result.list()).hasSize(1); @@ -119,28 +120,28 @@ void create_withLabels() { @Test void delete_resourceGroup() throws InterruptedException { String rgId = TEST_RG_PREFIX + "del-" + System.currentTimeMillis(); - CqnService service = getAICoreCqnService(); + RemoteService service = getAICoreService(); - service.run(Insert.into("AICore.resourceGroups").entry(Map.of("resourceGroupId", rgId))); + service.run(Insert.into(ResourceGroups_.CDS_NAME).entry(Map.of("resourceGroupId", rgId))); waitForResourceGroupProvisioned(service, rgId); assertThatCode( () -> service.run( - Delete.from("AICore.resourceGroups") + Delete.from(ResourceGroups_.CDS_NAME) .where(r -> r.get("resourceGroupId").eq(rgId)))) .doesNotThrowAnyException(); createdResourceGroupId = null; // already deleted } - private void waitForResourceGroupProvisioned(CqnService service, String rgId) + private void waitForResourceGroupProvisioned(RemoteService service, String rgId) throws InterruptedException { for (int i = 0; i < 30; i++) { Result result = service.run( - Select.from("AICore.resourceGroups").where(r -> r.get("resourceGroupId").eq(rgId))); + Select.from(ResourceGroups_.CDS_NAME).where(r -> r.get("resourceGroupId").eq(rgId))); if (!result.list().isEmpty()) { String status = (String) result.single().get("status"); if ("PROVISIONED".equals(status)) { diff --git a/samples/bookshop/.cdsrc.json b/samples/bookshop/.cdsrc.json index 94e9eda..f419d38 100644 --- a/samples/bookshop/.cdsrc.json +++ b/samples/bookshop/.cdsrc.json @@ -2,13 +2,13 @@ "requires": { "db": "hana", "AICore": { - "model": "com.sap.cds/ai" + "model": "@cap-js/ai/srv/AICoreService" }, "[production]": { "auth": "xsuaa" } }, "cdsc": { - "moduleLookupDirectories": ["node_modules/", "target/cds/"] + "moduleLookupDirectories": ["node_modules/"] } } diff --git a/samples/bookshop/srv/ai-core-service.cds b/samples/bookshop/srv/ai-core-service.cds index 7124cc4..1fc24c8 100644 --- a/samples/bookshop/srv/ai-core-service.cds +++ b/samples/bookshop/srv/ai-core-service.cds @@ -1,4 +1,4 @@ -using { AICore } from 'com.sap.cds/ai'; +using { AICore } from '@cap-js/ai/srv/AICoreService'; service AICoreShowcaseService @(requires: 'any') { diff --git a/samples/bookshop/srv/src/main/java/customer/bookshop/handlers/AICoreShowcaseHandler.java b/samples/bookshop/srv/src/main/java/customer/bookshop/handlers/AICoreShowcaseHandler.java index 5ebfdf3..e7ce877 100644 --- a/samples/bookshop/srv/src/main/java/customer/bookshop/handlers/AICoreShowcaseHandler.java +++ b/samples/bookshop/srv/src/main/java/customer/bookshop/handlers/AICoreShowcaseHandler.java @@ -2,7 +2,12 @@ import com.sap.cds.CdsData; import com.sap.cds.Result; -import com.sap.cds.feature.aicore.api.AICoreService; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.AICore_; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.Configurations_; +import com.sap.cds.feature.aicore.generated.cds4j.aicore.Deployments_; +import com.sap.cds.feature.aicore.api.DeploymentIdContext; +import com.sap.cds.feature.aicore.api.InferenceClientContext; +import com.sap.cds.feature.aicore.api.ResourceGroupContext; import com.sap.cds.feature.recommendation.api.RptInferenceClient; import com.sap.cds.feature.recommendation.api.RptModelSpec; import com.sap.cds.ql.Insert; @@ -11,6 +16,7 @@ import com.sap.cds.services.EventContext; import com.sap.cds.services.cds.CdsReadEventContext; import com.sap.cds.services.cds.CqnService; +import com.sap.cds.services.cds.RemoteService; import com.sap.cds.services.handler.EventHandler; import com.sap.cds.services.handler.annotations.On; import com.sap.cds.services.handler.annotations.ServiceName; @@ -28,8 +34,8 @@ public class AICoreShowcaseHandler implements EventHandler { @Autowired private CdsRuntime runtime; - private AICoreService getAICoreService() { - return runtime.getServiceCatalog().getService(AICoreService.class, AICoreService.DEFAULT_NAME); + private RemoteService getAICoreService() { + return runtime.getServiceCatalog().getService(RemoteService.class, AICore_.CDS_NAME); } // This handler is NOT required - the plugin automatically delegates reads on projections @@ -37,19 +43,25 @@ private AICoreService getAICoreService() { // programmatically, e.g. for custom filtering or post-processing. @On(event = CqnService.EVENT_READ, entity = "AICoreShowcaseService.Configurations") public void onReadConfigurations(CdsReadEventContext context) { - context.setResult(getAICoreService().run(Select.from("AICore.configurations"))); + context.setResult(getAICoreService().run(Select.from(Configurations_.CDS_NAME))); } @On(event = "setupTenantResources") public void onSetupTenantResources(EventContext context) { - String rgId = getAICoreService().resourceGroup(); + RemoteService service = getAICoreService(); + ResourceGroupContext rgCtx = ResourceGroupContext.create(); + service.emit(rgCtx); + String rgId = rgCtx.getResult(); context.put("result", rgId); context.setCompleted(); } @On(event = "getMyResourceGroup") public void onGetMyResourceGroup(EventContext context) { - String rgId = getAICoreService().resourceGroup(); + RemoteService service = getAICoreService(); + ResourceGroupContext rgCtx = ResourceGroupContext.create(); + service.emit(rgCtx); + String rgId = rgCtx.getResult(); context.put("result", rgId); context.setCompleted(); } @@ -57,7 +69,12 @@ public void onGetMyResourceGroup(EventContext context) { @On(event = "provisionRpt1") public void onProvisionRpt1(EventContext context) { String resourceGroupId = (String) context.get("resourceGroupId"); - String deploymentId = getAICoreService().deploymentId(resourceGroupId, RptModelSpec.rpt1()); + RemoteService service = getAICoreService(); + DeploymentIdContext depCtx = DeploymentIdContext.create(); + depCtx.setResourceGroupId(resourceGroupId); + depCtx.setSpec(RptModelSpec.rpt1()); + service.emit(depCtx); + String deploymentId = depCtx.getResult(); context.put("result", deploymentId); context.setCompleted(); } @@ -69,7 +86,7 @@ public void onStopDeployment(EventContext context) { getAICoreService() .run( - Update.entity("AICore.deployments") + Update.entity(Deployments_.CDS_NAME) .where(d -> d.get("id").eq(deploymentId)) .data( Map.of( @@ -90,7 +107,7 @@ public void onCreateConfiguration(EventContext context) { Result result = getAICoreService() .run( - Insert.into("AICore.configurations") + Insert.into(Configurations_.CDS_NAME) .entry( Map.of( "name", name, @@ -126,16 +143,14 @@ public void onPredictCategory(EventContext context) { Map.of( "ID", "ctx-5", "name", "Blender", "price", "89.99", "category", "Appliances"))); - AICoreService service = getAICoreService(); - String rg = service.resourceGroup(); - String deploymentId = service.deploymentId(rg, RptModelSpec.rpt1()); - RptInferenceClient client = - new RptInferenceClient(service.inferenceClient(rg, deploymentId), List.of("ID")); + RemoteService service = getAICoreService(); + RptInferenceClient client = createRptClient(service, List.of("ID")); List> results = new ArrayList<>(); for (Map product : products) { CdsData predictionRow = CdsData.create(new HashMap<>(product)); - List predictions = client.predict(predictionRow, contextRows, List.of("category")); + List predictions = + client.predict(predictionRow, contextRows, List.of("category")); for (CdsData prediction : predictions) { String id = (String) prediction.get("ID"); Object categoryObj = prediction.get("category"); @@ -158,4 +173,24 @@ private String extractPrediction(List predictionList) { } return predictionList.get(0).toString(); } + + /** Helper to resolve a ready-to-use RptInferenceClient from the AI Core RemoteService. */ + private static RptInferenceClient createRptClient( + RemoteService service, List keyNames) { + ResourceGroupContext rgCtx = ResourceGroupContext.create(); + service.emit(rgCtx); + String rg = rgCtx.getResult(); + + DeploymentIdContext depCtx = DeploymentIdContext.create(); + depCtx.setResourceGroupId(rg); + depCtx.setSpec(RptModelSpec.rpt1()); + service.emit(depCtx); + + InferenceClientContext infCtx = InferenceClientContext.create(); + infCtx.setResourceGroupId(rg); + infCtx.setDeploymentId(depCtx.getResult()); + service.emit(infCtx); + + return new RptInferenceClient(infCtx.getResult(), keyNames); + } }