Skip to content

feat(core): add entity graph endpoint and service#56

Draft
brandPittCode wants to merge 34 commits into
mainfrom
feat/entity-graph
Draft

feat(core): add entity graph endpoint and service#56
brandPittCode wants to merge 34 commits into
mainfrom
feat/entity-graph

Conversation

@brandPittCode
Copy link
Copy Markdown
Collaborator

@brandPittCode brandPittCode commented May 20, 2026

PR Description

What this PR Provides

This pull request introduces add new domain models and ports for entity graph traversal. Additionally, the package structure is refined to organize exceptions and services by subdomain.

Entity Graph Modeling

  • New domain models are introduced for entity graph traversal: EntityGraphNode and EntityGraphRelation, supporting hierarchical visualization and traversal of entity relationships.
  • A new port interface, EntityGraphRepositoryPort, defines the contract for retrieving entity relationship graphs with support for depth, property inclusion, and efficient lookups.

Domain Model Enhancements

  • A new EntityCompositeKey record is added to uniquely identify entities across templates, supporting composite key parsing, equality, and string representation.

Exception Handling and Validation Patterns

  • The domain exception guidelines are expanded to require specific unchecked exceptions for each business rule violation, discourage the use of generic exceptions (like IllegalArgumentException), and enforce descriptive naming and contextual messages. A validation service pattern is introduced for reusable, fail-fast validation logic, with concrete code examples and naming conventions.
  • The package structure is updated to organize exceptions and services by aggregate/subdomain, promoting clarity and maintainability.

Code Consistency

  • Several domain model and exception classes now explicitly import related types for clarity and consistency.

These changes collectively improve the robustness, clarity, and scalability of the domain layer.

Fixes

Review

The reviewer must double-check these points:

  • The reviewer has tested the feature
  • The reviewer has reviewed the implementation of the feature
  • The documentation has been updated
  • The feature implementation respects the Technical Doc / ADR previously produced
  • The Pull Request title has a ! after the type/scope to identify the breaking
    change in the release note and ensure we will release a major version.

How to test

Test Scenarios

Graph API Test Scenarios - Generic Guide

Overview

This guide explains how to test the entity graph feature without assuming any pre-existing data. You'll need to set up a simple 3-node graph structure to validate all filtering capabilities.


Graph Structure Required

To test all scenarios, you need a graph with the following characteristics:

Entities

Create 3 entities

  • Entity A (root node) - will be the starting point for all queries
  • Entity B (middle node) - connected to both A and C
  • Entity C (leaf node) - only connected to B

Properties

Each entity should have 2 properties to test property filtering:

  • Property 1 - any string property (e.g., tier, environment, status)
  • Property 2 - another string property (e.g., version, owner, region)

Use different values for each entity so you can verify property data is returned correctly.

Relations

Create 2 types of relations to test relation filtering:

  • Relation Type 1 - bidirectional chain (A → B → C)
  • Relation Type 2 - only between A and B (not extending to C)

Graph topology:

Entity A --[Relation-Type-1]----> Entity B --[Relation-Type-1]----> Entity C
Entity A --[Relation-Type-2]----> Entity B

Why this structure?

  • Tests depth traversal (3 levels)
  • Tests relation filtering (2 different relation types)
  • Tests property filtering (2 properties per node)
  • Tests partial connectivity (Relation-Type-2 stops at B, doesn't reach C)

Test Scenarios

✅ Scenario 1: Full Graph (No Filters)

Goal: Return all nodes and all edges

Request:

GET /api/v1/entities/{template}/{entityA}/graph?depth=3&include_data=false

Expected Behavior:

  • Returns 3 nodes (A, B, C)
  • Returns 3 edges (A→B type-1, A→B type-2, B→C type-1)
  • No property data in nodes (because include_data=false)

What to verify:

  • Node count = 3
  • Edge count = 3
  • Each edge has correct source, target, and type

✅ Scenario 2: Filter by Relation Type 1

Goal: Only traverse edges of Relation-Type-1

Request:

GET /api/v1/entities/{template}/{entityA}/graph?depth=3&include_data=false&relations=relation-type-1

Expected Behavior:

  • Returns 3 nodes (A, B, C) - all reachable via type-1 edges
  • Returns 2 edges (A→B type-1, B→C type-1)
  • Excludes A→B type-2 edge

What to verify:

  • Node count = 3
  • Edge count = 2
  • No edges with type: relation-type-2

✅ Scenario 3: Filter by Relation Type 2

Goal: Only traverse edges of Relation-Type-2

Request:

GET /api/v1/entities/{template}/{entityA}/graph?depth=3&include_data=false&relations=relation-type-2

Expected Behavior:

  • Returns 2 nodes (A, B only)
  • Returns 1 edge (A→B type-2)
  • Excludes Entity C (not reachable via type-2 edges)

What to verify:

  • Node count = 2
  • Edge count = 1
  • Entity C is not in the response (critical validation point)

✅ Scenario 4: Include All Properties

Goal: Return property data for all nodes

Request:

GET /api/v1/entities/{template}/{entityA}/graph?depth=3&include_data=true

Expected Behavior:

  • Returns 3 nodes with property data
  • Each node has a data object containing both property-1 and property-2
  • Edge structure remains the same (3 edges)

What to verify:

  • All nodes have data field
  • data.property-1 exists with correct value
  • data.property-2 exists with correct value

Example node structure:

{
  "id": "template:entityA",
  "templateIdentifier": "template",
  "identifier": "entityA",
  "name": "Entity A Name",
  "data": {
    "property-1": "value-a-1",
    "property-2": "value-a-2"
  }
}

✅ Scenario 5: Filter Property 1 Only

Goal: Return only property-1 in node data

Request:

GET /api/v1/entities/{template}/{entityA}/graph?depth=3&include_data=true&properties=property-1

Expected Behavior:

  • Returns 3 nodes with partial property data
  • Each node's data object contains only property-1
  • property-2 is excluded from the response

What to verify:

  • data.property-1 exists
  • data.property-2 does not exist (not null, completely absent)

✅ Scenario 6: Filter Multiple Properties

Goal: Return both property-1 and property-2

Request:

GET /api/v1/entities/{template}/{entityA}/graph?depth=3&include_data=true&properties=property-1&properties=property-2

Expected Behavior:

  • Returns 3 nodes with full property data
  • Each node's data contains both properties
  • Identical to Scenario 4 (because we're requesting all properties that exist)

What to verify:

  • Both data.property-1 and data.property-2 exist
  • Values match the entity's actual data

✅ Scenario 7: Non-Existent Property Filter

Goal: Request a property that doesn't exist on any entity

Request:

GET /api/v1/entities/{template}/{entityA}/graph?depth=3&include_data=true&properties=non-existent-property

Expected Behavior:

  • Returns 3 nodes without data field
  • Nodes contain id, templateIdentifier, identifier, name
  • The data field is completely omitted (not null, not {})

What to verify:

  • data field is absent from all nodes
  • Graph structure (nodes, edges) is still complete

✅ Scenario 8: Combine Relation + Property Filters

Goal: Apply both relation and property filters simultaneously

Request:

GET /api/v1/entities/{template}/{entityA}/graph?depth=3&include_data=true&relations=relation-type-1&properties=property-1

Expected Behavior:

  • Returns 3 nodes (A, B, C - all reachable via type-1)
  • Returns 2 edges (only type-1 edges)
  • Each node has data with only property-1

What to verify:

  • Node count = 3
  • Edge count = 2 (no type-2 edges)
  • data.property-1 exists
  • data.property-2 does not exist

❌ Scenario 9: Entity Not Found

Goal: Request a non-existent entity identifier

Request:

GET /api/v1/entities/{template}/non-existent-entity/graph?depth=3&include_data=false

Expected Behavior:

  • HTTP 404 Not Found
  • Error response with message explaining entity doesn't exist

What to verify:

  • Status code = 404
  • Error message mentions the template and identifier
  • Example: "Entity with template 'template-name' and identifier 'non-existent-entity' not found"

❌ Scenario 10: Template Not Found

Goal: Request an entity from a non-existent template

Request:

GET /api/v1/entities/non-existent-template/{entityA}/graph?depth=3&include_data=false

Expected Behavior:

  • HTTP 404 Not Found
  • Error response explaining template doesn't exist
  • Validation happens before entity lookup (fail-fast)

What to verify:

  • Status code = 404
  • Error message mentions the template identifier
  • Example: "Entity template with identifier 'non-existent-template' not found"

Critical test: Scenario 3 (filter by type-2) must return only 2 nodes. If it returns 3 nodes, the filtering is not working correctly.

Breaking changes (if any)

  • N/A

@brandPittCode brandPittCode changed the title Feat/entity graph feat(core): add entity graph endpoint and service May 20, 2026
@gitguardian
Copy link
Copy Markdown

gitguardian Bot commented May 28, 2026

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
30005138 Triggered Generic Password ce8afc9 src/test/java/com/decathlon/idp_core/AbstractIntegrationTest.java View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

Comment thread src/test/java/com/decathlon/idp_core/AbstractIntegrationTest.java Fixed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an entity relationship graph capability to idp-core, adding domain modeling + persistence traversal support and exposing a new REST endpoint that returns a flat nodes/edges graph suitable for UI visualization.

Changes:

  • Adds a new GET /api/v1/entities/{templateIdentifier}/{entityIdentifier}/graph endpoint returning a flat graph DTO (nodes + edges), with optional depth / relation-name / property-name filters.
  • Introduces new domain graph models and a domain service to build depth-limited graphs (including inbound + outbound relations) and applies relation/property filtering.
  • Adds a dedicated persistence adapter and repository queries (recursive CTE + batch fetch) plus expanded SQL seed data and both unit + integration tests.

Reviewed changes

Copilot reviewed 23 out of 33 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
src/test/resources/db/test/R__2_Insert_entities_test_data.sql Extends seeded entities/relations/properties for query-filter and graph endpoint tests.
src/test/resources/db/test/R__1_Insert_test_data.sql Ensures repeatable migrations clear child/parent tables in FK-safe order.
src/test/java/com/decathlon/idp_core/infrastructure/adapters/api/controller/EntityGraphControllerTest.java New integration coverage for graph endpoint (depth, relation filter, property filter, auth/error cases).
src/test/java/com/decathlon/idp_core/infrastructure/adapters/api/controller/EntityControllerTest.java Updates expected entity counts due to additional seeded entities.
src/test/java/com/decathlon/idp_core/domain/service/entity_graph/EntityGraphServiceTest.java New unit coverage for depth clamping, filtering, inbound/outbound relations, and cycle guard behavior.
src/main/java/com/decathlon/idp_core/infrastructure/adapters/persistence/repository/JpaRelationRepository.java Adjusts JPQL projection to return a typed summary record.
src/main/java/com/decathlon/idp_core/infrastructure/adapters/persistence/repository/JpaEntityRepository.java Adds recursive CTE queries + batch-fetch helpers used for graph traversal.
src/main/java/com/decathlon/idp_core/infrastructure/adapters/persistence/PostgresEntityGraphAdapter.java New persistence adapter implementing the graph repository port using CTE + batch loading.
src/main/java/com/decathlon/idp_core/infrastructure/adapters/api/mapper/entity/EntityGraphFlatDtoOutMapper.java Flattens the recursive domain graph into nodes/edges DTOs with de-duplication guards.
src/main/java/com/decathlon/idp_core/infrastructure/adapters/api/dto/out/entity/RelationAsTargetSummaryDtoOut.java New DTO for inbound relation summaries.
src/main/java/com/decathlon/idp_core/infrastructure/adapters/api/dto/out/entity/EntityGraphNodeFlatDtoOut.java New flat node DTO (optional data map for properties).
src/main/java/com/decathlon/idp_core/infrastructure/adapters/api/dto/out/entity/EntityGraphFlatDtoOut.java New top-level flat graph DTO (nodes + edges).
src/main/java/com/decathlon/idp_core/infrastructure/adapters/api/dto/out/entity/EntityGraphEdgeDtoOut.java New edge DTO representing directed relations.
src/main/java/com/decathlon/idp_core/infrastructure/adapters/api/controller/EntityGraphController.java New REST controller exposing the graph endpoint and query params.
src/main/java/com/decathlon/idp_core/infrastructure/adapters/api/configuration/SwaggerDescription.java Adds OpenAPI description constants for the graph endpoint and DTO fields.
src/main/java/com/decathlon/idp_core/domain/service/entity_graph/EntityGraphService.java New domain service assembling a depth-limited graph with filtering and cycle guards.
src/main/java/com/decathlon/idp_core/domain/port/EntityGraphRepositoryPort.java New driven port for graph retrieval from persistence.
src/main/java/com/decathlon/idp_core/domain/model/entity/Relation.java Adds explicit imports referenced by documentation/comments.
src/main/java/com/decathlon/idp_core/domain/model/entity/Property.java Adds explicit imports referenced by documentation/comments.
src/main/java/com/decathlon/idp_core/domain/model/entity/EntityCompositeKey.java New composite identifier record (template + identifier).
src/main/java/com/decathlon/idp_core/domain/model/entity/Entity.java Adds explicit import referenced by documentation/comments.
src/main/java/com/decathlon/idp_core/domain/model/entity_graph/EntityGraphRelation.java New domain model for graph relations (name + target nodes).
src/main/java/com/decathlon/idp_core/domain/model/entity_graph/EntityGraphNode.java New domain model for graph nodes (properties + in/out relations).
src/main/java/com/decathlon/idp_core/domain/exception/entity_template/RelationNameAlreadyExistsException.java Adds explicit import referenced by documentation/comments.
src/main/java/com/decathlon/idp_core/domain/exception/entity_template/PropertyNameAlreadyExistsException.java Adds explicit import referenced by documentation/comments.
src/main/java/com/decathlon/idp_core/domain/exception/entity_template/EntityTemplateIdentifierCannotChangeException.java Updates imports (but currently introduces an invalid Domain→Infrastructure dependency).
.pre-commit-config.yaml Small formatting tweaks to hook comment spacing.
.github/instructions/domain.instructions.md Expands documented domain exception/validation patterns and package organization guidance.

Comment on lines +68 to +71
JOIN relation r ON r.id = er.relation_id
JOIN relation_target_entities rte ON rte.relation_id = r.id
JOIN entity e2 ON e2.identifier = rte.target_entity_identifier
WHERE og.depth < :depth
Comment on lines +85 to +89
JOIN relation_target_entities rte ON rte.target_entity_identifier = e.identifier
JOIN relation r ON r.id = rte.relation_id
JOIN entity_relations er ON er.relation_id = r.id
JOIN entity e2 ON e2.id = er.entity_id
WHERE ig.depth < :depth
Comment on lines +117 to +121
JOIN entity_relations er ON er.entity_id = e.id
JOIN relation r ON r.id = er.relation_id
JOIN relation_target_entities rte ON rte.relation_id = r.id
JOIN entity e2 ON e2.identifier = rte.target_entity_identifier
WHERE og.depth < :depth
Comment on lines +135 to +139
JOIN relation_target_entities rte ON rte.target_entity_identifier = e.identifier
JOIN relation r ON r.id = rte.relation_id
JOIN entity_relations er ON er.relation_id = r.id
JOIN entity e2 ON e2.id = er.entity_id
WHERE ig.depth < :depth
Comment on lines +43 to +51
@Query("SELECT DISTINCT e FROM EntityJpaEntity e LEFT JOIN FETCH e.relations WHERE e.identifier IN :identifiers")
List<EntityJpaEntity> findAllByIdentifierInWithRelations(
@Param("identifiers") Collection<String> identifiers);

/// Fetch properties for entities that were already loaded. This is called after
/// findAllByIdentifierInWithRelations to complete the entity graph.
@Query("SELECT DISTINCT e FROM EntityJpaEntity e LEFT JOIN FETCH e.properties WHERE e.identifier IN :identifiers")
List<EntityJpaEntity> findAllByIdentifierInWithProperties(
@Param("identifiers") Collection<String> identifiers);
Comment on lines +39 to +41
/// @param relationNames when non-empty, only edges whose relation name is in
/// this set are
/// traversed; when empty, all relation types are followed
Comment on lines +11 to +13
/// @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 +10 to +15
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 +46 to +49
@RestController
@RequestMapping("/api/v1/entities")
@RequiredArgsConstructor
@Tag(name = "Entity Graph", description = "Entity relationship graph operations")
Comment on lines +24 to +25
-- Add to end of R__1_Insert_test_data.sql

@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants