Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d807c69
feat(core): add post entity
RVANDO12 Apr 24, 2026
e26e26c
feat(core): fix sonar issue and copilot review
RVANDO12 May 4, 2026
fd1b354
feat(core): fix sonar review
RVANDO12 May 4, 2026
a859554
feat(core): fix sonar qube and test
RVANDO12 May 4, 2026
3958089
feat(core): fix validation type check
RVANDO12 May 4, 2026
fb272c2
feat(core): fix review
RVANDO12 May 5, 2026
6f466d6
feat(core): fix end of file
RVANDO12 May 5, 2026
6db02d4
feat(core): call entity template service in entity service
brandPittCode May 6, 2026
d914968
Merge branch 'main' into feat/entity/post
brandPittCode May 11, 2026
a1212e0
feat(core): update the validate template methods calls
brandPittCode May 11, 2026
a24bb98
Merge branch 'main' into feat/entity/post
brandPittCode May 13, 2026
30f86d6
feat(core): update the validate template methods calls
brandPittCode May 13, 2026
5f12fec
feat(core): fix review
RVANDO12 May 18, 2026
ecc92b2
feat(core): add a entity graph service and endpoint
brandPittCode May 19, 2026
1293b54
feat(core): add a entity graph service and endpoint
brandPittCode May 19, 2026
7411267
feat(core): add a entity graph service and endpoint
brandPittCode May 19, 2026
7e9c556
feat(core): add a entity graph service and endpoint
brandPittCode May 20, 2026
b68ba4d
feat(entity-graph): add a entity graph service and endpoint
brandPittCode May 20, 2026
dd5536a
feat(entity-graph): add a entity graph service and endpoint
brandPittCode May 20, 2026
3b8c995
feat(entity-graph): add a entity graph service and endpoint
brandPittCode May 20, 2026
579397f
Merge branch 'feat/entity/post' into feat/entity-graph
brandPittCode May 20, 2026
1586490
Merge branch 'main' into feat/entity-graph
brandPittCode May 20, 2026
545078b
feat(entity-graph): add a entity graph service and endpoint
brandPittCode May 20, 2026
b89ec6d
feat(core): add a entity graph service and endpoint
brandPittCode May 22, 2026
63072ef
feat(core): add a entity graph service and endpoint
brandPittCode May 28, 2026
ce8afc9
feat(core): add a entity graph service and endpoint
brandPittCode May 28, 2026
8ef61d4
Merge branch 'main' into feat/entity-graph
brandPittCode May 28, 2026
d8b8830
feat(core): add a entity graph service and endpoint
brandPittCode May 28, 2026
cfc1c51
feat(core): add a entity graph service and endpoint
brandPittCode May 29, 2026
984db20
feat(core): add a entity graph service and endpoint
brandPittCode May 29, 2026
f3fbb2f
Merge branch 'main' into feat/entity-graph
brandPittCode May 29, 2026
1929d86
feat(core): add a entity graph service and endpoint
brandPittCode May 29, 2026
ad8296b
feat(core): add a entity graph service and endpoint
brandPittCode May 29, 2026
586863c
Merge branch 'main' into feat/entity-graph
brandPittCode May 29, 2026
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
139 changes: 137 additions & 2 deletions .github/instructions/domain.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,68 @@ applyTo: '**/domain/**/*.java'

## Exceptions

- Create specific unchecked exceptions for business rule violations (for example, `EntityTemplateNotFoundException`, `EntityTemplateAlreadyExistsException`).
### General Rules

- Create **specific unchecked exceptions** for each business rule violation (for example, `EntityTemplateNotFoundException`, `EntityAlreadyExistsException`).
- Domain exceptions must **not** contain HTTP status codes or REST-specific information.
- Map domain exceptions to HTTP status codes exclusively in the Infrastructure layer (`@ControllerAdvice`).

### Exception Clarity

- **Always prefer specific exceptions over generic ones**. Never throw `IllegalArgumentException` or `IllegalStateException` for business rule violations.
- Exception names must describe **what went wrong** from a business perspective (for example, `EntityTemplateNotFoundException`, not `TemplateException`).
- Exception messages must include **context**: what entity, what identifier, what operation was attempted.

### Validation Service Pattern

When a service method needs to validate preconditions (for example, "entity template must exist before creating entity"):

1. **Extract validation into a dedicated service** (for example, `EntityTemplateValidationService`)
2. **Use explicit method names** that describe the validation (for example, `validateTemplateExists`, `validateTemplateNotExists`)
3. **Throw specific exceptions** that carry business meaning (for example, `EntityTemplateNotFoundException`)
4. **Call validation first** (fail-fast) before executing the main operation

**Benefits:**

- **Clear error messages**: `EntityTemplateNotFoundException("web-service")` vs generic `IllegalArgumentException("Invalid template")`
- **Better HTTP mapping**: specific exceptions map to appropriate status codes (404 for not found, 409 for conflict)
- **Reusable validation**: multiple services can call `validateTemplateExists` without duplicating logic
- **Fail-fast**: validation happens before expensive operations (database queries, graph traversal)

### Exception Naming Convention

| Pattern | Example | When to Use |
| --------------------------------- | --------------------------------------- | ------------------------------ |
| `<Entity>NotFoundException` | `EntityTemplateNotFoundException` | Resource doesn't exist (404) |
| `<Entity>AlreadyExistsException` | `EntityTemplateAlreadyExistsException` | Duplicate key violation (409) |
| `<Entity>ValidationException` | `PropertyValidationException` | Business rule violation (400) |
| `<Operation>NotAllowedException` | `EntityDeletionNotAllowedException` | Operation forbidden (403/409) |

### Exception Structure

```java
public class EntityTemplateNotFoundException extends RuntimeException {

private final String identifier;

public EntityTemplateNotFoundException(String identifier) {
super(String.format("Entity template with identifier '%s' not found", identifier));
this.identifier = identifier;
}

public String getIdentifier() {
return identifier;
}
}
```

**Rules:**

- Extend `RuntimeException` (unchecked) for business exceptions
- Include a formatted message with all relevant context
- Store identifiers/keys as fields if needed for logging or error responses
- Never include stack traces in exception messages

## Constants

- Use a dedicated constants class (for example, `ValidationMessages.java`) for all validation messages.
Expand All @@ -58,6 +116,71 @@ applyTo: '**/domain/**/*.java'
- **Adapter-Level vs. Domain-Level**: syntactic checks (nulls, empty strings) belong on DTOs in the Infrastructure layer. Semantic checks (uniqueness, cross-field rules) belong in Domain Services.
- Throw a custom `DomainValidationException` (or similar unchecked exception) when rules are violated.

### Creating Validation Services

When validation logic is reused across multiple domain services:

1. **Create a dedicated validation service** (for example, `EntityTemplateValidationService`)
2. **Extract validation methods** with clear names: `validateTemplateExists`, `validateTemplateNotExists`, `validateTemplateNotReferenced`
3. **Always call validation first** before the main operation (fail-fast principle)

**Example validation service:**

```java
@Service
@RequiredArgsConstructor
public class EntityTemplateValidationService {

private final EntityTemplateRepositoryPort repository;

public void validateTemplateExists(String identifier) {
if (!repository.existsByIdentifier(identifier)) {
throw new EntityTemplateNotFoundException(identifier);
}
}

public void validateTemplateNotExists(String identifier) {
if (repository.existsByIdentifier(identifier)) {
throw new EntityTemplateAlreadyExistsException(identifier);
}
}

public void validateTemplateNotReferenced(String identifier) {
if (repository.hasEntities(identifier)) {
throw new EntityTemplateReferencedException(identifier,
"Cannot delete template that is referenced by entities");
}
}
}
```

**Usage (fail-fast):**

```java
@Service
@RequiredArgsConstructor
public class EntityService {

private final EntityTemplateValidationService templateValidation;
private final EntityRepositoryPort entityRepository;

@Transactional
public Entity createEntity(String templateIdentifier, String entityIdentifier, ...) {
// Validate template exists FIRST (fail-fast)
templateValidation.validateTemplateExists(templateIdentifier);

// Validate entity doesn't already exist
if (entityRepository.existsByIdentifier(entityIdentifier)) {
throw new EntityAlreadyExistsException(entityIdentifier);
}

// Main operation
Entity entity = new Entity(...);
return entityRepository.save(entity);
}
}
```

## Mapping

- Never use `ObjectMapper` or reflection-based libraries for internal layer mapping.
Expand All @@ -70,10 +193,22 @@ applyTo: '**/domain/**/*.java'
domain/
├── constant/ # Validation message constants
├── exception/ # Domain-specific exceptions
│ ├── entity/ # Entity-related exceptions
│ ├── entity_template/ # Template-related exceptions
│ └── property/ # Property-related exceptions│
├── model/
│ ├── entity/ # Core business records
│ ├── entity_template/ # Template records
│ └── enums/ # Business enums
├── port/ # Port interfaces (contracts for driven adapters)
├── port/ # Port interfaces (contracts for driven adapters)
└── service/ # Domain services (orchestration)
├── entity/ # Entity services
├── entity_template/ # Template validation services
└── entity_graph/ # Graph services
```

### Exception Package Organization

- Organize exceptions by aggregate/subdomain (for example, `entity/`, `entity_template/`, `property/`)
- Each exception class should have a clear, descriptive name that follows the naming conventions above
- Keep exception hierarchy flat. Avoid deep inheritance trees
18 changes: 9 additions & 9 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace # Trims trailing whitespace
- id: trailing-whitespace # Trims trailing whitespace
exclude: |
(?x)^(
.gitmodules|
Expand All @@ -12,23 +12,23 @@ repos:
.*\.drawio.*|
.*\.snap
)$
- id: check-yaml # Validates YAML files
- id: check-yaml # Validates YAML files
args:
- --allow-multiple-documents
- id: check-json # Validates JSON files
- id: check-case-conflict # Checks for files that would conflict in case-insensitive filesystems
- id: check-merge-conflict # Checks for files that contain merge conflict strings
- id: detect-private-key # Check for the existence of private keys
- id: check-executables-have-shebangs # Checks that executables have shebangs
- id: check-json # Validates JSON files
- id: check-case-conflict # Checks for files that would conflict in case-insensitive filesystems
- id: check-merge-conflict # Checks for files that contain merge conflict strings
- id: detect-private-key # Check for the existence of private keys
- id: check-executables-have-shebangs # Checks that executables have shebangs
exclude: |
(?x)^(
.*\.java
)$
- id: end-of-file-fixer # Makes sure files end in a newline and only a newline
- id: end-of-file-fixer # Makes sure files end in a newline and only a newline
- repo: https://github.com/adrienverge/yamllint
rev: v1.37.1
hooks:
- id: yamllint # Lints YAML files
- id: yamllint # Lints YAML files
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.47.0
hooks:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

import static com.decathlon.idp_core.domain.constant.ValidationMessages.TEMPLATE_IDENTIFIER_CANNOT_CHANGE;

import com.decathlon.idp_core.domain.model.entity_template.EntityTemplate;
import com.decathlon.idp_core.domain.service.entity_template.EntityTemplateService;
import com.decathlon.idp_core.infrastructure.adapters.api.handler.ApiExceptionHandler;

Comment on lines +5 to +8
/// Exception thrown when attempting to change an [EntityTemplate] identifier after creation.
///
/// **Why this exception exists:**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import static com.decathlon.idp_core.domain.constant.ValidationMessages.PROPERTY_NAME_ALREADY_EXISTS;

import com.decathlon.idp_core.domain.model.entity_template.EntityTemplate;

/// Exception thrown when attempting to create or update an [EntityTemplate] with duplicate property names.
///
/// This exception represents a business rule violation where unique constraints on property
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import static com.decathlon.idp_core.domain.constant.ValidationMessages.RELATION_NAME_ALREADY_EXISTS;

import com.decathlon.idp_core.domain.model.entity_template.EntityTemplate;

/// Exception thrown when attempting to create or update an [EntityTemplate] with duplicate relation names.
///
/// This exception represents a business rule violation where unique constraints on relation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

import jakarta.validation.constraints.NotBlank;

import com.decathlon.idp_core.domain.model.entity_template.EntityTemplate;

/// Domain entity representing a concrete instance of an [EntityTemplate].
///
/// Business invariants:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.decathlon.idp_core.domain.model.entity;

import java.util.Objects;

/**
* Composite key for uniquely identifying an entity across templates. Since the
* same identifier can exist in different templates, we need both fields.
*/
public record EntityCompositeKey(String templateIdentifier, String identifier) {
public static EntityCompositeKey fromString(String compositeKey) {
String[] parts = compositeKey.split(":", 2);
if (parts.length != 2) {
throw new IllegalArgumentException("Invalid composite key format: " + compositeKey);
}
return new EntityCompositeKey(parts[0], parts[1]);
Comment on lines +10 to +15
}

@Override
public String toString() {
return templateIdentifier + ":" + identifier;
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
EntityCompositeKey that = (EntityCompositeKey) o;
return Objects.equals(templateIdentifier, that.templateIdentifier)
&& Objects.equals(identifier, that.identifier);
}

@Override
public int hashCode() {
return Objects.hash(templateIdentifier, identifier);
}
}
Empty file.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

import jakarta.validation.constraints.NotBlank;

import com.decathlon.idp_core.domain.model.entity_template.PropertyDefinition;
import com.decathlon.idp_core.domain.model.entity_template.PropertyRules;
import com.decathlon.idp_core.domain.model.enums.PropertyType;

/// A concrete property instance belonging to an [Entity].
///
/// Represents actual business data values that conform to the constraints
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

import com.decathlon.idp_core.domain.model.entity_template.EntityTemplate;
import com.decathlon.idp_core.domain.model.entity_template.RelationDefinition;

/// A concrete relationship instance connecting entities in the business domain.
///
/// Represents actual business connections between entities that conform to the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.decathlon.idp_core.domain.model.entity_graph;

import java.util.List;

import com.decathlon.idp_core.domain.model.entity.Property;

/// A node in the entity relationship graph, containing summary information
/// and its resolved relations (recursively up to a configurable depth).
///
/// **Business purpose:**
/// - Visualizing entity dependency graphs
/// - Understanding relationship chains between entities
/// - Providing a hierarchical view of entity connections
///
/// @param templateIdentifier the template identifier this entity belongs to
/// @param identifier the business identifier of the entity
/// @param name the human-readable name of the entity
/// @param properties the entity's property instances; empty when not requested
/// @param relations the resolved outbound relations with their target graph nodes
/// @param relationsAsTarget incoming relations where this entity is the target
public record EntityGraphNode(String templateIdentifier, String identifier, String name,
List<Property> properties, List<EntityGraphRelation> relations,
List<EntityGraphRelation> relationsAsTarget) {
public EntityGraphNode {
properties = properties != null ? List.copyOf(properties) : List.of();
relations = relations != null ? List.copyOf(relations) : List.of();
relationsAsTarget = relationsAsTarget != null ? List.copyOf(relationsAsTarget) : List.of();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.decathlon.idp_core.domain.model.entity_graph;

import java.util.List;

/// Represents a single named relation in the entity graph with its resolved target nodes.
///
/// **Business purpose:**
/// - Groups related entities under their relation name
/// - Enables graph traversal by relation type
///
/// @param name the relation name as defined in the entity template
/// @param targetTemplateIdentifier the template identifier of the target entities
/// @param targets the resolved target entity graph nodes (recursively populated up to depth)
Comment on lines +11 to +13
public record EntityGraphRelation(String name, List<EntityGraphNode> targets) {
public EntityGraphRelation {
targets = targets != null ? List.copyOf(targets) : List.of();
}
}
Loading
Loading