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/DOMAIN_YAML_GUIDE.md
CHANGED
|
@@ -49,9 +49,9 @@ Las entidades de dominio generadas siguen estrictamente los principios de Domain
|
|
|
49
49
|
- ✅ Protección de invariantes del dominio
|
|
50
50
|
|
|
51
51
|
**✅ Constructores sin Validaciones Automáticas:**
|
|
52
|
-
- Los constructores asignan valores directamente sin validaciones
|
|
53
|
-
- Las validaciones se
|
|
54
|
-
-
|
|
52
|
+
- Los constructores asignan valores directamente sin validaciones de Bean Validation
|
|
53
|
+
- Las validaciones JSR-303 se declaran en `domain.yaml` y se aplican en la capa de aplicación (Command y CreateDto), no en el dominio
|
|
54
|
+
- Las reglas de invariantes de dominio deben implementarse manualmente en los métodos de negocio
|
|
55
55
|
|
|
56
56
|
**📦 Inmutabilidad de Value Objects:**
|
|
57
57
|
- Campos declarados como `final`
|
|
@@ -132,17 +132,28 @@ aggregates:
|
|
|
132
132
|
# Enumeraciones del dominio
|
|
133
133
|
- name: EnumName
|
|
134
134
|
values: []
|
|
135
|
+
|
|
136
|
+
events:
|
|
137
|
+
# Eventos de dominio que emite este agregado
|
|
138
|
+
- name: NombreEventoOcurrido
|
|
139
|
+
fields: []
|
|
140
|
+
# kafka: true # opcional — genera publicación a Kafka vía MessageBroker
|
|
135
141
|
```
|
|
136
142
|
|
|
137
143
|
### Ubicación del archivo
|
|
138
144
|
|
|
139
145
|
```
|
|
140
146
|
tu-proyecto/
|
|
141
|
-
└──
|
|
142
|
-
└──
|
|
143
|
-
└──
|
|
147
|
+
└── src/
|
|
148
|
+
└── main/
|
|
149
|
+
└── java/
|
|
150
|
+
└── com/example/myapp/ ← packagePath (ej: com.example.myapp)
|
|
151
|
+
└── orders/ ← moduleName
|
|
152
|
+
└── domain.yaml ← Aquí
|
|
144
153
|
```
|
|
145
154
|
|
|
155
|
+
Eva4j espera el `domain.yaml` dentro de la carpeta del módulo, que se encuentra bajo la ruta del package Java. Esta ubicación es creada automáticamente al ejecutar `eva add module <nombre>`.
|
|
156
|
+
|
|
146
157
|
---
|
|
147
158
|
|
|
148
159
|
## Definición de Agregados
|
|
@@ -257,6 +268,13 @@ fields:
|
|
|
257
268
|
**Propiedades soportadas:**
|
|
258
269
|
- `name`: Nombre del campo (obligatorio)
|
|
259
270
|
- `type`: Tipo de dato Java (obligatorio)
|
|
271
|
+
- `readOnly`: Excluye del constructor de negocio y CreateDto (ver [Control de Visibilidad](#control-de-visibilidad-de-campos))
|
|
272
|
+
- `hidden`: Excluye del ResponseDto
|
|
273
|
+
- `defaultValue`: Valor inicial para campos `readOnly` (ver [Ó `defaultValue`](#-defaultvalue-valor-inicial-para-campos-readonly))
|
|
274
|
+
- `validations`: Anotaciones JSR-303 en Command y CreateDto
|
|
275
|
+
- `reference`: Referencia semántica a otro agregado
|
|
276
|
+
- `annotations`: Anotaciones JPA personalizadas
|
|
277
|
+
- `isValueObject` / `isEmbedded`: Marcas explícitas de Value Object
|
|
260
278
|
|
|
261
279
|
#### Detección automática de tipos
|
|
262
280
|
|
|
@@ -407,6 +425,7 @@ Eva4j permite controlar qué campos participan en constructores, DTOs de creaci
|
|
|
407
425
|
|-------|---------------------|----------------------|-----------|-------------|
|
|
408
426
|
| **Normal** | ✅ Incluido | ✅ Incluido | ✅ Incluido | ✅ Incluido |
|
|
409
427
|
| **`readOnly: true`** | ❌ Excluido | ✅ Incluido | ❌ Excluido | ✅ Incluido |
|
|
428
|
+
| **`readOnly` + `defaultValue`** | ⚡ Asignado con default | ✅ Incluido | ❌ Excluido | ✅ Incluido |
|
|
410
429
|
| **`hidden: true`** | ✅ Incluido | ✅ Incluido | ✅ Incluido | ❌ Excluido |
|
|
411
430
|
| **Ambos flags** | ❌ Excluido | ✅ Incluido | ❌ Excluido | ❌ Excluido |
|
|
412
431
|
|
|
@@ -488,6 +507,98 @@ public record OrderResponseDto(
|
|
|
488
507
|
) {}
|
|
489
508
|
```
|
|
490
509
|
|
|
510
|
+
#### 🎯 `defaultValue` - Valor Inicial para campos `readOnly`
|
|
511
|
+
|
|
512
|
+
Permite asignar un valor inicial predecible a un campo `readOnly` directamente en `domain.yaml`. El generador emite la asignación en el **constructor de creación** de la entidad de dominio y añade `@Builder.Default` en la entidad JPA.
|
|
513
|
+
|
|
514
|
+
**Restricción:** Solo aplica a campos `readOnly: true`. Si se usa sin `readOnly`, eva4j emite un warning y lo ignora.
|
|
515
|
+
|
|
516
|
+
**Casos de uso típicos:**
|
|
517
|
+
- Estado inicial de enums (`PENDING`, `ACTIVE`, `DRAFT`)
|
|
518
|
+
- Contadores que comienzan en cero
|
|
519
|
+
- Totales/acumuladores antes de cálculos
|
|
520
|
+
- Flags booleanos con estado conocido al crear
|
|
521
|
+
|
|
522
|
+
**Sintaxis:**
|
|
523
|
+
```yaml
|
|
524
|
+
fields:
|
|
525
|
+
- name: totalAmount
|
|
526
|
+
type: BigDecimal
|
|
527
|
+
readOnly: true
|
|
528
|
+
defaultValue: "0.00" # ✅ BigDecimal literal
|
|
529
|
+
|
|
530
|
+
- name: itemCount
|
|
531
|
+
type: Integer
|
|
532
|
+
readOnly: true
|
|
533
|
+
defaultValue: 0 # ✅ Integer literal
|
|
534
|
+
|
|
535
|
+
- name: status
|
|
536
|
+
type: OrderStatus
|
|
537
|
+
readOnly: true
|
|
538
|
+
defaultValue: PENDING # ✅ Enum value (sin comillas)
|
|
539
|
+
|
|
540
|
+
- name: isActive
|
|
541
|
+
type: Boolean
|
|
542
|
+
readOnly: true
|
|
543
|
+
defaultValue: true # ✅ Boolean literal
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
**Código generado en la entidad de dominio:**
|
|
547
|
+
```java
|
|
548
|
+
// Constructor de creación — defaultValues asignados automáticamente
|
|
549
|
+
public Order(String orderNumber, String customerId) {
|
|
550
|
+
this.orderNumber = orderNumber;
|
|
551
|
+
this.customerId = customerId;
|
|
552
|
+
this.totalAmount = new BigDecimal("0.00"); // ← defaultValue
|
|
553
|
+
this.itemCount = 0; // ← defaultValue
|
|
554
|
+
this.status = OrderStatus.PENDING; // ← defaultValue
|
|
555
|
+
this.isActive = true; // ← defaultValue
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Constructor completo (reconstrucción desde BD) — sin defaultValues
|
|
559
|
+
public Order(String id, String orderNumber, String customerId,
|
|
560
|
+
BigDecimal totalAmount, Integer itemCount,
|
|
561
|
+
OrderStatus status, Boolean isActive, ...) {
|
|
562
|
+
// Todos los campos asignados desde parámetros
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
**Código generado en la entidad JPA:**
|
|
567
|
+
```java
|
|
568
|
+
@Builder.Default
|
|
569
|
+
private BigDecimal totalAmount = new BigDecimal("0.00");
|
|
570
|
+
|
|
571
|
+
@Builder.Default
|
|
572
|
+
private Integer itemCount = 0;
|
|
573
|
+
|
|
574
|
+
@Enumerated(EnumType.STRING)
|
|
575
|
+
@Builder.Default
|
|
576
|
+
private OrderStatus status = OrderStatus.PENDING;
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
**Tipos Java soportados y su forma de emisión:**
|
|
580
|
+
|
|
581
|
+
| Tipo | Ejemplo YAML | Literal Java emitido |
|
|
582
|
+
|------|-------------|----------------------|
|
|
583
|
+
| `String` | `defaultValue: hello` | `"hello"` |
|
|
584
|
+
| `Integer` | `defaultValue: 0` | `0` |
|
|
585
|
+
| `Long` | `defaultValue: 0` | `0L` |
|
|
586
|
+
| `Boolean` | `defaultValue: false` | `false` |
|
|
587
|
+
| `BigDecimal` | `defaultValue: "0.00"` | `new BigDecimal("0.00")` |
|
|
588
|
+
| `LocalDateTime` | `defaultValue: now` | `LocalDateTime.now()` |
|
|
589
|
+
| `LocalDate` | `defaultValue: now` | `LocalDate.now()` |
|
|
590
|
+
| `Instant` | `defaultValue: now` | `Instant.now()` |
|
|
591
|
+
| `UUID` | `defaultValue: random` | `UUID.randomUUID()` |
|
|
592
|
+
| Enum | `defaultValue: ACTIVE` | `EnumType.ACTIVE` |
|
|
593
|
+
|
|
594
|
+
**Compatibilidad con otros flags:**
|
|
595
|
+
|
|
596
|
+
| Combinación | Constructor Creación | JPA Builder | CreateDto | ResponseDto |
|
|
597
|
+
|------------|:-------------------:|:-----------:|:---------:|:-----------:|
|
|
598
|
+
| `readOnly` + `defaultValue` | ⚡ Asignado con default | `@Builder.Default` | ❌ | ✅ |
|
|
599
|
+
| `readOnly` + `hidden` + `defaultValue` | ⚡ Asignado con default | `@Builder.Default` | ❌ | ❌ |
|
|
600
|
+
| `defaultValue` sin `readOnly` | *Ignorado (warning)* | *Ignorado* | — | — |
|
|
601
|
+
|
|
491
602
|
#### 🙈 `hidden: true` - Campos Sensibles/Internos
|
|
492
603
|
|
|
493
604
|
Marca campos que **NO deben exponerse** en respuestas de API pero sí pueden recibirse en creación.
|
|
@@ -683,6 +794,57 @@ fields:
|
|
|
683
794
|
|
|
684
795
|
---
|
|
685
796
|
|
|
797
|
+
### Referencias entre Agregados (`reference:`)
|
|
798
|
+
|
|
799
|
+
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`**.
|
|
800
|
+
|
|
801
|
+
#### Sintaxis
|
|
802
|
+
|
|
803
|
+
```yaml
|
|
804
|
+
fields:
|
|
805
|
+
- name: customerId
|
|
806
|
+
type: String
|
|
807
|
+
reference:
|
|
808
|
+
aggregate: Customer # Nombre del agregado referenciado (PascalCase) — obligatorio
|
|
809
|
+
module: customers # Módulo donde vive el agregado — opcional
|
|
810
|
+
- name: productId
|
|
811
|
+
type: String
|
|
812
|
+
reference:
|
|
813
|
+
aggregate: Product
|
|
814
|
+
module: catalog
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
#### Comportamiento
|
|
818
|
+
|
|
819
|
+
- El tipo Java **no cambia** — sigue siendo `String`, `Long`, UUID, etc.
|
|
820
|
+
- JPA genera `@Column` normal — **sin** `@ManyToOne` ni `@JoinColumn`.
|
|
821
|
+
- En la entidad de dominio y en la entidad JPA se genera un **comentario Javadoc** que documenta la referencia.
|
|
822
|
+
- `module:` es opcional: puede omitirse si el agregado referenciado está en el mismo módulo.
|
|
823
|
+
- Si `reference:` está malformado (falta `aggregate`), eva4j lanza un error descriptivo.
|
|
824
|
+
|
|
825
|
+
#### Código Generado
|
|
826
|
+
|
|
827
|
+
```java
|
|
828
|
+
// domain/models/entities/Order.java
|
|
829
|
+
/** Cross-aggregate reference → Customer (module: customers) */
|
|
830
|
+
private String customerId;
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
```java
|
|
834
|
+
// infrastructure/database/entities/OrderJpa.java
|
|
835
|
+
@Column(name = "customer_id")
|
|
836
|
+
/** Cross-aggregate reference → Customer (module: customers) */
|
|
837
|
+
private String customerId;
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
#### Por qué no usar `@ManyToOne` entre agregados
|
|
841
|
+
|
|
842
|
+
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.
|
|
843
|
+
|
|
844
|
+
- **Ejemplo completo:** [examples/domain-multi-aggregate.yaml](../examples/domain-multi-aggregate.yaml)
|
|
845
|
+
|
|
846
|
+
---
|
|
847
|
+
|
|
686
848
|
### Validaciones JSR-303
|
|
687
849
|
|
|
688
850
|
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`.
|
|
@@ -1202,11 +1364,57 @@ valueObjects:
|
|
|
1202
1364
|
type: String
|
|
1203
1365
|
```
|
|
1204
1366
|
|
|
1367
|
+
### Value Objects con métodos de negocio
|
|
1368
|
+
|
|
1369
|
+
Los Value Objects pueden declarar métodos de negocio directamente en el `domain.yaml`. Estos se generan como métodos públicos en la clase del Value Object.
|
|
1370
|
+
|
|
1371
|
+
```yaml
|
|
1372
|
+
valueObjects:
|
|
1373
|
+
- name: Money
|
|
1374
|
+
fields:
|
|
1375
|
+
- name: amount
|
|
1376
|
+
type: BigDecimal
|
|
1377
|
+
- name: currency
|
|
1378
|
+
type: String
|
|
1379
|
+
methods:
|
|
1380
|
+
- name: add
|
|
1381
|
+
returnType: Money
|
|
1382
|
+
parameters:
|
|
1383
|
+
- name: other
|
|
1384
|
+
type: Money
|
|
1385
|
+
body: "return new Money(this.amount.add(other.getAmount()), this.currency);"
|
|
1386
|
+
|
|
1387
|
+
- name: isPositive
|
|
1388
|
+
returnType: boolean
|
|
1389
|
+
parameters: []
|
|
1390
|
+
body: "return this.amount.compareTo(BigDecimal.ZERO) > 0;"
|
|
1391
|
+
```
|
|
1392
|
+
|
|
1393
|
+
**Código generado:**
|
|
1394
|
+
```java
|
|
1395
|
+
public Money add(Money other) {
|
|
1396
|
+
return new Money(this.amount.add(other.getAmount()), this.currency);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
public boolean isPositive() {
|
|
1400
|
+
return this.amount.compareTo(BigDecimal.ZERO) > 0;
|
|
1401
|
+
}
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
**Propiedades de un método:**
|
|
1405
|
+
|
|
1406
|
+
| Propiedad | Descripción |
|
|
1407
|
+
|-----------|-------------|
|
|
1408
|
+
| `name` | Nombre del método |
|
|
1409
|
+
| `returnType` | Tipo de retorno Java |
|
|
1410
|
+
| `parameters` | Array de `{ name, type }` |
|
|
1411
|
+
| `body` | Cuerpo del método (string Java) |
|
|
1412
|
+
|
|
1205
1413
|
---
|
|
1206
1414
|
|
|
1207
1415
|
## Enums
|
|
1208
1416
|
|
|
1209
|
-
### Definición
|
|
1417
|
+
### Definición básica
|
|
1210
1418
|
|
|
1211
1419
|
```yaml
|
|
1212
1420
|
enums:
|
|
@@ -1263,6 +1471,199 @@ enums:
|
|
|
1263
1471
|
|
|
1264
1472
|
---
|
|
1265
1473
|
|
|
1474
|
+
### Enums con Transiciones de Estado
|
|
1475
|
+
|
|
1476
|
+
Cuando un enum representa un ciclo de vida de negocio, puede declarar `transitions` e `initialValue`. Eva4j genera automáticamente los métodos de transición, guards, helpers de consulta y el mapa de transiciones válidas.
|
|
1477
|
+
|
|
1478
|
+
#### Sintaxis
|
|
1479
|
+
|
|
1480
|
+
```yaml
|
|
1481
|
+
enums:
|
|
1482
|
+
- name: OrderStatus
|
|
1483
|
+
initialValue: PENDING # ← Estado inicial (se asigna en el constructor de creación)
|
|
1484
|
+
transitions:
|
|
1485
|
+
- from: PENDING
|
|
1486
|
+
to: CONFIRMED
|
|
1487
|
+
method: confirm
|
|
1488
|
+
|
|
1489
|
+
- from: CONFIRMED
|
|
1490
|
+
to: SHIPPED
|
|
1491
|
+
method: ship
|
|
1492
|
+
|
|
1493
|
+
- from: SHIPPED
|
|
1494
|
+
to: DELIVERED
|
|
1495
|
+
method: deliver
|
|
1496
|
+
|
|
1497
|
+
- from: [PENDING, CONFIRMED] # múltiples estados origen
|
|
1498
|
+
to: CANCELLED
|
|
1499
|
+
method: cancel
|
|
1500
|
+
guard: "this.status == OrderStatus.DELIVERED" # lanza BusinessException si se cumple
|
|
1501
|
+
|
|
1502
|
+
values:
|
|
1503
|
+
- PENDING
|
|
1504
|
+
- CONFIRMED
|
|
1505
|
+
- SHIPPED
|
|
1506
|
+
- DELIVERED
|
|
1507
|
+
- CANCELLED
|
|
1508
|
+
```
|
|
1509
|
+
|
|
1510
|
+
#### Propiedades de transición
|
|
1511
|
+
|
|
1512
|
+
| Propiedad | Tipo | Descripción |
|
|
1513
|
+
|-----------|------|-------------|
|
|
1514
|
+
| `from` | String \| Array | Estado(s) de origen válidos |
|
|
1515
|
+
| `to` | String | Estado destino |
|
|
1516
|
+
| `method` | String | Nombre del método que ejecuta la transición |
|
|
1517
|
+
| `guard` | String | Condición Java que lanza `BusinessException` si se cumple (opcional) |
|
|
1518
|
+
|
|
1519
|
+
#### Qué genera
|
|
1520
|
+
|
|
1521
|
+
**En el enum (`OrderStatus.java`):**
|
|
1522
|
+
```java
|
|
1523
|
+
public enum OrderStatus {
|
|
1524
|
+
PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED;
|
|
1525
|
+
|
|
1526
|
+
private static final Map<OrderStatus, Set<OrderStatus>> VALID_TRANSITIONS;
|
|
1527
|
+
// ... mapa estático inicializado
|
|
1528
|
+
|
|
1529
|
+
public boolean canTransitionTo(OrderStatus target) { ... }
|
|
1530
|
+
public OrderStatus transitionTo(OrderStatus target) { ... } // lanza InvalidStateTransitionException
|
|
1531
|
+
}
|
|
1532
|
+
```
|
|
1533
|
+
|
|
1534
|
+
**En la entidad raíz (`Order.java`):**
|
|
1535
|
+
```java
|
|
1536
|
+
// El constructor de creación NO recibe status (se auto-inicializa a PENDING)
|
|
1537
|
+
public Order(String orderNumber, String customerId) {
|
|
1538
|
+
this.orderNumber = orderNumber;
|
|
1539
|
+
this.customerId = customerId;
|
|
1540
|
+
this.status = OrderStatus.PENDING; // ← initialValue aplicado
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// Métodos de transición generados
|
|
1544
|
+
public void confirm() { this.status = this.status.transitionTo(OrderStatus.CONFIRMED); }
|
|
1545
|
+
public void ship() { this.status = this.status.transitionTo(OrderStatus.SHIPPED); }
|
|
1546
|
+
public void deliver() { this.status = this.status.transitionTo(OrderStatus.DELIVERED); }
|
|
1547
|
+
public void cancel() {
|
|
1548
|
+
if (this.status == OrderStatus.DELIVERED) {
|
|
1549
|
+
throw new BusinessException("Cannot execute 'cancel': business rule violated");
|
|
1550
|
+
}
|
|
1551
|
+
this.status = this.status.transitionTo(OrderStatus.CANCELLED);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// Helpers de consulta de estado
|
|
1555
|
+
public boolean isPending() { return this.status == OrderStatus.PENDING; }
|
|
1556
|
+
public boolean isConfirmed() { return this.status == OrderStatus.CONFIRMED; }
|
|
1557
|
+
// ... uno por cada valor del enum
|
|
1558
|
+
|
|
1559
|
+
// Helpers de disponibilidad de transición
|
|
1560
|
+
public boolean canConfirm() { return this.status.canTransitionTo(OrderStatus.CONFIRMED); }
|
|
1561
|
+
public boolean canCancel() { return this.status.canTransitionTo(OrderStatus.CANCELLED); }
|
|
1562
|
+
// ... uno por cada método de transición
|
|
1563
|
+
```
|
|
1564
|
+
|
|
1565
|
+
**Nota:** El campo con `initialValue` se trata implícitamente como `readOnly: true` — no aparece en el constructor de negocio ni en el `CreateDto`.
|
|
1566
|
+
|
|
1567
|
+
---
|
|
1568
|
+
|
|
1569
|
+
## Eventos de Dominio
|
|
1570
|
+
|
|
1571
|
+
Los eventos de dominio representan hechos significativos que ocurren dentro del agregado. Eva4j genera las clases de evento y el handler para publicarlos automáticamente tras commit de transacción.
|
|
1572
|
+
|
|
1573
|
+
### Sintaxis
|
|
1574
|
+
|
|
1575
|
+
```yaml
|
|
1576
|
+
aggregates:
|
|
1577
|
+
- name: Order
|
|
1578
|
+
entities:
|
|
1579
|
+
- name: order
|
|
1580
|
+
isRoot: true
|
|
1581
|
+
tableName: orders
|
|
1582
|
+
fields:
|
|
1583
|
+
- name: id
|
|
1584
|
+
type: String
|
|
1585
|
+
- name: orderNumber
|
|
1586
|
+
type: String
|
|
1587
|
+
|
|
1588
|
+
events:
|
|
1589
|
+
- name: OrderConfirmedEvent # PascalCase, idealmente en pasado
|
|
1590
|
+
fields:
|
|
1591
|
+
- name: orderId
|
|
1592
|
+
type: String
|
|
1593
|
+
- name: confirmedAt
|
|
1594
|
+
type: LocalDateTime
|
|
1595
|
+
|
|
1596
|
+
- name: OrderShippedEvent
|
|
1597
|
+
kafka: true # opcional — genera publicación a Kafka
|
|
1598
|
+
fields:
|
|
1599
|
+
- name: orderId
|
|
1600
|
+
type: String
|
|
1601
|
+
- name: trackingNumber
|
|
1602
|
+
type: String
|
|
1603
|
+
```
|
|
1604
|
+
|
|
1605
|
+
### Propiedades de un evento
|
|
1606
|
+
|
|
1607
|
+
| Propiedad | Tipo | Descripción |
|
|
1608
|
+
|-----------|------|-------------|
|
|
1609
|
+
| `name` | String | Nombre de la clase del evento (PascalCase) |
|
|
1610
|
+
| `fields` | Array | Campos que transporta el evento |
|
|
1611
|
+
| `kafka` | Boolean | Si `true`, genera llamada a `messageBroker.send{EventName}()` |
|
|
1612
|
+
|
|
1613
|
+
### Archivos generados
|
|
1614
|
+
|
|
1615
|
+
Para cada evento, eva4j genera dos archivos:
|
|
1616
|
+
|
|
1617
|
+
**1. `OrderConfirmedEvent.java`** — en `domain/models/events/`
|
|
1618
|
+
```java
|
|
1619
|
+
public final class OrderConfirmedEvent extends DomainEvent {
|
|
1620
|
+
private final String orderId;
|
|
1621
|
+
private final LocalDateTime confirmedAt;
|
|
1622
|
+
|
|
1623
|
+
public OrderConfirmedEvent(String aggregateId, String orderId, LocalDateTime confirmedAt) {
|
|
1624
|
+
super(aggregateId);
|
|
1625
|
+
this.orderId = orderId;
|
|
1626
|
+
this.confirmedAt = confirmedAt;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
public String getOrderId() { return orderId; }
|
|
1630
|
+
public LocalDateTime getConfirmedAt() { return confirmedAt; }
|
|
1631
|
+
}
|
|
1632
|
+
```
|
|
1633
|
+
|
|
1634
|
+
**2. `OrderDomainEventHandler.java`** — en `application/usecases/`
|
|
1635
|
+
```java
|
|
1636
|
+
@Component
|
|
1637
|
+
public class OrderDomainEventHandler {
|
|
1638
|
+
|
|
1639
|
+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
|
1640
|
+
public void handle(OrderConfirmedEvent event) {
|
|
1641
|
+
// Lógica post-commit — ej: notificaciones, métricas
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
|
1645
|
+
public void handle(OrderShippedEvent event) {
|
|
1646
|
+
messageBroker.sendOrderShippedEvent(event); // kafka: true
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
```
|
|
1650
|
+
|
|
1651
|
+
### Cómo publicar un evento
|
|
1652
|
+
|
|
1653
|
+
Los eventos se publican desde métodos de negocio de la entidad raíz usando el método heredado `raise()`:
|
|
1654
|
+
|
|
1655
|
+
```java
|
|
1656
|
+
// En Order.java (entidad de dominio)
|
|
1657
|
+
public void confirm() {
|
|
1658
|
+
this.status = this.status.transitionTo(OrderStatus.CONFIRMED);
|
|
1659
|
+
raise(new OrderConfirmedEvent(this.id, this.id, LocalDateTime.now()));
|
|
1660
|
+
}
|
|
1661
|
+
```
|
|
1662
|
+
|
|
1663
|
+
> **Nota:** `raise()` es provisto por la clase base de dominio. La publicación real ocurre tras el commit de la transacción gracias a `@TransactionalEventListener(AFTER_COMMIT)`.
|
|
1664
|
+
|
|
1665
|
+
---
|
|
1666
|
+
|
|
1266
1667
|
## Relaciones
|
|
1267
1668
|
|
|
1268
1669
|
eva4j soporta relaciones JPA bidireccionales completas con generación automática del lado inverso.
|
|
@@ -2447,22 +2848,26 @@ eva4j generate entities <module-name>
|
|
|
2447
2848
|
### ✅ Soportado
|
|
2448
2849
|
|
|
2449
2850
|
- Agregados con entidad raíz y secundarias
|
|
2450
|
-
- Value Objects embebidos
|
|
2451
|
-
- Enums con
|
|
2452
|
-
- Relaciones OneToMany, ManyToOne, OneToOne
|
|
2851
|
+
- Value Objects embebidos (con `methods` opcionales)
|
|
2852
|
+
- Enums simples y con transiciones de estado (`transitions`, `initialValue`)
|
|
2853
|
+
- Relaciones OneToMany, ManyToOne, OneToOne (bidireccionales automáticas)
|
|
2453
2854
|
- Tipos primitivos y de fecha Java
|
|
2454
|
-
- Colecciones de primitivos y VOs
|
|
2855
|
+
- Colecciones de primitivos y VOs (`List<T>`)
|
|
2455
2856
|
- IDs: String (UUID), Long/Integer (IDENTITY)
|
|
2456
2857
|
- Cascade y Fetch personalizados
|
|
2858
|
+
- Validaciones JSR-303 en Command y CreateDto
|
|
2859
|
+
- Auditoría automática (`audit.enabled`, `audit.trackUser`)
|
|
2860
|
+
- Control de visibilidad de campos (`readOnly`, `hidden`, `defaultValue`)
|
|
2861
|
+
- Referencias cross-agregado (`reference:`)
|
|
2862
|
+
- Domain Events (`events:` con soporte opcional de Kafka)
|
|
2863
|
+
- Soft delete a nivel de módulo (configurado en `eva add module`)
|
|
2457
2864
|
|
|
2458
2865
|
### 🚧 Próximamente
|
|
2459
2866
|
|
|
2460
|
-
-
|
|
2461
|
-
-
|
|
2462
|
-
-
|
|
2463
|
-
-
|
|
2464
|
-
- Índices y constraints
|
|
2465
|
-
- Herencia de entidades
|
|
2867
|
+
- Query methods personalizados en repositorios
|
|
2868
|
+
- Índices y constraints de BD declarados en YAML
|
|
2869
|
+
- Herencia de entidades JPA
|
|
2870
|
+
- Soporte de `Instant` como tipo de campo (actualmente solo para `defaultValue`)
|
|
2466
2871
|
|
|
2467
2872
|
---
|
|
2468
2873
|
|
|
@@ -2478,7 +2883,7 @@ R: Los enums son globales al módulo, solo usa el nombre: `type: OrderStatus`
|
|
|
2478
2883
|
R: Sí, pero debes definirlo en cada agregado (por ahora).
|
|
2479
2884
|
|
|
2480
2885
|
**P: ¿Qué pasa si regenero el código?**
|
|
2481
|
-
R:
|
|
2886
|
+
R: Eva4j usa checksums SHA-256 para detectar archivos que fueron modificados manualmente. Los archivos con cambios manuales **no se sobreescriben** — se muestra un aviso y se omiten. Usa `--force` para forzar la sobreescritura de todos los archivos.
|
|
2482
2887
|
|
|
2483
2888
|
**P: ¿Puedo personalizar las entidades generadas?**
|
|
2484
2889
|
R: Sí, modifica las plantillas en `templates/aggregate/`.
|
|
@@ -2487,10 +2892,9 @@ R: Sí, modifica las plantillas en `templates/aggregate/`.
|
|
|
2487
2892
|
|
|
2488
2893
|
## Recursos Adicionales
|
|
2489
2894
|
|
|
2490
|
-
- [Guía de Implementación](IMPLEMENTATION_SUMMARY.md)
|
|
2491
|
-
- [Guía de Testing](TESTING_GUIDE.md)
|
|
2492
2895
|
- [Referencia Rápida](QUICK_REFERENCE.md)
|
|
2493
|
-
- [
|
|
2896
|
+
- [Guía de Agentes IA](AGENTS.md)
|
|
2897
|
+
- [Características Futuras](FUTURE_FEATURES.md)
|
|
2494
2898
|
|
|
2495
2899
|
---
|
|
2496
2900
|
|