Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/src/concepts/relations.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ Each entity can link to **multiple** target entities.

---

## Unresolved identifiers for relations

The platform validates relation targets on every entity create, update, and patch, whether you use the REST API or webhook ingestion. Set `APP_RELATIONS_TARGET_VALIDATION_MODE` to choose how it handles unresolved targets. The default, `STRICT`, rejects any non-blank target identifier that does not resolve to an entity. In `TOLERANT` mode, the platform filters unresolved identifiers from optional relations, keeps resolved targets, and omits the relation if none resolve.

Both modes enforce required relations, target-template compatibility for resolved targets, and the existing `required` and `to_many` cardinality constraints. Tolerant mode checks cardinality after filtering unresolved optional targets. REST entity writes return `400 Bad Request` for validation failures, while webhook ingestion returns `422 Unprocessable Content`.

---

## Example: Complete Data Model

Here's a complete example with multiple templates and relations:
Expand Down
8 changes: 6 additions & 2 deletions docs/src/features/data-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,10 @@ sequenceDiagram
Route-->>Client: 201 Created
```

> [!TIP]
> If a `filter` expression returns `false`, the payload is silently
> skipped for that mapping. Other mappings on the same connector continue to run.

Each step in the pipeline is isolated in its own Camel sub-route:

| Step | Route ID | Responsibility |
Expand All @@ -223,8 +227,8 @@ Each step in the pipeline is isolated in its own Camel sub-route:
| **C** | `ingest-payload` | Apply each `EntityDynamicMapping` and persist entities |

> [!TIP]
> If a `filter` expression returns `false`, the payload is silently
> skipped for that mapping. Other mappings on the same connector continue to run.
> Webhook ingestion uses the same relation validation mode as REST entity writes. `STRICT` is the default and rejects unresolved target identifiers. Set `APP_RELATIONS_TARGET_VALIDATION_MODE=TOLERANT` to filter unresolved identifiers from optional relations; IDP-Core keeps resolved targets and omits the relation if none resolve.
> Both modes enforce required relations, target-template compatibility, and cardinality. Tolerant mode checks cardinality after filtering. A webhook relation validation failure returns `422 Unprocessable Content`.

---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.decathlon.idp_core.domain.model.enums;

/// Controls how relation target identifiers are checked during entity writes.
public enum RelationTargetValidationMode {
/// Requires every non-blank relation target identifier to resolve to an entity.
STRICT,

/// Filters unresolved identifiers from optional relations.
TOLERANT
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,9 @@ public Entity createEntity(@Valid Entity entity) {
// Enrich relations with target template identifiers from template definition
Entity enrichedEntity = enrichRelationsWithTargetTemplates(entity, template);

entityValidationService.validateForCreation(enrichedEntity, template);
Entity validatedEntity = entityValidationService.validateForCreation(enrichedEntity, template);

return entityRepository.save(enrichedEntity);
return entityRepository.save(validatedEntity);
}

/// Updates an existing entity identified by template and entity identifiers.
Expand Down Expand Up @@ -194,11 +194,11 @@ public Entity updateEntity(String templateIdentifier, String entityIdentifier,
// Enrich relations with target template identifiers from template definition
Entity enrichedEntity = enrichRelationsWithTargetTemplates(entityToSave, template);

entityValidationService.validateForUpdate(enrichedEntity, template);
Entity validatedEntity = entityValidationService.validateForUpdate(enrichedEntity, template);

// Only save if there are actual changes to avoid unnecessary audit records
if (hasEntityChanged(existingEntity, enrichedEntity)) {
return entityRepository.save(enrichedEntity);
if (hasEntityChanged(existingEntity, validatedEntity)) {
return entityRepository.save(validatedEntity);
}

return existingEntity;
Expand Down Expand Up @@ -247,10 +247,10 @@ public Entity patchEntity(String templateIdentifier, String entityIdentifier,
new ArrayList<>(mergedProperties.values()), new ArrayList<>(mergedRelations.values()));

Entity updatedEntity = enrichRelationsWithTargetTemplates(entityToSave, template);
entityValidationService.validateForUpdate(updatedEntity, template);
Entity validatedEntity = entityValidationService.validateForUpdate(updatedEntity, template);

if (hasEntityChanged(existingEntity, updatedEntity)) {
return entityRepository.save(updatedEntity);
if (hasEntityChanged(existingEntity, validatedEntity)) {
return entityRepository.save(validatedEntity);
}

return existingEntity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.decathlon.idp_core.domain.exception.entity.EntityValidationException;
import com.decathlon.idp_core.domain.model.entity.Entity;
import com.decathlon.idp_core.domain.model.entity.Property;
import com.decathlon.idp_core.domain.model.entity.Relation;
import com.decathlon.idp_core.domain.model.entity_template.EntityTemplate;
import com.decathlon.idp_core.domain.model.entity_template.PropertyDefinition;
import com.decathlon.idp_core.domain.port.EntityRepositoryPort;
Expand Down Expand Up @@ -45,13 +46,15 @@ public class EntityValidationService {
///
/// @param entity the entity to validate
/// @param template the already-resolved template the entity must conform to
/// @return the validated entity with unresolved optional relation targets
/// removed
/// @throws EntityValidationException when one or more validation rules are
/// violated
/// @throws EntityAlreadyExistsException if an entity with the same identifier
/// exists for the template
public void validateForCreation(Entity entity, EntityTemplate template) {
public Entity validateForCreation(Entity entity, EntityTemplate template) {
validateUniqueness(entity);
validateAgainstTemplate(template, entity);
return validateAgainstTemplate(template, entity);
}

/// Validates entity data for update operations.
Expand All @@ -62,22 +65,26 @@ public void validateForCreation(Entity entity, EntityTemplate template) {
///
/// @param entity the entity payload to validate
/// @param template the already-resolved template the entity must conform to
/// @return the validated entity with unresolved optional relation targets
/// removed
/// @throws EntityValidationException when one or more validation rules are
/// violated
void validateForUpdate(Entity entity, EntityTemplate template) {
validateAgainstTemplate(template, entity);
Entity validateForUpdate(Entity entity, EntityTemplate template) {
return validateAgainstTemplate(template, entity);
}

/// Validates entity properties against the template's property definitions,
/// Validates entity data against template property and relation definitions,
/// enforcing required fields and value rules.
///
/// @param template the entity template whose property definitions are used for
/// validation
/// @param entity the entity being validated, containing the actual property
/// values to check
/// @return the validated entity with unresolved optional relation targets
/// removed
/// @throws EntityValidationException if any property validation rules are
/// violated, including missing required properties
private void validateAgainstTemplate(EntityTemplate template, Entity entity) {
private Entity validateAgainstTemplate(EntityTemplate template, Entity entity) {
Violations violations = new Violations();
violations.addIfBlank(entity.name(), ENTITY_NAME_MANDATORY);

Expand All @@ -91,12 +98,15 @@ private void validateAgainstTemplate(EntityTemplate template, Entity entity) {
propertyValidationService.validatePropertiesAgainstTemplate(template, definitions,
propertiesByName, violations);

relationValidationService.validateRelationsAgainstTemplate(template, entity.relations(),
violations);
List<Relation> normalizedRelations = relationValidationService
.validateAndNormalizeRelationsAgainstTemplate(template, entity.relations(), violations);

if (!violations.isEmpty()) {
throw new EntityValidationException(violations.asList());
}

return new Entity(entity.id(), entity.templateIdentifier(), entity.name(), entity.identifier(),
entity.properties(), normalizedRelations);
}

/// Checks for existing entity with same template and identifier to prevent
Expand Down
Loading
Loading