eva4j 1.0.11 → 1.0.12
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 +15 -5
- package/DOMAIN_YAML_GUIDE.md +51 -0
- package/FUTURE_FEATURES.md +222 -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 +886 -40
- 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_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/domain-events.yaml +201 -0
- package/examples/domain-multi-aggregate.yaml +6 -0
- 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 +26 -4
- package/templates/aggregate/AggregateRepository.java.ejs +3 -2
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +15 -7
- package/templates/aggregate/AggregateRoot.java.ejs +33 -1
- package/templates/aggregate/DomainEntity.java.ejs +1 -1
- package/templates/aggregate/DomainEventHandler.java.ejs +62 -0
- package/templates/aggregate/DomainEventRecord.java.ejs +50 -0
- package/templates/aggregate/JpaAggregateRoot.java.ejs +1 -0
- package/templates/aggregate/JpaEntity.java.ejs +2 -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/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/AGENTS.md
CHANGED
|
@@ -387,19 +387,28 @@ void assignUser(User user) { // package-private
|
|
|
387
387
|
|
|
388
388
|
```bash
|
|
389
389
|
# Crear proyecto
|
|
390
|
-
|
|
390
|
+
eva create my-app
|
|
391
391
|
|
|
392
392
|
# Agregar módulo
|
|
393
|
-
|
|
393
|
+
eva add module users
|
|
394
394
|
|
|
395
395
|
# Generar entidades desde YAML
|
|
396
|
-
|
|
396
|
+
eva g entities users
|
|
397
397
|
|
|
398
398
|
# Generar use case
|
|
399
|
-
|
|
399
|
+
eva g usecase users ActivateUser
|
|
400
400
|
|
|
401
401
|
# Generar resource (REST)
|
|
402
|
-
|
|
402
|
+
eva g resource users
|
|
403
|
+
|
|
404
|
+
# Agregar cliente Temporal
|
|
405
|
+
eva add temporal-client
|
|
406
|
+
|
|
407
|
+
# Generar workflow Temporal
|
|
408
|
+
eva g temporal-flow users
|
|
409
|
+
|
|
410
|
+
# Generar actividad Temporal
|
|
411
|
+
eva g temporal-activity users
|
|
403
412
|
```
|
|
404
413
|
|
|
405
414
|
### Estructura de domain.yaml
|
|
@@ -558,6 +567,7 @@ Los campos en domain.yaml soportan las siguientes propiedades:
|
|
|
558
567
|
| **`readOnly`** | Boolean | `false` | **Excluye del constructor de negocio y CreateDto** |
|
|
559
568
|
| **`hidden`** | Boolean | `false` | **Excluye del ResponseDto** |
|
|
560
569
|
| **`validations`** | Array | `[]` | **Anotaciones JSR-303 en el Command y CreateDto** |
|
|
570
|
+
| **`reference`** | Object | `null` | **Declara referencia semántica a otro agregado (genera comentario Javadoc)** |
|
|
561
571
|
|
|
562
572
|
#### Flags de Visibilidad: `readOnly` y `hidden`
|
|
563
573
|
|
package/DOMAIN_YAML_GUIDE.md
CHANGED
|
@@ -683,6 +683,57 @@ fields:
|
|
|
683
683
|
|
|
684
684
|
---
|
|
685
685
|
|
|
686
|
+
### Referencias entre Agregados (`reference:`)
|
|
687
|
+
|
|
688
|
+
La propiedad `reference:` declara explícitamente que un campo es un puntero intencional a la raíz de otro agregado. El campo sigue siendo un tipo primitivo (`String`, `Long`, etc.) — **no se genera ningún `@ManyToOne`**.
|
|
689
|
+
|
|
690
|
+
#### Sintaxis
|
|
691
|
+
|
|
692
|
+
```yaml
|
|
693
|
+
fields:
|
|
694
|
+
- name: customerId
|
|
695
|
+
type: String
|
|
696
|
+
reference:
|
|
697
|
+
aggregate: Customer # Nombre del agregado referenciado (PascalCase) — obligatorio
|
|
698
|
+
module: customers # Módulo donde vive el agregado — opcional
|
|
699
|
+
- name: productId
|
|
700
|
+
type: String
|
|
701
|
+
reference:
|
|
702
|
+
aggregate: Product
|
|
703
|
+
module: catalog
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
#### Comportamiento
|
|
707
|
+
|
|
708
|
+
- El tipo Java **no cambia** — sigue siendo `String`, `Long`, UUID, etc.
|
|
709
|
+
- JPA genera `@Column` normal — **sin** `@ManyToOne` ni `@JoinColumn`.
|
|
710
|
+
- En la entidad de dominio y en la entidad JPA se genera un **comentario Javadoc** que documenta la referencia.
|
|
711
|
+
- `module:` es opcional: puede omitirse si el agregado referenciado está en el mismo módulo.
|
|
712
|
+
- Si `reference:` está malformado (falta `aggregate`), eva4j lanza un error descriptivo.
|
|
713
|
+
|
|
714
|
+
#### Código Generado
|
|
715
|
+
|
|
716
|
+
```java
|
|
717
|
+
// domain/models/entities/Order.java
|
|
718
|
+
/** Cross-aggregate reference → Customer (module: customers) */
|
|
719
|
+
private String customerId;
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
```java
|
|
723
|
+
// infrastructure/database/entities/OrderJpa.java
|
|
724
|
+
@Column(name = "customer_id")
|
|
725
|
+
/** Cross-aggregate reference → Customer (module: customers) */
|
|
726
|
+
private String customerId;
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
#### Por qué no usar `@ManyToOne` entre agregados
|
|
730
|
+
|
|
731
|
+
En DDD cada agregado es una unidad transaccional independiente. Un `@ManyToOne` cruzando límites crea un único grafo JPA que viola los límites transaccionales y crea dependencias de carga invisibles. La referencia por ID es el patrón correcto: el handler que necesite los datos del otro agregado los obtiene explícitamente via su propio repositorio.
|
|
732
|
+
|
|
733
|
+
- **Ejemplo completo:** [examples/domain-multi-aggregate.yaml](../examples/domain-multi-aggregate.yaml)
|
|
734
|
+
|
|
735
|
+
---
|
|
736
|
+
|
|
686
737
|
### Validaciones JSR-303
|
|
687
738
|
|
|
688
739
|
Eva4j soporta anotaciones Bean Validation (JSR-303/Jakarta Validation) en campos del `domain.yaml`. Las validaciones se generan **únicamente en la capa de aplicación**: en el `Create<Aggregate>Command` y en los `Create<Entity>Dto` de entidades secundarias. **No se aplican a entidades de dominio** ni a campos con `readOnly: true`.
|
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
|
-
###
|
|
743
|
+
### Implementación Realizada
|
|
719
744
|
|
|
720
|
-
####
|
|
745
|
+
#### ChecksumManager — `src/utils/checksum-manager.js`
|
|
721
746
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
```
|
|
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
|
|
727
751
|
|
|
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
|
-
}
|
|
734
|
-
|
|
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
|
-
```
|
|
742
|
-
|
|
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
|
-
#
|
|
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,109 @@ private Integer age;
|
|
|
942
967
|
|
|
943
968
|
---
|
|
944
969
|
|
|
970
|
+
## 15. Transactional Outbox Pattern
|
|
971
|
+
|
|
972
|
+
### Descripción
|
|
973
|
+
|
|
974
|
+
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.
|
|
975
|
+
|
|
976
|
+
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.
|
|
977
|
+
|
|
978
|
+
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.
|
|
979
|
+
|
|
980
|
+
**Nota:** El puerto `MessageBroker` ya generado no requiere cambios — solo se añade la capa de persistencia intermedia.
|
|
981
|
+
|
|
982
|
+
### Flujo del Patrón
|
|
983
|
+
|
|
984
|
+
```
|
|
985
|
+
BD Transaction:
|
|
986
|
+
→ INSERT INTO orders ...
|
|
987
|
+
→ INSERT INTO outbox_events (type, payload, published=false) ← misma TX
|
|
988
|
+
→ COMMIT
|
|
989
|
+
|
|
990
|
+
Proceso resiliente (polling o CDC con Debezium):
|
|
991
|
+
→ SELECT * FROM outbox_events WHERE published = false
|
|
992
|
+
→ Publica a Kafka / RabbitMQ / SNS
|
|
993
|
+
→ UPDATE outbox_events SET published = true
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
### Sintaxis Propuesta en domain.yaml
|
|
997
|
+
|
|
998
|
+
```yaml
|
|
999
|
+
aggregates:
|
|
1000
|
+
- name: Order
|
|
1001
|
+
events:
|
|
1002
|
+
- name: OrderPlaced
|
|
1003
|
+
kafka: true
|
|
1004
|
+
delivery: at-least-once # ← activa Outbox Pattern para este evento
|
|
1005
|
+
fields:
|
|
1006
|
+
- name: customerId
|
|
1007
|
+
type: String
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
### Código Generado (Outbox Table + Publisher)
|
|
1011
|
+
|
|
1012
|
+
```java
|
|
1013
|
+
@Entity
|
|
1014
|
+
@Table(name = "outbox_events")
|
|
1015
|
+
public class OutboxEvent {
|
|
1016
|
+
@Id
|
|
1017
|
+
private String id;
|
|
1018
|
+
private String aggregateType;
|
|
1019
|
+
private String aggregateId;
|
|
1020
|
+
private String eventType;
|
|
1021
|
+
@Column(columnDefinition = "TEXT")
|
|
1022
|
+
private String payload; // JSON serializado del evento
|
|
1023
|
+
private boolean published = false;
|
|
1024
|
+
private LocalDateTime createdAt;
|
|
1025
|
+
private LocalDateTime publishedAt;
|
|
1026
|
+
}
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
```java
|
|
1030
|
+
// OutboxEventPublisher — proceso de polling (cada 5s via @Scheduled)
|
|
1031
|
+
@Component
|
|
1032
|
+
public class OutboxEventPublisher {
|
|
1033
|
+
@Scheduled(fixedDelay = 5000)
|
|
1034
|
+
@Transactional
|
|
1035
|
+
public void publishPendingEvents() {
|
|
1036
|
+
List<OutboxEvent> pending = outboxRepository.findByPublishedFalse();
|
|
1037
|
+
pending.forEach(event -> {
|
|
1038
|
+
messageBroker.publishRaw(event.getEventType(), event.getPayload());
|
|
1039
|
+
event.markPublished();
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
### Prerrequisito
|
|
1046
|
+
|
|
1047
|
+
Domain Events (ítem 1) implementados y funcionando — este ítem solo añade persistencia intermedia, no reemplaza la arquitectura existente.
|
|
1048
|
+
|
|
1049
|
+
---
|
|
1050
|
+
|
|
945
1051
|
## Resumen de Prioridades
|
|
946
1052
|
|
|
947
1053
|
| # | Característica | Prioridad | Complejidad | Estado |
|
|
948
1054
|
|---|---|---|---|---|
|
|
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 |
|
|
1055
|
+
| 1 | Domain Events | Alta | Alta | ✅ Implementado |
|
|
1056
|
+
| 2 | Aggregate Boundaries por ID | Alta | Media | ✅ Implementado |
|
|
1057
|
+
| 3 | Soft Delete Completo | Alta | Baja | Parcial |
|
|
1058
|
+
| 4 | Paginación en Queries | Impl. | -- | ✅ Implementado |
|
|
1059
|
+
| 5 | Optimistic Locking | Media | Baja | Pendiente |
|
|
1060
|
+
| 6 | Read Models / Proyecciones | Media | Alta | Pendiente |
|
|
955
1061
|
| 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 |
|
|
1062
|
+
| 8 | Specifications Pattern | Media | Media | Pendiente |
|
|
1063
|
+
| 9 | JSON Schema para domain.yaml | Tooling | Media | Pendiente |
|
|
1064
|
+
| 10 | Generacion Incremental | Tooling | -- | ✅ Implementado |
|
|
1065
|
+
| 11 | eva4j doctor | Tooling | Media | Pendiente |
|
|
1066
|
+
| 12 | Tests Completos | Tooling | Media | Pendiente |
|
|
961
1067
|
| 13 | Auditoria completa | Impl. | -- | ✅ Implementado |
|
|
962
1068
|
| 14 | Validaciones JSR-303 | Impl. | -- | ✅ Implementado |
|
|
1069
|
+
| 15 | Transactional Outbox Pattern | Alta | Alta | Pendiente |
|
|
963
1070
|
|
|
964
1071
|
---
|
|
965
1072
|
|
|
966
|
-
**Ultima actualizacion:** 2026-02-
|
|
1073
|
+
**Ultima actualizacion:** 2026-02-25
|
|
967
1074
|
**Version de eva4j:** 1.x
|
|
968
1075
|
**Estado:** Documento de planificacion y referencia
|