eva4j 1.0.16 → 1.0.18
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 +220 -5
- package/DOMAIN_YAML_GUIDE.md +188 -3
- package/FUTURE_FEATURES.md +33 -52
- package/QUICK_REFERENCE.md +8 -4
- package/bin/eva4j.js +70 -2
- package/config/defaults.json +1 -0
- package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
- package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
- package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
- package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
- package/docs/commands/EVALUATE_SYSTEM.md +290 -10
- package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
- package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
- package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
- package/docs/commands/INDEX.md +27 -3
- package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
- package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
- package/docs/prototype/system/RISKS.md +277 -0
- package/docs/prototype/system/customers.yaml +133 -0
- package/docs/prototype/system/inventory.yaml +109 -0
- package/docs/prototype/system/notifications.yaml +131 -0
- package/docs/prototype/system/orders.yaml +241 -0
- package/docs/prototype/system/payments.yaml +256 -0
- package/docs/prototype/system/products.yaml +168 -0
- package/docs/prototype/system/system.yaml +269 -0
- package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
- package/examples/domain-events.yaml +26 -0
- package/examples/domain-read-models.yaml +113 -0
- package/examples/system/customer.yaml +89 -0
- package/examples/system/orders.yaml +119 -0
- package/examples/system/product.yaml +27 -0
- package/examples/system/system.yaml +80 -0
- package/package.json +1 -1
- package/read-model-spec.md +664 -0
- package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
- package/src/agents/design-gap-analyst.agent.md +383 -0
- package/src/agents/design-reviewer-temporal.agent.md +412 -0
- package/src/agents/design-reviewer.agent.md +34 -5
- package/src/agents/implement-use-cases.prompt.md +179 -0
- package/src/agents/ux-gap-analyst.agent.md +412 -0
- package/src/commands/add-rabbitmq-client.js +261 -0
- package/src/commands/add-temporal-client.js +22 -2
- package/src/commands/build.js +267 -11
- package/src/commands/evaluate-system.js +700 -13
- package/src/commands/generate-entities.js +560 -24
- 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-rabbitmq-event.js +665 -0
- package/src/commands/generate-rabbitmq-listener.js +205 -0
- package/src/commands/generate-record.js +2 -2
- package/src/commands/generate-resource.js +4 -1
- package/src/commands/generate-temporal-activity.js +970 -33
- package/src/commands/generate-temporal-flow.js +98 -38
- package/src/commands/generate-temporal-system.js +708 -0
- package/src/commands/generate-usecase.js +4 -1
- package/src/skills/build-system-yaml/SKILL.md +343 -2
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +253 -26
- package/src/skills/build-system-yaml/references/module-spec.md +90 -9
- package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
- package/src/skills/build-temporal-system/SKILL.md +752 -0
- package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
- package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
- package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
- package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
- package/src/skills/implement-use-case/SKILL.md +350 -0
- package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
- package/src/skills/requirements-elicitation/SKILL.md +228 -0
- package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
- package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
- package/src/utils/bounded-context-diagram.js +844 -0
- package/src/utils/config-manager.js +4 -2
- package/src/utils/domain-validator.js +495 -17
- package/src/utils/naming.js +20 -0
- package/src/utils/system-validator.js +169 -11
- package/src/utils/system-yaml-parser.js +318 -0
- package/src/utils/temporal-validator.js +497 -0
- package/src/utils/validator.js +3 -1
- package/src/utils/yaml-to-entity.js +281 -9
- 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/DomainEventHandler.java.ejs +116 -22
- package/templates/aggregate/JpaAggregateRoot.java.ejs +4 -4
- package/templates/aggregate/JpaEntity.java.ejs +2 -2
- package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
- package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
- package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
- package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
- package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
- package/templates/base/root/AGENTS.md.ejs +1 -1
- package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
- package/templates/crud/EndpointsController.java.ejs +1 -1
- package/templates/crud/ScaffoldCommand.java.ejs +5 -2
- package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
- package/templates/crud/ScaffoldQuery.java.ejs +5 -2
- package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
- package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
- package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
- package/templates/evaluate/report.html.ejs +1447 -90
- package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
- package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
- package/templates/ports/PortAclMapper.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +7 -22
- package/templates/ports/PortFeignClient.java.ejs +4 -0
- package/templates/ports/PortResponseDto.java.ejs +1 -1
- package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
- package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
- package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
- package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
- package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
- package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
- 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 +13 -0
- package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
- package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
- package/templates/read-model/ReadModelRepository.java.ejs +42 -0
- package/templates/read-model/ReadModelRepositoryImpl.java.ejs +85 -0
- package/templates/read-model/ReadModelSyncHandler.java.ejs +54 -0
- package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
- package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
- package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
- package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
- package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
- package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
- package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/NestedType.java.ejs +12 -0
- package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
- package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
- package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
- package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
- package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
- package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
- package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
- package/COMMAND_EVALUATION.md +0 -911
package/AGENTS.md
CHANGED
|
@@ -616,12 +616,16 @@ 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`)
|
|
628
|
+
- **C2-012** (error): nombre del agregado no coincide con la entidad raíz — el generador usa `aggregate.name` para imports/mappers pero la clase de dominio se genera desde `entity.name`, causando `cannot find symbol`
|
|
625
629
|
|
|
626
630
|
**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
631
|
|
|
@@ -658,6 +662,114 @@ public void confirm() {
|
|
|
658
662
|
|
|
659
663
|
**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
664
|
|
|
665
|
+
#### Propiedad `lifecycle`
|
|
666
|
+
|
|
667
|
+
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.
|
|
668
|
+
|
|
669
|
+
**Valores válidos:**
|
|
670
|
+
|
|
671
|
+
| Valor | Punto de emisión | Descripción |
|
|
672
|
+
|---|---|---|
|
|
673
|
+
| `create` | Constructor de creación de la entidad | UUID auto-generado como id antes de raise() |
|
|
674
|
+
| `update` | Método `update()` de la entidad raíz | Handler llama `existing.update(...)`; `raise()` interno |
|
|
675
|
+
| `delete` | DeleteCommandHandler, antes de `repository.delete()` | Requiere `hasSoftDelete: false`; genera `repository.delete(entity)` |
|
|
676
|
+
| `softDelete` | Método `softDelete()` de la entidad | Requiere `hasSoftDelete: true`; raise() después de `this.deletedAt = ...` |
|
|
677
|
+
|
|
678
|
+
**Ejemplo en domain.yaml:**
|
|
679
|
+
|
|
680
|
+
```yaml
|
|
681
|
+
events:
|
|
682
|
+
- name: ProductCreatedEvent
|
|
683
|
+
lifecycle: create
|
|
684
|
+
fields:
|
|
685
|
+
- name: productId
|
|
686
|
+
type: String
|
|
687
|
+
- name: name
|
|
688
|
+
type: String
|
|
689
|
+
- name: price
|
|
690
|
+
type: BigDecimal
|
|
691
|
+
|
|
692
|
+
- name: ProductUpdatedEvent
|
|
693
|
+
lifecycle: update
|
|
694
|
+
fields:
|
|
695
|
+
- name: productId
|
|
696
|
+
type: String
|
|
697
|
+
- name: name
|
|
698
|
+
type: String
|
|
699
|
+
|
|
700
|
+
- name: ProductDeletedEvent
|
|
701
|
+
lifecycle: delete
|
|
702
|
+
fields:
|
|
703
|
+
- name: productId
|
|
704
|
+
type: String
|
|
705
|
+
- name: deletedAt
|
|
706
|
+
type: LocalDateTime
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
**Resultado generado para `lifecycle: create`:**
|
|
710
|
+
|
|
711
|
+
```java
|
|
712
|
+
// En Product.java — constructor de creación
|
|
713
|
+
public Product(String name, String description, BigDecimal price) {
|
|
714
|
+
this.id = java.util.UUID.randomUUID().toString();
|
|
715
|
+
this.name = name;
|
|
716
|
+
this.description = description;
|
|
717
|
+
this.price = price;
|
|
718
|
+
raise(new ProductCreatedEvent(this.getId(), this.getName(), this.getPrice()));
|
|
719
|
+
}
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
**Resultado generado para `lifecycle: update`:**
|
|
723
|
+
|
|
724
|
+
```java
|
|
725
|
+
// En Product.java — método update()
|
|
726
|
+
public void update(String name, String description, BigDecimal price) {
|
|
727
|
+
this.name = name;
|
|
728
|
+
this.description = description;
|
|
729
|
+
this.price = price;
|
|
730
|
+
raise(new ProductUpdatedEvent(this.getId(), this.getName(), this.getPrice()));
|
|
731
|
+
}
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
```java
|
|
735
|
+
// En UpdateProductCommandHandler.java
|
|
736
|
+
Product existing = repository.findById(command.id())...;
|
|
737
|
+
existing.update(
|
|
738
|
+
command.name() != null ? command.name() : existing.getName(),
|
|
739
|
+
command.description() != null ? command.description() : existing.getDescription(),
|
|
740
|
+
command.price() != null ? command.price() : existing.getPrice()
|
|
741
|
+
);
|
|
742
|
+
repository.save(existing);
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
**Resultado generado para `lifecycle: delete`:**
|
|
746
|
+
|
|
747
|
+
```java
|
|
748
|
+
// En DeleteProductCommandHandler.java
|
|
749
|
+
Product entity = repository.findById(command.id())...;
|
|
750
|
+
entity.raise(new ProductDeletedEvent(entity.getId(), LocalDateTime.now()));
|
|
751
|
+
repository.delete(entity);
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
**Resultado generado para `lifecycle: softDelete`:**
|
|
755
|
+
|
|
756
|
+
```java
|
|
757
|
+
// En Product.java — método softDelete()
|
|
758
|
+
public void softDelete() {
|
|
759
|
+
if (this.deletedAt != null) { throw new IllegalStateException("..."); }
|
|
760
|
+
this.deletedAt = java.time.LocalDateTime.now();
|
|
761
|
+
raise(new ProductDeactivatedEvent(this.getId(), LocalDateTime.now()));
|
|
762
|
+
}
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
**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).
|
|
766
|
+
|
|
767
|
+
**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.
|
|
768
|
+
|
|
769
|
+
**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.
|
|
770
|
+
|
|
771
|
+
**Restricción:** un evento puede declarar `triggers` O `lifecycle`, no ambos. `lifecycle` aplica solo a eventos de la entidad raíz del agregado.
|
|
772
|
+
|
|
661
773
|
### Consumo de Eventos Externos (`listeners[]`)
|
|
662
774
|
|
|
663
775
|
```yaml
|
|
@@ -821,8 +933,94 @@ aggregates:
|
|
|
821
933
|
└── events: → Domain Events que PRODUCE (async, broker)
|
|
822
934
|
listeners: → Integration Events que CONSUME (async, broker)
|
|
823
935
|
ports: → Servicios HTTP que LLAMA (sync, Feign)
|
|
936
|
+
readModels: → Proyecciones locales mantenidas por eventos (async, broker)
|
|
824
937
|
```
|
|
825
938
|
|
|
939
|
+
### Proyecciones Locales (`readModels[]`)
|
|
940
|
+
|
|
941
|
+
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.
|
|
942
|
+
|
|
943
|
+
```yaml
|
|
944
|
+
# Nivel raíz, sibling de aggregates:, listeners:, ports:
|
|
945
|
+
readModels:
|
|
946
|
+
- name: ProductReadModel # PascalCase + sufijo "ReadModel" (OBLIGATORIO)
|
|
947
|
+
source: # Trazabilidad al módulo fuente (OBLIGATORIO)
|
|
948
|
+
module: products # Módulo fuente (kebab-case)
|
|
949
|
+
aggregate: Product # Agregado fuente (PascalCase)
|
|
950
|
+
tableName: rm_products # Tabla en BD (OBLIGATORIO, prefijo rm_)
|
|
951
|
+
fields: # Campos proyectados — subconjunto del fuente
|
|
952
|
+
- name: id
|
|
953
|
+
type: String
|
|
954
|
+
- name: name
|
|
955
|
+
type: String
|
|
956
|
+
- name: price
|
|
957
|
+
type: BigDecimal
|
|
958
|
+
- name: status
|
|
959
|
+
type: String
|
|
960
|
+
syncedBy: # Eventos que mantienen esta tabla (min 1)
|
|
961
|
+
- event: ProductCreatedEvent # Nombre del evento (PascalCase + sufijo Event)
|
|
962
|
+
action: UPSERT # Acción: UPSERT | DELETE | SOFT_DELETE
|
|
963
|
+
- event: ProductUpdatedEvent
|
|
964
|
+
action: UPSERT
|
|
965
|
+
- event: ProductDeactivatedEvent
|
|
966
|
+
action: SOFT_DELETE
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
### Artefactos generados por `readModels[]`
|
|
970
|
+
|
|
971
|
+
Por cada read model:
|
|
972
|
+
|
|
973
|
+
| Archivo generado | Descripción |
|
|
974
|
+
|---|---|
|
|
975
|
+
| `domain/models/readmodels/{Name}.java` | Clase de dominio (inmutable, sin setters, sin auditoría) |
|
|
976
|
+
| `infrastructure/database/entities/{Name}Jpa.java` | Entidad JPA (Lombok, `@Id` NO auto-generado) |
|
|
977
|
+
| `infrastructure/database/repositories/{Name}JpaRepository.java` | Spring Data JPA interface |
|
|
978
|
+
| `domain/repositories/{Name}Repository.java` | Interfaz de repositorio (puerto) |
|
|
979
|
+
| `infrastructure/database/repositories/{Name}RepositoryImpl.java` | Implementación del repositorio |
|
|
980
|
+
| `application/usecases/Sync{Source}ReadModelHandler.java` | Handler de sincronización (un método por evento) |
|
|
981
|
+
|
|
982
|
+
Por cada entrada en `syncedBy`:
|
|
983
|
+
|
|
984
|
+
| Archivo generado | Descripción |
|
|
985
|
+
|---|---|
|
|
986
|
+
| `application/events/{EventBase}IntegrationEvent.java` | Integration Event (reutilizado si ya existe) |
|
|
987
|
+
| `infrastructure/kafkaListener/{EventBase}ReadModelListener.java` | Kafka listener que delega al sync handler |
|
|
988
|
+
| `parameters/*/kafka.yaml` | Registro del topic (actualizado) |
|
|
989
|
+
|
|
990
|
+
### Acciones de sincronización
|
|
991
|
+
|
|
992
|
+
| Acción | Significado | Uso |
|
|
993
|
+
|---|---|---|
|
|
994
|
+
| `UPSERT` | Insertar si es nuevo, actualizar si existe | Creaciones, actualizaciones, cambios de estado |
|
|
995
|
+
| `DELETE` | Eliminar el registro permanentemente | Hard deletes en el módulo fuente |
|
|
996
|
+
| `SOFT_DELETE` | Marcar como inactivo con timestamp | Cuando el fuente usa soft delete |
|
|
997
|
+
|
|
998
|
+
> **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.
|
|
999
|
+
|
|
1000
|
+
### Reglas de `readModels[]`
|
|
1001
|
+
|
|
1002
|
+
- **`name:`** — PascalCase, **DEBE** terminar con `ReadModel`
|
|
1003
|
+
- **`tableName:`** — **DEBE** empezar con `rm_` (identificación visual en BD)
|
|
1004
|
+
- **`fields:`** — **DEBE** incluir un campo `id`
|
|
1005
|
+
- **`syncedBy:`** — **DEBE** tener al menos una entrada
|
|
1006
|
+
- **`source.module:`** — **NO PUEDE** ser el mismo módulo actual (readModels son para proyecciones cross-module)
|
|
1007
|
+
- **Topic derivado automáticamente** — Se deriva del nombre del evento: strip sufijo `Event` → SCREAMING_SNAKE_CASE. Override opcional con `topic:` explícito
|
|
1008
|
+
- **Sin auditoría** — Los readModels **nunca** tienen campos de auditoría
|
|
1009
|
+
- **Sin endpoints REST** — Los readModels **nunca** exponen endpoints REST
|
|
1010
|
+
- **Sin lógica de negocio** — La clase de dominio es inmutable (solo getters)
|
|
1011
|
+
|
|
1012
|
+
### Validaciones
|
|
1013
|
+
|
|
1014
|
+
| Código | Severidad | Regla |
|
|
1015
|
+
|---|---|---|
|
|
1016
|
+
| RM-001 | ERROR | `name` debe terminar con `ReadModel` |
|
|
1017
|
+
| RM-002 | ERROR | `tableName` debe empezar con `rm_` |
|
|
1018
|
+
| RM-004 | ERROR | `fields` debe incluir un campo `id` |
|
|
1019
|
+
| RM-005 | ERROR | `syncedBy` debe tener al menos una entrada |
|
|
1020
|
+
| RM-006 | ERROR | `syncedBy[].action` debe ser `UPSERT`, `DELETE` o `SOFT_DELETE` |
|
|
1021
|
+
| RM-009 | WARNING | `ports:` todavía tiene llamadas sync al mismo `source.module` — considerar removerlas |
|
|
1022
|
+
| RM-010 | ERROR | `source.module` es el mismo módulo actual |
|
|
1023
|
+
|
|
826
1024
|
Cuando una entidad tiene `hasSoftDelete: true`, eva4j genera eliminación lógica en lugar de física.
|
|
827
1025
|
|
|
828
1026
|
### Configuración en domain.yaml
|
|
@@ -1246,6 +1444,7 @@ private String customerId;
|
|
|
1246
1444
|
5. **SABER** que cualquier otro nombre genera un **scaffold** con `UnsupportedOperationException` — el desarrollador debe implementar el handler
|
|
1247
1445
|
6. **APLICAR** la regla anti-duplicado: si el mismo useCase aparece en v1 y v2, se genera solo una vez
|
|
1248
1446
|
7. **NOMBRAR** los controladores según la convención: `{Aggregate}{VersionCapitalized}Controller` (ej: `OrderV1Controller`)
|
|
1447
|
+
8. **SI** el módulo tiene **2+ agregados** → usar `basePath: ""` (string vacío, **NO** `basePath: /`) y paths absolutos por operación (ej: `/products`, `/categories/{id}`). Si tiene un solo agregado → `basePath: /recurso` con paths relativos
|
|
1249
1448
|
|
|
1250
1449
|
### Al Generar Código de Dominio
|
|
1251
1450
|
|
|
@@ -1478,7 +1677,13 @@ Al generar o modificar código, verificar:
|
|
|
1478
1677
|
- [ ] Value Object con comportamiento → declarar `methods` en lugar de lógica en entidad
|
|
1479
1678
|
- [ ] Evento de dominio → declarar en `events[]`, publicar con `raise()` en método de negocio
|
|
1480
1679
|
- [ ] Evento con `triggers: [methodName]` → el generador emite `raise()` automáticamente; args no resolubles quedan como `null /* TODO */`
|
|
1481
|
-
- [ ]
|
|
1680
|
+
- [ ] Evento con `lifecycle: create` → el generador emite UUID auto-generado + `raise()` en el constructor de creación
|
|
1681
|
+
- [ ] 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`
|
|
1682
|
+
- [ ] Evento con `lifecycle: delete` → el generador emite `raise()` en DeleteCommandHandler + genera `repository.delete(entity)` con publicación de eventos; `raise()` se genera como `public`
|
|
1683
|
+
- [ ] Evento con `lifecycle: softDelete` → el generador emite `raise()` dentro del método `softDelete()` de la entidad; requiere `hasSoftDelete: true`
|
|
1684
|
+
- [ ] Un evento puede declarar `triggers` O `lifecycle`, no ambos
|
|
1685
|
+
- [ ] Campos de lifecycle events son campos de la entidad raíz (excluyendo `{entityName}Id` y `*At` temporal) — `C2-010`
|
|
1686
|
+
- [ ] Sin `triggers` ni `lifecycle` en el evento → el dev llama a `raise()` manualmente
|
|
1482
1687
|
- [ ] Evento con broker → **no** usar `kafka: true`; si `eva add kafka-client` está instalado, `eva g entities` auto-cablea todos los eventos
|
|
1483
1688
|
- [ ] 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
1689
|
- [ ] Consumo de eventos externos → declarar en `listeners[]` (nivel raíz); `topic:` obligatorio en módulos standalone
|
|
@@ -1493,9 +1698,19 @@ Al generar o modificar código, verificar:
|
|
|
1493
1698
|
- [ ] Métodos con cuerpo (POST/PUT/PATCH) → incluir `body:`; campos de tipo objeto en `nestedTypes:`
|
|
1494
1699
|
- [ ] Tipo de dominio auto-derivado del nombre del método — usar `domainType:` para sobrescribir si es necesario
|
|
1495
1700
|
- [ ] Cada `service:` en `ports[]` genera: interfaz (devuelve modelos de dominio), FeignClient (devuelve DTOs infra), FeignAdapter (mapea ACL), FeignConfig + `urls.yaml`
|
|
1701
|
+
- [ ] Read models → declarar en `readModels[]` (nivel raíz); `name` debe terminar con `ReadModel`, `tableName` debe empezar con `rm_`
|
|
1702
|
+
- [ ] Read models nunca tienen auditoría, endpoints REST, ni lógica de negocio
|
|
1703
|
+
- [ ] Cada read model genera: clase de dominio inmutable, JPA entity (sin audit), repositorio (interface + impl), sync handler
|
|
1704
|
+
- [ ] Cada `syncedBy` entry genera: IntegrationEvent (reutilizado si ya existe), KafkaListener, registro de topic
|
|
1705
|
+
- [ ] Para `DELETE`/`SOFT_DELETE`, el listener bypasses IntegrationEvent — extrae `id` directamente y pasa `String id` al SyncHandler
|
|
1706
|
+
- [ ] `SOFT_DELETE` → `repository.softDeleteById(id)`; `DELETE` → `repository.deleteById(id)`; `UPSERT` → reconstruye modelo completo con IntegrationEvent + `repository.upsert()`
|
|
1707
|
+
- [ ] `source.module` nunca puede ser el mismo módulo (RM-010) — readModels son exclusivamente cross-module
|
|
1708
|
+
- [ ] ReadModel fields cubiertos por eventos UPSERT del productor — `C1-007`
|
|
1709
|
+
- [ ] ReadModel fields son subconjunto de los campos de la entidad raíz fuente (por C2-010, los lifecycle events no pueden emitir campos ajenos)
|
|
1710
|
+
- [ ] Topics de readModels se derivan automáticamente del nombre del evento (strip `Event` → SCREAMING_SNAKE_CASE)
|
|
1496
1711
|
|
|
1497
1712
|
---
|
|
1498
1713
|
|
|
1499
|
-
**Última actualización:** 2026-03-
|
|
1500
|
-
**Versión de eva4j:** 1.0.
|
|
1714
|
+
**Última actualización:** 2026-03-24
|
|
1715
|
+
**Versión de eva4j:** 1.0.15
|
|
1501
1716
|
**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.
|
|
@@ -2154,7 +2337,7 @@ public void removeOrderItem(OrderItem orderItem) {
|
|
|
2154
2337
|
|
|
2155
2338
|
**Genera en JPA:**
|
|
2156
2339
|
```java
|
|
2157
|
-
@OneToMany(mappedBy = "order", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}, fetch = FetchType.LAZY)
|
|
2340
|
+
@OneToMany(mappedBy = "order", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}, orphanRemoval = true, fetch = FetchType.LAZY)
|
|
2158
2341
|
@Builder.Default
|
|
2159
2342
|
private List<OrderItemJpa> orderItems = new ArrayList<>();
|
|
2160
2343
|
```
|
|
@@ -2382,6 +2565,8 @@ relationships:
|
|
|
2382
2565
|
| `DETACH` | Al separar el padre, separa los hijos | ⚠️ Rara vez necesario |
|
|
2383
2566
|
| `ALL` | Todas las operaciones anteriores | ⚠️ Solo si estás seguro |
|
|
2384
2567
|
|
|
2568
|
+
> **Nota:** eva4j genera automáticamente `orphanRemoval = true` en todas las relaciones `@OneToMany` y `@OneToOne(mappedBy)`. Esto asegura que al remover un hijo de la colección del padre, JPA elimina la fila de la base de datos. Es el comportamiento correcto en DDD: las entidades secundarias del agregado no tienen existencia independiente — su ciclo de vida lo controla la raíz.
|
|
2569
|
+
|
|
2385
2570
|
#### **Configuraciones Recomendadas:**
|
|
2386
2571
|
|
|
2387
2572
|
```yaml
|
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 |
|
package/QUICK_REFERENCE.md
CHANGED
|
@@ -120,13 +120,17 @@ eva detach order
|
|
|
120
120
|
|
|
121
121
|
## 🧩 Temporal Queue Model
|
|
122
122
|
|
|
123
|
+
Queues are **module-scoped** — each module gets its own set of queues prefixed with the module name in SCREAMING_SNAKE_CASE:
|
|
124
|
+
|
|
123
125
|
| Queue | Purpose | ActivityOptions var |
|
|
124
126
|
|-------|---------|---------------------|
|
|
125
|
-
| `
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
127
|
+
| `{MODULE}_WORKFLOW_QUEUE` | Workflow orchestration | — |
|
|
128
|
+
| `{MODULE}_LIGHT_TASK_QUEUE` | Fast activities < 30 s | `lightActivityOptions` |
|
|
129
|
+
| `{MODULE}_HEAVY_TASK_QUEUE` | Long-running ≤ 2 min | `heavyActivityOptions` |
|
|
130
|
+
|
|
131
|
+
Example for module `order`: `ORDER_WORKFLOW_QUEUE`, `ORDER_LIGHT_TASK_QUEUE`, `ORDER_HEAVY_TASK_QUEUE`.
|
|
128
132
|
|
|
129
|
-
Activities are registered automatically via Spring DI (`LightActivity` / `HeavyActivity` marker interfaces).
|
|
133
|
+
Activities are registered automatically via Spring DI (`{Module}LightActivity` / `{Module}HeavyActivity` marker interfaces). Each module has its own `{Module}TemporalWorkerConfig.java` — no shared `TemporalConfig.java` patching needed.
|
|
130
134
|
|
|
131
135
|
---
|
|
132
136
|
|