eva4j 1.0.11 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +441 -14
- package/DOMAIN_YAML_GUIDE.md +425 -21
- package/FUTURE_FEATURES.md +315 -115
- package/QUICK_REFERENCE.md +101 -153
- package/README.md +77 -70
- package/bin/eva4j.js +57 -1
- package/config/defaults.json +3 -0
- package/docs/commands/GENERATE_ENTITIES.md +662 -1968
- package/docs/commands/GENERATE_HTTP_EXCHANGE.md +274 -450
- package/docs/commands/GENERATE_KAFKA_EVENT.md +219 -498
- package/docs/commands/GENERATE_KAFKA_LISTENER.md +18 -18
- package/docs/commands/GENERATE_RECORD.md +335 -311
- package/docs/commands/GENERATE_TEMPORAL_ACTIVITY.md +174 -0
- package/docs/commands/GENERATE_TEMPORAL_FLOW.md +237 -0
- package/docs/commands/GENERATE_USECASE.md +216 -282
- package/docs/commands/INDEX.md +36 -7
- package/examples/doctor-evaluation.yaml +3 -3
- package/examples/domain-audit-complete.yaml +2 -2
- package/examples/domain-collections.yaml +2 -2
- package/examples/domain-ecommerce.yaml +2 -2
- package/examples/domain-events.yaml +201 -0
- package/examples/domain-field-visibility.yaml +11 -5
- package/examples/domain-multi-aggregate.yaml +12 -6
- package/examples/domain-one-to-many.yaml +1 -1
- package/examples/domain-one-to-one.yaml +1 -1
- package/examples/domain-secondary-onetomany.yaml +1 -1
- package/examples/domain-secondary-onetoone.yaml +1 -1
- package/examples/domain-simple.yaml +1 -1
- package/examples/domain-soft-delete.yaml +3 -3
- package/examples/domain-transitions.yaml +1 -1
- package/examples/domain-value-objects.yaml +1 -1
- package/package.json +2 -2
- package/src/commands/add-kafka-client.js +3 -1
- package/src/commands/add-temporal-client.js +286 -0
- package/src/commands/generate-entities.js +75 -4
- package/src/commands/generate-kafka-event.js +273 -89
- package/src/commands/generate-temporal-activity.js +228 -0
- package/src/commands/generate-temporal-flow.js +216 -0
- package/src/generators/module-generator.js +1 -0
- package/src/generators/shared-generator.js +26 -0
- package/src/utils/yaml-to-entity.js +93 -4
- package/templates/aggregate/AggregateRepository.java.ejs +3 -2
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +15 -7
- package/templates/aggregate/AggregateRoot.java.ejs +38 -2
- package/templates/aggregate/DomainEntity.java.ejs +6 -2
- package/templates/aggregate/DomainEventHandler.java.ejs +62 -0
- package/templates/aggregate/DomainEventRecord.java.ejs +50 -0
- package/templates/aggregate/JpaAggregateRoot.java.ejs +3 -1
- package/templates/aggregate/JpaEntity.java.ejs +3 -1
- package/templates/base/docker/kafka-services.yaml.ejs +2 -2
- package/templates/base/docker/temporal-services.yaml.ejs +29 -0
- package/templates/base/resources/parameters/develop/temporal.yaml.ejs +9 -0
- package/templates/base/resources/parameters/local/temporal.yaml.ejs +9 -0
- package/templates/base/resources/parameters/production/temporal.yaml.ejs +9 -0
- package/templates/base/resources/parameters/test/temporal.yaml.ejs +9 -0
- package/templates/base/root/AGENTS.md.ejs +916 -51
- package/templates/crud/Controller.java.ejs +36 -6
- package/templates/crud/ListQuery.java.ejs +6 -2
- package/templates/crud/ListQueryHandler.java.ejs +24 -10
- package/templates/crud/UpdateCommand.java.ejs +52 -0
- package/templates/crud/UpdateCommandHandler.java.ejs +105 -0
- package/templates/kafka-event/DomainEventHandlerMethod.ejs +1 -0
- package/templates/kafka-event/Event.java.ejs +23 -0
- package/templates/shared/application/dtos/PagedResponse.java.ejs +30 -0
- package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +104 -0
- package/templates/shared/domain/DomainEvent.java.ejs +40 -0
- package/templates/shared/interfaces/HeavyActivity.java.ejs +4 -0
- package/templates/shared/interfaces/LightActivity.java.ejs +4 -0
- package/templates/temporal-activity/ActivityImpl.java.ejs +14 -0
- package/templates/temporal-activity/ActivityInterface.java.ejs +11 -0
- package/templates/temporal-flow/WorkFlowImpl.java.ejs +64 -0
- package/templates/temporal-flow/WorkFlowInterface.java.ejs +19 -0
- package/templates/temporal-flow/WorkFlowService.java.ejs +49 -0
package/AGENTS.md
CHANGED
|
@@ -387,19 +387,28 @@ void assignUser(User user) { // package-private
|
|
|
387
387
|
|
|
388
388
|
```bash
|
|
389
389
|
# Crear proyecto
|
|
390
|
-
|
|
390
|
+
eva create my-app
|
|
391
391
|
|
|
392
392
|
# Agregar módulo
|
|
393
|
-
|
|
393
|
+
eva add module users
|
|
394
394
|
|
|
395
395
|
# Generar entidades desde YAML
|
|
396
|
-
|
|
396
|
+
eva g entities users
|
|
397
397
|
|
|
398
398
|
# Generar use case
|
|
399
|
-
|
|
399
|
+
eva g usecase users ActivateUser
|
|
400
400
|
|
|
401
401
|
# Generar resource (REST)
|
|
402
|
-
|
|
402
|
+
eva g resource users
|
|
403
|
+
|
|
404
|
+
# Agregar cliente Temporal
|
|
405
|
+
eva add temporal-client
|
|
406
|
+
|
|
407
|
+
# Generar workflow Temporal
|
|
408
|
+
eva g temporal-flow users
|
|
409
|
+
|
|
410
|
+
# Generar actividad Temporal
|
|
411
|
+
eva g temporal-activity users
|
|
403
412
|
```
|
|
404
413
|
|
|
405
414
|
### Estructura de domain.yaml
|
|
@@ -442,11 +451,194 @@ aggregates:
|
|
|
442
451
|
- ACTIVE
|
|
443
452
|
- INACTIVE
|
|
444
453
|
- SUSPENDED
|
|
454
|
+
|
|
455
|
+
events:
|
|
456
|
+
- name: UserRegisteredEvent
|
|
457
|
+
fields:
|
|
458
|
+
- name: userId
|
|
459
|
+
type: String
|
|
460
|
+
# kafka: true # opcional — publica a Kafka tras commit
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## ⚡ Características Avanzadas del domain.yaml
|
|
466
|
+
|
|
467
|
+
### Value Objects con Métodos
|
|
468
|
+
|
|
469
|
+
Los Value Objects pueden declarar métodos de negocio directamente en `domain.yaml`:
|
|
470
|
+
|
|
471
|
+
```yaml
|
|
472
|
+
valueObjects:
|
|
473
|
+
- name: Money
|
|
474
|
+
fields:
|
|
475
|
+
- name: amount
|
|
476
|
+
type: BigDecimal
|
|
477
|
+
- name: currency
|
|
478
|
+
type: String
|
|
479
|
+
methods:
|
|
480
|
+
- name: add
|
|
481
|
+
returnType: Money
|
|
482
|
+
parameters:
|
|
483
|
+
- name: other
|
|
484
|
+
type: Money
|
|
485
|
+
body: "return new Money(this.amount.add(other.getAmount()), this.currency);"
|
|
486
|
+
- name: isPositive
|
|
487
|
+
returnType: boolean
|
|
488
|
+
parameters: []
|
|
489
|
+
body: "return this.amount.compareTo(BigDecimal.ZERO) > 0;"
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Enums con Ciclo de Vida (Transitions)
|
|
493
|
+
|
|
494
|
+
Cuando un enum representa estados de negocio, declara `transitions` e `initialValue`:
|
|
495
|
+
|
|
496
|
+
```yaml
|
|
497
|
+
enums:
|
|
498
|
+
- name: OrderStatus
|
|
499
|
+
initialValue: PENDING # Auto-inicializa en constructor; excluido del CreateDto
|
|
500
|
+
transitions:
|
|
501
|
+
- from: PENDING
|
|
502
|
+
to: CONFIRMED
|
|
503
|
+
method: confirm
|
|
504
|
+
- from: CONFIRMED
|
|
505
|
+
to: SHIPPED
|
|
506
|
+
method: ship
|
|
507
|
+
- from: [PENDING, CONFIRMED] # múltiples orígenes
|
|
508
|
+
to: CANCELLED
|
|
509
|
+
method: cancel
|
|
510
|
+
guard: "this.status == OrderStatus.DELIVERED" # lanza BusinessException si se cumple
|
|
511
|
+
values: [PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED]
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Genera automáticamente **en la entidad raíz**: `confirm()`, `ship()`, `cancel()`, helpers `isPending()`, `isConfirmed()`, `canConfirm()`, `canCancel()`.
|
|
515
|
+
Genera automáticamente **en el enum**: `VALID_TRANSITIONS`, `canTransitionTo()`, `transitionTo()` (lanza `InvalidStateTransitionException` si la transición no es válida).
|
|
516
|
+
|
|
517
|
+
**Nota:** El campo con `initialValue` se trata como `readOnly: true` — no aparece en el constructor de negocio ni en el `CreateDto`.
|
|
518
|
+
|
|
519
|
+
### Eventos de Dominio (`events[]`)
|
|
520
|
+
|
|
521
|
+
```yaml
|
|
522
|
+
aggregates:
|
|
523
|
+
- name: Order
|
|
524
|
+
entities: [...]
|
|
525
|
+
events:
|
|
526
|
+
- name: OrderConfirmedEvent
|
|
527
|
+
fields:
|
|
528
|
+
- name: orderId
|
|
529
|
+
type: String
|
|
530
|
+
- name: confirmedAt
|
|
531
|
+
type: LocalDateTime
|
|
532
|
+
kafka: true # opcional — genera publicación a MessageBroker
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
Genera `OrderConfirmedEvent.java` (en `domain/models/events/`) que extiende `DomainEvent`, y `OrderDomainEventHandler.java` (en `application/usecases/`) con `@TransactionalEventListener(AFTER_COMMIT)`.
|
|
536
|
+
|
|
537
|
+
Publicar desde la entidad raíz usando `raise()` heredado:
|
|
538
|
+
|
|
539
|
+
```java
|
|
540
|
+
public void confirm() {
|
|
541
|
+
this.status = this.status.transitionTo(OrderStatus.CONFIRMED);
|
|
542
|
+
raise(new OrderConfirmedEvent(this.id, this.id, LocalDateTime.now()));
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
## �️ Soft Delete
|
|
549
|
+
|
|
550
|
+
Cuando una entidad tiene `hasSoftDelete: true`, eva4j genera eliminación lógica en lugar de física.
|
|
551
|
+
|
|
552
|
+
### Configuración en domain.yaml
|
|
553
|
+
|
|
554
|
+
```yaml
|
|
555
|
+
entities:
|
|
556
|
+
- name: product
|
|
557
|
+
isRoot: true
|
|
558
|
+
tableName: products
|
|
559
|
+
hasSoftDelete: true # ✅ Activa soft delete
|
|
560
|
+
audit:
|
|
561
|
+
enabled: true
|
|
562
|
+
fields:
|
|
563
|
+
- name: id
|
|
564
|
+
type: String
|
|
565
|
+
- name: name
|
|
566
|
+
type: String
|
|
445
567
|
```
|
|
446
568
|
|
|
569
|
+
### Comportamiento generado
|
|
570
|
+
|
|
571
|
+
```java
|
|
572
|
+
// Entidad JPA — filtrado automático con @SQLRestriction
|
|
573
|
+
@Entity
|
|
574
|
+
@SQLRestriction("deleted_at IS NULL")
|
|
575
|
+
public class ProductJpa extends AuditableEntity {
|
|
576
|
+
@Column(name = "deleted_at")
|
|
577
|
+
private LocalDateTime deletedAt;
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
```java
|
|
582
|
+
// Entidad de dominio — método de negocio
|
|
583
|
+
public class Product {
|
|
584
|
+
private LocalDateTime deletedAt;
|
|
585
|
+
|
|
586
|
+
public void softDelete() {
|
|
587
|
+
if (this.deletedAt != null) {
|
|
588
|
+
throw new IllegalStateException("Product is already deleted");
|
|
589
|
+
}
|
|
590
|
+
this.deletedAt = LocalDateTime.now();
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
public boolean isDeleted() {
|
|
594
|
+
return this.deletedAt != null;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Reglas para Agentes
|
|
600
|
+
|
|
601
|
+
- **NUNCA** usar `repository.deleteById()` cuando hay soft delete
|
|
602
|
+
- **SIEMPRE** usar `entity.softDelete()` + `repository.save(entity)`
|
|
603
|
+
- **NUNCA** exponer `deletedAt` en ResponseDtos
|
|
604
|
+
- **SIEMPRE** usar `@SQLRestriction("deleted_at IS NULL")` en la entidad JPA
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
608
|
+
## ⏱️ Temporal Workflows
|
|
609
|
+
|
|
610
|
+
Cuando se agrega soporte de Temporal con `eva add temporal-client`, se genera infraestructura para workflows duraderos.
|
|
611
|
+
|
|
612
|
+
### Archivos generados por `eva g temporal-flow <module>`
|
|
613
|
+
|
|
614
|
+
```
|
|
615
|
+
[module]/
|
|
616
|
+
├── application/workflows/
|
|
617
|
+
│ ├── OrderWorkflow.java # Interface (@WorkflowInterface)
|
|
618
|
+
│ └── OrderWorkflowImpl.java # Implementación (determinista)
|
|
619
|
+
└── infrastructure/temporal/
|
|
620
|
+
├── activities/
|
|
621
|
+
│ ├── OrderActivity.java # Interface (@ActivityInterface)
|
|
622
|
+
│ └── OrderActivityImpl.java # Implementación (con I/O)
|
|
623
|
+
└── workers/
|
|
624
|
+
└── OrderWorker.java # Registro del worker
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### Principios clave
|
|
628
|
+
|
|
629
|
+
- Los **Workflows deben ser deterministas** — sin `Math.random()`, `new Date()`, ni I/O
|
|
630
|
+
- Toda operación con efectos secundarios (DB, HTTP, emails) va en **Activities**
|
|
631
|
+
- Los **Use Cases** orquestan los workflows; las **Activities** ejecutan infraestructura
|
|
632
|
+
- Configuración de conexión en `resources/parameters/{env}/temporal.yaml`
|
|
633
|
+
|
|
634
|
+
### Templates relacionados en eva4j
|
|
635
|
+
|
|
636
|
+
- `templates/temporal-flow/` — workflow interface e implementación
|
|
637
|
+
- `templates/temporal-activity/` — activity interface, implementación y worker
|
|
638
|
+
|
|
447
639
|
---
|
|
448
640
|
|
|
449
|
-
##
|
|
641
|
+
## �🚨 Errores Comunes a Evitar
|
|
450
642
|
|
|
451
643
|
### ❌ NO Crear Constructor Vacío en Dominio
|
|
452
644
|
|
|
@@ -557,7 +749,9 @@ Los campos en domain.yaml soportan las siguientes propiedades:
|
|
|
557
749
|
| `enumValues` | Array | `[]` | Valores inline de enum |
|
|
558
750
|
| **`readOnly`** | Boolean | `false` | **Excluye del constructor de negocio y CreateDto** |
|
|
559
751
|
| **`hidden`** | Boolean | `false` | **Excluye del ResponseDto** |
|
|
752
|
+
| **`defaultValue`** | String/Number/Boolean | `null` | **Valor inicial en el constructor de creación (solo para `readOnly`)** |
|
|
560
753
|
| **`validations`** | Array | `[]` | **Anotaciones JSR-303 en el Command y CreateDto** |
|
|
754
|
+
| **`reference`** | Object | `null` | **Declara referencia semántica a otro agregado (genera comentario Javadoc)** |
|
|
561
755
|
|
|
562
756
|
#### Flags de Visibilidad: `readOnly` y `hidden`
|
|
563
757
|
|
|
@@ -591,9 +785,57 @@ fields:
|
|
|
591
785
|
|-------|---------------------|-----------|-------------|
|
|
592
786
|
| Normal | ✅ | ✅ | ✅ |
|
|
593
787
|
| `readOnly: true` | ❌ | ❌ | ✅ |
|
|
788
|
+
| `readOnly` + `defaultValue` | ⚡ Asignado con default | ❌ | ✅ |
|
|
594
789
|
| `hidden: true` | ✅ | ✅ | ❌ |
|
|
595
790
|
| Ambos flags | ❌ | ❌ | ❌ |
|
|
596
791
|
|
|
792
|
+
#### 🎯 `defaultValue` - Valor Inicial para campos `readOnly`
|
|
793
|
+
|
|
794
|
+
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.
|
|
795
|
+
|
|
796
|
+
```yaml
|
|
797
|
+
fields:
|
|
798
|
+
- name: totalAmount
|
|
799
|
+
type: BigDecimal
|
|
800
|
+
readOnly: true
|
|
801
|
+
defaultValue: "0.00" # ✅ Acumulador inicializado
|
|
802
|
+
|
|
803
|
+
- name: status
|
|
804
|
+
type: OrderStatus
|
|
805
|
+
readOnly: true
|
|
806
|
+
defaultValue: PENDING # ✅ Estado inicial del enum
|
|
807
|
+
|
|
808
|
+
- name: itemCount
|
|
809
|
+
type: Integer
|
|
810
|
+
readOnly: true
|
|
811
|
+
defaultValue: 0 # ✅ Contador inicializado
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
```java
|
|
815
|
+
// Constructor de creación — defaultValues aplicados
|
|
816
|
+
public Order(String orderNumber, String customerId) {
|
|
817
|
+
this.orderNumber = orderNumber;
|
|
818
|
+
this.customerId = customerId;
|
|
819
|
+
this.totalAmount = new BigDecimal("0.00"); // ← defaultValue
|
|
820
|
+
this.status = OrderStatus.PENDING; // ← defaultValue
|
|
821
|
+
this.itemCount = 0; // ← defaultValue
|
|
822
|
+
}
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
```java
|
|
826
|
+
// JPA — @Builder.Default para respetar el valor en el builder
|
|
827
|
+
@Builder.Default
|
|
828
|
+
private BigDecimal totalAmount = new BigDecimal("0.00");
|
|
829
|
+
|
|
830
|
+
@Enumerated(EnumType.STRING)
|
|
831
|
+
@Builder.Default
|
|
832
|
+
private OrderStatus status = OrderStatus.PENDING;
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
**Restricción:** `defaultValue` **solo aplica** a campos con `readOnly: true`. Usarlo en un campo no-readOnly genera un warning y es ignorado.
|
|
836
|
+
|
|
837
|
+
---
|
|
838
|
+
|
|
597
839
|
**Ejemplo práctico:**
|
|
598
840
|
```yaml
|
|
599
841
|
entities:
|
|
@@ -616,6 +858,87 @@ entities:
|
|
|
616
858
|
hidden: true
|
|
617
859
|
```
|
|
618
860
|
|
|
861
|
+
### Validaciones JSR-303 (`validations`)
|
|
862
|
+
|
|
863
|
+
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.
|
|
864
|
+
|
|
865
|
+
```yaml
|
|
866
|
+
fields:
|
|
867
|
+
- name: email
|
|
868
|
+
type: String
|
|
869
|
+
validations:
|
|
870
|
+
- type: NotBlank
|
|
871
|
+
message: "Email es requerido"
|
|
872
|
+
- type: Email
|
|
873
|
+
message: "Email inválido"
|
|
874
|
+
|
|
875
|
+
- name: username
|
|
876
|
+
type: String
|
|
877
|
+
validations:
|
|
878
|
+
- type: Size
|
|
879
|
+
min: 3
|
|
880
|
+
max: 50
|
|
881
|
+
message: "Username entre 3 y 50 caracteres"
|
|
882
|
+
|
|
883
|
+
- name: age
|
|
884
|
+
type: Integer
|
|
885
|
+
validations:
|
|
886
|
+
- type: Min
|
|
887
|
+
value: 18
|
|
888
|
+
- type: Max
|
|
889
|
+
value: 120
|
|
890
|
+
|
|
891
|
+
- name: price
|
|
892
|
+
type: BigDecimal
|
|
893
|
+
validations:
|
|
894
|
+
- type: Positive
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
**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`.
|
|
898
|
+
|
|
899
|
+
**Código generado en `CreateUserCommand.java`:**
|
|
900
|
+
```java
|
|
901
|
+
public record CreateUserCommand(
|
|
902
|
+
@NotBlank(message = "Email es requerido")
|
|
903
|
+
@Email(message = "Email inválido")
|
|
904
|
+
String email,
|
|
905
|
+
|
|
906
|
+
@Size(min = 3, max = 50, message = "Username entre 3 y 50 caracteres")
|
|
907
|
+
String username
|
|
908
|
+
) implements Command {}
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
### Referencias entre Agregados (`reference`)
|
|
912
|
+
|
|
913
|
+
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).
|
|
914
|
+
|
|
915
|
+
```yaml
|
|
916
|
+
fields:
|
|
917
|
+
- name: customerId
|
|
918
|
+
type: String
|
|
919
|
+
reference:
|
|
920
|
+
aggregate: Customer # Nombre del agregado referenciado (PascalCase)
|
|
921
|
+
module: customers # Módulo donde vive (opcional si es el mismo módulo)
|
|
922
|
+
|
|
923
|
+
- name: productId
|
|
924
|
+
type: String
|
|
925
|
+
reference:
|
|
926
|
+
aggregate: Product
|
|
927
|
+
module: catalog
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
**Código generado:**
|
|
931
|
+
```java
|
|
932
|
+
// En Order.java (domain entity)
|
|
933
|
+
/** Cross-aggregate reference → Customer (module: customers) */
|
|
934
|
+
private String customerId;
|
|
935
|
+
|
|
936
|
+
// En OrderJpa.java
|
|
937
|
+
@Column(name = "customer_id")
|
|
938
|
+
/** Cross-aggregate reference → Customer (module: customers) */
|
|
939
|
+
private String customerId;
|
|
940
|
+
```
|
|
941
|
+
|
|
619
942
|
### Tipos de Relaciones
|
|
620
943
|
|
|
621
944
|
- `OneToOne` - Relación uno a uno
|
|
@@ -627,6 +950,14 @@ entities:
|
|
|
627
950
|
|
|
628
951
|
## 🎯 Mejores Prácticas para Agentes
|
|
629
952
|
|
|
953
|
+
### Al Generar domain.yaml (Flujo SDD)
|
|
954
|
+
|
|
955
|
+
1. **SIEMPRE** incluir campo `id` en todas las entidades
|
|
956
|
+
2. **SI** el módulo requiere ciclo de vida → usar `transitions` + `initialValue` en el enum
|
|
957
|
+
3. **SI** un valor tiene lógica de negocio → declararlo como `valueObject` con `methods`
|
|
958
|
+
4. **SI** ocurren hechos relevantes de negocio → declarar `events[]` en el agregado
|
|
959
|
+
5. **DESPUÉS** de generar el `domain.yaml` → ejecutar `eva g entities <module>`
|
|
960
|
+
|
|
630
961
|
### Al Generar Código de Dominio
|
|
631
962
|
|
|
632
963
|
1. **NUNCA** crear constructor vacío en entidades de dominio
|
|
@@ -712,13 +1043,13 @@ HTTP Response (sin createdBy/updatedBy)
|
|
|
712
1043
|
|
|
713
1044
|
## 🧪 Testing
|
|
714
1045
|
|
|
715
|
-
### Tests de Dominio
|
|
1046
|
+
### Tests de Dominio (Unidad Pura)
|
|
716
1047
|
|
|
717
1048
|
```java
|
|
718
1049
|
@Test
|
|
719
1050
|
void shouldCreateUserWithValidData() {
|
|
720
1051
|
User user = new User("john", "john@example.com");
|
|
721
|
-
|
|
1052
|
+
|
|
722
1053
|
assertEquals("john", user.getUsername());
|
|
723
1054
|
assertEquals("john@example.com", user.getEmail());
|
|
724
1055
|
}
|
|
@@ -726,13 +1057,82 @@ void shouldCreateUserWithValidData() {
|
|
|
726
1057
|
@Test
|
|
727
1058
|
void shouldValidateBusinessRules() {
|
|
728
1059
|
User user = new User("john", "john@example.com");
|
|
729
|
-
|
|
1060
|
+
|
|
730
1061
|
assertThrows(IllegalArgumentException.class, () -> {
|
|
731
1062
|
user.changeEmail("invalid-email");
|
|
732
1063
|
});
|
|
733
1064
|
}
|
|
734
1065
|
```
|
|
735
1066
|
|
|
1067
|
+
### Object Mother Pattern
|
|
1068
|
+
|
|
1069
|
+
```java
|
|
1070
|
+
// src/test/java/[package]/user/domain/UserMother.java
|
|
1071
|
+
public class UserMother {
|
|
1072
|
+
|
|
1073
|
+
public static User valid() {
|
|
1074
|
+
return new User("john_doe", "john@example.com");
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
public static User withEmail(String email) {
|
|
1078
|
+
return new User("john_doe", email);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
### Repositorio Fake (In-Memory)
|
|
1084
|
+
|
|
1085
|
+
Para testear Use Cases sin base de datos:
|
|
1086
|
+
|
|
1087
|
+
```java
|
|
1088
|
+
public class UserRepositoryFake implements UserRepository {
|
|
1089
|
+
private final Map<String, User> store = new HashMap<>();
|
|
1090
|
+
|
|
1091
|
+
@Override
|
|
1092
|
+
public User save(User user) {
|
|
1093
|
+
store.put(user.getId(), user);
|
|
1094
|
+
return user;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
@Override
|
|
1098
|
+
public Optional<User> findById(String id) {
|
|
1099
|
+
return Optional.ofNullable(store.get(id));
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
public int count() { return store.size(); }
|
|
1103
|
+
}
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
### Tests de Use Cases
|
|
1107
|
+
|
|
1108
|
+
```java
|
|
1109
|
+
class CreateUserCommandHandlerTest {
|
|
1110
|
+
private final UserRepositoryFake userRepository = new UserRepositoryFake();
|
|
1111
|
+
private final CreateUserCommandHandler handler =
|
|
1112
|
+
new CreateUserCommandHandler(userRepository);
|
|
1113
|
+
|
|
1114
|
+
@Test
|
|
1115
|
+
void shouldCreateUser() {
|
|
1116
|
+
CreateUserCommand command = new CreateUserCommand("john", "john@example.com");
|
|
1117
|
+
|
|
1118
|
+
String userId = handler.handle(command);
|
|
1119
|
+
|
|
1120
|
+
assertNotNull(userId);
|
|
1121
|
+
assertEquals(1, userRepository.count());
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
### Estrategia por Capa
|
|
1127
|
+
|
|
1128
|
+
| Capa | Tipo de Test | Framework |
|
|
1129
|
+
|------|--------------|-----------|
|
|
1130
|
+
| Domain entities | Unidad pura | JUnit 5 |
|
|
1131
|
+
| Use cases | Unidad con Fakes | JUnit 5 + Fake repos |
|
|
1132
|
+
| Application mappers | Unidad | JUnit 5 |
|
|
1133
|
+
| Repository implementations | Integración | Testcontainers |
|
|
1134
|
+
| REST controllers | Integración | MockMvc |
|
|
1135
|
+
|
|
736
1136
|
---
|
|
737
1137
|
|
|
738
1138
|
## 📖 Documentos Relacionados
|
|
@@ -748,23 +1148,50 @@ void shouldValidateBusinessRules() {
|
|
|
748
1148
|
|
|
749
1149
|
Al generar o modificar código, verificar:
|
|
750
1150
|
|
|
1151
|
+
**Entidades de Dominio:**
|
|
751
1152
|
- [ ] Entidades de dominio **sin constructor vacío**
|
|
752
1153
|
- [ ] Entidades de dominio **sin setters públicos**
|
|
753
1154
|
- [ ] Métodos de negocio con **validaciones explícitas**
|
|
1155
|
+
- [ ] Value Objects **inmutables**
|
|
1156
|
+
- [ ] Sin anotaciones JSR-303 en entidades de dominio
|
|
1157
|
+
|
|
1158
|
+
**Entidades JPA:**
|
|
754
1159
|
- [ ] Entidades JPA con **Lombok y herencia correcta**
|
|
1160
|
+
- [ ] `@SQLRestriction("deleted_at IS NULL")` cuando `hasSoftDelete: true`
|
|
1161
|
+
- [ ] No incluye campos de auditoría heredados en `@Builder`
|
|
1162
|
+
|
|
1163
|
+
**Mappers:**
|
|
755
1164
|
- [ ] Mappers **excluyen campos de auditoría**
|
|
756
1165
|
- [ ] Mappers **excluyen campos readOnly en creación**
|
|
757
1166
|
- [ ] Mappers **excluyen campos hidden en respuestas**
|
|
1167
|
+
- [ ] Relaciones bidireccionales con métodos `assign*()`
|
|
1168
|
+
|
|
1169
|
+
**DTOs:**
|
|
758
1170
|
- [ ] DTOs de respuesta **sin createdBy/updatedBy**
|
|
759
1171
|
- [ ] DTOs de respuesta **sin campos hidden**
|
|
760
1172
|
- [ ] DTOs de creación **sin campos readOnly**
|
|
761
|
-
- [ ]
|
|
762
|
-
|
|
763
|
-
|
|
1173
|
+
- [ ] Usando Java Records
|
|
1174
|
+
|
|
1175
|
+
**Validaciones:**
|
|
764
1176
|
- [ ] Validaciones JSR-303 **solo en Command y CreateDto, nunca en dominio**
|
|
1177
|
+
- [ ] `@Valid` en parámetros de endpoints REST
|
|
1178
|
+
|
|
1179
|
+
**Auditoría:**
|
|
1180
|
+
- [ ] Configuración de auditoría cuando `trackUser: true`
|
|
1181
|
+
- [ ] `@EnableJpaAuditing` con `auditorAwareRef = "auditorProvider"` en Application
|
|
1182
|
+
|
|
1183
|
+
**Soft Delete (cuando aplica):**
|
|
1184
|
+
- [ ] Usar `entity.softDelete()` + `repository.save()` — nunca `deleteById()`
|
|
1185
|
+
- [ ] `deletedAt` no expuesto en ResponseDto
|
|
1186
|
+
|
|
1187
|
+
**Características Avanzadas (cuando aplica):**
|
|
1188
|
+
- [ ] Enum con ciclo de vida → usar `transitions` + `initialValue`, no setters manuales
|
|
1189
|
+
- [ ] Value Object con comportamiento → declarar `methods` en lugar de lógica en entidad
|
|
1190
|
+
- [ ] Evento de dominio → declarar en `events[]`, publicar con `raise()` en método de negocio
|
|
1191
|
+
- [ ] Evento con Kafka → agregar `kafka: true` al evento
|
|
765
1192
|
|
|
766
1193
|
---
|
|
767
1194
|
|
|
768
|
-
**Última actualización:** 2026-02
|
|
769
|
-
**Versión de eva4j:** 1.
|
|
1195
|
+
**Última actualización:** 2026-03-02
|
|
1196
|
+
**Versión de eva4j:** 1.0.12
|
|
770
1197
|
**Estado:** Documento de referencia para agentes IA
|