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/DOMAIN_YAML_GUIDE.md
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
- [Validaciones JSR-303](#validaciones-jsr-303)
|
|
14
14
|
- [Relaciones](#relaciones)
|
|
15
15
|
- [Tipos de Datos](#tipos-de-datos)
|
|
16
|
+
- [Sección endpoints](#sección-endpoints)
|
|
17
|
+
- [Sección listeners](#sección-listeners)
|
|
16
18
|
- [Ejemplos Completos](#ejemplos-completos)
|
|
17
19
|
|
|
18
20
|
---
|
|
@@ -134,10 +136,17 @@ aggregates:
|
|
|
134
136
|
values: []
|
|
135
137
|
|
|
136
138
|
events:
|
|
137
|
-
# Eventos de dominio que emite este agregado
|
|
139
|
+
# Eventos de dominio que emite este agregado (dentro del agregado)
|
|
138
140
|
- name: NombreEventoOcurrido
|
|
139
141
|
fields: []
|
|
140
|
-
|
|
142
|
+
|
|
143
|
+
# listeners: — eventos externos que CONSUME este módulo (nivel raíz)
|
|
144
|
+
listeners:
|
|
145
|
+
- event: ExternalEvent # Nombre del evento (PascalCase + Event)
|
|
146
|
+
producer: other-module # Módulo que lo produce
|
|
147
|
+
topic: TOPIC_NAME # Topic Kafka (obligatorio en módulos standalone)
|
|
148
|
+
useCase: HandleExternalEvent # Caso de uso que maneja el evento
|
|
149
|
+
fields: [] # Payload del Integration Event recibido
|
|
141
150
|
```
|
|
142
151
|
|
|
143
152
|
### Ubicación del archivo
|
|
@@ -1279,6 +1288,100 @@ CREATE TABLE reviews (
|
|
|
1279
1288
|
|
|
1280
1289
|
---
|
|
1281
1290
|
|
|
1291
|
+
## Soft Delete
|
|
1292
|
+
|
|
1293
|
+
Cuando una entidad tiene `hasSoftDelete: true`, eva4j genera **eliminación lógica** en lugar de física: el registro no se borra de la base de datos, sino que se marca con un timestamp `deletedAt`.
|
|
1294
|
+
|
|
1295
|
+
### Configuración en domain.yaml
|
|
1296
|
+
|
|
1297
|
+
```yaml
|
|
1298
|
+
aggregates:
|
|
1299
|
+
- name: Order
|
|
1300
|
+
entities:
|
|
1301
|
+
- name: order
|
|
1302
|
+
isRoot: true # ← OBLIGATORIO: solo aplica en la raíz del agregado
|
|
1303
|
+
tableName: orders
|
|
1304
|
+
hasSoftDelete: true # ✅ Activa soft delete
|
|
1305
|
+
audit:
|
|
1306
|
+
enabled: true
|
|
1307
|
+
fields:
|
|
1308
|
+
- name: id
|
|
1309
|
+
type: String
|
|
1310
|
+
- name: orderNumber
|
|
1311
|
+
type: String
|
|
1312
|
+
|
|
1313
|
+
- name: orderItem # ← Entidad secundaria
|
|
1314
|
+
tableName: order_items
|
|
1315
|
+
# hasSoftDelete: true ← ❌ INCORRECTO — se ignora con warning
|
|
1316
|
+
# Las secundarias se eliminan físicamente
|
|
1317
|
+
# vía CASCADE desde la raíz
|
|
1318
|
+
```
|
|
1319
|
+
|
|
1320
|
+
> **Regla de alcance:** `hasSoftDelete` **solo es válido en la entidad raíz** (`isRoot: true`). Ponerlo en una entidad secundaria emite un warning y es ignorado.
|
|
1321
|
+
|
|
1322
|
+
### Por qué solo en la raíz
|
|
1323
|
+
|
|
1324
|
+
En DDD, una entidad secundaria como `OrderItem` no tiene existencia independiente — su ciclo de vida lo controla el agregado raíz. Nunca se elimina directamente: se elimina removiéndola de la colección del raíz y JPA gestiona el `DELETE` físico vía `cascade`. Aplicar `@SQLRestriction` en una entidad secundaria rompería las relaciones: los items "eliminados" desaparecerían silenciosamente del `@OneToMany`, creando inconsistencias difíciles de detectar.
|
|
1325
|
+
|
|
1326
|
+
### Qué genera el flag
|
|
1327
|
+
|
|
1328
|
+
**Entidad de dominio:**
|
|
1329
|
+
```java
|
|
1330
|
+
public class Order {
|
|
1331
|
+
private LocalDateTime deletedAt; // ← Inyectado automáticamente
|
|
1332
|
+
|
|
1333
|
+
public void softDelete() {
|
|
1334
|
+
if (this.deletedAt != null) {
|
|
1335
|
+
throw new IllegalStateException("Order is already deleted");
|
|
1336
|
+
}
|
|
1337
|
+
this.deletedAt = java.time.LocalDateTime.now();
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
public boolean isDeleted() {
|
|
1341
|
+
return this.deletedAt != null;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
```
|
|
1345
|
+
|
|
1346
|
+
**Entidad JPA:**
|
|
1347
|
+
```java
|
|
1348
|
+
@SQLRestriction("deleted_at IS NULL") // ← Filtra automáticamente en TODAS las queries
|
|
1349
|
+
@Entity
|
|
1350
|
+
@Table(name = "orders")
|
|
1351
|
+
public class OrderJpa extends AuditableEntity {
|
|
1352
|
+
private LocalDateTime deletedAt; // ← Columna deleted_at
|
|
1353
|
+
}
|
|
1354
|
+
```
|
|
1355
|
+
|
|
1356
|
+
**DeleteCommandHandler:**
|
|
1357
|
+
```java
|
|
1358
|
+
// Soft delete: busca → marca → guarda (nunca deleteById)
|
|
1359
|
+
Order order = repository.findById(id)
|
|
1360
|
+
.orElseThrow(() -> new OrderNotFoundException(id));
|
|
1361
|
+
order.softDelete();
|
|
1362
|
+
repository.save(order);
|
|
1363
|
+
```
|
|
1364
|
+
|
|
1365
|
+
### Campos excluidos automáticamente
|
|
1366
|
+
|
|
1367
|
+
| Artefacto | `deletedAt` incluido | Motivo |
|
|
1368
|
+
|---|---|---|
|
|
1369
|
+
| Constructor completo (reconstrucción desde BD) | ✅ | Necesario para hidratar el estado |
|
|
1370
|
+
| Constructor de creación (nuevo objeto) | ❌ | Inicia siempre como `null` |
|
|
1371
|
+
| `CreateDto` / `CreateCommand` | ❌ | No se crea un objeto ya eliminado |
|
|
1372
|
+
| `ResponseDto` | ❌ | Metadato interno, no se expone en API |
|
|
1373
|
+
| `toJpa()` en el mapper | ✅ | Persiste el timestamp cuando se llama `softDelete()` |
|
|
1374
|
+
|
|
1375
|
+
### Notas importantes
|
|
1376
|
+
|
|
1377
|
+
- ✅ Compatible con `audit: { enabled: true }` y `audit: { trackUser: true }` — se pueden combinar
|
|
1378
|
+
- ✅ `@SQLRestriction` hace invisibles los registros eliminados en **todas** las queries automáticamente, sin necesidad de filtros manuales
|
|
1379
|
+
- ✅ `deletedAt` **no debe** definirse manualmente en `fields` — el generador lo inyecta
|
|
1380
|
+
- ❌ `hasSoftDelete: true` en una entidad secundaria es ignorado con warning
|
|
1381
|
+
- ❌ Cuando `hasSoftDelete: true`, usar `repository.deleteById()` genera un `DeleteCommandHandler` incorrecto — el generador lo previene automáticamente
|
|
1382
|
+
|
|
1383
|
+
---
|
|
1384
|
+
|
|
1282
1385
|
## Value Objects
|
|
1283
1386
|
|
|
1284
1387
|
Los Value Objects son objetos inmutables que representan conceptos del dominio sin identidad propia.
|
|
@@ -1594,7 +1697,7 @@ aggregates:
|
|
|
1594
1697
|
type: LocalDateTime
|
|
1595
1698
|
|
|
1596
1699
|
- name: OrderShippedEvent
|
|
1597
|
-
|
|
1700
|
+
topic: ORDER_SHIPPED # opcional — override del topic auto-derivado
|
|
1598
1701
|
fields:
|
|
1599
1702
|
- name: orderId
|
|
1600
1703
|
type: String
|
|
@@ -1604,11 +1707,16 @@ aggregates:
|
|
|
1604
1707
|
|
|
1605
1708
|
### Propiedades de un evento
|
|
1606
1709
|
|
|
1607
|
-
| Propiedad | Tipo | Descripción |
|
|
1608
|
-
|
|
1609
|
-
| `name` | String | Nombre de la clase del evento (PascalCase) |
|
|
1610
|
-
| `fields` | Array | Campos que transporta el evento |
|
|
1611
|
-
| `
|
|
1710
|
+
| Propiedad | Tipo | Obligatorio | Descripción |
|
|
1711
|
+
|-----------|------|-------------|-------------|
|
|
1712
|
+
| `name` | String | ✅ | Nombre de la clase del evento (PascalCase) |
|
|
1713
|
+
| `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. |
|
|
1715
|
+
| `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
|
+
|
|
1717
|
+
> **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
|
+
|
|
1719
|
+
> **`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`.
|
|
1612
1720
|
|
|
1613
1721
|
### Archivos generados
|
|
1614
1722
|
|
|
@@ -1643,7 +1751,7 @@ public class OrderDomainEventHandler {
|
|
|
1643
1751
|
|
|
1644
1752
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
|
1645
1753
|
public void handle(OrderShippedEvent event) {
|
|
1646
|
-
messageBroker.sendOrderShippedEvent(event);
|
|
1754
|
+
messageBroker.sendOrderShippedEvent(event);
|
|
1647
1755
|
}
|
|
1648
1756
|
}
|
|
1649
1757
|
```
|
|
@@ -1664,6 +1772,316 @@ public void confirm() {
|
|
|
1664
1772
|
|
|
1665
1773
|
---
|
|
1666
1774
|
|
|
1775
|
+
## Sección listeners
|
|
1776
|
+
|
|
1777
|
+
La sección `listeners:` declara los eventos externos que **consume** este módulo. Es el complemento de `events:` (producción): mientras que `events:` vive _dentro_ del agregado porque pertenece al modelo de dominio, `listeners:` vive en el **nivel raíz** del `domain.yaml` porque es una responsabilidad de integración/infraestructura.
|
|
1778
|
+
|
|
1779
|
+
> **Requiere broker instalado.** El generador solo produce archivos de listener cuando `eva add kafka-client` ha sido ejecutado en el proyecto. Sin broker, la sección es ignorada.
|
|
1780
|
+
|
|
1781
|
+
### Sintaxis
|
|
1782
|
+
|
|
1783
|
+
```yaml
|
|
1784
|
+
# Nivel raíz — sibling de aggregates:
|
|
1785
|
+
listeners:
|
|
1786
|
+
- event: PaymentApprovedEvent # PascalCase + sufijo Event
|
|
1787
|
+
producer: payments # Módulo que produce el evento (referencia documental)
|
|
1788
|
+
topic: PAYMENT_APPROVED # Topic Kafka — obligatorio en módulos standalone
|
|
1789
|
+
useCase: ConfirmOrder # Caso de uso invocado al consumir (PascalCase)
|
|
1790
|
+
fields: # Payload del Integration Event recibido
|
|
1791
|
+
- name: orderId
|
|
1792
|
+
type: String
|
|
1793
|
+
- name: approvedAt
|
|
1794
|
+
type: LocalDateTime
|
|
1795
|
+
- name: details
|
|
1796
|
+
type: PaymentDetails # Campo objeto → declarado en nestedTypes:
|
|
1797
|
+
nestedTypes: # Opcional: records auxiliares para campos objeto
|
|
1798
|
+
- name: paymentDetails # camelCase → PascalCase en el record generado
|
|
1799
|
+
fields:
|
|
1800
|
+
- name: paymentId
|
|
1801
|
+
type: String
|
|
1802
|
+
- name: amount
|
|
1803
|
+
type: BigDecimal
|
|
1804
|
+
```
|
|
1805
|
+
|
|
1806
|
+
### Propiedades
|
|
1807
|
+
|
|
1808
|
+
| Propiedad | Requerido | Descripción |
|
|
1809
|
+
|-----------|-----------|-------------|
|
|
1810
|
+
| `event` | ✅ | Nombre del evento en PascalCase, con sufijo `Event` |
|
|
1811
|
+
| `producer` | ✅ | Módulo que produce el evento (solo referencia documental, no genera código) |
|
|
1812
|
+
| `topic` | ✅ | Topic Kafka. Si existe `system.yaml`, el generador puede inferirlo de `integrations.async[].topic`; en módulos **standalone** es obligatorio declararlo explícitamente. |
|
|
1813
|
+
| `useCase` | ✅ | Nombre del caso de uso que maneja el evento (PascalCase) |
|
|
1814
|
+
| `fields` | ✅ | Campos del payload recibido; genera el record `IntegrationEvent` y tipifica el Command despachado |
|
|
1815
|
+
| `nestedTypes` | ❌ | Records auxiliares para campos de tipo objeto en `fields:`. Cada entrada genera un `.java` record en `application/events/`. |
|
|
1816
|
+
|
|
1817
|
+
### Archivos generados
|
|
1818
|
+
|
|
1819
|
+
Para cada entrada en `listeners:`, eva4j genera **5 archivos** (más un record por cada entrada en `nestedTypes:`):
|
|
1820
|
+
|
|
1821
|
+
| # | Archivo | Ubicación | Descripción |
|
|
1822
|
+
|---|---------|-----------|-------------|
|
|
1823
|
+
| 0 | `{NestedName}.java` *(por nestedType)* | `application/events/` | Record auxiliar para campos objeto |
|
|
1824
|
+
| 1 | `{Name}IntegrationEvent.java` | `application/events/` | Record contrato tipado (documentación + tests) |
|
|
1825
|
+
| 2 | `{Name}KafkaListener.java` | `infrastructure/kafkaListener/` | `@KafkaListener` — deserializa y despacha |
|
|
1826
|
+
| 3 | `kafka.yaml` *(todas las envs)* | `resources/parameters/*/` | Topic registrado bajo `topics:` |
|
|
1827
|
+
| 4 | `{UseCase}Command.java` | `application/commands/` | Comando tipado despachado desde el listener |
|
|
1828
|
+
| 5 | `{UseCase}CommandHandler.java` | `application/usecases/` | Stub del handler (implementar la lógica aquí) |
|
|
1829
|
+
|
|
1830
|
+
### Código generado
|
|
1831
|
+
|
|
1832
|
+
**`PaymentDetails.java`** — `application/events/` (de `nestedTypes:`)
|
|
1833
|
+
```java
|
|
1834
|
+
public record PaymentDetails(
|
|
1835
|
+
String paymentId,
|
|
1836
|
+
BigDecimal amount
|
|
1837
|
+
) {}
|
|
1838
|
+
```
|
|
1839
|
+
|
|
1840
|
+
**`PaymentApprovedIntegrationEvent.java`** — `application/events/`
|
|
1841
|
+
```java
|
|
1842
|
+
public record PaymentApprovedIntegrationEvent(
|
|
1843
|
+
String orderId,
|
|
1844
|
+
LocalDateTime approvedAt,
|
|
1845
|
+
PaymentDetails details
|
|
1846
|
+
) {}
|
|
1847
|
+
```
|
|
1848
|
+
|
|
1849
|
+
**`ConfirmOrderCommand.java`** — `application/commands/`
|
|
1850
|
+
```java
|
|
1851
|
+
import com.example.orders.application.events.PaymentDetails;
|
|
1852
|
+
|
|
1853
|
+
public record ConfirmOrderCommand(
|
|
1854
|
+
String orderId,
|
|
1855
|
+
LocalDateTime approvedAt,
|
|
1856
|
+
PaymentDetails details
|
|
1857
|
+
) implements Command {}
|
|
1858
|
+
```
|
|
1859
|
+
|
|
1860
|
+
**`PaymentApprovedKafkaListener.java`** — `infrastructure/kafkaListener/`
|
|
1861
|
+
```java
|
|
1862
|
+
import com.example.orders.application.events.PaymentDetails;
|
|
1863
|
+
|
|
1864
|
+
@Component
|
|
1865
|
+
public class PaymentApprovedKafkaListener {
|
|
1866
|
+
|
|
1867
|
+
private final UseCaseMediator useCaseMediator;
|
|
1868
|
+
private final ObjectMapper objectMapper;
|
|
1869
|
+
|
|
1870
|
+
@Value("${topics.payment-approved}")
|
|
1871
|
+
private String paymentApprovedTopic;
|
|
1872
|
+
|
|
1873
|
+
public PaymentApprovedKafkaListener(UseCaseMediator useCaseMediator,
|
|
1874
|
+
ObjectMapper objectMapper) {
|
|
1875
|
+
this.useCaseMediator = useCaseMediator;
|
|
1876
|
+
this.objectMapper = objectMapper;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
@KafkaListener(topics = "${topics.payment-approved}")
|
|
1880
|
+
public void handle(EventEnvelope<Map<String, Object>> event, Acknowledgment ack) {
|
|
1881
|
+
String orderId = objectMapper.convertValue(event.data().get("orderId"), String.class);
|
|
1882
|
+
LocalDateTime approvedAt = objectMapper.convertValue(
|
|
1883
|
+
event.data().get("approvedAt"), LocalDateTime.class);
|
|
1884
|
+
PaymentDetails details = objectMapper.convertValue(
|
|
1885
|
+
event.data().get("details"), PaymentDetails.class);
|
|
1886
|
+
useCaseMediator.dispatch(new ConfirmOrderCommand(orderId, approvedAt, details));
|
|
1887
|
+
ack.acknowledge();
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
```
|
|
1891
|
+
|
|
1892
|
+
**`ConfirmOrderCommandHandler.java`** — `application/usecases/`
|
|
1893
|
+
```java
|
|
1894
|
+
@ApplicationComponent
|
|
1895
|
+
public class ConfirmOrderCommandHandler implements CommandHandler<ConfirmOrderCommand> {
|
|
1896
|
+
|
|
1897
|
+
@Override
|
|
1898
|
+
public void handle(ConfirmOrderCommand command) {
|
|
1899
|
+
// TODO: implement ConfirmOrder business logic
|
|
1900
|
+
throw new UnsupportedOperationException("ConfirmOrderCommandHandler not yet implemented");
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
```
|
|
1904
|
+
|
|
1905
|
+
### Deserialización — cómo funciona
|
|
1906
|
+
|
|
1907
|
+
El consumidor Kafka recibe un `EventEnvelope<Map<String, Object>>`. El listener generado extrae cada campo con `objectMapper.convertValue()`, que maneja:
|
|
1908
|
+
|
|
1909
|
+
- Primitivos y strings: `convertValue(map.get("field"), String.class)`
|
|
1910
|
+
- Fechas: `convertValue(map.get("date"), LocalDateTime.class)` — requiere el módulo Jackson JavaTime
|
|
1911
|
+
- Objetos / nested types: `convertValue(map.get("details"), PaymentDetails.class)` — funciona automáticamente para Java records
|
|
1912
|
+
- Listas: `convertValue(map.get("items"), typeFactory.constructCollectionType(List.class, ItemType.class))`
|
|
1913
|
+
|
|
1914
|
+
### `nestedTypes:` — cuándo usarlo
|
|
1915
|
+
|
|
1916
|
+
Usar `nestedTypes:` cuando uno de los `fields:` es un objeto estructurado (no un tipo Java primitivo). Cada entrada genera un Java record en `application/events/` que se importa automáticamente tanto en el `KafkaListener` como en el `Command`.
|
|
1917
|
+
|
|
1918
|
+
El nombre se declara en `camelCase` y el generador lo normaliza a `PascalCase`:
|
|
1919
|
+
```yaml
|
|
1920
|
+
nestedTypes:
|
|
1921
|
+
- name: paymentDetails # → PaymentDetails.java
|
|
1922
|
+
fields:
|
|
1923
|
+
- name: paymentId
|
|
1924
|
+
type: String
|
|
1925
|
+
- name: amount
|
|
1926
|
+
type: BigDecimal
|
|
1927
|
+
```
|
|
1928
|
+
|
|
1929
|
+
`{Name}IntegrationEvent.java` **no necesita importar** los nested types porque vive en el mismo paquete `application/events/`.
|
|
1930
|
+
|
|
1931
|
+
### Regla de resolución de `topic:`
|
|
1932
|
+
|
|
1933
|
+
| Escenario | Comportamiento |
|
|
1934
|
+
|-----------|---------------|
|
|
1935
|
+
| Módulo standalone (solo `domain.yaml`) | `topic:` **obligatorio** — no hay otra fuente de verdad |
|
|
1936
|
+
| Proyecto con `system.yaml` | `topic:` puede omitirse; se infiere de `integrations.async[].topic` |
|
|
1937
|
+
| `topic:` declarado explícitamente con `system.yaml` | El valor declarado tiene **precedencia** sobre la inferencia |
|
|
1938
|
+
|
|
1939
|
+
### Contraste: producción vs. consumo
|
|
1940
|
+
|
|
1941
|
+
```
|
|
1942
|
+
domain.yaml
|
|
1943
|
+
├── aggregates:
|
|
1944
|
+
│ └── [Aggregate]
|
|
1945
|
+
│ └── events: → Domain Events que PRODUCE (domain/models/events/)
|
|
1946
|
+
│
|
|
1947
|
+
├── listeners: → Integration Events que CONSUME (infrastructure/kafkaListener/)
|
|
1948
|
+
│
|
|
1949
|
+
└── ports: → Servicios HTTP que LLAMA (infrastructure/adapters/{service}/)
|
|
1950
|
+
```
|
|
1951
|
+
|
|
1952
|
+
---
|
|
1953
|
+
|
|
1954
|
+
## Sección ports
|
|
1955
|
+
|
|
1956
|
+
`ports:` es una sección de nivel raíz (sibling de `aggregates:` y `listeners:`) que declara los servicios HTTP síncronos que el módulo llama, implementados con Spring Cloud OpenFeign.
|
|
1957
|
+
|
|
1958
|
+
**Regla de agrupación:** una **entrada** = un **método**. Entradas que comparten el mismo `service:` se generan en un único FeignClient.
|
|
1959
|
+
|
|
1960
|
+
### Propiedades de una entrada en `ports[]`
|
|
1961
|
+
|
|
1962
|
+
| Propiedad | Tipo | Obligatorio | Descripción |
|
|
1963
|
+
|-----------|------|-------------|-------------|
|
|
1964
|
+
| `name` | String | ✅ | Nombre del método (camelCase) |
|
|
1965
|
+
| `service` | String | ✅ | Nombre del servicio/interfaz (PascalCase). Agrupa métodos en un FeignClient |
|
|
1966
|
+
| `target` | String | ❌ | Módulo destino — referencia documental; no afecta la generación |
|
|
1967
|
+
| `baseUrl` | String | ❌* | URL base del servicio. Declarar solo en la **primera entrada** del `service:` |
|
|
1968
|
+
| `http` | String | ✅ | Verbo HTTP + path: `GET /resource/{id}`, `POST /resource`, etc. |
|
|
1969
|
+
| `fields` | Array | ❌ | Campos de la respuesta → genera `{MethodPascal}ResponseDto.java` |
|
|
1970
|
+
| `body` | Array | ❌ | Campos del cuerpo de la petición (solo POST/PUT/PATCH) → genera `{MethodPascal}RequestDto.java` |
|
|
1971
|
+
| `returnList` | Boolean | ❌ | `true` → retorno `List<{MethodPascal}ResponseDto>`. Default: `false` |
|
|
1972
|
+
| `nestedTypes` | Array | ❌ | Records auxiliares para campos de tipo objeto en `body:` o `fields:` |
|
|
1973
|
+
|
|
1974
|
+
*Si se omite `baseUrl` en todas las entradas de un `service:`, se emite un warning y se usa `http://localhost:8080`.
|
|
1975
|
+
|
|
1976
|
+
### Reglas de uso
|
|
1977
|
+
|
|
1978
|
+
- **`baseUrl:`** — declarar únicamente en la primera entrada del `service:`. Eva4j la registra en `parameters/{env}/urls.yaml` como `{module-kebab}.{service-kebab}.base-url`.
|
|
1979
|
+
- **`body:`** — válido solo en POST, PUT y PATCH. En GET/DELETE emite warning y se ignora.
|
|
1980
|
+
- **`returnList: true`** — el tipo de retorno pasa de `{Method}ResponseDto` a `List<{Method}ResponseDto>`.
|
|
1981
|
+
- **`fields:` omitido** → retorno `void` en la interfaz del puerto y en el FeignClient.
|
|
1982
|
+
- **`nestedTypes:`** — se generan como records separados en `application/dtos/`. Se deduplican por nombre dentro del mismo `service:`. Aplica tanto para campos en `body:` como en `fields:`.
|
|
1983
|
+
|
|
1984
|
+
### Estructura mínima
|
|
1985
|
+
|
|
1986
|
+
```yaml
|
|
1987
|
+
# Un solo método GET, respuesta simple
|
|
1988
|
+
ports:
|
|
1989
|
+
- name: findScreeningById
|
|
1990
|
+
service: ScreeningService
|
|
1991
|
+
target: screenings
|
|
1992
|
+
baseUrl: http://localhost:8081
|
|
1993
|
+
http: GET /screenings/{id}
|
|
1994
|
+
fields:
|
|
1995
|
+
- name: id
|
|
1996
|
+
type: String
|
|
1997
|
+
- name: startTime
|
|
1998
|
+
type: LocalDateTime
|
|
1999
|
+
```
|
|
2000
|
+
|
|
2001
|
+
### Ejemplo con todos los patrones
|
|
2002
|
+
|
|
2003
|
+
```yaml
|
|
2004
|
+
ports:
|
|
2005
|
+
# GET con variable de ruta
|
|
2006
|
+
- name: findScreeningById
|
|
2007
|
+
service: ScreeningService
|
|
2008
|
+
target: screenings
|
|
2009
|
+
baseUrl: http://localhost:8081 # ← solo en la primera entrada del service
|
|
2010
|
+
http: GET /screenings/{id}
|
|
2011
|
+
fields:
|
|
2012
|
+
- name: id
|
|
2013
|
+
type: String
|
|
2014
|
+
- name: startTime
|
|
2015
|
+
type: LocalDateTime
|
|
2016
|
+
|
|
2017
|
+
# GET retornando lista
|
|
2018
|
+
- name: findAvailableSeats
|
|
2019
|
+
service: ScreeningService # mismo service → mismo FeignClient
|
|
2020
|
+
target: screenings
|
|
2021
|
+
http: GET /screenings/{id}/seats
|
|
2022
|
+
returnList: true # → List<FindAvailableSeatResponseDto>
|
|
2023
|
+
fields:
|
|
2024
|
+
- name: seatId
|
|
2025
|
+
type: String
|
|
2026
|
+
- name: seatType
|
|
2027
|
+
type: String
|
|
2028
|
+
|
|
2029
|
+
# POST con body + nestedType + respuesta
|
|
2030
|
+
- name: processPayment
|
|
2031
|
+
service: PaymentGateway
|
|
2032
|
+
target: payment-gateway-external
|
|
2033
|
+
baseUrl: https://api.payments.example.com
|
|
2034
|
+
http: POST /payments
|
|
2035
|
+
body:
|
|
2036
|
+
- name: amount
|
|
2037
|
+
type: BigDecimal
|
|
2038
|
+
- name: paymentMethod
|
|
2039
|
+
type: PaymentMethodInput # tipo objeto → declarar en nestedTypes:
|
|
2040
|
+
nestedTypes:
|
|
2041
|
+
- name: paymentMethodInput
|
|
2042
|
+
fields:
|
|
2043
|
+
- name: type
|
|
2044
|
+
type: String
|
|
2045
|
+
- name: cardToken
|
|
2046
|
+
type: String
|
|
2047
|
+
fields:
|
|
2048
|
+
- name: paymentId
|
|
2049
|
+
type: String
|
|
2050
|
+
- name: status
|
|
2051
|
+
type: String
|
|
2052
|
+
|
|
2053
|
+
# DELETE void (sin fields)
|
|
2054
|
+
- name: cancelPayment
|
|
2055
|
+
service: PaymentGateway
|
|
2056
|
+
target: payment-gateway-external
|
|
2057
|
+
http: DELETE /payments/{id}
|
|
2058
|
+
# fields: omitido → retorno void
|
|
2059
|
+
```
|
|
2060
|
+
|
|
2061
|
+
### Artefactos generados por `service:` único
|
|
2062
|
+
|
|
2063
|
+
| Archivo | Descripción |
|
|
2064
|
+
|---------|-------------|
|
|
2065
|
+
| `domain/repositories/{ServiceName}.java` | Interfaz del puerto secundario |
|
|
2066
|
+
| `infrastructure/adapters/{service}/{ServiceName}FeignClient.java` | `@FeignClient` tipado |
|
|
2067
|
+
| `infrastructure/adapters/{service}/{ServiceName}FeignAdapter.java` | `@Component implements {ServiceName}` |
|
|
2068
|
+
| `infrastructure/adapters/{service}/{ServiceName}FeignConfig.java` | Configuración de timeouts |
|
|
2069
|
+
| `parameters/{env}/urls.yaml` | Propiedad `{module}.{service}.base-url` |
|
|
2070
|
+
|
|
2071
|
+
### Artefactos generados por método
|
|
2072
|
+
|
|
2073
|
+
| Archivo | Condición |
|
|
2074
|
+
|---------|-----------|
|
|
2075
|
+
| `application/dtos/{MethodPascal}ResponseDto.java` | Cuando `fields:` tiene elementos |
|
|
2076
|
+
| `application/dtos/{MethodPascal}RequestDto.java` | Cuando `body:` tiene elementos (POST/PUT/PATCH) |
|
|
2077
|
+
| `application/dtos/{NestedTypePascal}.java` | Por cada entrada en `nestedTypes:` |
|
|
2078
|
+
|
|
2079
|
+
### Referencia
|
|
2080
|
+
|
|
2081
|
+
Ver ejemplo completo en [`examples/domain-ports.yaml`](examples/domain-ports.yaml).
|
|
2082
|
+
|
|
2083
|
+
---
|
|
2084
|
+
|
|
1667
2085
|
## Relaciones
|
|
1668
2086
|
|
|
1669
2087
|
eva4j soporta relaciones JPA bidireccionales completas con generación automática del lado inverso.
|
|
@@ -2441,6 +2859,154 @@ private List<AddressJpa> addresses = new ArrayList<>();
|
|
|
2441
2859
|
|
|
2442
2860
|
---
|
|
2443
2861
|
|
|
2862
|
+
## Sección endpoints
|
|
2863
|
+
|
|
2864
|
+
La sección `endpoints:` es **opcional** y se declara como clave hermana de `aggregates:` en el YAML. Cuando está presente, controla **qué use cases y controladores REST se generan**. Cuando está ausente, el generador usa el flujo interactivo tradicional (5 CRUD fijos por aggregate root).
|
|
2865
|
+
|
|
2866
|
+
### Comportamiento condicional
|
|
2867
|
+
|
|
2868
|
+
| Condición | Comportamiento |
|
|
2869
|
+
|-----------|---------------|
|
|
2870
|
+
| `endpoints:` **ausente** | Pregunta interactiva "¿Generar CRUD?" → genera 5 use cases estándar |
|
|
2871
|
+
| `endpoints:` **presente** | Genera automáticamente solo los use cases declarados en `operations[]` |
|
|
2872
|
+
|
|
2873
|
+
### Sintaxis
|
|
2874
|
+
|
|
2875
|
+
```yaml
|
|
2876
|
+
# Sección endpoints: sibling de aggregates:
|
|
2877
|
+
endpoints:
|
|
2878
|
+
basePath: /orders # Ruta base (incluida en @RequestMapping "/api/{version}{basePath}")
|
|
2879
|
+
versions:
|
|
2880
|
+
- version: v1 # Versión del API (ej: v1, v2, v1-beta)
|
|
2881
|
+
operations:
|
|
2882
|
+
- method: GET # HTTP method (GET, POST, PUT, PATCH, DELETE)
|
|
2883
|
+
path: /{id} # Path relativo al basePath (/ para la raíz)
|
|
2884
|
+
useCase: GetOrder # Nombre del use case (PascalCase)
|
|
2885
|
+
description: "Obtener pedido por ID" # Descripción para Swagger
|
|
2886
|
+
```
|
|
2887
|
+
|
|
2888
|
+
### Campos de `endpoints:`
|
|
2889
|
+
|
|
2890
|
+
| Campo | Tipo | Requerido | Descripción |
|
|
2891
|
+
|-------|------|-----------|-------------|
|
|
2892
|
+
| `basePath` | String | Sí | Ruta base del recurso (ej: `/orders`) |
|
|
2893
|
+
| `versions` | Array | Sí | Lista de versiones de API |
|
|
2894
|
+
| `versions[].version` | String | Sí | Identificador de versión (ej: `v1`) |
|
|
2895
|
+
| `versions[].operations` | Array | Sí | Lista de endpoints a generar |
|
|
2896
|
+
| `operations[].method` | String | Sí | Verbo HTTP: `GET`, `POST`, `PUT`, `PATCH`, `DELETE` |
|
|
2897
|
+
| `operations[].path` | String | Sí | Path relativo (ej: `/`, `/{id}`, `/{id}/confirm`) |
|
|
2898
|
+
| `operations[].useCase` | String | Sí | Nombre del use case en PascalCase |
|
|
2899
|
+
| `operations[].description` | String | No | Descripción para la anotación `@Operation` de Swagger |
|
|
2900
|
+
|
|
2901
|
+
### Tipo inferido (`type`)
|
|
2902
|
+
|
|
2903
|
+
El tipo del use case se infiere automáticamente del método HTTP:
|
|
2904
|
+
|
|
2905
|
+
| HTTP method | Tipo inferido | Genera |
|
|
2906
|
+
|-------------|--------------|--------|
|
|
2907
|
+
| `GET` | `query` | `{UseCaseName}Query` + `{UseCaseName}QueryHandler` |
|
|
2908
|
+
| `POST`, `PUT`, `PATCH`, `DELETE` | `command` | `{UseCaseName}Command` + `{UseCaseName}CommandHandler` |
|
|
2909
|
+
|
|
2910
|
+
### Use cases estándar vs. scaffold
|
|
2911
|
+
|
|
2912
|
+
| Categoría | Nombres Match | Generado |
|
|
2913
|
+
|-----------|---------------|---------|
|
|
2914
|
+
| **Estándar** | `Create{Aggregate}`, `Update{Aggregate}`, `Delete{Aggregate}`, `Get{Aggregate}`, `FindAll{PluralAggregate}` | Implementación completa con lógica de repositorio |
|
|
2915
|
+
| **Scaffold** | Cualquier otro nombre (`ConfirmOrder`, `ActivateProduct`, etc.) | Clase con `// TODO` — el desarrollador completa la lógica |
|
|
2916
|
+
|
|
2917
|
+
Los use cases estándar reutilizan los templates CRUD existentes (implementación idéntica al flujo sin `endpoints:`). Los scaffolds generan archivos con `UnsupportedOperationException` y comentarios guía.
|
|
2918
|
+
|
|
2919
|
+
### Regla anti-duplicado (multi-versión)
|
|
2920
|
+
|
|
2921
|
+
Cuando el mismo `useCase` aparece en múltiples versiones (ej: `CreateProduct` en v1 y v2), el generador crea el Command/Query + Handler **solo una vez** (en la primera versión donde aparece). Los controladores de las versiones posteriores importan y referencian el mismo use case sin regenerarlo.
|
|
2922
|
+
|
|
2923
|
+
```yaml
|
|
2924
|
+
endpoints:
|
|
2925
|
+
basePath: /products
|
|
2926
|
+
versions:
|
|
2927
|
+
- version: v1
|
|
2928
|
+
operations:
|
|
2929
|
+
- { method: POST, path: /, useCase: CreateProduct } # ← genera CreateProductCommand + Handler
|
|
2930
|
+
|
|
2931
|
+
- version: v2
|
|
2932
|
+
operations:
|
|
2933
|
+
- { method: POST, path: /, useCase: CreateProduct } # ← NO regenera, solo referencia en V2Controller
|
|
2934
|
+
- { method: PUT, path: /{id}/activate, useCase: ActivateProduct } # ← nuevo scaffold
|
|
2935
|
+
```
|
|
2936
|
+
|
|
2937
|
+
### Nombres de controladores generados
|
|
2938
|
+
|
|
2939
|
+
Con `endpoints:`, el controlador se nombra `{Aggregate}{VersionCapitalized}Controller`:
|
|
2940
|
+
|
|
2941
|
+
| Aggregate | Version | Clase generada | Archivo |
|
|
2942
|
+
|-----------|---------|---------------|---------|
|
|
2943
|
+
| `Order` | `v1` | `OrderV1Controller` | `controllers/order/v1/OrderV1Controller.java` |
|
|
2944
|
+
| `Product` | `v2` | `ProductV2Controller` | `controllers/product/v2/ProductV2Controller.java` |
|
|
2945
|
+
|
|
2946
|
+
> Sin `endpoints:`, el controlador se llama `{Aggregate}Controller` y usa la versión ingresada en el prompt.
|
|
2947
|
+
|
|
2948
|
+
### Ejemplo básico (una versión)
|
|
2949
|
+
|
|
2950
|
+
```yaml
|
|
2951
|
+
aggregates:
|
|
2952
|
+
- name: Order
|
|
2953
|
+
entities:
|
|
2954
|
+
- name: order
|
|
2955
|
+
isRoot: true
|
|
2956
|
+
tableName: orders
|
|
2957
|
+
fields:
|
|
2958
|
+
- { name: id, type: String }
|
|
2959
|
+
- { name: orderNumber, type: String }
|
|
2960
|
+
- { name: status, type: OrderStatus, readOnly: true }
|
|
2961
|
+
|
|
2962
|
+
enums:
|
|
2963
|
+
- name: OrderStatus
|
|
2964
|
+
initialValue: PENDING
|
|
2965
|
+
values: [PENDING, CONFIRMED, SHIPPED, CANCELLED]
|
|
2966
|
+
|
|
2967
|
+
endpoints:
|
|
2968
|
+
basePath: /orders
|
|
2969
|
+
versions:
|
|
2970
|
+
- version: v1
|
|
2971
|
+
operations:
|
|
2972
|
+
- { method: GET, path: /{id}, useCase: GetOrder, description: "Obtener pedido" }
|
|
2973
|
+
- { method: GET, path: /, useCase: FindAllOrders, description: "Listar pedidos" }
|
|
2974
|
+
- { method: POST, path: /, useCase: CreateOrder, description: "Crear pedido" }
|
|
2975
|
+
- { method: DELETE, path: /{id}, useCase: DeleteOrder, description: "Eliminar pedido" }
|
|
2976
|
+
- { method: PUT, path: /{id}/confirm, useCase: ConfirmOrder, description: "Confirmar pedido" }
|
|
2977
|
+
```
|
|
2978
|
+
|
|
2979
|
+
**Archivos generados:**
|
|
2980
|
+
```
|
|
2981
|
+
application/
|
|
2982
|
+
commands/
|
|
2983
|
+
CreateOrderCommand.java ← estándar (completo)
|
|
2984
|
+
DeleteOrderCommand.java ← estándar (completo)
|
|
2985
|
+
ConfirmOrderCommand.java ← scaffold (TODO)
|
|
2986
|
+
queries/
|
|
2987
|
+
GetOrderQuery.java ← estándar (completo)
|
|
2988
|
+
FindAllOrdersQuery.java ← estándar (completo)
|
|
2989
|
+
usecases/
|
|
2990
|
+
CreateOrderCommandHandler.java ← estándar
|
|
2991
|
+
DeleteOrderCommandHandler.java ← estándar
|
|
2992
|
+
ConfirmOrderCommandHandler.java ← scaffold
|
|
2993
|
+
GetOrderQueryHandler.java ← estándar
|
|
2994
|
+
FindAllOrdersQueryHandler.java ← estándar
|
|
2995
|
+
dtos/
|
|
2996
|
+
OrderResponseDto.java
|
|
2997
|
+
mappers/
|
|
2998
|
+
OrderApplicationMapper.java
|
|
2999
|
+
infrastructure/rest/controllers/order/
|
|
3000
|
+
v1/
|
|
3001
|
+
OrderV1Controller.java ← controller con 5 métodos declarados
|
|
3002
|
+
```
|
|
3003
|
+
|
|
3004
|
+
### Ejemplo multi-versión
|
|
3005
|
+
|
|
3006
|
+
Ver [`examples/domain-endpoints-versioned.yaml`](examples/domain-endpoints-versioned.yaml) para un ejemplo completo con v1 y v2, incluyendo la regla anti-duplicado y scaffolds.
|
|
3007
|
+
|
|
3008
|
+
---
|
|
3009
|
+
|
|
2444
3010
|
## Ejemplos Completos
|
|
2445
3011
|
|
|
2446
3012
|
### Ejemplo 1: E-Commerce (Order)
|
|
@@ -2860,7 +3426,7 @@ eva4j generate entities <module-name>
|
|
|
2860
3426
|
- Control de visibilidad de campos (`readOnly`, `hidden`, `defaultValue`)
|
|
2861
3427
|
- Referencias cross-agregado (`reference:`)
|
|
2862
3428
|
- Domain Events (`events:` con soporte opcional de Kafka)
|
|
2863
|
-
- Soft delete
|
|
3429
|
+
- Soft delete por entidad raíz (`hasSoftDelete: true` en `isRoot: true`) ✅ Implementado
|
|
2864
3430
|
|
|
2865
3431
|
### 🚧 Próximamente
|
|
2866
3432
|
|