Feature ETP-3354: Dynamic DAS mapping layer#223
Feature ETP-3354: Dynamic DAS mapping layer#223sebastianbarrozo wants to merge 53 commits intodevelopfrom
Conversation
Phase 1: Dynamic Metadata Service - Standard stack identified (Spring Boot 3 + Caffeine cache) - Architecture patterns documented (service layer, records, @Cacheable) - Pitfalls catalogued (N+1 queries, lazy loading, self-invocation) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 01: Dynamic Metadata Service - 2 plan(s) in 2 wave(s) - 1 parallel (wave 1), 1 sequential (wave 2 depends on wave 1) - Ready for execution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Split Plan 01-01 into 01-01 (models+interface) and 01-02 (service impl). Renumbered test plan to 01-03. Removed non-autonomous Task 2 from test plan and merged verification into Task 1. Added testPreloadCache test. Fixed must_haves wording. Removed ambiguous conditional guidance. Updated wave assignments and dependencies. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Caffeine and spring-boot-starter-cache dependencies to build.gradle - Create FieldMappingType enum with four mapping types (DM, JM, CV, JP) - Create immutable FieldMetadata record for field mapping configuration - Create immutable EntityMetadata record for projection entity metadata - Create immutable ProjectionMetadata record with entity lookup helper These foundational types establish the metadata model for the dynamic DAS system. All records are immutable for safe caching.
- Define public API for metadata queries: getProjection, getProjectionEntity, getFields - Add getAllProjectionNames for listing all registered projections - Add invalidateCache for manual cache invalidation - Interface includes comprehensive Javadoc for each method This establishes the API contract that the service implementation (Plan 02) will fulfill.
Tasks completed: 2/2 - Task 1: Add cache dependencies and create metadata models - Task 2: Create DynamicMetadataService interface SUMMARY: .planning/phases/01-dynamic-metadata-service/01-01-SUMMARY.md
- Added MetadataCacheConfig with @EnableCaching - Configured projections and projectionsByName caches - Set 500 max entries, 24hr expiration, stats recording - Uses Caffeine with Spring Cache abstraction Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Service with EntityManager and CacheManager injection - preloadCache() method with @eventlistener(ApplicationReadyEvent) - JPQL queries with JOIN FETCH for projections + entities + fields - Hibernate.initialize() for lazy relationships - @Cacheable on getProjection(String name) - JPA-to-record conversion: toProjectionMetadata/toEntityMetadata/toFieldMetadata - FieldMappingType.fromCode() usage with error handling - getProjectionEntity delegates to getProjection().flatMap() - getFields iterates cache, falls back to DB query - getAllProjectionNames returns cache keySet - @CacheEvict on invalidateCache() - unwrapCacheValue and loadFieldsFromDb helper methods Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2 - Create MetadataCacheConfig - Create DynamicMetadataServiceImpl SUMMARY: .planning/phases/01-dynamic-metadata-service/01-02-SUMMARY.md
- Test projection loading and caching - Test all four field mapping types (DM, JM, CV, JP) - Test cache invalidation - Test entity navigation - Test field retrieval from cache and database - Test preload functionality - Test getAllProjectionNames - 15 test methods covering all scenarios Note: Tests cannot be executed due to pre-existing compilation issues in the entities module (unrelated to this work). Tests are structurally correct and ready to run once compilation issues are resolved. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 1/1 - Created DynamicMetadataServiceTest with 15 comprehensive test methods SUMMARY: .planning/phases/01-dynamic-metadata-service/01-03-SUMMARY.md Note: Tests cannot be executed due to pre-existing project-wide compilation issues in entity code generation (unrelated to this work). Tests are structurally correct and ready to run once compilation blockers resolved.
Includes diagnostic tools Phase 1 (Dynamic Metadata Service) complete and verified (14/14 must-haves). Added MetadataDiagnosticController with HTML explorer, JWT testing, Postman export, cache reload, and system info modal. Fixed FreeMarker template compilation issues. Added EM/CM field mapping types, in-development module filter, and DbPublicKeyInitializer for local JWT validation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 02: Generic DTO Converter - Implementation decisions documented - Phase boundary established - Portability strategy: develop in RX, reimplement in Classic - Key decisions: Hibernate PropertyAccessor, nested EM objects, externalId for writes, compatible type coercion Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 02: Generic DTO Converter - Standard stack identified (Spring, Hibernate 6, BeanUtils) - Architecture patterns documented (Strategy per FieldMappingType) - Pitfalls catalogued (null handling, cycles, type coercion, audit fields) - Code examples from generated converters analyzed
Phase 02: Generic DTO Converter - 3 plans in 3 waves - Wave 1: Foundation (strategy interface, PropertyAccessor, ConversionContext, DM/CV/CM strategies) - Wave 2: Complex strategies (EM, JM, JP) + DynamicDTOConverter orchestrator - Wave 3: Unit tests for converter and strategies - Ready for execution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add BeanUtils dependency for nested property access - Add Apache Commons BeanUtils 1.9.4 dependency to build.gradle - Create FieldConversionStrategy interface with readField/writeField contracts - Create ConversionContext for cycle detection with isVisited() method - Create ConversionException for conversion error handling - Create PropertyAccessorService using BeanUtils for nested property access - Support for fullDto field in ConversionContext for JM write strategies
DirectMapping, ConstantValue, and ComputedMapping strategies - Create DirectMappingStrategy with readField using getNestedProperty + handleBaseObject - DirectMappingStrategy writeField handles Date/numeric type coercion - Create ConstantValueStrategy using mappingUtils.constantValue() for reads - ConstantValueStrategy writeField is no-op (constants are read-only) - Create ComputedMappingStrategy delegating to ConstantValueStrategy - All strategies are @component Spring beans implementing FieldConversionStrategy
Tasks completed: 2/2 - Task 1: Create converter foundation classes and add BeanUtils dependency - Task 2: Create DirectMapping, ConstantValue, and ComputedMapping strategies SUMMARY: .planning/phases/02-generic-dto-converter/02-01-SUMMARY.md
…gies - EntityMappingStrategy: recursive entity conversion with cycle detection, handles both many-to-one and one-to-many (Collection), resolves write references via ExternalIdService, uses @lazy to break circular dependency with DynamicDTOConverter - JavaMappingStrategy: delegates read to DTOReadMapping and write to DTOWriteMapping beans resolved by Spring qualifier from field metadata, passes full DTO from ConversionContext for write operations - JsonPathStrategy: extracts values from JSON string properties using com.jayway.jsonpath.JsonPath.read(), applies MappingUtils.handleBaseObject() for type coercion, write operations are no-op (read-only strategy) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bidirectional entity-to-DTO conversion - Strategy map wiring all 6 FieldMappingType values to their implementations - convertToMap: Entity -> Map using field metadata and ConversionContext for cycle detection propagation across recursive EM conversions - convertToEntity: Map -> Entity with mandatory field validation (excluding CV/CM constants), field population via strategy dispatch, and automatic audit field integration via AuditServiceInterceptor - Entity instantiation via AD_Table.javaClassName JPQL lookup with ConcurrentHashMap cache for repeated lookups - findEntityMetadataById helper for EM strategy to resolve related entity metadata by scanning all projections Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… plan Tasks completed: 2/2 - Task 1: EntityMapping, JavaMapping, and JsonPath strategies - Task 2: DynamicDTOConverter orchestrator SUMMARY: .planning/phases/02-generic-dto-converter/02-02-SUMMARY.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… unit tests - DirectMappingStrategy: 7 tests covering read/write, null handling, nested paths, Date coercion - EntityMappingStrategy: 6 tests covering cycle detection stub, recursive conversion, ExternalId resolution - Tests use @ExtendWith(MockitoExtension.class), AAA pattern, descriptive method names - Matches project test conventions from DynamicMetadataServiceTest Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 14 tests: convertToMap (6) and convertToEntity (8) - Strategy routing: DM, CV, JM delegation verified - Mandatory validation: throws ConversionException, excludes CV/CM fields - Audit integration: setAuditValues called for BaseRXObject, skipped for plain Object - Graceful degradation: strategy exceptions result in null field values - ConversionContext fullDto propagation verified via ArgumentCaptor - Field order preservation via LinkedHashMap assertion Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2 - Task 1: DirectMappingStrategy (7 tests) and EntityMappingStrategy (6 tests) - Task 2: DynamicDTOConverter orchestrator (14 tests) SUMMARY: .planning/phases/02-generic-dto-converter/02-03-SUMMARY.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
BaseRXObject does not define getId() — the id field lives on each generated entity subclass. Use PropertyAccessorService to read it dynamically. Also mark Phase 2 plans complete in ROADMAP. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 03: Generic Repository Layer - Implementation decisions documented - Phase boundary established - CRUD, entity resolution, transactions, ExternalId flow covered Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 3: Generic Repository Layer - Standard stack identified (EntityManager, CriteriaBuilder, Jakarta Validator) - Architecture patterns documented (entity class resolution, dynamic filtering, save/update flow) - Exact order of operations from BaseDTORepositoryDefault analyzed - Pitfalls catalogued (persist vs merge, double save, validator id skip, external id timing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 03: Generic Repository Layer - 2 plan(s) in 2 wave(s) - Wave 1: EntityClassResolver + DynamicRepository (CRUD/batch/pagination) - Wave 2: Unit tests for repository layer - Ready for execution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address 5 checker issues: - BLOCKER: Pre-instantiate new entities via EntityClassResolver before passing to converter (avoids AD_Table.javaClassName path) - BLOCKER: Remove duplicate auditService.setAuditValues() call from repository (converter already handles it internally) - WARNING: Split DynamicRepository into read ops (Task 2) and write ops (Task 3) for scope management - WARNING: Reframe implementation-focused must_haves truth to user-observable behavior - WARNING: Document convertExternalToInternalId as Phase 4 controller responsibility (FR-5 partially covered) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…eption - EntityClassResolver scans Hibernate metamodel at startup via @eventlistener(ApplicationReadyEvent) - Builds ConcurrentHashMap indexes by @table(name) and static TABLE_ID field - resolveByTableId() and resolveByTableName() with DynamicRepositoryException on miss - DynamicRepositoryException extends RuntimeException with message and cause constructors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Translates top-level "id" field from external to internal ID - Translates ENTITY_MAPPING reference fields using related entity tableId - Handles both String IDs and nested Map objects with "id" key - Constructor-injects ExternalIdService and DynamicDTOConverter - Mutates DTO map in place for controller-level ID translation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Logs all dynamic endpoints at startup via @eventlistener(ApplicationReadyEvent) - Entities with restEndPoint=false logged at DEBUG level, not registered - isRestEndpoint validates entity REST access by projection + externalName - resolveEntityByExternalName returns Optional<EntityMetadata> for URL resolution - Matches externalName first, falls back to entity name if externalName is null Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2 - Create ExternalIdTranslationService - Create DynamicEndpointRegistry SUMMARY: .planning/phases/04-generic-rest-controller/04-01-SUMMARY.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add @RestController with @RequestMapping("/{projectionName}/{entityName}") - Implement resolveEntityMetadata helper for validation and entity lookup - Add findAll endpoint with pagination, sorting, and filter support - Add findById endpoint with EntityNotFoundException -> 404 mapping - Constructor-inject DynamicRepository, DynamicEndpointRegistry, ExternalIdTranslationService Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… support - Add POST endpoint with Jayway JsonPath parsing (exact BindedRestController pattern) - Support JSONArray batch creation via saveBatch repository method - Add PUT endpoint with path ID override and ObjectMapper parsing - External ID translation applied before repository delegation - Error handling: JsonProcessingException -> 400, ResponseStatusException -> rethrow - POST returns 201 for both single and batch, PUT returns 201 (matches BindedRestController) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2 - Create DynamicRestController with GET endpoints - Add POST and PUT endpoints with json_path and batch support SUMMARY: .planning/phases/04-generic-rest-controller/04-02-SUMMARY.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ExternalIdTranslationService and DynamicEndpointRegistry tests - ExternalIdTranslationServiceTest: 8 tests covering id translation, EM field translation (String/Map), skip logic, passthrough, multiple fields - DynamicEndpointRegistryTest: 8 tests covering entity resolution by externalName, name fallback, restEndPoint gating, startup logging
…ethods - GET findAll: paginated results, filter param cleanup, 404 for missing projection, 404 for restEndPoint=false - GET findById: success 200, 404 not found, 404 restEndPoint=false - POST create: single 201, batch 201, json_path extraction, empty body 400, default json_path, translateExternalIds - PUT update: 201 status, id from path variable, translateExternalIds called
Tasks completed: 2/2 - Task 1: ExternalIdTranslationServiceTest (8 tests) and DynamicEndpointRegistryTest (8 tests) - Task 2: DynamicRestControllerTest (16 tests) SUMMARY: .planning/phases/04-generic-rest-controller/04-03-SUMMARY.md
Phase 4 verified: 9/9 must-haves passed - ExternalIdTranslationService (180 lines, 8 tests) - DynamicEndpointRegistry (140 lines, 8 tests) - DynamicRestController (296 lines, 16 tests) - Total: 32 unit tests across 3 test classes VERIFICATION: .planning/phases/04-generic-rest-controller/04-VERIFICATION.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…me handling - Add conditional X-Ray debug logging in controller, repository and converter - Fix MultipleBagFetchException by splitting entity+field loading into two queries - Use JOIN FETCH for module to avoid lazy loading issues - Remove unnecessary .toUpperCase() on projection names - Add moduleInDevelopment flag to EntityMetadata and ProjectionMetadata - Update tests to match new metadata model signatures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
...s_core/com.etendorx.das/src/main/java/com/etendorx/das/controller/DynamicRestController.java
Fixed
Show fixed
Hide fixed
...s_core/com.etendorx.das/src/main/java/com/etendorx/das/controller/DynamicRestController.java
Fixed
Show fixed
Hide fixed
...s_core/com.etendorx.das/src/main/java/com/etendorx/das/controller/DynamicRestController.java
Fixed
Show fixed
Hide fixed
|
|
||
| externalIdTranslationService.translateExternalIds(dto, entityMeta); | ||
|
|
||
| Map<String, Object> result = repository.update( |
Check failure
Code scanning / SnykCode
Unsafe Reflection Error
| public ResponseEntity<Object> create( | ||
| @PathVariable String projectionName, | ||
| @PathVariable String entityName, | ||
| @RequestBody String rawEntity, |
Check notice
Code scanning / SnykCode
Spring Cross-Site Request Forgery (CSRF) Note
| @PathVariable String projectionName, | ||
| @PathVariable String entityName, | ||
| @PathVariable String id, | ||
| @RequestBody String rawEntity) { |
Check notice
Code scanning / SnykCode
Spring Cross-Site Request Forgery (CSRF) Note
|
Warning Git Police 👮One or more commit messages do not meet the required standards. Please correct them.
|
3cfbe14 to
012e829
Compare
etendobot
left a comment
There was a problem hiding this comment.
Changes requested by agent. Please resolve blocking issues.
- Refactor DynamicRestController, ExternalIdTranslationService and DynamicMetadataServiceImpl into smaller private methods - Extract string constants in DynamicRepository and GenerateProtoFile - Remove unused auditService from DynamicRepository and its test - Disable static mapping code generation in GenerateEntities - Remove unused fields and parameters in GenerateProtoFile Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| String entityName) { | ||
| logBatchStart(entityMeta, projectionName, entityName, rawDataArray.size()); | ||
| List<Map<String, Object>> dtos = convertArrayToDtos(rawDataArray, entityMeta); | ||
| List<Map<String, Object>> results = repository.saveBatch( |
Check failure
Code scanning / SnykCode
Unsafe Reflection Error
| String entityName) { | ||
| logSingleEntityStart(entityMeta, projectionName, entityName); | ||
| externalIdTranslationService.translateExternalIds(dto, entityMeta); | ||
| Map<String, Object> result = repository.save(dto, projectionName, entityMeta.name()); |
Check failure
Code scanning / SnykCode
Unsafe Reflection Error
| ObjectMapper objectMapper = new ObjectMapper(); | ||
| Map<String, Object> dto = objectMapper.readValue(rawEntity, Map.class); | ||
| externalIdTranslationService.translateExternalIds(dto, entityMeta); | ||
| Map<String, Object> result = repository.save(dto, projectionName, entityMeta.name()); |
Check failure
Code scanning / SnykCode
Unsafe Reflection Error
| } | ||
|
|
||
| private boolean shouldTranslateField(FieldMetadata field, Map<String, Object> dto) { | ||
| return field.fieldMapping() == FieldMappingType.ENTITY_MAPPING |
There was a problem hiding this comment.
Suggestion
Consider renaming 'shouldTranslateField' to 'isEligibleForTranslation' for better clarity.
private boolean isEligibleForTranslation(FieldMetadata field, Map<String, Object> dto) {Rationale: Renaming the method to 'isEligibleForTranslation' better conveys the intent of checking conditions before proceeding with the action.
|
|
||
| private boolean shouldTranslateField(FieldMetadata field, Map<String, Object> dto) { | ||
| return field.fieldMapping() == FieldMappingType.ENTITY_MAPPING | ||
| && dto.get(field.name()) != null; |
There was a problem hiding this comment.
Suggestion
Add a 'containsKey' check before 'get' for defensive consistency.
return field.fieldMapping() == FieldMappingType.ENTITY_MAPPING
&& dto.containsKey(field.name())
&& dto.get(field.name()) != null;Rationale: Explicitly checking 'containsKey' is a defensive programming best practice when working with Map-based DTOs to distinguish between a missing key and a null value.
| public final static String GENERATED_DIR = "/../build/tmp/generated"; | ||
| public static final String GENERATED_DIR = "/../build/tmp/generated"; | ||
| private static final String ENTITY_SCAN_TEMPLATE = "/org/openbravo/base/gen/entityscan.ftl"; | ||
| private static final Logger log = LogManager.getLogger(); |
There was a problem hiding this comment.
Suggestion
Good use of constants for template paths.
private static final String ENTITY_SCAN_TEMPLATE = "/org/openbravo/base/gen/entityscan.ftl";Rationale: Extracted a hardcoded string into a private static final constant. This improves maintainability and follows standard Java practices for reusable strings.
| log.error("JSON processing error while creating entity", e); | ||
| throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid JSON format"); | ||
| } catch (ResponseStatusException e) { | ||
| throw e; |
There was a problem hiding this comment.
Suggestion
Simplify the exception handling by combining or restructuring the catch blocks.
} catch (Exception e) {
+ if (e instanceof ResponseStatusException) {
+ throw e;
+ }
+ log.error("Error while creating entity", e);
+ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
+ }Rationale: While the current implementation works, catching and re-throwing a specific exception just to catch its parent afterwards is slightly redundant. Simplifying the catch block improves readability.
| } | ||
|
|
||
| private ResponseEntity<Object> processFallbackCreation(String rawEntity, | ||
| EntityMetadata entityMeta, |
There was a problem hiding this comment.
Suggestion
Define ObjectMapper as a static final constant instead of instantiating it inside the method.
private static final ObjectMapper objectMapper = new ObjectMapper();
private ResponseEntity<Object> processFallbackCreation(String rawEntity,
EntityMetadata entityMeta,
String projectionName,
String entityName)
throws JsonProcessingException {
Map<String, Object> dto = objectMapper.readValue(rawEntity, Map.class);Rationale: ObjectMapper is thread-safe and expensive to initialize. Reusing a single instance as a class constant is a standard performance best practice in Spring/Java applications.
| } | ||
|
|
||
| private void processProjection(ETRXProjection projection, Cache cache) { | ||
| try { |
There was a problem hiding this comment.
Suggestion
Add a null check for the projection name before putting it in the cache.
private void processProjection(ETRXProjection projection, Cache cache) {
try {
initializeProjectionRelationships(projection);
ProjectionMetadata metadata = toProjectionMetadata(projection);
if (projection.getName() != null) {
cache.put(projection.getName(), metadata);
log.debug("Preloaded projection: {}", projection.getName());
} else {
log.warn("Projection name is null for ID: {}, skipping cache", projection.getId());
}
} catch (Exception e) {
log.error("Failed to preload projection: {}", projection.getName(), e);
}
}Rationale: While loadProjectionsFromDatabase filters for projections in development, it is defensive to check for null names before using them as cache keys to prevent NullPointerException or inconsistent cache state.
| public class GenerateProtoFile { | ||
| private static final Logger log = LogManager.getLogger(); | ||
| private static final String MOBILESYNC_DTO_PATH = "/modules/com.etendorx.integration.mobilesync/src-gen/main/java/com/etendorx/integration/mobilesync/dto"; | ||
| private static final String MOBILESYNC_ENTITIES_PACKAGE = "com.etendorx.integration.mobilesync.entities"; |
There was a problem hiding this comment.
Suggestion
Verify if these paths should remain hardcoded or if they could eventually be parameterized via gradle.properties or a configuration file to support custom integration modules.
private static final String MOBILESYNC_DTO_PATH = "/modules/com.etendorx.integration.mobilesync/src-gen/main/java/com/etendorx/integration/mobilesync/dto";Rationale: Centralizing these paths as constants is a good practice as they are used across multiple generator methods. This improves maintainability if the module structure changes.
| * @param templatePath | ||
| * @param prefix | ||
| * @param sufix | ||
| * @throws FileNotFoundException |
There was a problem hiding this comment.
Suggestion
Update the Javadoc to remove the @param tags for the removed parameters (computedColumns, includeViews, prefix).
/**
* ...
* @param pathEtendoRx
* @param projection
* @param repositories
* @param sourcefilePath
* @param templatePath
* @param sufix
* @throws FileNotFoundException
*/
private void generateSourcefile(String pathEtendoRx, Projection projection,
List<HashMap<String, Object>> repositories,
String sourcefilePath, String templatePath, String sufix)Rationale: The Javadoc parameters no longer match the method signature after the refactoring. Removing unused @param tags improves documentation accuracy.
|
|
||
| public void setEntitiesModelMap(Map<String, Entity> entitiesModelMap) { | ||
| this.entitiesModelMap = entitiesModelMap; | ||
| this.entitiesModelMap = MetadataUtil.generateEntitiesMap(entitiesModel); |
There was a problem hiding this comment.
Suggestion
Ensure that the 'entitiesModel' list is not required by any other logic (e.g., templates) before finalizing the removal.
public void setEntitiesModel(List<Entity> entitiesModel) {
this.entitiesModelMap = MetadataUtil.generateEntitiesMap(entitiesModel);
}Rationale: Removing the unused 'entitiesModel' list and its setter simplifies the class state, as only the map is required for entity lookups during generation.
| public class DynamicRepository { | ||
|
|
||
| private static final String ENTITY_NOT_FOUND_MESSAGE = "Entity metadata not found for projection: "; | ||
| private static final String ENTITY_SUFFIX = ", entity: "; |
There was a problem hiding this comment.
Suggestion
The introduction of constants for error messages is a good practice.
private static final String ENTITY_NOT_FOUND_MESSAGE = "Entity metadata not found for projection: ";Rationale: Moving repetitive string literals to constants improves maintainability and follows clean code practices.
| } | ||
|
|
||
| if (entityMeta.moduleInDevelopment()) { | ||
| log.info("[X-Ray] Repository.save | operation={} class={} dtoId={}", |
There was a problem hiding this comment.
Suggestion
Moving the logging block after the entity existence check provides more accurate diagnostic information.
if (entityMeta.moduleInDevelopment()) {
log.info("[X-Ray] Repository.save | operation={} class={} dtoId={}",
isNew ? "INSERT" : "UPDATE", entityClass.getSimpleName(), dtoId);
}Rationale: Moving the log block after the ID check ensures that the logged isNew status and existingEntity state are consistent with the actual operation performed.

Summary
Test plan
🤖 Generated with Claude Code