eva4j 1.0.12 → 1.0.14
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 +468 -9
- package/DOMAIN_YAML_GUIDE.md +524 -21
- package/FUTURE_FEATURES.md +94 -1
- package/bin/eva4j.js +31 -1
- package/design-system.md +797 -0
- package/docs/commands/EVALUATE_SYSTEM.md +542 -0
- package/docs/commands/GENERATE_ENTITIES.md +399 -2355
- package/docs/commands/GENERATE_RECORD.md +335 -311
- package/docs/commands/INDEX.md +10 -1
- package/examples/doctor-evaluation.yaml +3 -3
- package/examples/domain-audit-complete.yaml +2 -2
- package/examples/domain-collections.yaml +2 -2
- package/examples/domain-ecommerce.yaml +2 -2
- package/examples/domain-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 +1 -1
- package/examples/domain-field-visibility.yaml +11 -5
- package/examples/domain-multi-aggregate.yaml +6 -6
- package/examples/domain-one-to-many.yaml +1 -1
- package/examples/domain-one-to-one.yaml +1 -1
- package/examples/domain-secondary-onetomany.yaml +1 -1
- package/examples/domain-secondary-onetoone.yaml +1 -1
- package/examples/domain-simple.yaml +1 -1
- package/examples/domain-soft-delete.yaml +3 -3
- package/examples/domain-transitions.yaml +1 -1
- package/examples/domain-value-objects.yaml +1 -1
- package/examples/system.yaml +289 -0
- package/package.json +1 -1
- package/src/commands/create.js +6 -3
- package/src/commands/evaluate-system.js +384 -0
- package/src/commands/generate-entities.js +677 -14
- package/src/commands/generate-kafka-event.js +59 -5
- package/src/commands/generate-system.js +243 -0
- package/src/generators/base-generator.js +9 -1
- package/src/utils/naming.js +3 -2
- package/src/utils/system-validator.js +314 -0
- package/src/utils/yaml-to-entity.js +100 -4
- package/templates/aggregate/AggregateRepository.java.ejs +5 -0
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +9 -0
- package/templates/aggregate/AggregateRoot.java.ejs +5 -1
- package/templates/aggregate/DomainEntity.java.ejs +5 -1
- package/templates/aggregate/DomainEventHandler.java.ejs +24 -20
- package/templates/aggregate/JpaAggregateRoot.java.ejs +2 -1
- package/templates/aggregate/JpaEntity.java.ejs +2 -1
- package/templates/aggregate/JpaRepository.java.ejs +5 -0
- package/templates/base/root/AGENTS.md.ejs +916 -51
- package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1103 -0
- package/templates/base/root/skill-build-domain-yaml.ejs +292 -0
- package/templates/base/root/skill-build-system-yaml.ejs +252 -0
- package/templates/base/root/system.yaml.ejs +97 -0
- 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/ScaffoldCommand.java.ejs +12 -0
- package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
- package/templates/crud/ScaffoldQuery.java.ejs +12 -0
- package/templates/crud/ScaffoldQueryHandler.java.ejs +40 -0
- package/templates/crud/SubEntityAddCommand.java.ejs +17 -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/evaluate/report.html.ejs +971 -0
- package/templates/kafka-event/Event.java.ejs +7 -0
package/AGENTS.md
CHANGED
|
@@ -451,11 +451,223 @@ aggregates:
|
|
|
451
451
|
- ACTIVE
|
|
452
452
|
- INACTIVE
|
|
453
453
|
- SUSPENDED
|
|
454
|
+
|
|
455
|
+
events:
|
|
456
|
+
- name: UserRegisteredEvent
|
|
457
|
+
fields:
|
|
458
|
+
- name: userId
|
|
459
|
+
type: String
|
|
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.
|
|
463
|
+
```
|
|
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
|
+
---
|
|
468
|
+
|
|
469
|
+
## ⚡ Características Avanzadas del domain.yaml
|
|
470
|
+
|
|
471
|
+
### Value Objects con Métodos
|
|
472
|
+
|
|
473
|
+
Los Value Objects pueden declarar métodos de negocio directamente en `domain.yaml`:
|
|
474
|
+
|
|
475
|
+
```yaml
|
|
476
|
+
valueObjects:
|
|
477
|
+
- name: Money
|
|
478
|
+
fields:
|
|
479
|
+
- name: amount
|
|
480
|
+
type: BigDecimal
|
|
481
|
+
- name: currency
|
|
482
|
+
type: String
|
|
483
|
+
methods:
|
|
484
|
+
- name: add
|
|
485
|
+
returnType: Money
|
|
486
|
+
parameters:
|
|
487
|
+
- name: other
|
|
488
|
+
type: Money
|
|
489
|
+
body: "return new Money(this.amount.add(other.getAmount()), this.currency);"
|
|
490
|
+
- name: isPositive
|
|
491
|
+
returnType: boolean
|
|
492
|
+
parameters: []
|
|
493
|
+
body: "return this.amount.compareTo(BigDecimal.ZERO) > 0;"
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Enums con Ciclo de Vida (Transitions)
|
|
497
|
+
|
|
498
|
+
Cuando un enum representa estados de negocio, declara `transitions` e `initialValue`:
|
|
499
|
+
|
|
500
|
+
```yaml
|
|
501
|
+
enums:
|
|
502
|
+
- name: OrderStatus
|
|
503
|
+
initialValue: PENDING # Auto-inicializa en constructor; excluido del CreateDto
|
|
504
|
+
transitions:
|
|
505
|
+
- from: PENDING
|
|
506
|
+
to: CONFIRMED
|
|
507
|
+
method: confirm
|
|
508
|
+
- from: CONFIRMED
|
|
509
|
+
to: SHIPPED
|
|
510
|
+
method: ship
|
|
511
|
+
- from: [PENDING, CONFIRMED] # múltiples orígenes
|
|
512
|
+
to: CANCELLED
|
|
513
|
+
method: cancel
|
|
514
|
+
guard: "this.status == OrderStatus.DELIVERED" # lanza BusinessException si se cumple
|
|
515
|
+
values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Genera automáticamente **en la entidad raíz**: `confirm()`, `ship()`, `cancel()`, helpers `isPending()`, `isConfirmed()`, `canConfirm()`, `canCancel()`.
|
|
519
|
+
Genera automáticamente **en el enum**: `VALID_TRANSITIONS`, `canTransitionTo()`, `transitionTo()` (lanza `InvalidStateTransitionException` si la transición no es válida).
|
|
520
|
+
|
|
521
|
+
**Nota:** El campo con `initialValue` se trata como `readOnly: true` — no aparece en el constructor de negocio ni en el `CreateDto`.
|
|
522
|
+
|
|
523
|
+
### Eventos de Dominio (`events[]`)
|
|
524
|
+
|
|
525
|
+
```yaml
|
|
526
|
+
aggregates:
|
|
527
|
+
- name: Order
|
|
528
|
+
entities: [...]
|
|
529
|
+
events:
|
|
530
|
+
- name: OrderConfirmed
|
|
531
|
+
fields:
|
|
532
|
+
- name: orderId
|
|
533
|
+
type: String
|
|
534
|
+
- name: confirmedAt
|
|
535
|
+
type: LocalDateTime
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
Genera `OrderConfirmed.java` (en `domain/models/events/`) que extiende `DomainEvent`, y `OrderDomainEventHandler.java` (en `application/usecases/`) con `@TransactionalEventListener(AFTER_COMMIT)`.
|
|
539
|
+
|
|
540
|
+
**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:
|
|
541
|
+
|
|
542
|
+
| Archivo generado | Descripción |
|
|
543
|
+
|---|---|
|
|
544
|
+
| `application/events/OrderConfirmedIntegrationEvent.java` | Record broker-facing (Integration Event) |
|
|
545
|
+
| `application/ports/MessageBroker.java` | Puerto broker-agnóstico (creado/actualizado) |
|
|
546
|
+
| `infrastructure/adapters/kafkaMessageBroker/…` | Adaptador Kafka (creado/actualizado) |
|
|
547
|
+
| `shared/…/kafkaConfig/KafkaConfig.java` | Bean `NewTopic` (actualizado) |
|
|
548
|
+
| `parameters/*/kafka.yaml` | Configuración de topic (actualizada) |
|
|
549
|
+
|
|
550
|
+
**Domain Event vs Integration Event:**
|
|
551
|
+
- **Domain Event** (`domain/models/events/OrderConfirmed.java`) — señal interna del bounded context. Nunca depende de infraestructura.
|
|
552
|
+
- **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.
|
|
553
|
+
|
|
554
|
+
El `DomainEventHandler` mapea un Domain Event a un Integration Event:
|
|
555
|
+
```java
|
|
556
|
+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
|
557
|
+
public void onOrderConfirmed(OrderConfirmed event) {
|
|
558
|
+
messageBroker.publishOrderConfirmedIntegrationEvent(
|
|
559
|
+
new OrderConfirmedIntegrationEvent(event.getOrderId(), event.getConfirmedAt())
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
Publicar desde la entidad raíz usando `raise()` heredado:
|
|
565
|
+
|
|
566
|
+
```java
|
|
567
|
+
public void confirm() {
|
|
568
|
+
this.status = this.status.transitionTo(OrderStatus.CONFIRMED);
|
|
569
|
+
raise(new OrderConfirmed(this.id, LocalDateTime.now()));
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
**Nota:** el flag `kafka: true` por evento ya no es necesario — todos los eventos se cablearán automáticamente cuando haya un broker instalado.
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
## �️ Soft Delete
|
|
578
|
+
|
|
579
|
+
Cuando una entidad tiene `hasSoftDelete: true`, eva4j genera eliminación lógica en lugar de física.
|
|
580
|
+
|
|
581
|
+
### Configuración en domain.yaml
|
|
582
|
+
|
|
583
|
+
```yaml
|
|
584
|
+
entities:
|
|
585
|
+
- name: product
|
|
586
|
+
isRoot: true
|
|
587
|
+
tableName: products
|
|
588
|
+
hasSoftDelete: true # ✅ Activa soft delete
|
|
589
|
+
audit:
|
|
590
|
+
enabled: true
|
|
591
|
+
fields:
|
|
592
|
+
- name: id
|
|
593
|
+
type: String
|
|
594
|
+
- name: name
|
|
595
|
+
type: String
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### Comportamiento generado
|
|
599
|
+
|
|
600
|
+
```java
|
|
601
|
+
// Entidad JPA — filtrado automático con @SQLRestriction
|
|
602
|
+
@Entity
|
|
603
|
+
@SQLRestriction("deleted_at IS NULL")
|
|
604
|
+
public class ProductJpa extends AuditableEntity {
|
|
605
|
+
@Column(name = "deleted_at")
|
|
606
|
+
private LocalDateTime deletedAt;
|
|
607
|
+
}
|
|
454
608
|
```
|
|
455
609
|
|
|
610
|
+
```java
|
|
611
|
+
// Entidad de dominio — método de negocio
|
|
612
|
+
public class Product {
|
|
613
|
+
private LocalDateTime deletedAt;
|
|
614
|
+
|
|
615
|
+
public void softDelete() {
|
|
616
|
+
if (this.deletedAt != null) {
|
|
617
|
+
throw new IllegalStateException("Product is already deleted");
|
|
618
|
+
}
|
|
619
|
+
this.deletedAt = LocalDateTime.now();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
public boolean isDeleted() {
|
|
623
|
+
return this.deletedAt != null;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### Reglas para Agentes
|
|
629
|
+
|
|
630
|
+
- **NUNCA** usar `repository.deleteById()` cuando hay soft delete
|
|
631
|
+
- **SIEMPRE** usar `entity.softDelete()` + `repository.save(entity)`
|
|
632
|
+
- **NUNCA** exponer `deletedAt` en ResponseDtos
|
|
633
|
+
- **SIEMPRE** usar `@SQLRestriction("deleted_at IS NULL")` en la entidad JPA
|
|
634
|
+
|
|
456
635
|
---
|
|
457
636
|
|
|
458
|
-
##
|
|
637
|
+
## ⏱️ Temporal Workflows
|
|
638
|
+
|
|
639
|
+
Cuando se agrega soporte de Temporal con `eva add temporal-client`, se genera infraestructura para workflows duraderos.
|
|
640
|
+
|
|
641
|
+
### Archivos generados por `eva g temporal-flow <module>`
|
|
642
|
+
|
|
643
|
+
```
|
|
644
|
+
[module]/
|
|
645
|
+
├── application/workflows/
|
|
646
|
+
│ ├── OrderWorkflow.java # Interface (@WorkflowInterface)
|
|
647
|
+
│ └── OrderWorkflowImpl.java # Implementación (determinista)
|
|
648
|
+
└── infrastructure/temporal/
|
|
649
|
+
├── activities/
|
|
650
|
+
│ ├── OrderActivity.java # Interface (@ActivityInterface)
|
|
651
|
+
│ └── OrderActivityImpl.java # Implementación (con I/O)
|
|
652
|
+
└── workers/
|
|
653
|
+
└── OrderWorker.java # Registro del worker
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### Principios clave
|
|
657
|
+
|
|
658
|
+
- Los **Workflows deben ser deterministas** — sin `Math.random()`, `new Date()`, ni I/O
|
|
659
|
+
- Toda operación con efectos secundarios (DB, HTTP, emails) va en **Activities**
|
|
660
|
+
- Los **Use Cases** orquestan los workflows; las **Activities** ejecutan infraestructura
|
|
661
|
+
- Configuración de conexión en `resources/parameters/{env}/temporal.yaml`
|
|
662
|
+
|
|
663
|
+
### Templates relacionados en eva4j
|
|
664
|
+
|
|
665
|
+
- `templates/temporal-flow/` — workflow interface e implementación
|
|
666
|
+
- `templates/temporal-activity/` — activity interface, implementación y worker
|
|
667
|
+
|
|
668
|
+
---
|
|
669
|
+
|
|
670
|
+
## �🚨 Errores Comunes a Evitar
|
|
459
671
|
|
|
460
672
|
### ❌ NO Crear Constructor Vacío en Dominio
|
|
461
673
|
|
|
@@ -566,6 +778,7 @@ Los campos en domain.yaml soportan las siguientes propiedades:
|
|
|
566
778
|
| `enumValues` | Array | `[]` | Valores inline de enum |
|
|
567
779
|
| **`readOnly`** | Boolean | `false` | **Excluye del constructor de negocio y CreateDto** |
|
|
568
780
|
| **`hidden`** | Boolean | `false` | **Excluye del ResponseDto** |
|
|
781
|
+
| **`defaultValue`** | String/Number/Boolean | `null` | **Valor inicial en el constructor de creación (solo para `readOnly`)** |
|
|
569
782
|
| **`validations`** | Array | `[]` | **Anotaciones JSR-303 en el Command y CreateDto** |
|
|
570
783
|
| **`reference`** | Object | `null` | **Declara referencia semántica a otro agregado (genera comentario Javadoc)** |
|
|
571
784
|
|
|
@@ -601,9 +814,57 @@ fields:
|
|
|
601
814
|
|-------|---------------------|-----------|-------------|
|
|
602
815
|
| Normal | ✅ | ✅ | ✅ |
|
|
603
816
|
| `readOnly: true` | ❌ | ❌ | ✅ |
|
|
817
|
+
| `readOnly` + `defaultValue` | ⚡ Asignado con default | ❌ | ✅ |
|
|
604
818
|
| `hidden: true` | ✅ | ✅ | ❌ |
|
|
605
819
|
| Ambos flags | ❌ | ❌ | ❌ |
|
|
606
820
|
|
|
821
|
+
#### 🎯 `defaultValue` - Valor Inicial para campos `readOnly`
|
|
822
|
+
|
|
823
|
+
Cuando un campo `readOnly` tiene un valor inicial predecible (ej: contadores, estados iniciales), se puede declarar en `domain.yaml` con `defaultValue`. El generador emite la asignación en el **constructor de creación** del dominio y `@Builder.Default` en la entidad JPA.
|
|
824
|
+
|
|
825
|
+
```yaml
|
|
826
|
+
fields:
|
|
827
|
+
- name: totalAmount
|
|
828
|
+
type: BigDecimal
|
|
829
|
+
readOnly: true
|
|
830
|
+
defaultValue: "0.00" # ✅ Acumulador inicializado
|
|
831
|
+
|
|
832
|
+
- name: status
|
|
833
|
+
type: OrderStatus
|
|
834
|
+
readOnly: true
|
|
835
|
+
defaultValue: PENDING # ✅ Estado inicial del enum
|
|
836
|
+
|
|
837
|
+
- name: itemCount
|
|
838
|
+
type: Integer
|
|
839
|
+
readOnly: true
|
|
840
|
+
defaultValue: 0 # ✅ Contador inicializado
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
```java
|
|
844
|
+
// Constructor de creación — defaultValues aplicados
|
|
845
|
+
public Order(String orderNumber, String customerId) {
|
|
846
|
+
this.orderNumber = orderNumber;
|
|
847
|
+
this.customerId = customerId;
|
|
848
|
+
this.totalAmount = new BigDecimal("0.00"); // ← defaultValue
|
|
849
|
+
this.status = OrderStatus.PENDING; // ← defaultValue
|
|
850
|
+
this.itemCount = 0; // ← defaultValue
|
|
851
|
+
}
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
```java
|
|
855
|
+
// JPA — @Builder.Default para respetar el valor en el builder
|
|
856
|
+
@Builder.Default
|
|
857
|
+
private BigDecimal totalAmount = new BigDecimal("0.00");
|
|
858
|
+
|
|
859
|
+
@Enumerated(EnumType.STRING)
|
|
860
|
+
@Builder.Default
|
|
861
|
+
private OrderStatus status = OrderStatus.PENDING;
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
**Restricción:** `defaultValue` **solo aplica** a campos con `readOnly: true`. Usarlo en un campo no-readOnly genera un warning y es ignorado.
|
|
865
|
+
|
|
866
|
+
---
|
|
867
|
+
|
|
607
868
|
**Ejemplo práctico:**
|
|
608
869
|
```yaml
|
|
609
870
|
entities:
|
|
@@ -626,6 +887,87 @@ entities:
|
|
|
626
887
|
hidden: true
|
|
627
888
|
```
|
|
628
889
|
|
|
890
|
+
### Validaciones JSR-303 (`validations`)
|
|
891
|
+
|
|
892
|
+
Se declaran en el campo y se aplican **únicamente** en el `Command` y `CreateDto` de la capa de aplicación. **Nunca** en las entidades de dominio.
|
|
893
|
+
|
|
894
|
+
```yaml
|
|
895
|
+
fields:
|
|
896
|
+
- name: email
|
|
897
|
+
type: String
|
|
898
|
+
validations:
|
|
899
|
+
- type: NotBlank
|
|
900
|
+
message: "Email es requerido"
|
|
901
|
+
- type: Email
|
|
902
|
+
message: "Email inválido"
|
|
903
|
+
|
|
904
|
+
- name: username
|
|
905
|
+
type: String
|
|
906
|
+
validations:
|
|
907
|
+
- type: Size
|
|
908
|
+
min: 3
|
|
909
|
+
max: 50
|
|
910
|
+
message: "Username entre 3 y 50 caracteres"
|
|
911
|
+
|
|
912
|
+
- name: age
|
|
913
|
+
type: Integer
|
|
914
|
+
validations:
|
|
915
|
+
- type: Min
|
|
916
|
+
value: 18
|
|
917
|
+
- type: Max
|
|
918
|
+
value: 120
|
|
919
|
+
|
|
920
|
+
- name: price
|
|
921
|
+
type: BigDecimal
|
|
922
|
+
validations:
|
|
923
|
+
- type: Positive
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
**Anotaciones disponibles:** `NotNull`, `NotBlank`, `NotEmpty`, `Email`, `Size` (min/max), `Min` (value), `Max` (value), `Pattern` (regexp), `Digits` (integer/fraction), `Positive`, `PositiveOrZero`, `Negative`, `Past`, `Future`, `AssertTrue`, `AssertFalse`.
|
|
927
|
+
|
|
928
|
+
**Código generado en `CreateUserCommand.java`:**
|
|
929
|
+
```java
|
|
930
|
+
public record CreateUserCommand(
|
|
931
|
+
@NotBlank(message = "Email es requerido")
|
|
932
|
+
@Email(message = "Email inválido")
|
|
933
|
+
String email,
|
|
934
|
+
|
|
935
|
+
@Size(min = 3, max = 50, message = "Username entre 3 y 50 caracteres")
|
|
936
|
+
String username
|
|
937
|
+
) implements Command {}
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
### Referencias entre Agregados (`reference`)
|
|
941
|
+
|
|
942
|
+
Declara explícitamente que un campo es un ID de otro agregado. El tipo Java **no cambia** — sigue siendo `String`, `Long`, etc. — pero se genera un comentario Javadoc que documenta la dependencia. **No genera `@ManyToOne`** (correcto en DDD: cada agregado es una unidad transaccional independiente).
|
|
943
|
+
|
|
944
|
+
```yaml
|
|
945
|
+
fields:
|
|
946
|
+
- name: customerId
|
|
947
|
+
type: String
|
|
948
|
+
reference:
|
|
949
|
+
aggregate: Customer # Nombre del agregado referenciado (PascalCase)
|
|
950
|
+
module: customers # Módulo donde vive (opcional si es el mismo módulo)
|
|
951
|
+
|
|
952
|
+
- name: productId
|
|
953
|
+
type: String
|
|
954
|
+
reference:
|
|
955
|
+
aggregate: Product
|
|
956
|
+
module: catalog
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
**Código generado:**
|
|
960
|
+
```java
|
|
961
|
+
// En Order.java (domain entity)
|
|
962
|
+
/** Cross-aggregate reference → Customer (module: customers) */
|
|
963
|
+
private String customerId;
|
|
964
|
+
|
|
965
|
+
// En OrderJpa.java
|
|
966
|
+
@Column(name = "customer_id")
|
|
967
|
+
/** Cross-aggregate reference → Customer (module: customers) */
|
|
968
|
+
private String customerId;
|
|
969
|
+
```
|
|
970
|
+
|
|
629
971
|
### Tipos de Relaciones
|
|
630
972
|
|
|
631
973
|
- `OneToOne` - Relación uno a uno
|
|
@@ -637,6 +979,25 @@ entities:
|
|
|
637
979
|
|
|
638
980
|
## 🎯 Mejores Prácticas para Agentes
|
|
639
981
|
|
|
982
|
+
### Al Generar domain.yaml (Flujo SDD)
|
|
983
|
+
|
|
984
|
+
1. **SIEMPRE** incluir campo `id` en todas las entidades
|
|
985
|
+
2. **SI** el módulo requiere ciclo de vida → usar `transitions` + `initialValue` en el enum
|
|
986
|
+
3. **SI** un valor tiene lógica de negocio → declararlo como `valueObject` con `methods`
|
|
987
|
+
4. **SI** ocurren hechos relevantes de negocio → declarar `events[]` en el agregado
|
|
988
|
+
5. **SI** el módulo expone endpoints REST específicos → declarar `endpoints:` con versiones y operaciones
|
|
989
|
+
6. **DESPUÉS** de generar el `domain.yaml` → ejecutar `eva g entities <module>`
|
|
990
|
+
|
|
991
|
+
### Al Usar `endpoints:` en domain.yaml
|
|
992
|
+
|
|
993
|
+
1. **SIEMPRE** declarar `endpoints:` cuando el API REST tiene comportamientos custom (confirmar, cancelar, activar, etc.)
|
|
994
|
+
2. **NUNCA** usar `endpoints:` si solo necesitas CRUD estándar — el flujo interactivo es más simple
|
|
995
|
+
3. **SIEMPRE** usar PascalCase para los nombres de `useCase` (ej: `ConfirmOrder`, no `confirmOrder`)
|
|
996
|
+
4. **CONOCER** cuáles son los 5 use cases estándar por aggregate: `Create{E}`, `Update{E}`, `Delete{E}`, `Get{E}`, `FindAll{E}s` — estos generan implementación completa
|
|
997
|
+
5. **SABER** que cualquier otro nombre genera un **scaffold** con `UnsupportedOperationException` — el desarrollador debe implementar el handler
|
|
998
|
+
6. **APLICAR** la regla anti-duplicado: si el mismo useCase aparece en v1 y v2, se genera solo una vez
|
|
999
|
+
7. **NOMBRAR** los controladores según la convención: `{Aggregate}{VersionCapitalized}Controller` (ej: `OrderV1Controller`)
|
|
1000
|
+
|
|
640
1001
|
### Al Generar Código de Dominio
|
|
641
1002
|
|
|
642
1003
|
1. **NUNCA** crear constructor vacío en entidades de dominio
|
|
@@ -722,13 +1083,13 @@ HTTP Response (sin createdBy/updatedBy)
|
|
|
722
1083
|
|
|
723
1084
|
## 🧪 Testing
|
|
724
1085
|
|
|
725
|
-
### Tests de Dominio
|
|
1086
|
+
### Tests de Dominio (Unidad Pura)
|
|
726
1087
|
|
|
727
1088
|
```java
|
|
728
1089
|
@Test
|
|
729
1090
|
void shouldCreateUserWithValidData() {
|
|
730
1091
|
User user = new User("john", "john@example.com");
|
|
731
|
-
|
|
1092
|
+
|
|
732
1093
|
assertEquals("john", user.getUsername());
|
|
733
1094
|
assertEquals("john@example.com", user.getEmail());
|
|
734
1095
|
}
|
|
@@ -736,13 +1097,82 @@ void shouldCreateUserWithValidData() {
|
|
|
736
1097
|
@Test
|
|
737
1098
|
void shouldValidateBusinessRules() {
|
|
738
1099
|
User user = new User("john", "john@example.com");
|
|
739
|
-
|
|
1100
|
+
|
|
740
1101
|
assertThrows(IllegalArgumentException.class, () -> {
|
|
741
1102
|
user.changeEmail("invalid-email");
|
|
742
1103
|
});
|
|
743
1104
|
}
|
|
744
1105
|
```
|
|
745
1106
|
|
|
1107
|
+
### Object Mother Pattern
|
|
1108
|
+
|
|
1109
|
+
```java
|
|
1110
|
+
// src/test/java/[package]/user/domain/UserMother.java
|
|
1111
|
+
public class UserMother {
|
|
1112
|
+
|
|
1113
|
+
public static User valid() {
|
|
1114
|
+
return new User("john_doe", "john@example.com");
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
public static User withEmail(String email) {
|
|
1118
|
+
return new User("john_doe", email);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
### Repositorio Fake (In-Memory)
|
|
1124
|
+
|
|
1125
|
+
Para testear Use Cases sin base de datos:
|
|
1126
|
+
|
|
1127
|
+
```java
|
|
1128
|
+
public class UserRepositoryFake implements UserRepository {
|
|
1129
|
+
private final Map<String, User> store = new HashMap<>();
|
|
1130
|
+
|
|
1131
|
+
@Override
|
|
1132
|
+
public User save(User user) {
|
|
1133
|
+
store.put(user.getId(), user);
|
|
1134
|
+
return user;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
@Override
|
|
1138
|
+
public Optional<User> findById(String id) {
|
|
1139
|
+
return Optional.ofNullable(store.get(id));
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
public int count() { return store.size(); }
|
|
1143
|
+
}
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
### Tests de Use Cases
|
|
1147
|
+
|
|
1148
|
+
```java
|
|
1149
|
+
class CreateUserCommandHandlerTest {
|
|
1150
|
+
private final UserRepositoryFake userRepository = new UserRepositoryFake();
|
|
1151
|
+
private final CreateUserCommandHandler handler =
|
|
1152
|
+
new CreateUserCommandHandler(userRepository);
|
|
1153
|
+
|
|
1154
|
+
@Test
|
|
1155
|
+
void shouldCreateUser() {
|
|
1156
|
+
CreateUserCommand command = new CreateUserCommand("john", "john@example.com");
|
|
1157
|
+
|
|
1158
|
+
String userId = handler.handle(command);
|
|
1159
|
+
|
|
1160
|
+
assertNotNull(userId);
|
|
1161
|
+
assertEquals(1, userRepository.count());
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
### Estrategia por Capa
|
|
1167
|
+
|
|
1168
|
+
| Capa | Tipo de Test | Framework |
|
|
1169
|
+
|------|--------------|-----------|
|
|
1170
|
+
| Domain entities | Unidad pura | JUnit 5 |
|
|
1171
|
+
| Use cases | Unidad con Fakes | JUnit 5 + Fake repos |
|
|
1172
|
+
| Application mappers | Unidad | JUnit 5 |
|
|
1173
|
+
| Repository implementations | Integración | Testcontainers |
|
|
1174
|
+
| REST controllers | Integración | MockMvc |
|
|
1175
|
+
|
|
746
1176
|
---
|
|
747
1177
|
|
|
748
1178
|
## 📖 Documentos Relacionados
|
|
@@ -758,23 +1188,52 @@ void shouldValidateBusinessRules() {
|
|
|
758
1188
|
|
|
759
1189
|
Al generar o modificar código, verificar:
|
|
760
1190
|
|
|
1191
|
+
**Entidades de Dominio:**
|
|
761
1192
|
- [ ] Entidades de dominio **sin constructor vacío**
|
|
762
1193
|
- [ ] Entidades de dominio **sin setters públicos**
|
|
763
1194
|
- [ ] Métodos de negocio con **validaciones explícitas**
|
|
1195
|
+
- [ ] Value Objects **inmutables**
|
|
1196
|
+
- [ ] Sin anotaciones JSR-303 en entidades de dominio
|
|
1197
|
+
|
|
1198
|
+
**Entidades JPA:**
|
|
764
1199
|
- [ ] Entidades JPA con **Lombok y herencia correcta**
|
|
1200
|
+
- [ ] `@SQLRestriction("deleted_at IS NULL")` cuando `hasSoftDelete: true`
|
|
1201
|
+
- [ ] No incluye campos de auditoría heredados en `@Builder`
|
|
1202
|
+
|
|
1203
|
+
**Mappers:**
|
|
765
1204
|
- [ ] Mappers **excluyen campos de auditoría**
|
|
766
1205
|
- [ ] Mappers **excluyen campos readOnly en creación**
|
|
767
1206
|
- [ ] Mappers **excluyen campos hidden en respuestas**
|
|
1207
|
+
- [ ] Relaciones bidireccionales con métodos `assign*()`
|
|
1208
|
+
|
|
1209
|
+
**DTOs:**
|
|
768
1210
|
- [ ] DTOs de respuesta **sin createdBy/updatedBy**
|
|
769
1211
|
- [ ] DTOs de respuesta **sin campos hidden**
|
|
770
1212
|
- [ ] DTOs de creación **sin campos readOnly**
|
|
771
|
-
- [ ]
|
|
772
|
-
|
|
773
|
-
|
|
1213
|
+
- [ ] Usando Java Records
|
|
1214
|
+
|
|
1215
|
+
**Validaciones:**
|
|
774
1216
|
- [ ] Validaciones JSR-303 **solo en Command y CreateDto, nunca en dominio**
|
|
1217
|
+
- [ ] `@Valid` en parámetros de endpoints REST
|
|
1218
|
+
|
|
1219
|
+
**Auditoría:**
|
|
1220
|
+
- [ ] Configuración de auditoría cuando `trackUser: true`
|
|
1221
|
+
- [ ] `@EnableJpaAuditing` con `auditorAwareRef = "auditorProvider"` en Application
|
|
1222
|
+
|
|
1223
|
+
**Soft Delete (cuando aplica):**
|
|
1224
|
+
- [ ] Usar `entity.softDelete()` + `repository.save()` — nunca `deleteById()`
|
|
1225
|
+
- [ ] `deletedAt` no expuesto en ResponseDto
|
|
1226
|
+
|
|
1227
|
+
**Características Avanzadas (cuando aplica):**
|
|
1228
|
+
- [ ] Enum con ciclo de vida → usar `transitions` + `initialValue`, no setters manuales
|
|
1229
|
+
- [ ] Value Object con comportamiento → declarar `methods` en lugar de lógica en entidad
|
|
1230
|
+
- [ ] Evento de dominio → declarar en `events[]`, publicar con `raise()` en método de negocio
|
|
1231
|
+
- [ ] Evento con broker → **no** usar `kafka: true`; si `eva add kafka-client` está instalado, `eva g entities` auto-cablea todos los eventos
|
|
1232
|
+
- [ ] Distinguir entre Domain Event (`domain/models/events/X.java`) e Integration Event (`application/events/XIntegrationEvent.java`) — cambios de broker solo afectan al adaptador `MessageBroker`
|
|
1233
|
+
- [ ] Endpoints REST específicos → declarar `endpoints:` con versiones y operaciones; usar nombres estándar para implementación completa
|
|
775
1234
|
|
|
776
1235
|
---
|
|
777
1236
|
|
|
778
|
-
**Última actualización:** 2026-
|
|
779
|
-
**Versión de eva4j:** 1.
|
|
1237
|
+
**Última actualización:** 2026-03-11
|
|
1238
|
+
**Versión de eva4j:** 1.0.13
|
|
780
1239
|
**Estado:** Documento de referencia para agentes IA
|