eva4j 1.0.13 → 1.0.15
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 +314 -10
- package/COMMAND_EVALUATION.md +15 -16
- package/DOMAIN_YAML_GUIDE.md +576 -10
- package/FUTURE_FEATURES.md +1627 -1168
- package/README.md +318 -13
- package/bin/eva4j.js +34 -0
- package/config/defaults.json +1 -0
- package/design-system.md +797 -0
- package/docs/commands/EVALUATE_SYSTEM.md +994 -0
- package/docs/commands/GENERATE_ENTITIES.md +795 -6
- package/docs/commands/INDEX.md +10 -1
- package/examples/domain-endpoints-relations.yaml +353 -0
- package/examples/domain-endpoints-versioned.yaml +144 -0
- package/examples/domain-endpoints.yaml +135 -0
- package/examples/domain-events.yaml +166 -20
- package/examples/domain-listeners.yaml +212 -0
- package/examples/domain-one-to-many.yaml +1 -0
- package/examples/domain-one-to-one.yaml +1 -0
- package/examples/domain-ports.yaml +414 -0
- package/examples/domain-soft-delete.yaml +47 -44
- package/examples/system/notification.yaml +147 -0
- package/examples/system/product.yaml +185 -0
- package/examples/system/system.yaml +112 -0
- package/examples/system-report.html +971 -0
- package/examples/system.yaml +332 -0
- package/package.json +2 -1
- package/src/commands/build.js +714 -0
- package/src/commands/create.js +7 -3
- package/src/commands/detach.js +1 -0
- package/src/commands/evaluate-system.js +610 -0
- package/src/commands/generate-entities.js +1331 -49
- package/src/commands/generate-http-exchange.js +2 -0
- package/src/commands/generate-kafka-event.js +98 -11
- package/src/generators/base-generator.js +8 -1
- package/src/generators/postman-generator.js +188 -0
- package/src/generators/shared-generator.js +10 -0
- package/src/utils/config-manager.js +54 -0
- package/src/utils/context-builder.js +1 -0
- package/src/utils/domain-diagram.js +192 -0
- package/src/utils/domain-validator.js +970 -0
- package/src/utils/fake-data.js +376 -0
- package/src/utils/naming.js +3 -2
- package/src/utils/system-validator.js +434 -0
- package/src/utils/yaml-to-entity.js +302 -8
- package/templates/aggregate/AggregateMapper.java.ejs +3 -2
- package/templates/aggregate/AggregateRepository.java.ejs +8 -2
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +13 -3
- package/templates/aggregate/AggregateRoot.java.ejs +60 -2
- package/templates/aggregate/DomainEventHandler.java.ejs +27 -20
- package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
- package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
- package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
- package/templates/aggregate/JpaRepository.java.ejs +5 -0
- package/templates/base/gradle/build.gradle.ejs +3 -2
- package/templates/base/root/AGENTS.md.ejs +306 -45
- package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1663 -0
- package/templates/base/root/skill-build-system-yaml.ejs +1446 -0
- package/templates/base/root/system.yaml.ejs +97 -0
- package/templates/crud/ApplicationMapper.java.ejs +4 -0
- package/templates/crud/Controller.java.ejs +4 -4
- package/templates/crud/CreateCommand.java.ejs +4 -0
- package/templates/crud/CreateItemDto.java.ejs +4 -0
- package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
- package/templates/crud/DeleteCommandHandler.java.ejs +10 -2
- package/templates/crud/EndpointsController.java.ejs +178 -0
- package/templates/crud/FindByQuery.java.ejs +17 -0
- package/templates/crud/FindByQueryHandler.java.ejs +57 -0
- package/templates/crud/ListQuery.java.ejs +1 -1
- package/templates/crud/ListQueryHandler.java.ejs +8 -8
- package/templates/crud/ScaffoldCommand.java.ejs +12 -0
- package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
- package/templates/crud/ScaffoldQuery.java.ejs +13 -0
- package/templates/crud/ScaffoldQueryHandler.java.ejs +41 -0
- package/templates/crud/SubEntityAddCommand.java.ejs +21 -0
- package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
- package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
- package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
- package/templates/crud/TransitionCommand.java.ejs +9 -0
- package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
- package/templates/crud/UpdateCommand.java.ejs +4 -0
- package/templates/evaluate/report.html.ejs +1363 -0
- package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
- package/templates/kafka-event/Event.java.ejs +16 -0
- package/templates/kafka-listener/KafkaController.java.ejs +1 -1
- package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
- package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
- package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
- package/templates/kafka-listener/ListenerCommandHandler.java.ejs +23 -0
- package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
- package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
- package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
- package/templates/mock/MockEvent.java.ejs +10 -0
- package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
- package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
- package/templates/mock/SpringEventListener.java.ejs +61 -0
- package/templates/ports/PortDomainModel.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +67 -0
- package/templates/ports/PortFeignClient.java.ejs +45 -0
- package/templates/ports/PortFeignConfig.java.ejs +24 -0
- package/templates/ports/PortInterface.java.ejs +45 -0
- package/templates/ports/PortNestedType.java.ejs +28 -0
- package/templates/ports/PortRequestDto.java.ejs +30 -0
- package/templates/ports/PortResponseDto.java.ejs +28 -0
- package/templates/postman/Collection.json.ejs +1 -1
- package/templates/postman/UnifiedCollection.json.ejs +185 -0
- package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
package/AGENTS.md
CHANGED
|
@@ -457,9 +457,17 @@ aggregates:
|
|
|
457
457
|
fields:
|
|
458
458
|
- name: userId
|
|
459
459
|
type: String
|
|
460
|
-
#
|
|
460
|
+
# Nota: el flag kafka: true ya no es necesario.
|
|
461
|
+
# Si el proyecto tiene un broker instalado (eva add kafka-client),
|
|
462
|
+
# eva g entities cablea automáticamente todos los eventos declarados.
|
|
461
463
|
```
|
|
462
464
|
|
|
465
|
+
El `domain.yaml` también soporta una sección `endpoints:` opcional (sibling de `aggregates:`) para declarar los endpoints REST. Ver sección [⚡ Características Avanzadas](#-características-avanzadas-del-domainyaml) para detalles.
|
|
466
|
+
|
|
467
|
+
El `domain.yaml` también soporta una sección `listeners:` opcional (sibling de `aggregates:`) para declarar los eventos externos que **consume** este módulo. Ver sección [⚡ Características Avanzadas](#-características-avanzadas-del-domainyaml) para detalles.
|
|
468
|
+
|
|
469
|
+
El `domain.yaml` también soporta una sección `ports:` opcional (sibling de `aggregates:`) para declarar los servicios HTTP síncronos que **llama** este módulo. Ver sección [⚡ Características Avanzadas](#-características-avanzadas-del-domainyaml) para detalles.
|
|
470
|
+
|
|
463
471
|
---
|
|
464
472
|
|
|
465
473
|
## ⚡ Características Avanzadas del domain.yaml
|
|
@@ -522,30 +530,298 @@ Genera automáticamente **en el enum**: `VALID_TRANSITIONS`, `canTransitionTo()`
|
|
|
522
530
|
aggregates:
|
|
523
531
|
- name: Order
|
|
524
532
|
entities: [...]
|
|
533
|
+
enums:
|
|
534
|
+
- name: OrderStatus
|
|
535
|
+
transitions:
|
|
536
|
+
- from: DRAFT
|
|
537
|
+
to: PLACED
|
|
538
|
+
method: place
|
|
539
|
+
- from: PLACED
|
|
540
|
+
to: CANCELLED
|
|
541
|
+
method: cancel
|
|
525
542
|
events:
|
|
526
|
-
- name:
|
|
543
|
+
- name: OrderPlaced
|
|
544
|
+
topic: ORDER_PLACED # opcional: sobreescribe el topic auto-derivado
|
|
545
|
+
triggers:
|
|
546
|
+
- place # ← conecta la transición con este evento
|
|
527
547
|
fields:
|
|
528
548
|
- name: orderId
|
|
529
549
|
type: String
|
|
530
550
|
- name: confirmedAt
|
|
531
551
|
type: LocalDateTime
|
|
532
|
-
|
|
552
|
+
|
|
553
|
+
- name: OrderCancelled
|
|
554
|
+
triggers:
|
|
555
|
+
- cancel
|
|
556
|
+
fields:
|
|
557
|
+
- name: reason # campo no resuelto → null /* TODO: provide reason */
|
|
558
|
+
type: String
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
#### Propiedad `topic` (opcional)
|
|
562
|
+
|
|
563
|
+
Sobreescribe el nombre del topic Kafka auto-derivado para este evento.
|
|
564
|
+
|
|
565
|
+
**Regla de derivación por defecto:** el generador quita el sufijo `Event` del nombre de la clase antes de convertir a SCREAMING_SNAKE_CASE:
|
|
566
|
+
- `ProductPublishedEvent` → `PRODUCT_PUBLISHED` ✓ (no `PRODUCT_PUBLISHED_EVENT`)
|
|
567
|
+
- `OrderCancelled` → `ORDER_CANCELLED` (sin sufijo, sin cambios)
|
|
568
|
+
|
|
569
|
+
**Cuándo usar `topic:` explícito:**
|
|
570
|
+
- El producer y el consumer de otro módulo deben usar exactamente el mismo nombre.
|
|
571
|
+
- Si el consumer en `listeners[]` declara `topic: MY_CUSTOM_TOPIC`, declara el mismo valor aquí para que el match sea garantizado.
|
|
572
|
+
|
|
573
|
+
```yaml
|
|
574
|
+
events:
|
|
575
|
+
- name: ProductPublishedEvent
|
|
576
|
+
# topic auto-derivado: PRODUCT_PUBLISHED (sufijo 'Event' eliminado)
|
|
577
|
+
triggers: [publish]
|
|
578
|
+
fields: [...]
|
|
579
|
+
|
|
580
|
+
- name: OrderReadyEvent
|
|
581
|
+
topic: ORDER_READY_FOR_PICKUP # override explícito
|
|
582
|
+
triggers: [markReady]
|
|
583
|
+
fields: [...]
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
> **Nota:** el flag `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`.
|
|
587
|
+
|
|
588
|
+
#### Propiedad `triggers`
|
|
589
|
+
|
|
590
|
+
Lista de nombres de métodos de transición que publican este evento. El generador emite automáticamente `raise(new XEvent(...))` dentro de cada método listado.
|
|
591
|
+
|
|
592
|
+
**Reglas de resolución de argumentos (en orden):**
|
|
593
|
+
|
|
594
|
+
| Condición del campo del evento | Argumento generado |
|
|
595
|
+
|---|---|
|
|
596
|
+
| Siempre (primer arg, aggregateId del DomainEvent base) | `this.getId()` |
|
|
597
|
+
| Nombre = `{entityName}Id` (ej: `orderId` en `Order`) | **Ignorado** en el Domain Event class — mapeado a `event.getAggregateId()` en el Integration Event |
|
|
598
|
+
| Nombre coincide con un campo de la entidad | `this.get{Field}()` |
|
|
599
|
+
| Nombre termina en `At` + tipo `LocalDateTime` | `LocalDateTime.now()` |
|
|
600
|
+
| No resuelto | `null /* TODO: provide {fieldName} */` |
|
|
601
|
+
|
|
602
|
+
> **Convención:** Sí declarar `{entityName}Id` en `events[].fields` cuando el evento **cruza módulos via Kafka** — es necesario para que el id viaje en el payload del Integration Event. El generador lo mapea automáticamente a `event.getAggregateId()` en el handler, evitando la duplicación en el Domain Event class interno.
|
|
603
|
+
|
|
604
|
+
**Resultado generado:**
|
|
605
|
+
|
|
606
|
+
```java
|
|
607
|
+
public void place() {
|
|
608
|
+
this.status = this.status.transitionTo(OrderStatus.PLACED);
|
|
609
|
+
raise(new OrderPlaced(this.getId(), this.getId(), LocalDateTime.now()));
|
|
610
|
+
// ^—aggregateId ^—orderId ^—confirmedAt
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
public void cancel() {
|
|
614
|
+
this.status = this.status.transitionTo(OrderStatus.CANCELLED);
|
|
615
|
+
raise(new OrderCancelled(this.getId(), null /* TODO: provide reason */));
|
|
616
|
+
}
|
|
533
617
|
```
|
|
534
618
|
|
|
535
|
-
|
|
619
|
+
Si un evento **no declara `triggers`**, el desarrollador debe llamar a `raise()` manualmente dentro del método de negocio.
|
|
620
|
+
|
|
621
|
+
**Validaciones generadas:**
|
|
622
|
+
- **C2-004** (error): trigger referencia un método que no existe en ninguna transición del módulo
|
|
623
|
+
- **C2-005** (info): transición sin ningún evento asociado — considera declarar `triggers`
|
|
624
|
+
- **C2-001** se silencia automáticamente para transiciones que ya tienen `triggers`
|
|
625
|
+
|
|
626
|
+
**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
|
+
|
|
628
|
+
| Archivo generado | Descripción |
|
|
629
|
+
|---|---|
|
|
630
|
+
| `application/events/OrderConfirmedIntegrationEvent.java` | Record broker-facing (Integration Event) |
|
|
631
|
+
| `application/ports/MessageBroker.java` | Puerto broker-agnóstico (creado/actualizado) |
|
|
632
|
+
| `infrastructure/adapters/kafkaMessageBroker/…` | Adaptador Kafka (creado/actualizado) |
|
|
633
|
+
| `shared/…/kafkaConfig/KafkaConfig.java` | Bean `NewTopic` (actualizado) |
|
|
634
|
+
| `parameters/*/kafka.yaml` | Configuración de topic (actualizada) |
|
|
635
|
+
|
|
636
|
+
**Domain Event vs Integration Event:**
|
|
637
|
+
- **Domain Event** (`domain/models/events/OrderConfirmed.java`) — señal interna del bounded context. Nunca depende de infraestructura.
|
|
638
|
+
- **Integration Event** (`application/events/OrderConfirmedIntegrationEvent.java`) — proyección para el broker. Cambiar de Kafka a RabbitMQ solo requiere cambiar el adaptador `MessageBroker`; los Domain Events no se modifican nunca.
|
|
639
|
+
|
|
640
|
+
El `DomainEventHandler` mapea un Domain Event a un Integration Event:
|
|
641
|
+
```java
|
|
642
|
+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
|
643
|
+
public void onOrderConfirmed(OrderConfirmed event) {
|
|
644
|
+
messageBroker.publishOrderConfirmedIntegrationEvent(
|
|
645
|
+
new OrderConfirmedIntegrationEvent(event.getOrderId(), event.getConfirmedAt())
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
```
|
|
536
649
|
|
|
537
650
|
Publicar desde la entidad raíz usando `raise()` heredado:
|
|
538
651
|
|
|
539
652
|
```java
|
|
540
653
|
public void confirm() {
|
|
541
654
|
this.status = this.status.transitionTo(OrderStatus.CONFIRMED);
|
|
542
|
-
raise(new
|
|
655
|
+
raise(new OrderConfirmed(this.id, LocalDateTime.now()));
|
|
543
656
|
}
|
|
544
657
|
```
|
|
545
658
|
|
|
659
|
+
**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
|
+
|
|
661
|
+
### Consumo de Eventos Externos (`listeners[]`)
|
|
662
|
+
|
|
663
|
+
```yaml
|
|
664
|
+
# Nivel raíz, sibling de aggregates:
|
|
665
|
+
listeners:
|
|
666
|
+
- event: PaymentApprovedEvent # PascalCase + sufijo Event
|
|
667
|
+
producer: payments # Módulo que lo produce (referencia documental)
|
|
668
|
+
topic: PAYMENT_APPROVED # Topic Kafka — obligatorio en módulos standalone
|
|
669
|
+
useCase: ConfirmOrder # Caso de uso que maneja el evento (PascalCase)
|
|
670
|
+
fields: # Campos del payload recibido
|
|
671
|
+
- name: orderId
|
|
672
|
+
type: String
|
|
673
|
+
- name: approvedAt
|
|
674
|
+
type: LocalDateTime
|
|
675
|
+
- name: details # Tipo complejo → declarar en nestedTypes
|
|
676
|
+
type: PaymentDetails
|
|
677
|
+
nestedTypes: # Records auxiliares para campos de tipo objeto
|
|
678
|
+
- name: paymentDetails # camelCase → normalizado a PaymentDetails
|
|
679
|
+
fields:
|
|
680
|
+
- name: paymentId
|
|
681
|
+
type: String
|
|
682
|
+
- name: method
|
|
683
|
+
type: String
|
|
684
|
+
- name: amount
|
|
685
|
+
type: BigDecimal
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
Genera por cada entrada (hasta **6 artefactos**):
|
|
689
|
+
|
|
690
|
+
| Archivo generado | Descripción |
|
|
691
|
+
|---|---|
|
|
692
|
+
| `application/events/PaymentDetails.java` | Record auxiliar (uno por `nestedTypes` entry) |
|
|
693
|
+
| `application/events/PaymentApprovedIntegrationEvent.java` | Record tipado con los `fields` declarados |
|
|
694
|
+
| `infrastructure/kafkaListener/PaymentApprovedKafkaListener.java` | `@KafkaListener` → deserializa y despacha al `useCase` |
|
|
695
|
+
| `parameters/*/kafka.yaml` | Registro del topic en `topics:` |
|
|
696
|
+
| `application/commands/ConfirmOrderCommand.java` | Command tipado para el `useCase` |
|
|
697
|
+
| `application/usecases/ConfirmOrderCommandHandler.java` | Handler stub — implementar la lógica de negocio aquí |
|
|
698
|
+
|
|
699
|
+
**Deserialización:** el listener usa `EventEnvelope<Map<String,Object>>` + `objectMapper.convertValue()` para deserializar cada campo del payload de forma robusta y tipada.
|
|
700
|
+
|
|
701
|
+
**Regla de `topic:`:**
|
|
702
|
+
- Módulo standalone (sin `system.yaml`) → `topic:` **obligatorio**
|
|
703
|
+
- Proyecto con `system.yaml` → puede omitirse; el generador lo infiere de `integrations.async[].topic`
|
|
704
|
+
- Declarado explícitamente → tiene **precedencia** sobre la inferencia
|
|
705
|
+
|
|
706
|
+
**`nestedTypes:` — cuándo usarlo:**
|
|
707
|
+
Declara un `nestedType` cuando un campo del payload es un **objeto anidado** (no un escalar). El generador produce un record en el mismo paquete `application/events/`, que tanto la `IntegrationEvent` como el `Command` y el `KafkaListener` usan directamente.
|
|
708
|
+
|
|
709
|
+
**Colisión de nombres entre módulos:** cuando varios módulos consumen el mismo evento Kafka, el generador produce clases listener con el mismo nombre (ej: `PaymentApprovedKafkaListener` en `orders` y en `notifications`). Esto es seguro porque el generador usa `@Component("<moduleName>.<listenerClassName>")` para calificar el bean y evitar `ConflictingBeanDefinitionException`. **No se requiere acción del agente** — a diferencia de `ports[]`, donde el nombre de `service:` debe ser único por módulo.
|
|
710
|
+
|
|
711
|
+
**Contraste eventos producidos vs. consumidos:**
|
|
712
|
+
```
|
|
713
|
+
aggregates:
|
|
714
|
+
└── events: → Domain Events que PRODUCE (domain/models/events/)
|
|
715
|
+
|
|
716
|
+
listeners: → Integration Events que CONSUME (infrastructure/kafkaListener/)
|
|
717
|
+
```
|
|
718
|
+
|
|
546
719
|
---
|
|
547
720
|
|
|
548
|
-
|
|
721
|
+
### Clientes HTTP Síncronos (`ports[]`)
|
|
722
|
+
|
|
723
|
+
```yaml
|
|
724
|
+
# Nivel raíz, sibling de aggregates: y listeners:
|
|
725
|
+
# Un método = una entrada; entries del mismo service: forman un solo FeignClient.
|
|
726
|
+
|
|
727
|
+
ports:
|
|
728
|
+
- name: findScreeningById # nombre del método (camelCase)
|
|
729
|
+
service: ScreeningService # agrupa en una interfaz/FeignClient (PascalCase)
|
|
730
|
+
target: screenings # módulo destino (referencia documental)
|
|
731
|
+
baseUrl: http://localhost:8081 # → parameters/*/urls.yaml (primera entrada del service)
|
|
732
|
+
http: GET /screenings/{id} # verbo + path (igual que en system.yaml exposes:)
|
|
733
|
+
fields: # campos de respuesta → {MethodPascal}ResponseDto.java
|
|
734
|
+
- name: id
|
|
735
|
+
type: String
|
|
736
|
+
- name: startTime
|
|
737
|
+
type: LocalDateTime
|
|
738
|
+
|
|
739
|
+
- name: findAvailableSeats
|
|
740
|
+
service: ScreeningService # mismo service → mismo FeignClient
|
|
741
|
+
target: screenings
|
|
742
|
+
http: GET /screenings/{id}/seats
|
|
743
|
+
returnList: true # → List<FindAvailableSeatResponseDto>
|
|
744
|
+
fields:
|
|
745
|
+
- name: seatId
|
|
746
|
+
type: String
|
|
747
|
+
- name: seatType
|
|
748
|
+
type: String
|
|
749
|
+
|
|
750
|
+
- name: processPayment
|
|
751
|
+
service: PaymentGateway
|
|
752
|
+
target: payment-gateway-external
|
|
753
|
+
baseUrl: https://api.payments.example.com
|
|
754
|
+
http: POST /payments
|
|
755
|
+
body: # @RequestBody → ProcessPaymentRequestDto.java
|
|
756
|
+
- name: amount
|
|
757
|
+
type: BigDecimal
|
|
758
|
+
- name: paymentMethod
|
|
759
|
+
type: PaymentMethodInput # tipo objeto → declarar en nestedTypes:
|
|
760
|
+
nestedTypes:
|
|
761
|
+
- name: paymentMethodInput
|
|
762
|
+
fields:
|
|
763
|
+
- name: type
|
|
764
|
+
type: String
|
|
765
|
+
- name: cardToken
|
|
766
|
+
type: String
|
|
767
|
+
fields: # respuesta → ProcessPaymentResponseDto.java
|
|
768
|
+
- name: paymentId
|
|
769
|
+
type: String
|
|
770
|
+
- name: status
|
|
771
|
+
type: String
|
|
772
|
+
|
|
773
|
+
- name: cancelPayment
|
|
774
|
+
service: PaymentGateway
|
|
775
|
+
target: payment-gateway-external
|
|
776
|
+
http: DELETE /payments/{id}
|
|
777
|
+
# fields: omitido → retorno void
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
### Artefactos generados por `ports[]`
|
|
781
|
+
|
|
782
|
+
Por cada `service:` único:
|
|
783
|
+
|
|
784
|
+
| Archivo generado | Descripción |
|
|
785
|
+
|---|---|
|
|
786
|
+
| `domain/repositories/{ServiceName}.java` | Interfaz del puerto secundario (devuelve modelos de dominio) |
|
|
787
|
+
| `infrastructure/adapters/{service}/{ServiceName}FeignClient.java` | Cliente Feign tipado (devuelve DTOs infra) |
|
|
788
|
+
| `infrastructure/adapters/{service}/{ServiceName}FeignAdapter.java` | `@Component implements {ServiceName}` — actúa como ACL |
|
|
789
|
+
| `infrastructure/adapters/{service}/{ServiceName}FeignConfig.java` | Timeouts Feign |
|
|
790
|
+
| `parameters/*/urls.yaml` | Base URL parametrizada |
|
|
791
|
+
|
|
792
|
+
Por cada modelo de dominio único derivado de los métodos con `fields:`:
|
|
793
|
+
|
|
794
|
+
| Archivo generado | Descripción |
|
|
795
|
+
|---|---|
|
|
796
|
+
| `domain/models/{service}/{DomainType}.java` | Modelo de dominio (ACL) — abstracción interna |
|
|
797
|
+
|
|
798
|
+
Por cada método:
|
|
799
|
+
|
|
800
|
+
| Archivo generado | Condición |
|
|
801
|
+
|---|---|
|
|
802
|
+
| `infrastructure/adapters/{service}/{MethodPascal}Dto.java` | Cuando `fields:` presente — DTO infra (forma externa) |
|
|
803
|
+
| `application/dtos/{MethodPascal}RequestDto.java` | Cuando `body:` presente (POST/PUT/PATCH) |
|
|
804
|
+
| `application/dtos/{NestedTypePascal}.java` | Cuando `nestedTypes:` declarado |
|
|
805
|
+
|
|
806
|
+
**Patrón ACL:** Los DTOs de infraestructura (forma de la API externa) viven en `infrastructure/adapters/{service}/`. Los modelos de dominio (abstracción interna) viven en `domain/models/{service}/`. El `FeignAdapter` mapea `InfraDto → DomainModel` inline con métodos privados `to{DomainType}()`. Si la API externa cambia, solo hay que actualizar el adaptador.
|
|
807
|
+
|
|
808
|
+
### Reglas de `ports[]`
|
|
809
|
+
|
|
810
|
+
- **`service:`** — PascalCase, agrupa métodos en un mismo FeignClient. **Si varios módulos llaman al mismo servicio externo, cada módulo debe usar un nombre de `service:` propio que refleje su bounded context** (ej: `OrderCustomerService` en `orders`, `DeliveryCustomerService` en `deliveries`). Reutilizar el mismo nombre (`CustomerService`) en módulos distintos causa colisión de beans Spring (`ConflictingBeanDefinitionException`) porque el generador produce un `FeignAdapter` con el mismo nombre de clase en cada módulo
|
|
811
|
+
- **`baseUrl:`** — declarar solo en la primera entrada de cada `service:`; si se omite en todas → warning + `http://localhost:8080`
|
|
812
|
+
- **`body:`** — solo en POST/PUT/PATCH; en GET/DELETE emite warning y se ignora
|
|
813
|
+
- **`domainType:`** — sobrescribe el tipo de dominio auto-derivado del nombre del método (ej: `domainType: Seat` en `findAvailableSeats`)
|
|
814
|
+
- **`returnList: true`** — el tipo de retorno es `List<{DomainType}>` en la interfaz y `List<{InfraDto}>` en el FeignClient (default: `false`)
|
|
815
|
+
- **`nestedTypes:`** — records auxiliares en `application/dtos/`; mismo patrón que `listeners:`
|
|
816
|
+
- **`fields:` omitido** → retorno `void` en interfaz y FeignClient
|
|
817
|
+
|
|
818
|
+
**Contraste async vs sync:**
|
|
819
|
+
```
|
|
820
|
+
aggregates:
|
|
821
|
+
└── events: → Domain Events que PRODUCE (async, broker)
|
|
822
|
+
listeners: → Integration Events que CONSUME (async, broker)
|
|
823
|
+
ports: → Servicios HTTP que LLAMA (sync, Feign)
|
|
824
|
+
```
|
|
549
825
|
|
|
550
826
|
Cuando una entidad tiene `hasSoftDelete: true`, eva4j genera eliminación lógica en lugar de física.
|
|
551
827
|
|
|
@@ -598,6 +874,8 @@ public class Product {
|
|
|
598
874
|
|
|
599
875
|
### Reglas para Agentes
|
|
600
876
|
|
|
877
|
+
- **SOLO** aplicar `hasSoftDelete: true` en la **entidad raíz** del agregado (`isRoot: true`)
|
|
878
|
+
- **NUNCA** poner `hasSoftDelete: true` en entidades secundarias — el ciclo de vida de estas lo controla la raíz mediante `cascade`; si se ignora, el generador emite un warning y descarta el flag
|
|
601
879
|
- **NUNCA** usar `repository.deleteById()` cuando hay soft delete
|
|
602
880
|
- **SIEMPRE** usar `entity.softDelete()` + `repository.save(entity)`
|
|
603
881
|
- **NUNCA** exponer `deletedAt` en ResponseDtos
|
|
@@ -956,7 +1234,18 @@ private String customerId;
|
|
|
956
1234
|
2. **SI** el módulo requiere ciclo de vida → usar `transitions` + `initialValue` en el enum
|
|
957
1235
|
3. **SI** un valor tiene lógica de negocio → declararlo como `valueObject` con `methods`
|
|
958
1236
|
4. **SI** ocurren hechos relevantes de negocio → declarar `events[]` en el agregado
|
|
959
|
-
5. **
|
|
1237
|
+
5. **SI** el módulo expone endpoints REST específicos → declarar `endpoints:` con versiones y operaciones
|
|
1238
|
+
6. **DESPUÉS** de generar el `domain.yaml` → ejecutar `eva g entities <module>`
|
|
1239
|
+
|
|
1240
|
+
### Al Usar `endpoints:` en domain.yaml
|
|
1241
|
+
|
|
1242
|
+
1. **SIEMPRE** declarar `endpoints:` cuando el API REST tiene comportamientos custom (confirmar, cancelar, activar, etc.)
|
|
1243
|
+
2. **NUNCA** usar `endpoints:` si solo necesitas CRUD estándar — el flujo interactivo es más simple
|
|
1244
|
+
3. **SIEMPRE** usar PascalCase para los nombres de `useCase` (ej: `ConfirmOrder`, no `confirmOrder`)
|
|
1245
|
+
4. **CONOCER** cuáles son los 5 use cases estándar por aggregate: `Create{E}`, `Update{E}`, `Delete{E}`, `Get{E}`, `FindAll{Plural(E)}` — estos generan implementación completa (e.g. `FindAllOrders`, `FindAllDeliveries`, `FindAllCategories`)
|
|
1246
|
+
5. **SABER** que cualquier otro nombre genera un **scaffold** con `UnsupportedOperationException` — el desarrollador debe implementar el handler
|
|
1247
|
+
6. **APLICAR** la regla anti-duplicado: si el mismo useCase aparece en v1 y v2, se genera solo una vez
|
|
1248
|
+
7. **NOMBRAR** los controladores según la convención: `{Aggregate}{VersionCapitalized}Controller` (ej: `OrderV1Controller`)
|
|
960
1249
|
|
|
961
1250
|
### Al Generar Código de Dominio
|
|
962
1251
|
|
|
@@ -1188,10 +1477,25 @@ Al generar o modificar código, verificar:
|
|
|
1188
1477
|
- [ ] Enum con ciclo de vida → usar `transitions` + `initialValue`, no setters manuales
|
|
1189
1478
|
- [ ] Value Object con comportamiento → declarar `methods` en lugar de lógica en entidad
|
|
1190
1479
|
- [ ] Evento de dominio → declarar en `events[]`, publicar con `raise()` en método de negocio
|
|
1191
|
-
- [ ] Evento con
|
|
1480
|
+
- [ ] Evento con `triggers: [methodName]` → el generador emite `raise()` automáticamente; args no resolubles quedan como `null /* TODO */`
|
|
1481
|
+
- [ ] Sin `triggers` en el evento → el dev llama a `raise()` manualmente
|
|
1482
|
+
- [ ] Evento con broker → **no** usar `kafka: true`; si `eva add kafka-client` está instalado, `eva g entities` auto-cablea todos los eventos
|
|
1483
|
+
- [ ] 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
|
+
- [ ] Consumo de eventos externos → declarar en `listeners[]` (nivel raíz); `topic:` obligatorio en módulos standalone
|
|
1485
|
+
- [ ] Cada `listener` genera hasta 6 artefactos: NestedType(s) → IntegrationEvent → KafkaListener → kafka.yaml → Command → CommandHandler
|
|
1486
|
+
- [ ] Varios módulos pueden consumir el mismo evento Kafka sin colisión — el generador califica el bean automáticamente con `@Component("moduleName.listenerClassName")`
|
|
1487
|
+
- [ ] Campos de tipo objeto en listeners → declarar `nestedTypes:` para generar records auxiliares en `application/events/`
|
|
1488
|
+
- [ ] Endpoints REST específicos → declarar `endpoints:` con versiones y operaciones; usar nombres estándar para implementación completa
|
|
1489
|
+
- [ ] Clientes HTTP síncronos → declarar en `ports[]` (nivel raíz); `baseUrl:` en la primera entrada de cada `service:`
|
|
1490
|
+
- [ ] Si varios módulos llaman al mismo servicio → cada uno usa un `service:` con nombre propio del bounded context (ej: `OrderCustomerService`, `DeliveryCustomerService`) — nunca el mismo nombre genérico en módulos distintos
|
|
1491
|
+
- [ ] Métodos con respuesta → incluir `fields:` en la entrada del puerto; sin `fields:` = retorno `void`
|
|
1492
|
+
- [ ] Respuestas en lista → agregar `returnList: true` en el método correspondiente
|
|
1493
|
+
- [ ] Métodos con cuerpo (POST/PUT/PATCH) → incluir `body:`; campos de tipo objeto en `nestedTypes:`
|
|
1494
|
+
- [ ] Tipo de dominio auto-derivado del nombre del método — usar `domainType:` para sobrescribir si es necesario
|
|
1495
|
+
- [ ] Cada `service:` en `ports[]` genera: interfaz (devuelve modelos de dominio), FeignClient (devuelve DTOs infra), FeignAdapter (mapea ACL), FeignConfig + `urls.yaml`
|
|
1192
1496
|
|
|
1193
1497
|
---
|
|
1194
1498
|
|
|
1195
|
-
**Última actualización:** 2026-03-
|
|
1196
|
-
**Versión de eva4j:** 1.0.
|
|
1499
|
+
**Última actualización:** 2026-03-12
|
|
1500
|
+
**Versión de eva4j:** 1.0.14
|
|
1197
1501
|
**Estado:** Documento de referencia para agentes IA
|
package/COMMAND_EVALUATION.md
CHANGED
|
@@ -412,20 +412,20 @@ aggregates:
|
|
|
412
412
|
|
|
413
413
|
---
|
|
414
414
|
|
|
415
|
-
### 4. Soft Delete
|
|
415
|
+
### 4. Soft Delete ✅ Implementado
|
|
416
416
|
|
|
417
417
|
**Impacto**: Medio - Común en apps business
|
|
418
418
|
|
|
419
|
-
####
|
|
419
|
+
#### Sintaxis
|
|
420
420
|
```yaml
|
|
421
|
-
#
|
|
422
|
-
|
|
423
|
-
name: order
|
|
424
|
-
|
|
421
|
+
# ✅ Soportado — solo en la entidad raíz (isRoot: true)
|
|
422
|
+
entities:
|
|
423
|
+
- name: order
|
|
424
|
+
isRoot: true
|
|
425
|
+
hasSoftDelete: true # Genera deletedAt, softDelete(), @SQLRestriction
|
|
425
426
|
```
|
|
426
427
|
|
|
427
|
-
**
|
|
428
|
-
**Prioridad**: 🟡 Media
|
|
428
|
+
**Estado**: Implementado en `yaml-to-entity.js` + templates `AggregateRoot`, `JpaAggregateRoot`, repositorios y `DeleteCommandHandler`.
|
|
429
429
|
|
|
430
430
|
---
|
|
431
431
|
|
|
@@ -558,7 +558,7 @@ aggregates:
|
|
|
558
558
|
|
|
559
559
|
1. ❌ **Validaciones JSR-303** (0%)
|
|
560
560
|
2. ❌ **Auditoría (createdAt, updatedAt)** (0%)
|
|
561
|
-
3.
|
|
561
|
+
3. ✅ **Soft delete** (implementado con `hasSoftDelete: true`)
|
|
562
562
|
4. ❌ **Índices y constraints** (0%)
|
|
563
563
|
5. ❌ **Herencia de entidades** (0%)
|
|
564
564
|
6. ❌ **DTOs de aplicación** (0%)
|
|
@@ -598,7 +598,7 @@ aggregates:
|
|
|
598
598
|
|
|
599
599
|
| # | Mejora | Impacto | Esfuerzo | Prioridad |
|
|
600
600
|
|---|--------|---------|----------|-----------|
|
|
601
|
-
| 5 | Soft delete |
|
|
601
|
+
| 5 | ~~Soft delete~~ | ✅ Implementado | — | — |
|
|
602
602
|
| 6 | ManyToMany completo | 🟡 Medio | 6h | 🟡 Media |
|
|
603
603
|
| 7 | OneToOne avanzado | 🟡 Bajo | 4h | 🔵 Baja |
|
|
604
604
|
|
|
@@ -640,9 +640,7 @@ aggregates:
|
|
|
640
640
|
|
|
641
641
|
### Mediano Plazo
|
|
642
642
|
|
|
643
|
-
4.
|
|
644
|
-
- Útil para muchas apps
|
|
645
|
-
- Buena relación esfuerzo/beneficio
|
|
643
|
+
4. ~~**Soft Delete**~~ ✅ Implementado con `hasSoftDelete: true`
|
|
646
644
|
|
|
647
645
|
5. **ManyToMany completo**
|
|
648
646
|
- Completa el soporte de relaciones JPA
|
|
@@ -794,15 +792,16 @@ entities:
|
|
|
794
792
|
|
|
795
793
|
---
|
|
796
794
|
|
|
797
|
-
### 4. Soft Delete
|
|
795
|
+
### 4. Soft Delete ✅ Implementado
|
|
798
796
|
|
|
799
797
|
```yaml
|
|
800
798
|
entities:
|
|
801
799
|
- name: Order
|
|
802
|
-
|
|
800
|
+
isRoot: true
|
|
801
|
+
hasSoftDelete: true # Genera deletedAt, softDelete(), isDeleted(), @SQLRestriction
|
|
803
802
|
```
|
|
804
803
|
|
|
805
|
-
**
|
|
804
|
+
**Implementado**: `deletedAt` inyectado automáticamente, `@SQLRestriction("deleted_at IS NULL")` en JPA, `softDelete()` + `isDeleted()` en dominio, `DeleteCommandHandler` usa borrado lógico.
|
|
806
805
|
|
|
807
806
|
---
|
|
808
807
|
|