eva4j 1.0.16 → 1.0.17
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 +218 -5
- package/DOMAIN_YAML_GUIDE.md +185 -2
- package/FUTURE_FEATURES.md +33 -52
- package/docs/commands/EVALUATE_SYSTEM.md +18 -2
- package/examples/domain-events.yaml +26 -0
- package/examples/domain-read-models.yaml +113 -0
- package/package.json +1 -1
- package/read-model-spec.md +664 -0
- package/src/agents/design-reviewer.agent.md +3 -0
- package/src/commands/generate-entities.js +254 -10
- package/src/commands/generate-http-exchange.js +3 -0
- package/src/commands/generate-kafka-event.js +3 -0
- package/src/commands/generate-kafka-listener.js +3 -0
- package/src/commands/generate-record.js +2 -2
- package/src/commands/generate-resource.js +4 -1
- package/src/commands/generate-temporal-activity.js +4 -1
- package/src/commands/generate-temporal-flow.js +4 -1
- package/src/commands/generate-usecase.js +4 -1
- package/src/skills/build-system-yaml/SKILL.md +122 -1
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +205 -24
- package/src/skills/build-system-yaml/references/module-spec.md +33 -1
- package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
- package/src/utils/config-manager.js +4 -2
- package/src/utils/domain-validator.js +230 -14
- package/src/utils/naming.js +10 -0
- package/src/utils/validator.js +3 -1
- package/src/utils/yaml-to-entity.js +272 -3
- package/templates/aggregate/AggregateRepository.java.ejs +4 -0
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +8 -0
- package/templates/aggregate/AggregateRoot.java.ejs +38 -4
- package/templates/aggregate/JpaAggregateRoot.java.ejs +2 -2
- package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
- package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
- package/templates/read-model/ReadModelDomain.java.ejs +46 -0
- package/templates/read-model/ReadModelJpa.java.ejs +58 -0
- package/templates/read-model/ReadModelJpaRepository.java.ejs +11 -0
- package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
- package/templates/read-model/ReadModelRepository.java.ejs +42 -0
- package/templates/read-model/ReadModelRepositoryImpl.java.ejs +81 -0
- package/templates/read-model/ReadModelSyncHandler.java.ejs +52 -0
- package/test-c2010.js +49 -0
- package/test-update-compat.js +109 -0
- package/test-update-lifecycle.js +121 -0
package/AGENTS.md
CHANGED
|
@@ -616,12 +616,15 @@ public void cancel() {
|
|
|
616
616
|
}
|
|
617
617
|
```
|
|
618
618
|
|
|
619
|
-
Si un evento **no declara `triggers
|
|
619
|
+
Si un evento **no declara `triggers`** ni `lifecycle`, el desarrollador debe llamar a `raise()` manualmente dentro del método de negocio.
|
|
620
620
|
|
|
621
621
|
**Validaciones generadas:**
|
|
622
|
-
- **C2-004** (error): trigger referencia un método que no existe en ninguna transición del módulo
|
|
622
|
+
- **C2-004** (error): trigger referencia un método que no existe en ninguna transición del módulo (se omite para eventos con `lifecycle`)
|
|
623
623
|
- **C2-005** (info): transición sin ningún evento asociado — considera declarar `triggers`
|
|
624
624
|
- **C2-001** se silencia automáticamente para transiciones que ya tienen `triggers`
|
|
625
|
+
- **C2-008** (error): valor de `lifecycle` inválido (no es `create`, `update`, `delete` ni `softDelete`)
|
|
626
|
+
- **C2-009** (warning): `lifecycle: softDelete` sin `hasSoftDelete: true` en la entidad raíz, o `lifecycle: delete` con `hasSoftDelete: true`
|
|
627
|
+
- **C2-010** (error): campo de lifecycle event no existe en la entidad raíz del agregado (excluyendo `{entityName}Id` y campos `*At` + `LocalDateTime`)
|
|
625
628
|
|
|
626
629
|
**Auto-wiring de broker:** Si el proyecto tiene un broker de mensajería instalado (`eva add kafka-client`), `eva g entities` genera automáticamente la capa de Integration Events para **todos** los eventos declarados — sin necesidad de ejecutar `eva g kafka-event` por separado:
|
|
627
630
|
|
|
@@ -658,6 +661,114 @@ public void confirm() {
|
|
|
658
661
|
|
|
659
662
|
**Nota:** el flag `kafka: true` por evento ya no es necesario — todos los eventos se cablearán automáticamente cuando haya un broker instalado.
|
|
660
663
|
|
|
664
|
+
#### Propiedad `lifecycle`
|
|
665
|
+
|
|
666
|
+
Conecta un evento a una operación CRUD del ciclo de vida del agregado. A diferencia de `triggers` (que se conecta a métodos de transición de estado), `lifecycle` emite `raise()` automáticamente en el punto CRUD correspondiente.
|
|
667
|
+
|
|
668
|
+
**Valores válidos:**
|
|
669
|
+
|
|
670
|
+
| Valor | Punto de emisión | Descripción |
|
|
671
|
+
|---|---|---|
|
|
672
|
+
| `create` | Constructor de creación de la entidad | UUID auto-generado como id antes de raise() |
|
|
673
|
+
| `update` | Método `update()` de la entidad raíz | Handler llama `existing.update(...)`; `raise()` interno |
|
|
674
|
+
| `delete` | DeleteCommandHandler, antes de `repository.delete()` | Requiere `hasSoftDelete: false`; genera `repository.delete(entity)` |
|
|
675
|
+
| `softDelete` | Método `softDelete()` de la entidad | Requiere `hasSoftDelete: true`; raise() después de `this.deletedAt = ...` |
|
|
676
|
+
|
|
677
|
+
**Ejemplo en domain.yaml:**
|
|
678
|
+
|
|
679
|
+
```yaml
|
|
680
|
+
events:
|
|
681
|
+
- name: ProductCreatedEvent
|
|
682
|
+
lifecycle: create
|
|
683
|
+
fields:
|
|
684
|
+
- name: productId
|
|
685
|
+
type: String
|
|
686
|
+
- name: name
|
|
687
|
+
type: String
|
|
688
|
+
- name: price
|
|
689
|
+
type: BigDecimal
|
|
690
|
+
|
|
691
|
+
- name: ProductUpdatedEvent
|
|
692
|
+
lifecycle: update
|
|
693
|
+
fields:
|
|
694
|
+
- name: productId
|
|
695
|
+
type: String
|
|
696
|
+
- name: name
|
|
697
|
+
type: String
|
|
698
|
+
|
|
699
|
+
- name: ProductDeletedEvent
|
|
700
|
+
lifecycle: delete
|
|
701
|
+
fields:
|
|
702
|
+
- name: productId
|
|
703
|
+
type: String
|
|
704
|
+
- name: deletedAt
|
|
705
|
+
type: LocalDateTime
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
**Resultado generado para `lifecycle: create`:**
|
|
709
|
+
|
|
710
|
+
```java
|
|
711
|
+
// En Product.java — constructor de creación
|
|
712
|
+
public Product(String name, String description, BigDecimal price) {
|
|
713
|
+
this.id = java.util.UUID.randomUUID().toString();
|
|
714
|
+
this.name = name;
|
|
715
|
+
this.description = description;
|
|
716
|
+
this.price = price;
|
|
717
|
+
raise(new ProductCreatedEvent(this.getId(), this.getName(), this.getPrice()));
|
|
718
|
+
}
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
**Resultado generado para `lifecycle: update`:**
|
|
722
|
+
|
|
723
|
+
```java
|
|
724
|
+
// En Product.java — método update()
|
|
725
|
+
public void update(String name, String description, BigDecimal price) {
|
|
726
|
+
this.name = name;
|
|
727
|
+
this.description = description;
|
|
728
|
+
this.price = price;
|
|
729
|
+
raise(new ProductUpdatedEvent(this.getId(), this.getName(), this.getPrice()));
|
|
730
|
+
}
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
```java
|
|
734
|
+
// En UpdateProductCommandHandler.java
|
|
735
|
+
Product existing = repository.findById(command.id())...;
|
|
736
|
+
existing.update(
|
|
737
|
+
command.name() != null ? command.name() : existing.getName(),
|
|
738
|
+
command.description() != null ? command.description() : existing.getDescription(),
|
|
739
|
+
command.price() != null ? command.price() : existing.getPrice()
|
|
740
|
+
);
|
|
741
|
+
repository.save(existing);
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
**Resultado generado para `lifecycle: delete`:**
|
|
745
|
+
|
|
746
|
+
```java
|
|
747
|
+
// En DeleteProductCommandHandler.java
|
|
748
|
+
Product entity = repository.findById(command.id())...;
|
|
749
|
+
entity.raise(new ProductDeletedEvent(entity.getId(), LocalDateTime.now()));
|
|
750
|
+
repository.delete(entity);
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
**Resultado generado para `lifecycle: softDelete`:**
|
|
754
|
+
|
|
755
|
+
```java
|
|
756
|
+
// En Product.java — método softDelete()
|
|
757
|
+
public void softDelete() {
|
|
758
|
+
if (this.deletedAt != null) { throw new IllegalStateException("..."); }
|
|
759
|
+
this.deletedAt = java.time.LocalDateTime.now();
|
|
760
|
+
raise(new ProductDeactivatedEvent(this.getId(), LocalDateTime.now()));
|
|
761
|
+
}
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
**Resolución de argumentos:** usa las mismas reglas que `triggers` (match por nombre de campo, VO unwrapping, `LocalDateTime.now()` para campos *At, `null /* TODO */` para no resueltos).
|
|
765
|
+
|
|
766
|
+
**Visibilidad de `raise()`:** cuando un evento usa `lifecycle: delete`, el método `raise()` en la entidad se genera como `public` (en vez de `protected`) para permitir que los handlers de aplicación lo invoquen. Con `lifecycle: update`, `raise()` permanece `protected` porque se invoca internamente desde el método `update()` de la entidad.
|
|
767
|
+
|
|
768
|
+
**Infraestructura de repositorio:** cuando un evento usa `lifecycle: delete`, el generador agrega automáticamente `void delete(Entity entity)` al repositorio (interface + implementación). La implementación publica los eventos pendientes antes de la eliminación física.
|
|
769
|
+
|
|
770
|
+
**Restricción:** un evento puede declarar `triggers` O `lifecycle`, no ambos. `lifecycle` aplica solo a eventos de la entidad raíz del agregado.
|
|
771
|
+
|
|
661
772
|
### Consumo de Eventos Externos (`listeners[]`)
|
|
662
773
|
|
|
663
774
|
```yaml
|
|
@@ -821,8 +932,94 @@ aggregates:
|
|
|
821
932
|
└── events: → Domain Events que PRODUCE (async, broker)
|
|
822
933
|
listeners: → Integration Events que CONSUME (async, broker)
|
|
823
934
|
ports: → Servicios HTTP que LLAMA (sync, Feign)
|
|
935
|
+
readModels: → Proyecciones locales mantenidas por eventos (async, broker)
|
|
824
936
|
```
|
|
825
937
|
|
|
938
|
+
### Proyecciones Locales (`readModels[]`)
|
|
939
|
+
|
|
940
|
+
Un Read Model es una **proyección local de datos de otro bounded context**, mantenida mediante eventos de dominio. Elimina dependencias síncronas (HTTP) entre módulos, mejorando autonomía, resiliencia y rendimiento.
|
|
941
|
+
|
|
942
|
+
```yaml
|
|
943
|
+
# Nivel raíz, sibling de aggregates:, listeners:, ports:
|
|
944
|
+
readModels:
|
|
945
|
+
- name: ProductReadModel # PascalCase + sufijo "ReadModel" (OBLIGATORIO)
|
|
946
|
+
source: # Trazabilidad al módulo fuente (OBLIGATORIO)
|
|
947
|
+
module: products # Módulo fuente (kebab-case)
|
|
948
|
+
aggregate: Product # Agregado fuente (PascalCase)
|
|
949
|
+
tableName: rm_products # Tabla en BD (OBLIGATORIO, prefijo rm_)
|
|
950
|
+
fields: # Campos proyectados — subconjunto del fuente
|
|
951
|
+
- name: id
|
|
952
|
+
type: String
|
|
953
|
+
- name: name
|
|
954
|
+
type: String
|
|
955
|
+
- name: price
|
|
956
|
+
type: BigDecimal
|
|
957
|
+
- name: status
|
|
958
|
+
type: String
|
|
959
|
+
syncedBy: # Eventos que mantienen esta tabla (min 1)
|
|
960
|
+
- event: ProductCreatedEvent # Nombre del evento (PascalCase + sufijo Event)
|
|
961
|
+
action: UPSERT # Acción: UPSERT | DELETE | SOFT_DELETE
|
|
962
|
+
- event: ProductUpdatedEvent
|
|
963
|
+
action: UPSERT
|
|
964
|
+
- event: ProductDeactivatedEvent
|
|
965
|
+
action: SOFT_DELETE
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
### Artefactos generados por `readModels[]`
|
|
969
|
+
|
|
970
|
+
Por cada read model:
|
|
971
|
+
|
|
972
|
+
| Archivo generado | Descripción |
|
|
973
|
+
|---|---|
|
|
974
|
+
| `domain/models/readmodels/{Name}.java` | Clase de dominio (inmutable, sin setters, sin auditoría) |
|
|
975
|
+
| `infrastructure/database/entities/{Name}Jpa.java` | Entidad JPA (Lombok, `@Id` NO auto-generado) |
|
|
976
|
+
| `infrastructure/database/repositories/{Name}JpaRepository.java` | Spring Data JPA interface |
|
|
977
|
+
| `domain/repositories/{Name}Repository.java` | Interfaz de repositorio (puerto) |
|
|
978
|
+
| `infrastructure/database/repositories/{Name}RepositoryImpl.java` | Implementación del repositorio |
|
|
979
|
+
| `application/usecases/Sync{Source}ReadModelHandler.java` | Handler de sincronización (un método por evento) |
|
|
980
|
+
|
|
981
|
+
Por cada entrada en `syncedBy`:
|
|
982
|
+
|
|
983
|
+
| Archivo generado | Descripción |
|
|
984
|
+
|---|---|
|
|
985
|
+
| `application/events/{EventBase}IntegrationEvent.java` | Integration Event (reutilizado si ya existe) |
|
|
986
|
+
| `infrastructure/kafkaListener/{EventBase}ReadModelListener.java` | Kafka listener que delega al sync handler |
|
|
987
|
+
| `parameters/*/kafka.yaml` | Registro del topic (actualizado) |
|
|
988
|
+
|
|
989
|
+
### Acciones de sincronización
|
|
990
|
+
|
|
991
|
+
| Acción | Significado | Uso |
|
|
992
|
+
|---|---|---|
|
|
993
|
+
| `UPSERT` | Insertar si es nuevo, actualizar si existe | Creaciones, actualizaciones, cambios de estado |
|
|
994
|
+
| `DELETE` | Eliminar el registro permanentemente | Hard deletes en el módulo fuente |
|
|
995
|
+
| `SOFT_DELETE` | Marcar como inactivo con timestamp | Cuando el fuente usa soft delete |
|
|
996
|
+
|
|
997
|
+
> **Nota:** Para acciones `DELETE` y `SOFT_DELETE`, el listener **no usa IntegrationEvent** — extrae directamente el `id` del payload y lo pasa como `String` al `SyncHandler`. El `SyncHandler` llama a `repository.deleteById(id)` o `repository.softDeleteById(id)` respectivamente. El generador tampoco produce el archivo IntegrationEvent para estas acciones.
|
|
998
|
+
|
|
999
|
+
### Reglas de `readModels[]`
|
|
1000
|
+
|
|
1001
|
+
- **`name:`** — PascalCase, **DEBE** terminar con `ReadModel`
|
|
1002
|
+
- **`tableName:`** — **DEBE** empezar con `rm_` (identificación visual en BD)
|
|
1003
|
+
- **`fields:`** — **DEBE** incluir un campo `id`
|
|
1004
|
+
- **`syncedBy:`** — **DEBE** tener al menos una entrada
|
|
1005
|
+
- **`source.module:`** — **NO PUEDE** ser el mismo módulo actual (readModels son para proyecciones cross-module)
|
|
1006
|
+
- **Topic derivado automáticamente** — Se deriva del nombre del evento: strip sufijo `Event` → SCREAMING_SNAKE_CASE. Override opcional con `topic:` explícito
|
|
1007
|
+
- **Sin auditoría** — Los readModels **nunca** tienen campos de auditoría
|
|
1008
|
+
- **Sin endpoints REST** — Los readModels **nunca** exponen endpoints REST
|
|
1009
|
+
- **Sin lógica de negocio** — La clase de dominio es inmutable (solo getters)
|
|
1010
|
+
|
|
1011
|
+
### Validaciones
|
|
1012
|
+
|
|
1013
|
+
| Código | Severidad | Regla |
|
|
1014
|
+
|---|---|---|
|
|
1015
|
+
| RM-001 | ERROR | `name` debe terminar con `ReadModel` |
|
|
1016
|
+
| RM-002 | ERROR | `tableName` debe empezar con `rm_` |
|
|
1017
|
+
| RM-004 | ERROR | `fields` debe incluir un campo `id` |
|
|
1018
|
+
| RM-005 | ERROR | `syncedBy` debe tener al menos una entrada |
|
|
1019
|
+
| RM-006 | ERROR | `syncedBy[].action` debe ser `UPSERT`, `DELETE` o `SOFT_DELETE` |
|
|
1020
|
+
| RM-009 | WARNING | `ports:` todavía tiene llamadas sync al mismo `source.module` — considerar removerlas |
|
|
1021
|
+
| RM-010 | ERROR | `source.module` es el mismo módulo actual |
|
|
1022
|
+
|
|
826
1023
|
Cuando una entidad tiene `hasSoftDelete: true`, eva4j genera eliminación lógica en lugar de física.
|
|
827
1024
|
|
|
828
1025
|
### Configuración en domain.yaml
|
|
@@ -1478,7 +1675,13 @@ Al generar o modificar código, verificar:
|
|
|
1478
1675
|
- [ ] Value Object con comportamiento → declarar `methods` en lugar de lógica en entidad
|
|
1479
1676
|
- [ ] Evento de dominio → declarar en `events[]`, publicar con `raise()` en método de negocio
|
|
1480
1677
|
- [ ] Evento con `triggers: [methodName]` → el generador emite `raise()` automáticamente; args no resolubles quedan como `null /* TODO */`
|
|
1481
|
-
- [ ]
|
|
1678
|
+
- [ ] Evento con `lifecycle: create` → el generador emite UUID auto-generado + `raise()` en el constructor de creación
|
|
1679
|
+
- [ ] Evento con `lifecycle: update` → el generador emite método `update()` en la entidad raíz con `raise()` interno; handler llama `existing.update(...)`; `raise()` permanece `protected`
|
|
1680
|
+
- [ ] Evento con `lifecycle: delete` → el generador emite `raise()` en DeleteCommandHandler + genera `repository.delete(entity)` con publicación de eventos; `raise()` se genera como `public`
|
|
1681
|
+
- [ ] Evento con `lifecycle: softDelete` → el generador emite `raise()` dentro del método `softDelete()` de la entidad; requiere `hasSoftDelete: true`
|
|
1682
|
+
- [ ] Un evento puede declarar `triggers` O `lifecycle`, no ambos
|
|
1683
|
+
- [ ] Campos de lifecycle events son campos de la entidad raíz (excluyendo `{entityName}Id` y `*At` temporal) — `C2-010`
|
|
1684
|
+
- [ ] Sin `triggers` ni `lifecycle` en el evento → el dev llama a `raise()` manualmente
|
|
1482
1685
|
- [ ] Evento con broker → **no** usar `kafka: true`; si `eva add kafka-client` está instalado, `eva g entities` auto-cablea todos los eventos
|
|
1483
1686
|
- [ ] Distinguir entre Domain Event (`domain/models/events/X.java`) e Integration Event (`application/events/XIntegrationEvent.java`) — cambios de broker solo afectan al adaptador `MessageBroker`
|
|
1484
1687
|
- [ ] Consumo de eventos externos → declarar en `listeners[]` (nivel raíz); `topic:` obligatorio en módulos standalone
|
|
@@ -1493,9 +1696,19 @@ Al generar o modificar código, verificar:
|
|
|
1493
1696
|
- [ ] Métodos con cuerpo (POST/PUT/PATCH) → incluir `body:`; campos de tipo objeto en `nestedTypes:`
|
|
1494
1697
|
- [ ] Tipo de dominio auto-derivado del nombre del método — usar `domainType:` para sobrescribir si es necesario
|
|
1495
1698
|
- [ ] Cada `service:` en `ports[]` genera: interfaz (devuelve modelos de dominio), FeignClient (devuelve DTOs infra), FeignAdapter (mapea ACL), FeignConfig + `urls.yaml`
|
|
1699
|
+
- [ ] Read models → declarar en `readModels[]` (nivel raíz); `name` debe terminar con `ReadModel`, `tableName` debe empezar con `rm_`
|
|
1700
|
+
- [ ] Read models nunca tienen auditoría, endpoints REST, ni lógica de negocio
|
|
1701
|
+
- [ ] Cada read model genera: clase de dominio inmutable, JPA entity (sin audit), repositorio (interface + impl), sync handler
|
|
1702
|
+
- [ ] Cada `syncedBy` entry genera: IntegrationEvent (reutilizado si ya existe), KafkaListener, registro de topic
|
|
1703
|
+
- [ ] Para `DELETE`/`SOFT_DELETE`, el listener bypasses IntegrationEvent — extrae `id` directamente y pasa `String id` al SyncHandler
|
|
1704
|
+
- [ ] `SOFT_DELETE` → `repository.softDeleteById(id)`; `DELETE` → `repository.deleteById(id)`; `UPSERT` → reconstruye modelo completo con IntegrationEvent + `repository.upsert()`
|
|
1705
|
+
- [ ] `source.module` nunca puede ser el mismo módulo (RM-010) — readModels son exclusivamente cross-module
|
|
1706
|
+
- [ ] ReadModel fields cubiertos por eventos UPSERT del productor — `C1-007`
|
|
1707
|
+
- [ ] ReadModel fields son subconjunto de los campos de la entidad raíz fuente (por C2-010, los lifecycle events no pueden emitir campos ajenos)
|
|
1708
|
+
- [ ] Topics de readModels se derivan automáticamente del nombre del evento (strip `Event` → SCREAMING_SNAKE_CASE)
|
|
1496
1709
|
|
|
1497
1710
|
---
|
|
1498
1711
|
|
|
1499
|
-
**Última actualización:** 2026-03-
|
|
1500
|
-
**Versión de eva4j:** 1.0.
|
|
1712
|
+
**Última actualización:** 2026-03-24
|
|
1713
|
+
**Versión de eva4j:** 1.0.15
|
|
1501
1714
|
**Estado:** Documento de referencia para agentes IA
|
package/DOMAIN_YAML_GUIDE.md
CHANGED
|
@@ -139,6 +139,9 @@ aggregates:
|
|
|
139
139
|
# Eventos de dominio que emite este agregado (dentro del agregado)
|
|
140
140
|
- name: NombreEventoOcurrido
|
|
141
141
|
fields: []
|
|
142
|
+
# Opciones mutuamente excluyentes para emisión automática:
|
|
143
|
+
# triggers: [methodName] # Emite raise() dentro de un método de transición
|
|
144
|
+
# lifecycle: create # Emite raise() en operación CRUD (create|update|delete|softDelete)
|
|
142
145
|
|
|
143
146
|
# listeners: — eventos externos que CONSUME este módulo (nivel raíz)
|
|
144
147
|
listeners:
|
|
@@ -147,6 +150,18 @@ listeners:
|
|
|
147
150
|
topic: TOPIC_NAME # Topic Kafka (obligatorio en módulos standalone)
|
|
148
151
|
useCase: HandleExternalEvent # Caso de uso que maneja el evento
|
|
149
152
|
fields: [] # Payload del Integration Event recibido
|
|
153
|
+
|
|
154
|
+
# readModels: — proyecciones locales de datos de otro módulo (nivel raíz)
|
|
155
|
+
readModels:
|
|
156
|
+
- name: ProductReadModel # PascalCase + sufijo ReadModel
|
|
157
|
+
source:
|
|
158
|
+
module: products # Módulo fuente
|
|
159
|
+
aggregate: Product # Agregado fuente
|
|
160
|
+
tableName: rm_products # Tabla SQL (prefijo rm_)
|
|
161
|
+
fields: [] # Campos proyectados
|
|
162
|
+
syncedBy: # Eventos que sincronizan la tabla
|
|
163
|
+
- event: ProductCreatedEvent
|
|
164
|
+
action: UPSERT # UPSERT | DELETE | SOFT_DELETE
|
|
150
165
|
```
|
|
151
166
|
|
|
152
167
|
### Ubicación del archivo
|
|
@@ -1711,13 +1726,62 @@ aggregates:
|
|
|
1711
1726
|
|-----------|------|-------------|-------------|
|
|
1712
1727
|
| `name` | String | ✅ | Nombre de la clase del evento (PascalCase) |
|
|
1713
1728
|
| `fields` | Array | ✅ | Campos que transporta el evento |
|
|
1714
|
-
| `triggers` | Array\<String\> | ➖ | Nombres de métodos de transición que publican este evento. El generador emite `raise(new XEvent(...))` automáticamente. |
|
|
1729
|
+
| `triggers` | Array\<String\> | ➖ | Nombres de métodos de transición que publican este evento. El generador emite `raise(new XEvent(...))` automáticamente. Mutuamente excluyente con `lifecycle`. |
|
|
1730
|
+
| `lifecycle` | String | ➖ | Operación CRUD que publica este evento: `create`, `update`, `delete`, `softDelete`. Mutuamente excluyente con `triggers`. |
|
|
1715
1731
|
| `topic` | String | ➖ | **Override del topic Kafka.** Por defecto el generador quita el sufijo `Event` del nombre de la clase: `ProductPublishedEvent` → `PRODUCT_PUBLISHED`. Úsalo cuando el consumer en `listeners[]` de otro módulo declara un topic diferente al auto-derivado. |
|
|
1716
1732
|
|
|
1717
1733
|
> **Regla de derivación de topic:** `ProductPublishedEvent` → quita `Event` → `ProductPublished` → SCREAMING_SNAKE → `PRODUCT_PUBLISHED`. Esto garantiza que el producer y el consumer coincidan cuando el consumer declara `topic: PRODUCT_PUBLISHED` en `listeners[]`.
|
|
1718
1734
|
|
|
1719
1735
|
> **`kafka: true`** ya no es necesario — si el proyecto tiene `kafka-client` instalado, todos los eventos se cablearán automáticamente al ejecutar `eva g entities`.
|
|
1720
1736
|
|
|
1737
|
+
### Ejemplo con `lifecycle:`
|
|
1738
|
+
|
|
1739
|
+
```yaml
|
|
1740
|
+
events:
|
|
1741
|
+
- name: ProductCreatedEvent
|
|
1742
|
+
lifecycle: create # raise() en el constructor de creación
|
|
1743
|
+
fields:
|
|
1744
|
+
- name: productId
|
|
1745
|
+
type: String
|
|
1746
|
+
- name: name
|
|
1747
|
+
type: String
|
|
1748
|
+
|
|
1749
|
+
- name: ProductUpdatedEvent
|
|
1750
|
+
lifecycle: update # raise() en UpdateCommandHandler antes de save()
|
|
1751
|
+
fields:
|
|
1752
|
+
- name: productId
|
|
1753
|
+
type: String
|
|
1754
|
+
- name: name
|
|
1755
|
+
type: String
|
|
1756
|
+
|
|
1757
|
+
- name: ProductDeletedEvent
|
|
1758
|
+
lifecycle: delete # raise() en DeleteCommandHandler; requiere !hasSoftDelete
|
|
1759
|
+
fields:
|
|
1760
|
+
- name: productId
|
|
1761
|
+
type: String
|
|
1762
|
+
- name: deletedAt
|
|
1763
|
+
type: LocalDateTime
|
|
1764
|
+
|
|
1765
|
+
- name: ProductDeactivatedEvent
|
|
1766
|
+
lifecycle: softDelete # raise() en softDelete(); requiere hasSoftDelete: true
|
|
1767
|
+
fields:
|
|
1768
|
+
- name: productId
|
|
1769
|
+
type: String
|
|
1770
|
+
- name: deletedAt
|
|
1771
|
+
type: LocalDateTime
|
|
1772
|
+
```
|
|
1773
|
+
|
|
1774
|
+
**Puntos de emisión:**
|
|
1775
|
+
|
|
1776
|
+
| `lifecycle` | Punto de emisión | UUID auto-gen | `raise()` visibility |
|
|
1777
|
+
|---|---|---|---|
|
|
1778
|
+
| `create` | Constructor de creación | ✅ `this.id = UUID.randomUUID().toString()` | `protected` |
|
|
1779
|
+
| `update` | `UpdateCommandHandler`, antes de `repository.save()` | — | `public` |
|
|
1780
|
+
| `delete` | `DeleteCommandHandler`, antes de `repository.delete()` | — | `public` |
|
|
1781
|
+
| `softDelete` | Método `softDelete()` de la entidad | — | `protected` |
|
|
1782
|
+
|
|
1783
|
+
> **Validaciones:** `C2-008` (error) si el valor de lifecycle no es válido. `C2-009` (warning) si `lifecycle: softDelete` sin `hasSoftDelete: true`, o `lifecycle: delete` con `hasSoftDelete: true`. `C2-010` (error) si un campo del lifecycle event no existe en la entidad raíz del agregado.
|
|
1784
|
+
|
|
1721
1785
|
### Archivos generados
|
|
1722
1786
|
|
|
1723
1787
|
Para cada evento, eva4j genera dos archivos:
|
|
@@ -1946,7 +2010,9 @@ domain.yaml
|
|
|
1946
2010
|
│
|
|
1947
2011
|
├── listeners: → Integration Events que CONSUME (infrastructure/kafkaListener/)
|
|
1948
2012
|
│
|
|
1949
|
-
|
|
2013
|
+
├── ports: → Servicios HTTP que LLAMA (infrastructure/adapters/{service}/)
|
|
2014
|
+
│
|
|
2015
|
+
└── readModels: → Proyecciones locales mantenidas por eventos (infrastructure/kafkaListener/)
|
|
1950
2016
|
```
|
|
1951
2017
|
|
|
1952
2018
|
---
|
|
@@ -2082,6 +2148,123 @@ Ver ejemplo completo en [`examples/domain-ports.yaml`](examples/domain-ports.yam
|
|
|
2082
2148
|
|
|
2083
2149
|
---
|
|
2084
2150
|
|
|
2151
|
+
## Sección readModels
|
|
2152
|
+
|
|
2153
|
+
`readModels:` es una sección de nivel raíz (sibling de `aggregates:`, `listeners:` y `ports:`) que declara **proyecciones locales de datos de otro bounded context**, mantenidas mediante eventos de dominio. Elimina dependencias síncronas (HTTP) entre módulos.
|
|
2154
|
+
|
|
2155
|
+
### Estructura completa
|
|
2156
|
+
|
|
2157
|
+
```yaml
|
|
2158
|
+
readModels:
|
|
2159
|
+
- name: ProductReadModel # PascalCase + sufijo "ReadModel" (OBLIGATORIO)
|
|
2160
|
+
source: # Trazabilidad al módulo fuente (OBLIGATORIO)
|
|
2161
|
+
module: products # Módulo fuente (kebab-case)
|
|
2162
|
+
aggregate: Product # Agregado fuente (PascalCase)
|
|
2163
|
+
tableName: rm_products # Tabla en BD (OBLIGATORIO, prefijo rm_)
|
|
2164
|
+
fields: # Campos proyectados (OBLIGATORIO, debe incluir id)
|
|
2165
|
+
- name: id
|
|
2166
|
+
type: String
|
|
2167
|
+
- name: name
|
|
2168
|
+
type: String
|
|
2169
|
+
- name: price
|
|
2170
|
+
type: BigDecimal
|
|
2171
|
+
- name: status
|
|
2172
|
+
type: String
|
|
2173
|
+
syncedBy: # Eventos que mantienen la tabla (OBLIGATORIO, min 1)
|
|
2174
|
+
- event: ProductCreatedEvent # Nombre del evento (PascalCase)
|
|
2175
|
+
action: UPSERT # UPSERT | DELETE | SOFT_DELETE
|
|
2176
|
+
- event: ProductUpdatedEvent
|
|
2177
|
+
action: UPSERT
|
|
2178
|
+
- event: ProductDeactivatedEvent
|
|
2179
|
+
action: SOFT_DELETE
|
|
2180
|
+
```
|
|
2181
|
+
|
|
2182
|
+
### Propiedades
|
|
2183
|
+
|
|
2184
|
+
| Propiedad | Tipo | Obligatorio | Descripción |
|
|
2185
|
+
|---|---|---|---|
|
|
2186
|
+
| `name` | String | SÍ | Nombre del read model. **DEBE** terminar con `ReadModel`. PascalCase. |
|
|
2187
|
+
| `source` | Object | SÍ | Trazabilidad al módulo y agregado fuente. |
|
|
2188
|
+
| `source.module` | String | SÍ | Módulo que posee los datos. Kebab-case. |
|
|
2189
|
+
| `source.aggregate` | String | SÍ | Agregado fuente. PascalCase. |
|
|
2190
|
+
| `tableName` | String | SÍ | Nombre de tabla SQL. **DEBE** empezar con `rm_`. |
|
|
2191
|
+
| `fields` | Array | SÍ | Campos a proyectar. Debe incluir `id`. |
|
|
2192
|
+
| `syncedBy` | Array | SÍ | Eventos que sincronizan esta tabla. Mínimo 1. |
|
|
2193
|
+
| `syncedBy[].event` | String | SÍ | Nombre del evento. PascalCase. |
|
|
2194
|
+
| `syncedBy[].action` | String | SÍ | Uno de: `UPSERT`, `DELETE`, `SOFT_DELETE`. |
|
|
2195
|
+
| `syncedBy[].topic` | String | NO | Override del topic Kafka. Por defecto se auto-deriva del nombre del evento. |
|
|
2196
|
+
|
|
2197
|
+
### Acciones de sincronización
|
|
2198
|
+
|
|
2199
|
+
| Acción | Significado | Cuándo usarla |
|
|
2200
|
+
|---|---|---|
|
|
2201
|
+
| `UPSERT` | Insertar si no existe, actualizar si existe | Creaciones, actualizaciones, cambios de estado |
|
|
2202
|
+
| `DELETE` | Eliminar permanentemente | Hard deletes en el módulo fuente |
|
|
2203
|
+
| `SOFT_DELETE` | Marcar como inactivo con timestamp | Cuando el fuente usa soft delete |
|
|
2204
|
+
|
|
2205
|
+
### Derivación automática de topics
|
|
2206
|
+
|
|
2207
|
+
El topic se deriva del nombre del evento: se elimina el sufijo `Event` y se convierte a SCREAMING_SNAKE_CASE:
|
|
2208
|
+
|
|
2209
|
+
- `ProductCreatedEvent` → `PRODUCT_CREATED`
|
|
2210
|
+
- `CustomerUpdatedEvent` → `CUSTOMER_UPDATED`
|
|
2211
|
+
- `OrderCancelled` → `ORDER_CANCELLED`
|
|
2212
|
+
|
|
2213
|
+
Para sobrescribir, usar `topic:` explícito en la entrada de `syncedBy`.
|
|
2214
|
+
|
|
2215
|
+
### Artefactos generados
|
|
2216
|
+
|
|
2217
|
+
**Por cada read model (6 archivos):**
|
|
2218
|
+
|
|
2219
|
+
| Archivo | Descripción |
|
|
2220
|
+
|---|---|
|
|
2221
|
+
| `domain/models/readmodels/{Name}.java` | Clase de dominio inmutable (sin setters, sin auditoría) |
|
|
2222
|
+
| `infrastructure/database/entities/{Name}Jpa.java` | Entidad JPA con Lombok. `@Id` no auto-generado |
|
|
2223
|
+
| `infrastructure/database/repositories/{Name}JpaRepository.java` | Spring Data JPA interface |
|
|
2224
|
+
| `domain/repositories/{Name}Repository.java` | Interfaz de repositorio (puerto) |
|
|
2225
|
+
| `infrastructure/database/repositories/{Name}RepositoryImpl.java` | Implementación del repositorio |
|
|
2226
|
+
| `application/usecases/Sync{Source}ReadModelHandler.java` | Handler con un método por evento |
|
|
2227
|
+
|
|
2228
|
+
**Por cada entrada en `syncedBy` (hasta 3 archivos):**
|
|
2229
|
+
|
|
2230
|
+
| Archivo | Descripción |
|
|
2231
|
+
|---|---|
|
|
2232
|
+
| `application/events/{EventBase}IntegrationEvent.java` | Integration Event (reutilizado si ya existe) |
|
|
2233
|
+
| `infrastructure/kafkaListener/{EventBase}ReadModelListener.java` | Kafka listener que delega al sync handler |
|
|
2234
|
+
| `parameters/*/kafka.yaml` | Registro del topic |
|
|
2235
|
+
|
|
2236
|
+
### Validaciones
|
|
2237
|
+
|
|
2238
|
+
| Código | Severidad | Regla |
|
|
2239
|
+
|---|---|---|
|
|
2240
|
+
| RM-001 | ERROR | `name` debe terminar con `ReadModel` |
|
|
2241
|
+
| RM-002 | ERROR | `tableName` debe empezar con `rm_` |
|
|
2242
|
+
| RM-004 | ERROR | `fields` debe incluir un campo `id` |
|
|
2243
|
+
| RM-005 | ERROR | `syncedBy` debe tener al menos una entrada |
|
|
2244
|
+
| RM-006 | ERROR | `syncedBy[].action` debe ser `UPSERT`, `DELETE` o `SOFT_DELETE` |
|
|
2245
|
+
| RM-008 / C1-007 | WARNING | Campo del readModel no cubierto por ningún evento UPSERT del productor — siempre será null |
|
|
2246
|
+
| RM-009 | WARNING | `ports:` tiene llamadas sync al mismo `source.module` — considerar eliminarlas |
|
|
2247
|
+
| RM-010 | ERROR | `source.module` es el mismo módulo actual |
|
|
2248
|
+
|
|
2249
|
+
### Diferencias con agregados
|
|
2250
|
+
|
|
2251
|
+
| Característica | Aggregate | Read Model |
|
|
2252
|
+
|---|---|---|
|
|
2253
|
+
| Sección YAML | `aggregates:` | `readModels:` |
|
|
2254
|
+
| Prefijo de tabla | Ninguno | `rm_` |
|
|
2255
|
+
| Lógica de negocio | Sí | No |
|
|
2256
|
+
| Emite eventos | Sí | Nunca |
|
|
2257
|
+
| Endpoints REST | Opcional | Nunca |
|
|
2258
|
+
| Clase de dominio | Entidad rica | Clase inmutable |
|
|
2259
|
+
| Campos de auditoría | Opcionales | Nunca |
|
|
2260
|
+
| `source:` requerido | No | Sí |
|
|
2261
|
+
|
|
2262
|
+
### Referencia
|
|
2263
|
+
|
|
2264
|
+
Ver ejemplo completo en [`examples/domain-read-models.yaml`](examples/domain-read-models.yaml).
|
|
2265
|
+
|
|
2266
|
+
---
|
|
2267
|
+
|
|
2085
2268
|
## Relaciones
|
|
2086
2269
|
|
|
2087
2270
|
eva4j soporta relaciones JPA bidireccionales completas con generación automática del lado inverso.
|
package/FUTURE_FEATURES.md
CHANGED
|
@@ -459,66 +459,47 @@ public ResponseEntity<ErrorDto> handleOptimisticLock(ObjectOptimisticLockingFail
|
|
|
459
459
|
|
|
460
460
|
---
|
|
461
461
|
|
|
462
|
-
## 6. Read Models Separados (Proyecciones)
|
|
462
|
+
## 6. Read Models Separados (Proyecciones) ✅
|
|
463
|
+
|
|
464
|
+
> **Implementado en v1.0.15** — Sección `readModels:` en `domain.yaml` con generación automática de proyecciones locales mantenidas por eventos Kafka. Ver [`DOMAIN_YAML_GUIDE.md`](DOMAIN_YAML_GUIDE.md#sección-readmodels) y [`read-model-spec.md`](read-model-spec.md).
|
|
463
465
|
|
|
464
466
|
### Descripción
|
|
465
467
|
|
|
466
|
-
|
|
468
|
+
Proyecciones locales de datos de otro bounded context, mantenidas mediante eventos de dominio. Elimina dependencias síncronas (HTTP) entre módulos.
|
|
467
469
|
|
|
468
|
-
### Sintaxis
|
|
470
|
+
### Sintaxis Implementada
|
|
469
471
|
|
|
470
472
|
```yaml
|
|
471
|
-
|
|
472
|
-
- name:
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
473
|
+
readModels:
|
|
474
|
+
- name: ProductReadModel
|
|
475
|
+
source:
|
|
476
|
+
module: products
|
|
477
|
+
aggregate: Product
|
|
478
|
+
tableName: rm_products
|
|
479
|
+
fields:
|
|
480
|
+
- name: id
|
|
481
|
+
type: String
|
|
482
|
+
- name: name
|
|
483
|
+
type: String
|
|
484
|
+
- name: price
|
|
485
|
+
type: BigDecimal
|
|
486
|
+
syncedBy:
|
|
487
|
+
- event: ProductCreatedEvent
|
|
488
|
+
action: UPSERT
|
|
489
|
+
- event: ProductUpdatedEvent
|
|
490
|
+
action: UPSERT
|
|
491
|
+
- event: ProductDeactivatedEvent
|
|
492
|
+
action: SOFT_DELETE
|
|
490
493
|
```
|
|
491
494
|
|
|
492
|
-
###
|
|
495
|
+
### Artefactos Generados
|
|
493
496
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
Integer getItemCount();
|
|
501
|
-
OrderStatus getStatus();
|
|
502
|
-
}
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
```java
|
|
506
|
-
@Query(value = """
|
|
507
|
-
SELECT
|
|
508
|
-
o.id,
|
|
509
|
-
o.order_number AS orderNumber,
|
|
510
|
-
c.name AS customerName,
|
|
511
|
-
o.total_amount AS totalAmount,
|
|
512
|
-
COUNT(i.id) AS itemCount,
|
|
513
|
-
o.status
|
|
514
|
-
FROM orders o
|
|
515
|
-
JOIN customers c ON c.id = o.customer_id
|
|
516
|
-
LEFT JOIN order_items i ON i.order_id = o.id
|
|
517
|
-
WHERE o.deleted = false
|
|
518
|
-
GROUP BY o.id, c.name
|
|
519
|
-
""", nativeQuery = true)
|
|
520
|
-
Page<OrderSummaryProjection> findOrderSummaries(Pageable pageable);
|
|
521
|
-
```
|
|
497
|
+
- Clase de dominio inmutable (`domain/models/readmodels/`)
|
|
498
|
+
- Entidad JPA sin auditoría (`infrastructure/database/entities/`)
|
|
499
|
+
- Repositorio (interfaz + impl)
|
|
500
|
+
- Sync handler con un método por evento
|
|
501
|
+
- Kafka listeners (uno por entry en `syncedBy`)
|
|
502
|
+
- Integration Events (reutilizados si ya existen)
|
|
522
503
|
|
|
523
504
|
---
|
|
524
505
|
|
|
@@ -1607,7 +1588,7 @@ Sin DataFaker, el seeder genera datos con patrones simples (`"User 0"`, `"user0@
|
|
|
1607
1588
|
| 3 | Soft Delete Completo | Alta | Baja | ✅ Implementado |
|
|
1608
1589
|
| 4 | Paginación en Queries | Impl. | -- | ✅ Implementado |
|
|
1609
1590
|
| 5 | Optimistic Locking | Media | Baja | Pendiente |
|
|
1610
|
-
| 6 | Read Models / Proyecciones | Media | Alta |
|
|
1591
|
+
| 6 | Read Models / Proyecciones | Media | Alta | ✅ Implementado |
|
|
1611
1592
|
| 7 | Enums con Transiciones | Impl. | -- | ✅ Implementado |
|
|
1612
1593
|
| 8 | Specifications Pattern | Media | Media | Pendiente |
|
|
1613
1594
|
| 9 | JSON Schema para domain.yaml | Tooling | Media | Pendiente |
|
|
@@ -472,7 +472,7 @@ Verifies that the producer → consumer graph is coherent at the code level: eve
|
|
|
472
472
|
| C1-002 | 🔴 error | `listeners[]` references an event that no domain module produces |
|
|
473
473
|
| C1-003 | 🔴 error | Field in `listener.fields` does not exist in the producer event |
|
|
474
474
|
| C1-004 | 🔴 error | Field exists in both producer and consumer but with incompatible types |
|
|
475
|
-
| C1-005 | 🔴 error | `system.yaml` registers a module as consumer but that module has no matching `listener` |
|
|
475
|
+
| C1-005 | 🔴 error | `system.yaml` registers a module as consumer but that module has no matching `listener` or `readModels[].syncedBy` |
|
|
476
476
|
| C1-006 | 🔴 error | `listener.producer` references the wrong producer module |
|
|
477
477
|
|
|
478
478
|
#### C1-001 — Produced event with no consumers in system.yaml
|
|
@@ -534,7 +534,9 @@ listeners:
|
|
|
534
534
|
|
|
535
535
|
**Fix:** Align the field type in the listener with the type declared in the producer event.
|
|
536
536
|
|
|
537
|
-
#### C1-005 — Registered consumer has no listener in domain.yaml
|
|
537
|
+
#### C1-005 — Registered consumer has no listener or readModel.syncedBy in domain.yaml
|
|
538
|
+
|
|
539
|
+
For `useCase:` consumers, the module must have a matching `listeners[]` entry:
|
|
538
540
|
|
|
539
541
|
```yaml
|
|
540
542
|
# system.yaml registers notifications as consumer:
|
|
@@ -548,6 +550,20 @@ consumers:
|
|
|
548
550
|
|
|
549
551
|
**Fix:** Add a `listeners[]` entry for `OrderCreatedEvent` in `notifications/domain.yaml`.
|
|
550
552
|
|
|
553
|
+
For `readModel:` consumers, the module must have a matching `readModels[].syncedBy[]` entry:
|
|
554
|
+
|
|
555
|
+
```yaml
|
|
556
|
+
# system.yaml registers orders as readModel consumer:
|
|
557
|
+
consumers:
|
|
558
|
+
- module: orders
|
|
559
|
+
readModel: ProductReadModel
|
|
560
|
+
# orders/domain.yaml must have readModels[].syncedBy[].event matching the event
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
**Message:** `[C1-005] system.yaml registra 'orders' como consumidor readModel de 'ProductCreatedEvent' pero el módulo no tiene readModels[].syncedBy con ese evento`
|
|
564
|
+
|
|
565
|
+
**Fix:** Add a `readModels[]` entry with a `syncedBy[]` entry for the event in the consumer module's `domain.yaml`.
|
|
566
|
+
|
|
551
567
|
#### C1-006 — listener.producer references wrong module
|
|
552
568
|
|
|
553
569
|
```yaml
|
|
@@ -305,6 +305,32 @@ aggregates:
|
|
|
305
305
|
- name: estimatedDeliveryDate # no resuelto → null /* TODO */
|
|
306
306
|
type: LocalDate
|
|
307
307
|
|
|
308
|
+
# ─── Eventos con lifecycle: (alterntiva a triggers:) ──────────────
|
|
309
|
+
# Cuando el evento se produce en una operación CRUD y no en una transición de estado,
|
|
310
|
+
# se usa lifecycle: en lugar de triggers:
|
|
311
|
+
# El generador emite raise() automáticamente en el punto CRUD correspondiente.
|
|
312
|
+
|
|
313
|
+
- name: OrderCreatedEvent
|
|
314
|
+
lifecycle: create # raise() en el constructor de creación
|
|
315
|
+
fields: # UUID auto-generado como id antes de raise()
|
|
316
|
+
- name: orderId
|
|
317
|
+
type: String
|
|
318
|
+
- name: customerName # → this.getCustomerName() si existe en la entidad
|
|
319
|
+
type: String
|
|
320
|
+
- name: createdAt
|
|
321
|
+
type: LocalDateTime # → LocalDateTime.now()
|
|
322
|
+
|
|
323
|
+
- name: OrderUpdatedEvent
|
|
324
|
+
lifecycle: update # raise() en el método update() de la entidad raíz
|
|
325
|
+
fields:
|
|
326
|
+
- name: orderId
|
|
327
|
+
type: String
|
|
328
|
+
- name: customerName
|
|
329
|
+
type: String
|
|
330
|
+
|
|
331
|
+
# lifecycle: delete requiere hasSoftDelete: false en la entidad raíz
|
|
332
|
+
# lifecycle: softDelete requiere hasSoftDelete: true en la entidad raíz
|
|
333
|
+
|
|
308
334
|
# ─── Eventos externos que este módulo CONSUME ────────────────────────────────
|
|
309
335
|
# Nivel raíz, sibling de aggregates:
|
|
310
336
|
# Requiere broker instalado (eva add kafka-client) para generación.
|