Skip to content

Commit 5f9f035

Browse files
thiagohoraclaude
andcommitted
[OPIK-7352] [BE] validate config-entity referenced ids on ingest
Extends referenced-id validation to config-entity references (verify-then- enforce: confirmed zero non-UUIDv7 ids across prod projects, datasets, dataset_versions, prompts, prompt_versions, alerts, dashboards, webhooks, automation_rules, and guardrails, so enforcement rejects no existing data). Adds null-safe validateIdNotInFutureIfPresent(Async) helpers and validates: projectId across span/trace update + batch-update, feedback scores (single + batch), guardrails, assertion results, annotation queues, experiments, optimizations, alerts, automation-rule evaluators, prompts + prompt versions, dashboards, and thread open/close; feedback/guardrail sourceQueueId; guardrail secondaryId; experiment optimizationId + datasetVersionId; dataset-item datasetId + copy-from ids. Upgrades the guardrail/assertion referenced entityId from version-only to the shared not-in-future policy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d56664d commit 5f9f035

15 files changed

Lines changed: 56 additions & 16 deletions

apps/opik-backend/src/main/java/com/comet/opik/domain/AlertService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,7 @@ private Alert prepareAlert(Alert alert, String userName, String workspaceId) {
555555

556556
UUID id = alert.id() == null ? idGenerator.generateId() : alert.id();
557557
IdGenerator.validateVersion(id, "Alert");
558+
idGenerator.validateIdNotInFutureIfPresent(alert.projectId(), "project");
558559

559560
UUID webhookId = alert.webhook().id() == null ? idGenerator.generateId() : alert.webhook().id();
560561
IdGenerator.validateVersion(webhookId, "Webhook");

apps/opik-backend/src/main/java/com/comet/opik/domain/AnnotationQueueService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ private Mono<AnnotationQueue.AnnotationQueuePage> enhancePageWithProjectNames(
261261
private AnnotationQueue prepareAnnotationQueue(AnnotationQueue annotationQueue) {
262262
UUID id = annotationQueue.id() == null ? idGenerator.generateId() : annotationQueue.id();
263263
IdGenerator.validateVersion(id, "AnnotationQueue");
264+
idGenerator.validateIdNotInFutureIfPresent(annotationQueue.projectId(), "project");
264265

265266
log.debug("Preparing annotation queue with id '{}', name '{}', project '{}'",
266267
id, annotationQueue.name(), annotationQueue.projectId());

apps/opik-backend/src/main/java/com/comet/opik/domain/AssertionResultService.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class AssertionResultServiceImpl implements AssertionResultService {
4343
private final @NonNull AssertionResultDAO assertionResultDAO;
4444
private final @NonNull ProjectService projectService;
4545
private final @NonNull EventBus eventBus;
46+
private final @NonNull IdGenerator idGenerator;
4647

4748
@Override
4849
public Mono<Long> insertBatch(@NonNull EntityType entityType,
@@ -63,7 +64,10 @@ public Mono<Void> saveBatch(@NonNull EntityType entityType,
6364
}
6465

6566
// Validate up front so a bad id fails fast and independently of project-name normalisation.
66-
assertionResults.forEach(item -> IdGenerator.validateVersion(item.entityId(), entityType.getType()));
67+
assertionResults.forEach(item -> {
68+
idGenerator.validateIdNotInFuture(item.entityId(), entityType.getType());
69+
idGenerator.validateIdNotInFutureIfPresent(item.projectId(), "project");
70+
});
6771

6872
return Mono.deferContextual(ctx -> {
6973
String workspaceId = ctx.get(RequestContext.WORKSPACE_ID);

apps/opik-backend/src/main/java/com/comet/opik/domain/DashboardService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public Dashboard create(@NonNull Dashboard dashboard, @NonNull DashboardScope sc
8383
// Generate ID if not provided
8484
var dashboardId = dashboard.id() != null ? dashboard.id() : idGenerator.generateId();
8585
IdGenerator.validateVersion(dashboardId, "dashboard");
86+
idGenerator.validateIdNotInFutureIfPresent(dashboard.projectId(), "project");
8687

8788
final UUID resolvedProjectId;
8889
if (StringUtils.isNotBlank(dashboard.projectName()) && dashboard.projectId() == null) {

apps/opik-backend/src/main/java/com/comet/opik/domain/DatasetItemService.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1572,6 +1572,10 @@ private List<DatasetItem> prepareAddedItems(DatasetItemChanges changes, UUID dat
15721572
@WithSpan
15731573
public Mono<DatasetVersion> save(@NonNull DatasetItemBatch batch) {
15741574

1575+
idGenerator.validateIdNotInFutureIfPresent(batch.datasetId(), "dataset");
1576+
idGenerator.validateIdNotInFutureIfPresent(batch.copyFromDatasetId(), "dataset");
1577+
idGenerator.validateIdNotInFutureIfPresent(batch.copyFromVersionId(), "dataset version");
1578+
15751579
if (!featureFlags.isDatasetVersioningEnabled()) {
15761580
// Legacy: save to legacy table
15771581
log.info("Saving items to legacy table for dataset '{}'", batch.datasetId());

apps/opik-backend/src/main/java/com/comet/opik/domain/ExperimentService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,9 @@ private Set<UUID> getPromptVersionIds(Experiment experiment) {
495495
public Mono<UUID> create(@NonNull Experiment experiment) {
496496
var id = experiment.id() == null ? idGenerator.generateId() : experiment.id();
497497
IdGenerator.validateVersion(id, "Experiment");
498+
idGenerator.validateIdNotInFutureIfPresent(experiment.projectId(), "project");
499+
idGenerator.validateIdNotInFutureIfPresent(experiment.optimizationId(), "optimization");
500+
idGenerator.validateIdNotInFutureIfPresent(experiment.datasetVersionId(), "dataset version");
498501
var name = StringUtils.getIfBlank(experiment.name(), nameGenerator::generateName);
499502
return resolveProjectId(experiment)
500503
.flatMap(resolvedExperiment -> datasetService

apps/opik-backend/src/main/java/com/comet/opik/domain/FeedbackScoreService.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ public Mono<Void> scoreTrace(@NonNull UUID traceId, @NonNull FeedbackScore score
102102
String userName = ctx.get(RequestContext.USER_NAME);
103103

104104
idGenerator.validateIdNotInFuture(traceId, EntityType.TRACE.getType());
105+
idGenerator.validateIdNotInFutureIfPresent(score.sourceQueueId(), "annotation queue");
105106
return traceDAO.getProjectIdFromTrace(traceId)
106107
.switchIfEmpty(Mono.error(failWithNotFound("Trace", traceId)))
107108
.flatMap(projectId -> getAuthor()
@@ -121,6 +122,7 @@ public Mono<Void> scoreSpan(@NonNull UUID spanId, @NonNull FeedbackScore score)
121122
String userName = ctx.get(RequestContext.USER_NAME);
122123

123124
idGenerator.validateIdNotInFuture(spanId, EntityType.SPAN.getType());
125+
idGenerator.validateIdNotInFutureIfPresent(score.sourceQueueId(), "annotation queue");
124126
return spanDAO.getProjectIdFromSpan(spanId)
125127
.switchIfEmpty(Mono.error(failWithNotFound("Span", spanId)))
126128
.flatMap(projectId -> getAuthor()
@@ -177,6 +179,8 @@ private Mono<Void> processScoreBatch(EntityType entityType, List<FeedbackScoreBa
177179
.stream()
178180
.map(score -> {
179181
idGenerator.validateIdNotInFuture(score.id(), entityType.getType()); // validate span/trace id
182+
idGenerator.validateIdNotInFutureIfPresent(score.projectId(), "project");
183+
idGenerator.validateIdNotInFutureIfPresent(score.sourceQueueId(), "annotation queue");
180184

181185
return score.toBuilder()
182186
.projectName(WorkspaceUtils.getProjectName(score.projectName()))

apps/opik-backend/src/main/java/com/comet/opik/domain/GuardrailsService.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ public Mono<Void> addTraceGuardrails(List<Guardrail> guardrails) {
5151
.stream()
5252
.map(guardrail -> {
5353
UUID id = idGenerator.generateId();
54-
IdGenerator.validateVersion(guardrail.entityId(), entityType.getType()); // validate trace id
54+
idGenerator.validateIdNotInFuture(guardrail.entityId(), entityType.getType());
55+
idGenerator.validateIdNotInFuture(guardrail.secondaryId(), "guardrail secondary");
56+
idGenerator.validateIdNotInFutureIfPresent(guardrail.projectId(), "project");
5557

5658
return guardrail.toBuilder()
5759
.id(id)

apps/opik-backend/src/main/java/com/comet/opik/domain/IdGenerator.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ public interface IdGenerator {
4848

4949
Mono<UUID> validateIdNotInFutureAsync(UUID id, String resource);
5050

51+
/**
52+
* Null-safe variant of {@link #validateIdNotInFuture} for optional referenced ids (e.g. an optional
53+
* {@code projectId} that may be resolved by name instead). No-op when {@code id} is null.
54+
*/
55+
default void validateIdNotInFutureIfPresent(UUID id, String resource) {
56+
if (id != null) {
57+
validateIdNotInFuture(id, resource);
58+
}
59+
}
60+
61+
default Mono<UUID> validateIdNotInFutureIfPresentAsync(UUID id, String resource) {
62+
return id == null ? Mono.empty() : validateIdNotInFutureAsync(id, resource);
63+
}
64+
5165
static Mono<UUID> validateVersionAsync(@NonNull UUID id, String resource) {
5266
return Mono.fromCallable(() -> {
5367
validateVersion(id, resource);

apps/opik-backend/src/main/java/com/comet/opik/domain/OptimizationService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ private OptimizationSearchCriteria resolveDatasetNameFilter(
176176
public Mono<UUID> upsert(@NonNull Optimization optimization) {
177177
UUID id = optimization.id() == null ? idGenerator.generateId() : optimization.id();
178178
IdGenerator.validateVersion(id, "Optimization");
179+
idGenerator.validateIdNotInFutureIfPresent(optimization.projectId(), "project");
179180

180181
// Detect if this is a Studio optimization (has studioConfig in the request)
181182
boolean isStudioOptimization = optimization.studioConfig() != null;

0 commit comments

Comments
 (0)