eva4j 1.0.11 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/AGENTS.md +441 -14
  2. package/DOMAIN_YAML_GUIDE.md +425 -21
  3. package/FUTURE_FEATURES.md +315 -115
  4. package/QUICK_REFERENCE.md +101 -153
  5. package/README.md +77 -70
  6. package/bin/eva4j.js +57 -1
  7. package/config/defaults.json +3 -0
  8. package/docs/commands/GENERATE_ENTITIES.md +662 -1968
  9. package/docs/commands/GENERATE_HTTP_EXCHANGE.md +274 -450
  10. package/docs/commands/GENERATE_KAFKA_EVENT.md +219 -498
  11. package/docs/commands/GENERATE_KAFKA_LISTENER.md +18 -18
  12. package/docs/commands/GENERATE_RECORD.md +335 -311
  13. package/docs/commands/GENERATE_TEMPORAL_ACTIVITY.md +174 -0
  14. package/docs/commands/GENERATE_TEMPORAL_FLOW.md +237 -0
  15. package/docs/commands/GENERATE_USECASE.md +216 -282
  16. package/docs/commands/INDEX.md +36 -7
  17. package/examples/doctor-evaluation.yaml +3 -3
  18. package/examples/domain-audit-complete.yaml +2 -2
  19. package/examples/domain-collections.yaml +2 -2
  20. package/examples/domain-ecommerce.yaml +2 -2
  21. package/examples/domain-events.yaml +201 -0
  22. package/examples/domain-field-visibility.yaml +11 -5
  23. package/examples/domain-multi-aggregate.yaml +12 -6
  24. package/examples/domain-one-to-many.yaml +1 -1
  25. package/examples/domain-one-to-one.yaml +1 -1
  26. package/examples/domain-secondary-onetomany.yaml +1 -1
  27. package/examples/domain-secondary-onetoone.yaml +1 -1
  28. package/examples/domain-simple.yaml +1 -1
  29. package/examples/domain-soft-delete.yaml +3 -3
  30. package/examples/domain-transitions.yaml +1 -1
  31. package/examples/domain-value-objects.yaml +1 -1
  32. package/package.json +2 -2
  33. package/src/commands/add-kafka-client.js +3 -1
  34. package/src/commands/add-temporal-client.js +286 -0
  35. package/src/commands/generate-entities.js +75 -4
  36. package/src/commands/generate-kafka-event.js +273 -89
  37. package/src/commands/generate-temporal-activity.js +228 -0
  38. package/src/commands/generate-temporal-flow.js +216 -0
  39. package/src/generators/module-generator.js +1 -0
  40. package/src/generators/shared-generator.js +26 -0
  41. package/src/utils/yaml-to-entity.js +93 -4
  42. package/templates/aggregate/AggregateRepository.java.ejs +3 -2
  43. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +15 -7
  44. package/templates/aggregate/AggregateRoot.java.ejs +38 -2
  45. package/templates/aggregate/DomainEntity.java.ejs +6 -2
  46. package/templates/aggregate/DomainEventHandler.java.ejs +62 -0
  47. package/templates/aggregate/DomainEventRecord.java.ejs +50 -0
  48. package/templates/aggregate/JpaAggregateRoot.java.ejs +3 -1
  49. package/templates/aggregate/JpaEntity.java.ejs +3 -1
  50. package/templates/base/docker/kafka-services.yaml.ejs +2 -2
  51. package/templates/base/docker/temporal-services.yaml.ejs +29 -0
  52. package/templates/base/resources/parameters/develop/temporal.yaml.ejs +9 -0
  53. package/templates/base/resources/parameters/local/temporal.yaml.ejs +9 -0
  54. package/templates/base/resources/parameters/production/temporal.yaml.ejs +9 -0
  55. package/templates/base/resources/parameters/test/temporal.yaml.ejs +9 -0
  56. package/templates/base/root/AGENTS.md.ejs +916 -51
  57. package/templates/crud/Controller.java.ejs +36 -6
  58. package/templates/crud/ListQuery.java.ejs +6 -2
  59. package/templates/crud/ListQueryHandler.java.ejs +24 -10
  60. package/templates/crud/UpdateCommand.java.ejs +52 -0
  61. package/templates/crud/UpdateCommandHandler.java.ejs +105 -0
  62. package/templates/kafka-event/DomainEventHandlerMethod.ejs +1 -0
  63. package/templates/kafka-event/Event.java.ejs +23 -0
  64. package/templates/shared/application/dtos/PagedResponse.java.ejs +30 -0
  65. package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +104 -0
  66. package/templates/shared/domain/DomainEvent.java.ejs +40 -0
  67. package/templates/shared/interfaces/HeavyActivity.java.ejs +4 -0
  68. package/templates/shared/interfaces/LightActivity.java.ejs +4 -0
  69. package/templates/temporal-activity/ActivityImpl.java.ejs +14 -0
  70. package/templates/temporal-activity/ActivityInterface.java.ejs +11 -0
  71. package/templates/temporal-flow/WorkFlowImpl.java.ejs +64 -0
  72. package/templates/temporal-flow/WorkFlowInterface.java.ejs +19 -0
  73. package/templates/temporal-flow/WorkFlowService.java.ejs +49 -0
@@ -12,7 +12,7 @@ Este documento describe las mejoras planificadas para futuras versiones de eva4j
12
12
  - [Soft Delete Completo](#3-soft-delete-completo)
13
13
 
14
14
  ### � Media Prioridad
15
- - [Paginación en Queries](#4-paginación-en-queries)
15
+ - [Paginación en Queries](#4-paginación-en-queries)
16
16
  - [Optimistic Locking](#5-optimistic-locking)
17
17
  - [Read Models Separados](#6-read-models-separados-proyecciones)
18
18
  - [Enums con Comportamiento y Transiciones](#7-enums-con-comportamiento-y-transiciones) ✅
@@ -27,6 +27,10 @@ Este documento describe las mejoras planificadas para futuras versiones de eva4j
27
27
  ### ✅ Implementado
28
28
  - [Auditoría de Tiempo y Usuario](#13-auditoría-implementada)
29
29
  - [Validaciones JSR-303](#14-validaciones-jsr-303-implementado)
30
+ - [Enums con Comportamiento y Transiciones](#7-enums-con-comportamiento-y-transiciones)
31
+ - [Generación Incremental / Diff](#10-generación-incremental--diff)
32
+ - [Paginación en Queries](#4-paginación-en-queries)
33
+ - [Aggregate Boundaries por ID](#2-aggregate-boundaries-por-id-implementado)
30
34
 
31
35
  ---
32
36
 
@@ -175,15 +179,15 @@ public class OrderEventListener {
175
179
 
176
180
  ---
177
181
 
178
- ## 2. Aggregate Boundaries por ID
182
+ ## 2. Aggregate Boundaries por ID
179
183
 
180
184
  ### Descripción
181
185
 
182
- En DDD, las referencias entre agregados distintos deben realizarse **por ID**, nunca con `@ManyToOne` cruzado. Hoy eva4j genera referencias JPA directas entre todos los agregados del mismo módulo, creando un único grafo de entidades JPA en vez de agregados independientes.
186
+ Eva4j ya genera correctamente el patrón DDD de referencia por ID: los campos que apuntan a otro agregado se generan como tipos primitivos (`String`, `Long`, etc.) sin ningún `@ManyToOne` cruzado. Esta feature añade **declaración semántica explícita** mediante la propiedad `reference:` en el campo, que permite documentar la intención en el YAML y generar un comentario Javadoc en el código.
183
187
 
184
- Esto impide escalar los agregados de forma independiente y crea dependencias de carga que violan los límites transaccionales.
188
+ Sin `reference:`, un campo `customerId: String` es indistinguible de cualquier otro `String`. Con `reference:`, el generador sabe que es un puntero intencional al agregado `Customer` del módulo `customers`.
185
189
 
186
- ### Sintaxis Propuesta
190
+ ### Sintaxis
187
191
 
188
192
  ```yaml
189
193
  aggregates:
@@ -197,8 +201,8 @@ aggregates:
197
201
  - name: customerId
198
202
  type: String
199
203
  reference:
200
- aggregate: Customer
201
- module: customers
204
+ aggregate: Customer # Nombre del agregado (PascalCase) — obligatorio
205
+ module: customers # Módulo donde vive el agregado — opcional
202
206
  - name: productId
203
207
  type: String
204
208
  reference:
@@ -206,36 +210,45 @@ aggregates:
206
210
  module: catalog
207
211
  ```
208
212
 
213
+ ### Comportamiento
214
+
215
+ - El tipo Java **no cambia** — sigue siendo `String`, `Long`, etc.
216
+ - JPA genera `@Column` normal — **sin** `@ManyToOne` ni `@JoinColumn`.
217
+ - Se genera un **comentario Javadoc** en la entidad de dominio y en la entidad JPA.
218
+ - `module:` es opcional: se puede omitir si el agregado referenciado está en el mismo módulo.
219
+ - Si `reference:` está malformado (falta `aggregate`), eva4j lanza un error descriptivo.
220
+
209
221
  ### Código Generado
210
222
 
211
223
  ```java
212
224
  // domain/models/entities/Order.java
213
- public class Order {
214
- private String id;
215
- private String customerId; // Solo el ID, nunca Customer customer
216
- private String productId;
217
- }
225
+ /** Cross-aggregate reference → Customer (module: customers) */
226
+ private String customerId;
227
+
228
+ /** Cross-aggregate reference → Product (module: catalog) */
229
+ private String productId;
218
230
  ```
219
231
 
220
232
  ```java
221
- public class GetOrderWithCustomerQueryHandler {
222
- private final OrderRepository orderRepository;
223
- private final CustomerServiceClient customerClient;
224
-
225
- public OrderWithCustomerDto handle(GetOrderWithCustomerQuery query) {
226
- Order order = orderRepository.findById(query.orderId()).orElseThrow();
227
- CustomerSummary customer = customerClient.findById(order.getCustomerId());
228
- return new OrderWithCustomerDto(order, customer);
229
- }
230
- }
233
+ // infrastructure/database/entities/OrderJpa.java
234
+ @Column(name = "customer_id")
235
+ /** Cross-aggregate reference → Customer (module: customers) */
236
+ private String customerId;
237
+
238
+ @Column(name = "product_id")
239
+ /** Cross-aggregate reference → Product (module: catalog) */
240
+ private String productId;
231
241
  ```
232
242
 
233
- ### Advertencia generada cuando se detecta cross-aggregate
243
+ ### Archivos Modificados
234
244
 
235
- ```
236
- ⚠️ WARNING: Order.customer uses a direct JPA reference to Customer aggregate.
237
- Consider using customerId (reference by ID) instead.
238
- ```
245
+ | Archivo | Cambio |
246
+ |---|---|
247
+ | `src/utils/yaml-to-entity.js` | ✅ Destructura y valida `reference:` en `parseProperty()` |
248
+ | `templates/aggregate/AggregateRoot.java.ejs` | ✅ Genera comentario Javadoc en campos con `reference` |
249
+ | `templates/aggregate/JpaAggregateRoot.java.ejs` | ✅ Genera comentario Javadoc en campos con `reference` |
250
+ | `templates/aggregate/JpaEntity.java.ejs` | ✅ Genera comentario Javadoc en campos con `reference` |
251
+ | `examples/domain-multi-aggregate.yaml` | ✅ Actualizado con `reference:` en `productId` y `warehouseId` |
239
252
 
240
253
  ---
241
254
 
@@ -320,74 +333,86 @@ public ResponseEntity<Void> restore(@PathVariable String id) {
320
333
 
321
334
  ---
322
335
 
323
- ## 4. Paginación en Queries
336
+ ## 4. Paginación en Queries
324
337
 
325
338
  ### Descripción
326
339
 
327
- Actualmente `ListQueryHandler` devuelve `List<T>` completo sin límite. En producción, cualquier colección que pueda crecer sin límite debe estar paginada. Esto también implica soporte para filtros dinámicos y ordenamiento.
340
+ Implementado como **paginación siempre activa** en todos los módulos generados. `GET /` ya no devuelve `List<T>` sin límite devuelve un `PagedResponse<T>` propio con `content`, `page`, `size`, `totalElements` y `totalPages`. Sin flags ni configuración adicional en `domain.yaml`.
328
341
 
329
- ### Sintaxis Propuesta
342
+ ### Implementación Realizada
330
343
 
331
- ```yaml
332
- aggregates:
333
- - name: Order
334
- queries:
335
- - name: FindAllOrders
336
- paginated: true
337
- filters:
338
- - field: status
339
- type: OrderStatus
340
- - field: customerId
341
- type: String
342
- sort:
343
- defaultField: createdAt
344
- defaultDirection: DESC
344
+ #### PagedResponse — `shared/application/dtos/PagedResponse.java`
345
+
346
+ Record genérico generado una vez por proyecto en la capa shared. Desacoplado de Spring Data `Page<T>` para no exponer internals de Spring en la API:
347
+
348
+ ```java
349
+ public record PagedResponse<T>(
350
+ List<T> content,
351
+ int page,
352
+ int size,
353
+ long totalElements,
354
+ int totalPages
355
+ ) {
356
+ public static <T> PagedResponse<T> of(
357
+ List<T> content, int page, int size, long totalElements) {
358
+ int totalPages = size == 0 ? 1 : (int) Math.ceil((double) totalElements / size);
359
+ return new PagedResponse<>(content, page, size, totalElements, totalPages);
360
+ }
361
+ }
345
362
  ```
346
363
 
347
- ### Código Generado
364
+ #### Query con parámetros de paginación
348
365
 
349
366
  ```java
350
367
  public record FindAllOrdersQuery(
351
- OrderStatus status,
352
- String customerId,
353
368
  int page,
354
369
  int size,
355
370
  String sortBy,
356
371
  String sortDirection
357
- ) {}
372
+ ) implements Query<PagedResponse<OrderResponseDto>> {}
358
373
  ```
359
374
 
375
+ #### Handler paginado
376
+
360
377
  ```java
361
- public Page<OrderResponseDto> handle(FindAllOrdersQuery query) {
362
- Pageable pageable = PageRequest.of(
363
- query.page(), query.size(),
364
- Sort.by(Sort.Direction.fromString(query.sortDirection()), query.sortBy())
365
- );
366
- Page<Order> orders = orderRepository.findAll(
367
- OrderSpecifications.build(query.status(), query.customerId()), pageable
368
- );
369
- return orders.map(mapper::toDto);
378
+ public PagedResponse<OrderResponseDto> handle(FindAllOrdersQuery query) {
379
+ Sort sort = Sort.by(Sort.Direction.fromString(query.sortDirection()), query.sortBy());
380
+ Pageable pageable = PageRequest.of(query.page(), query.size(), sort);
381
+ Page<Order> page = repository.findAll(pageable);
382
+ List<OrderResponseDto> content = page.getContent().stream().map(mapper::toDto).toList();
383
+ return PagedResponse.of(content, page.getNumber(), page.getSize(), page.getTotalElements());
370
384
  }
371
385
  ```
372
386
 
373
- ```java
374
- public class OrderSpecifications {
375
- public static Specification<OrderJpa> build(OrderStatus status, String customerId) {
376
- return Specification
377
- .where(hasStatus(status))
378
- .and(hasCustomerId(customerId));
379
- }
380
- private static Specification<OrderJpa> hasStatus(OrderStatus status) {
381
- return (root, query, cb) ->
382
- status == null ? null : cb.equal(root.get("status"), status);
383
- }
384
- private static Specification<OrderJpa> hasCustomerId(String customerId) {
385
- return (root, query, cb) ->
386
- customerId == null ? null : cb.equal(root.get("customerId"), customerId);
387
- }
387
+ #### Endpoint REST
388
+
389
+ ```bash
390
+ # Defaults: page=0, size=20, sortBy=id, sortDirection=ASC
391
+ GET /api/v1/orders?page=0&size=10&sortBy=createdAt&sortDirection=DESC
392
+
393
+ # Respuesta
394
+ {
395
+ "content": [...],
396
+ "page": 0,
397
+ "size": 10,
398
+ "totalElements": 87,
399
+ "totalPages": 9
388
400
  }
389
401
  ```
390
402
 
403
+ #### Archivos modificados
404
+
405
+ | Archivo | Cambio |
406
+ |---|---|
407
+ | `templates/shared/application/dtos/PagedResponse.java.ejs` | ✅ Nuevo template shared |
408
+ | `src/generators/shared-generator.js` | ✅ Método `generatePagedResponse()` |
409
+ | `src/commands/generate-entities.js` | ✅ Llama `generatePagedResponse` en cada `g entities` |
410
+ | `templates/crud/ListQuery.java.ejs` | ✅ Parámetros de paginación |
411
+ | `templates/crud/ListQueryHandler.java.ejs` | ✅ `PageRequest` + `PagedResponse` |
412
+ | `templates/aggregate/AggregateRepository.java.ejs` | ✅ `Page<X> findAll(Pageable)` |
413
+ | `templates/aggregate/AggregateRepositoryImpl.java.ejs` | ✅ Implementación `jpaRepository.findAll(pageable).map(...)` |
414
+ | `templates/crud/Controller.java.ejs` | ✅ `@RequestParam` page/size/sortBy/sortDirection |
415
+
391
416
  ---
392
417
 
393
418
  ## 5. Optimistic Locking
@@ -709,50 +734,50 @@ domain.yaml:41:7 error Relationship type "OneToFew" is not valid.
709
734
 
710
735
  ---
711
736
 
712
- ## 10. Generación Incremental / Diff
737
+ ## 10. Generación Incremental / Diff
713
738
 
714
739
  ### Descripción
715
740
 
716
- Actualmente `eva4j g entities` regenera **todos** los archivos, sobreescribiendo cualquier modificación manual. Este es el mayor bloqueador para adopción en proyectos reales donde el desarrollador personaliza la lógica generada.
741
+ Implementado como **safe mode con checksums SHA-256**. `eva4j g entities` (y `g usecase`, `g resource`) detecta si un archivo generado fue modificado manualmente después de su generación y lo omite automáticamente en re-ejecuciones. El flag `--force` permite sobreescribir cuando se desea regenerar intencionalmente.
717
742
 
718
- ### Estrategias Propuestas
719
-
720
- #### Opción A: Archivos base + archivos de extensión
721
-
722
- ```
723
- order/application/mappers/
724
- ├── OrderApplicationMapperBase.java <- regenerado siempre
725
- └── OrderApplicationMapper.java <- creado una vez, el dev lo personaliza
726
- ```
743
+ ### Implementación Realizada
727
744
 
728
- ```java
729
- // OrderApplicationMapperBase.java — regenerado en cada g entities
730
- public abstract class OrderApplicationMapperBase {
731
- public Order fromCommand(CreateOrderCommand command) { /* generado */ }
732
- public OrderResponseDto toDto(Order entity) { /* generado */ }
733
- }
745
+ #### ChecksumManager — `src/utils/checksum-manager.js`
734
746
 
735
- // OrderApplicationMapper.java creado una sola vez
736
- @Component
737
- public class OrderApplicationMapper extends OrderApplicationMapperBase {
738
- // Override opcional de métodos base
739
- // Métodos adicionales del proyecto aquí
740
- }
741
- ```
747
+ Almacena hashes SHA-256 de cada archivo escrito en un archivo `.eva4j-checksums.json` por módulo (junto al `domain.yaml`). Métodos clave:
748
+ - `wasModified(destPath, generatedContent)` — compara hash en disco vs hash almacenado
749
+ - `recordWrite(destPath, content)` registra hash del archivo recién escrito
750
+ - `save()` persiste la base de datos de checksums
742
751
 
743
- #### Opción B: Flag --safe en el CLI
752
+ #### Safe mode en `renderAndWrite()` `src/utils/template-engine.js`
744
753
 
745
754
  ```bash
746
- # Regenera solo archivos que NO fueron modificados manualmente
747
- eva4j g entities order --safe
755
+ # Comportamiento por defecto (safe mode)
756
+ eva4j g entities orders
748
757
 
749
758
  # Output:
750
- # OK Order.java -- regenerado (sin cambios previos)
751
- # OK OrderJpa.java -- regenerado (sin cambios previos)
752
- # SKIP OrderApplicationMapper.java -- omitido (modificado manualmente)
753
- # SKIP CreateOrderCommandHandler.java -- omitido (modificado manualmente)
759
+ # Order.java -- regenerado (sin cambios previos)
760
+ # OrderJpa.java -- regenerado (sin cambios previos)
761
+ # ⚠️ SKIP OrderApplicationMapper.java -- omitido (modificado manualmente — use --force to overwrite)
762
+ # ⚠️ SKIP CreateOrderCommandHandler.java -- omitido (modificado manualmente)
763
+
764
+ # Con --force: sobreescribe todo
765
+ eva4j g entities orders --force
754
766
  ```
755
767
 
768
+ #### Comandos con safe mode integrado
769
+
770
+ | Comando | Estado |
771
+ |---|---|
772
+ | `eva4j g entities <module>` | ✅ Integrado |
773
+ | `eva4j g usecase <module> <name>` | ✅ Integrado |
774
+ | `eva4j g resource <module>` | ✅ Integrado |
775
+ | `eva4j create` / `eva4j add module` | ⚠️ Out of scope (archivos de scaffolding inicial, no se re-ejecutan) |
776
+
777
+ #### Nota sobre portabilidad
778
+
779
+ `.eva4j-checksums.json` está en `.gitignore` por diseño — es estado local de la máquina de desarrollo. En un `git clone` fresco, la primera re-ejecución regenerará todos los archivos (comportamiento correcto en ese contexto).
780
+
756
781
  ---
757
782
 
758
783
  ## 11. Comando `eva4j doctor`
@@ -942,27 +967,202 @@ private Integer age;
942
967
 
943
968
  ---
944
969
 
970
+ ## 16. `defaultValue` para campos `readOnly` (Implementado)
971
+
972
+ Permite especificar un valor inicial para campos `readOnly` directamente en `domain.yaml`. El valor se emite en el **constructor de creación** de la entidad de dominio y como field initializer con `@Builder.Default` en la entidad JPA.
973
+
974
+ ### Sintaxis
975
+
976
+ ```yaml
977
+ entities:
978
+ - name: order
979
+ fields:
980
+ - name: status
981
+ type: OrderStatus
982
+ readOnly: true
983
+ defaultValue: PENDING # Enum value
984
+
985
+ - name: totalAmount
986
+ type: BigDecimal
987
+ readOnly: true
988
+ defaultValue: "0.00" # BigDecimal literal
989
+
990
+ - name: itemCount
991
+ type: Integer
992
+ readOnly: true
993
+ defaultValue: 0 # Integer literal
994
+
995
+ - name: isActive
996
+ type: Boolean
997
+ readOnly: true
998
+ defaultValue: true # Boolean literal
999
+ ```
1000
+
1001
+ ### Código Generado — Dominio
1002
+
1003
+ ```java
1004
+ // Constructor de creación — defaultValue asignado automáticamente
1005
+ public Order(String orderNumber, String customerId) {
1006
+ this.orderNumber = orderNumber;
1007
+ this.customerId = customerId;
1008
+ // readOnly fields initialized with defaultValue:
1009
+ this.status = OrderStatus.PENDING;
1010
+ this.totalAmount = new BigDecimal("0.00");
1011
+ this.itemCount = 0;
1012
+ this.isActive = true;
1013
+ }
1014
+ ```
1015
+
1016
+ ### Código Generado — JPA
1017
+
1018
+ ```java
1019
+ @Builder.Default
1020
+ private OrderStatus status = OrderStatus.PENDING;
1021
+
1022
+ @Builder.Default
1023
+ private BigDecimal totalAmount = new BigDecimal("0.00");
1024
+
1025
+ @Builder.Default
1026
+ private Integer itemCount = 0;
1027
+ ```
1028
+
1029
+ ### Tipos soportados
1030
+
1031
+ | Tipo Java | Ejemplo YAML | Java emitido |
1032
+ |-----------|-------------|---------------|
1033
+ | `String` | `defaultValue: hello` | `"hello"` |
1034
+ | `Integer` / `Long` | `defaultValue: 0` | `0` / `0L` |
1035
+ | `Boolean` | `defaultValue: false` | `false` |
1036
+ | `BigDecimal` | `defaultValue: "0.00"` | `new BigDecimal("0.00")` |
1037
+ | `LocalDateTime` | `defaultValue: now` | `LocalDateTime.now()` |
1038
+ | `LocalDate` | `defaultValue: now` | `LocalDate.now()` |
1039
+ | `Instant` | `defaultValue: now` | `Instant.now()` |
1040
+ | `UUID` | `defaultValue: random` | `UUID.randomUUID()` |
1041
+ | Enum | `defaultValue: ACTIVE` | `EnumType.ACTIVE` |
1042
+
1043
+ ### Reglas
1044
+
1045
+ - `defaultValue` **solo es válido** en campos con `readOnly: true`. Si se usa en un campo no-readOnly, se emite un warning y se ignora.
1046
+ - El campo **sigue siendo readOnly** — no aparece en el constructor de negocio ni en `CreateDto`.
1047
+ - En campos con `autoInit` (enum con `initialValue`), `defaultValue` es ignorado — `autoInit` tiene precedencia.
1048
+
1049
+ ### Archivos Modificados
1050
+
1051
+ | Archivo | Cambio |
1052
+ |---|---|
1053
+ | `src/utils/yaml-to-entity.js` | ✅ `computeJavaDefaultValue()` + `defaultValue` en `parseProperty()` |
1054
+ | `templates/aggregate/AggregateRoot.java.ejs` | ✅ Emite `this.field = defaultValue` en constructor de creación |
1055
+ | `templates/aggregate/DomainEntity.java.ejs` | ✅ Mismo cambio para entidades secundarias |
1056
+ | `templates/aggregate/JpaAggregateRoot.java.ejs` | ✅ `@Builder.Default` + field initializer |
1057
+ | `templates/aggregate/JpaEntity.java.ejs` | ✅ Mismo cambio para entidades JPA secundarias |
1058
+ | `examples/domain-field-visibility.yaml` | ✅ Ejemplos con `defaultValue` en campos readOnly |
1059
+
1060
+ ---
1061
+
1062
+ ## 15. Transactional Outbox Pattern
1063
+
1064
+ ### Descripción
1065
+
1066
+ El **Transactional Outbox Pattern** es la evolución natural de los Domain Events implementados (ítem 1). Resuelve el caso donde el proceso muere después del commit de BD pero antes de que `ApplicationEventPublisher` llegue a publicar al broker externo — en ese escenario, el evento se pierde silenciosamente.
1067
+
1068
+ El patrón garantiza **at-least-once delivery**: los eventos son almacenados en la misma transacción que el agregado y un proceso separado los publica de forma resiliente.
1069
+
1070
+ Los Domain Events ya implementados (`ApplicationEventPublisher` + `@TransactionalEventListener(AFTER_COMMIT)`) son suficientes para la mayoría de sistemas. Esta feature es necesaria para dominios críticos: pagos, auditoría regulatoria, inventario en tiempo real.
1071
+
1072
+ **Nota:** El puerto `MessageBroker` ya generado no requiere cambios — solo se añade la capa de persistencia intermedia.
1073
+
1074
+ ### Flujo del Patrón
1075
+
1076
+ ```
1077
+ BD Transaction:
1078
+ → INSERT INTO orders ...
1079
+ → INSERT INTO outbox_events (type, payload, published=false) ← misma TX
1080
+ → COMMIT
1081
+
1082
+ Proceso resiliente (polling o CDC con Debezium):
1083
+ → SELECT * FROM outbox_events WHERE published = false
1084
+ → Publica a Kafka / RabbitMQ / SNS
1085
+ → UPDATE outbox_events SET published = true
1086
+ ```
1087
+
1088
+ ### Sintaxis Propuesta en domain.yaml
1089
+
1090
+ ```yaml
1091
+ aggregates:
1092
+ - name: Order
1093
+ events:
1094
+ - name: OrderPlaced
1095
+ kafka: true
1096
+ delivery: at-least-once # ← activa Outbox Pattern para este evento
1097
+ fields:
1098
+ - name: customerId
1099
+ type: String
1100
+ ```
1101
+
1102
+ ### Código Generado (Outbox Table + Publisher)
1103
+
1104
+ ```java
1105
+ @Entity
1106
+ @Table(name = "outbox_events")
1107
+ public class OutboxEvent {
1108
+ @Id
1109
+ private String id;
1110
+ private String aggregateType;
1111
+ private String aggregateId;
1112
+ private String eventType;
1113
+ @Column(columnDefinition = "TEXT")
1114
+ private String payload; // JSON serializado del evento
1115
+ private boolean published = false;
1116
+ private LocalDateTime createdAt;
1117
+ private LocalDateTime publishedAt;
1118
+ }
1119
+ ```
1120
+
1121
+ ```java
1122
+ // OutboxEventPublisher — proceso de polling (cada 5s via @Scheduled)
1123
+ @Component
1124
+ public class OutboxEventPublisher {
1125
+ @Scheduled(fixedDelay = 5000)
1126
+ @Transactional
1127
+ public void publishPendingEvents() {
1128
+ List<OutboxEvent> pending = outboxRepository.findByPublishedFalse();
1129
+ pending.forEach(event -> {
1130
+ messageBroker.publishRaw(event.getEventType(), event.getPayload());
1131
+ event.markPublished();
1132
+ });
1133
+ }
1134
+ }
1135
+ ```
1136
+
1137
+ ### Prerrequisito
1138
+
1139
+ Domain Events (ítem 1) implementados y funcionando — este ítem solo añade persistencia intermedia, no reemplaza la arquitectura existente.
1140
+
1141
+ ---
1142
+
945
1143
  ## Resumen de Prioridades
946
1144
 
947
1145
  | # | Característica | Prioridad | Complejidad | Estado |
948
1146
  |---|---|---|---|---|
949
- | 1 | Domain Events | Alta | Alta | Pendiente |
950
- | 2 | Aggregate Boundaries por ID | Alta | Media | Pendiente |
951
- | 3 | Soft Delete Completo | Alta | Baja | Parcial |
952
- | 4 | Paginación en Queries | Media | Media | Pendiente |
953
- | 5 | Optimistic Locking | Media | Baja | Pendiente |
954
- | 6 | Read Models / Proyecciones | Media | Alta | Pendiente |
1147
+ | 1 | Domain Events | Alta | Alta | Implementado |
1148
+ | 2 | Aggregate Boundaries por ID | Alta | Media | Implementado |
1149
+ | 3 | Soft Delete Completo | Alta | Baja | Parcial |
1150
+ | 4 | Paginación en Queries | Impl. | -- | Implementado |
1151
+ | 5 | Optimistic Locking | Media | Baja | Pendiente |
1152
+ | 6 | Read Models / Proyecciones | Media | Alta | Pendiente |
955
1153
  | 7 | Enums con Transiciones | Impl. | -- | ✅ Implementado |
956
- | 8 | Specifications Pattern | Media | Media | Pendiente |
957
- | 9 | JSON Schema para domain.yaml | Tooling | Media | Pendiente |
958
- | 10 | Generacion Incremental | Tooling | Alta | Pendiente |
959
- | 11 | eva4j doctor | Tooling | Media | Pendiente |
960
- | 12 | Tests Completos | Tooling | Media | Pendiente |
1154
+ | 8 | Specifications Pattern | Media | Media | Pendiente |
1155
+ | 9 | JSON Schema para domain.yaml | Tooling | Media | Pendiente |
1156
+ | 10 | Generacion Incremental | Tooling | -- | Implementado |
1157
+ | 11 | eva4j doctor | Tooling | Media | Pendiente |
1158
+ | 12 | Tests Completos | Tooling | Media | Pendiente |
961
1159
  | 13 | Auditoria completa | Impl. | -- | ✅ Implementado |
962
1160
  | 14 | Validaciones JSR-303 | Impl. | -- | ✅ Implementado |
1161
+ | 15 | Transactional Outbox Pattern | Alta | Alta | Pendiente |
1162
+ | 16 | `defaultValue` para campos `readOnly` | Impl. | -- | ✅ Implementado |
963
1163
 
964
1164
  ---
965
1165
 
966
- **Ultima actualizacion:** 2026-02-22
1166
+ **Ultima actualizacion:** 2026-03-04
967
1167
  **Version de eva4j:** 1.x
968
1168
  **Estado:** Documento de planificacion y referencia