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.
- package/AGENTS.md +441 -14
- package/DOMAIN_YAML_GUIDE.md +425 -21
- package/FUTURE_FEATURES.md +315 -115
- package/QUICK_REFERENCE.md +101 -153
- package/README.md +77 -70
- package/bin/eva4j.js +57 -1
- package/config/defaults.json +3 -0
- package/docs/commands/GENERATE_ENTITIES.md +662 -1968
- package/docs/commands/GENERATE_HTTP_EXCHANGE.md +274 -450
- package/docs/commands/GENERATE_KAFKA_EVENT.md +219 -498
- package/docs/commands/GENERATE_KAFKA_LISTENER.md +18 -18
- package/docs/commands/GENERATE_RECORD.md +335 -311
- package/docs/commands/GENERATE_TEMPORAL_ACTIVITY.md +174 -0
- package/docs/commands/GENERATE_TEMPORAL_FLOW.md +237 -0
- package/docs/commands/GENERATE_USECASE.md +216 -282
- package/docs/commands/INDEX.md +36 -7
- package/examples/doctor-evaluation.yaml +3 -3
- package/examples/domain-audit-complete.yaml +2 -2
- package/examples/domain-collections.yaml +2 -2
- package/examples/domain-ecommerce.yaml +2 -2
- package/examples/domain-events.yaml +201 -0
- package/examples/domain-field-visibility.yaml +11 -5
- package/examples/domain-multi-aggregate.yaml +12 -6
- package/examples/domain-one-to-many.yaml +1 -1
- package/examples/domain-one-to-one.yaml +1 -1
- package/examples/domain-secondary-onetomany.yaml +1 -1
- package/examples/domain-secondary-onetoone.yaml +1 -1
- package/examples/domain-simple.yaml +1 -1
- package/examples/domain-soft-delete.yaml +3 -3
- package/examples/domain-transitions.yaml +1 -1
- package/examples/domain-value-objects.yaml +1 -1
- package/package.json +2 -2
- package/src/commands/add-kafka-client.js +3 -1
- package/src/commands/add-temporal-client.js +286 -0
- package/src/commands/generate-entities.js +75 -4
- package/src/commands/generate-kafka-event.js +273 -89
- package/src/commands/generate-temporal-activity.js +228 -0
- package/src/commands/generate-temporal-flow.js +216 -0
- package/src/generators/module-generator.js +1 -0
- package/src/generators/shared-generator.js +26 -0
- package/src/utils/yaml-to-entity.js +93 -4
- package/templates/aggregate/AggregateRepository.java.ejs +3 -2
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +15 -7
- package/templates/aggregate/AggregateRoot.java.ejs +38 -2
- package/templates/aggregate/DomainEntity.java.ejs +6 -2
- package/templates/aggregate/DomainEventHandler.java.ejs +62 -0
- package/templates/aggregate/DomainEventRecord.java.ejs +50 -0
- package/templates/aggregate/JpaAggregateRoot.java.ejs +3 -1
- package/templates/aggregate/JpaEntity.java.ejs +3 -1
- package/templates/base/docker/kafka-services.yaml.ejs +2 -2
- package/templates/base/docker/temporal-services.yaml.ejs +29 -0
- package/templates/base/resources/parameters/develop/temporal.yaml.ejs +9 -0
- package/templates/base/resources/parameters/local/temporal.yaml.ejs +9 -0
- package/templates/base/resources/parameters/production/temporal.yaml.ejs +9 -0
- package/templates/base/resources/parameters/test/temporal.yaml.ejs +9 -0
- package/templates/base/root/AGENTS.md.ejs +916 -51
- package/templates/crud/Controller.java.ejs +36 -6
- package/templates/crud/ListQuery.java.ejs +6 -2
- package/templates/crud/ListQueryHandler.java.ejs +24 -10
- package/templates/crud/UpdateCommand.java.ejs +52 -0
- package/templates/crud/UpdateCommandHandler.java.ejs +105 -0
- package/templates/kafka-event/DomainEventHandlerMethod.ejs +1 -0
- package/templates/kafka-event/Event.java.ejs +23 -0
- package/templates/shared/application/dtos/PagedResponse.java.ejs +30 -0
- package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +104 -0
- package/templates/shared/domain/DomainEvent.java.ejs +40 -0
- package/templates/shared/interfaces/HeavyActivity.java.ejs +4 -0
- package/templates/shared/interfaces/LightActivity.java.ejs +4 -0
- package/templates/temporal-activity/ActivityImpl.java.ejs +14 -0
- package/templates/temporal-activity/ActivityInterface.java.ejs +11 -0
- package/templates/temporal-flow/WorkFlowImpl.java.ejs +64 -0
- package/templates/temporal-flow/WorkFlowInterface.java.ejs +19 -0
- package/templates/temporal-flow/WorkFlowService.java.ejs +49 -0
package/FUTURE_FEATURES.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
###
|
|
243
|
+
### Archivos Modificados
|
|
234
244
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
342
|
+
### Implementación Realizada
|
|
330
343
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
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
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
);
|
|
366
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
-
####
|
|
752
|
+
#### Safe mode en `renderAndWrite()` — `src/utils/template-engine.js`
|
|
744
753
|
|
|
745
754
|
```bash
|
|
746
|
-
#
|
|
747
|
-
eva4j g entities
|
|
755
|
+
# Comportamiento por defecto (safe mode)
|
|
756
|
+
eva4j g entities orders
|
|
748
757
|
|
|
749
758
|
# Output:
|
|
750
|
-
#
|
|
751
|
-
#
|
|
752
|
-
# SKIP OrderApplicationMapper.java
|
|
753
|
-
# SKIP CreateOrderCommandHandler.java
|
|
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 |
|
|
950
|
-
| 2 | Aggregate Boundaries por ID |
|
|
951
|
-
| 3 | Soft Delete Completo |
|
|
952
|
-
| 4 | Paginación en Queries |
|
|
953
|
-
| 5 | Optimistic Locking |
|
|
954
|
-
| 6 | Read Models / Proyecciones |
|
|
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 |
|
|
957
|
-
| 9 | JSON Schema para domain.yaml |
|
|
958
|
-
| 10 | Generacion Incremental |
|
|
959
|
-
| 11 | eva4j doctor |
|
|
960
|
-
| 12 | Tests Completos |
|
|
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-
|
|
1166
|
+
**Ultima actualizacion:** 2026-03-04
|
|
967
1167
|
**Version de eva4j:** 1.x
|
|
968
1168
|
**Estado:** Documento de planificacion y referencia
|