eva4j 1.0.14 → 1.0.16
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/skills/skill-creator/LICENSE.txt +202 -0
- package/.agents/skills/skill-creator/SKILL.md +485 -0
- package/.agents/skills/skill-creator/agents/analyzer.md +274 -0
- package/.agents/skills/skill-creator/agents/comparator.md +202 -0
- package/.agents/skills/skill-creator/agents/grader.md +223 -0
- package/.agents/skills/skill-creator/assets/eval_review.html +146 -0
- package/.agents/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/.agents/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/.agents/skills/skill-creator/references/schemas.md +430 -0
- package/.agents/skills/skill-creator/scripts/__init__.py +0 -0
- package/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/.agents/skills/skill-creator/scripts/generate_report.py +326 -0
- package/.agents/skills/skill-creator/scripts/improve_description.py +247 -0
- package/.agents/skills/skill-creator/scripts/package_skill.py +136 -0
- package/.agents/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/.agents/skills/skill-creator/scripts/run_eval.py +310 -0
- package/.agents/skills/skill-creator/scripts/run_loop.py +328 -0
- package/.agents/skills/skill-creator/scripts/utils.py +47 -0
- package/AGENTS.md +268 -6
- package/COMMAND_EVALUATION.md +15 -16
- package/DOMAIN_YAML_GUIDE.md +430 -14
- package/FUTURE_FEATURES.md +1627 -1168
- package/README.md +461 -13
- package/bin/eva4j.js +32 -14
- package/config/defaults.json +1 -0
- package/docs/commands/EVALUATE_SYSTEM.md +746 -261
- package/docs/commands/EXPORT_DIAGRAM.md +153 -0
- package/docs/commands/GENERATE_ENTITIES.md +599 -6
- package/docs/commands/INDEX.md +7 -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 +46 -3
- package/package.json +2 -1
- package/src/agents/design-reviewer.agent.md +163 -0
- package/src/commands/build.js +714 -0
- package/src/commands/create.js +1 -0
- package/src/commands/detach.js +149 -108
- package/src/commands/evaluate-system.js +234 -8
- package/src/commands/export-diagram.js +522 -0
- package/src/commands/generate-entities.js +685 -66
- package/src/commands/generate-http-exchange.js +2 -0
- package/src/commands/generate-kafka-event.js +43 -10
- package/src/generators/base-generator.js +18 -6
- package/src/generators/postman-generator.js +188 -0
- package/src/generators/shared-generator.js +12 -2
- package/src/skills/build-system-yaml/SKILL.md +323 -0
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +410 -0
- package/src/skills/build-system-yaml/references/module-spec.md +367 -0
- package/src/skills/build-system-yaml/references/system-yaml-spec.md +178 -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 +1020 -0
- package/src/utils/fake-data.js +376 -0
- package/src/utils/system-validator.js +319 -199
- package/src/utils/yaml-to-entity.js +272 -7
- package/templates/aggregate/AggregateMapper.java.ejs +3 -2
- package/templates/aggregate/AggregateRepository.java.ejs +3 -2
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +6 -5
- package/templates/aggregate/AggregateRoot.java.ejs +60 -2
- package/templates/aggregate/DomainEventHandler.java.ejs +4 -1
- 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/base/docker/Dockerfile.ejs +21 -0
- package/templates/base/gradle/build.gradle.ejs +3 -2
- package/templates/base/root/AGENTS.md.ejs +306 -45
- 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/CreateCommandHandler.java.ejs +3 -6
- package/templates/crud/CreateItemDto.java.ejs +4 -0
- package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
- package/templates/crud/DeleteCommandHandler.java.ejs +12 -6
- package/templates/crud/EndpointsController.java.ejs +6 -6
- package/templates/crud/FindByQuery.java.ejs +1 -1
- package/templates/crud/FindByQueryHandler.java.ejs +3 -9
- package/templates/crud/GetQueryHandler.java.ejs +2 -5
- package/templates/crud/ListQuery.java.ejs +1 -1
- package/templates/crud/ListQueryHandler.java.ejs +8 -14
- package/templates/crud/ScaffoldCommandHandler.java.ejs +2 -4
- package/templates/crud/ScaffoldQuery.java.ejs +3 -2
- package/templates/crud/ScaffoldQueryHandler.java.ejs +5 -6
- package/templates/crud/SubEntityAddCommand.java.ejs +4 -0
- package/templates/crud/SubEntityAddCommandHandler.java.ejs +2 -6
- package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +2 -7
- package/templates/crud/TransitionCommandHandler.java.ejs +2 -6
- package/templates/crud/UpdateCommand.java.ejs +4 -0
- package/templates/crud/UpdateCommandHandler.java.ejs +2 -5
- package/templates/evaluate/report.html.ejs +394 -2
- package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
- package/templates/kafka-event/Event.java.ejs +9 -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 +25 -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/annotations/LogAfter.java.ejs +2 -0
- package/templates/shared/annotations/LogBefore.java.ejs +2 -0
- package/templates/shared/annotations/LogExceptions.java.ejs +2 -0
- package/templates/shared/annotations/LogLevel.java.ejs +8 -0
- package/templates/shared/annotations/LogTimer.java.ejs +1 -0
- package/templates/shared/annotations/Loggable.java.ejs +17 -0
- package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
- package/templates/shared/configurations/loggerConfig/HandlerLogs.java.ejs +120 -32
- package/templates/shared/errorMessage/ErrorResponse.java.ejs +23 -0
- package/templates/shared/handlerException/HandlerExceptions.java.ejs +99 -79
- package/src/commands/generate-system.js +0 -243
- package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +0 -1103
- package/templates/base/root/skill-build-domain-yaml.ejs +0 -292
- package/templates/base/root/skill-build-system-yaml.ejs +0 -252
- package/templates/shared/errorMessage/ErrorMessage.java.ejs +0 -5
- package/templates/shared/errorMessage/FullErrorMessage.java.ejs +0 -9
- package/templates/shared/errorMessage/ShortErrorMessage.java.ejs +0 -6
package/DOMAIN_YAML_GUIDE.md
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
- [Relaciones](#relaciones)
|
|
15
15
|
- [Tipos de Datos](#tipos-de-datos)
|
|
16
16
|
- [Sección endpoints](#sección-endpoints)
|
|
17
|
+
- [Sección listeners](#sección-listeners)
|
|
17
18
|
- [Ejemplos Completos](#ejemplos-completos)
|
|
18
19
|
|
|
19
20
|
---
|
|
@@ -135,10 +136,17 @@ aggregates:
|
|
|
135
136
|
values: []
|
|
136
137
|
|
|
137
138
|
events:
|
|
138
|
-
# Eventos de dominio que emite este agregado
|
|
139
|
+
# Eventos de dominio que emite este agregado (dentro del agregado)
|
|
139
140
|
- name: NombreEventoOcurrido
|
|
140
141
|
fields: []
|
|
141
|
-
|
|
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
|
|
142
150
|
```
|
|
143
151
|
|
|
144
152
|
### Ubicación del archivo
|
|
@@ -1280,6 +1288,100 @@ CREATE TABLE reviews (
|
|
|
1280
1288
|
|
|
1281
1289
|
---
|
|
1282
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
|
+
|
|
1283
1385
|
## Value Objects
|
|
1284
1386
|
|
|
1285
1387
|
Los Value Objects son objetos inmutables que representan conceptos del dominio sin identidad propia.
|
|
@@ -1595,7 +1697,7 @@ aggregates:
|
|
|
1595
1697
|
type: LocalDateTime
|
|
1596
1698
|
|
|
1597
1699
|
- name: OrderShippedEvent
|
|
1598
|
-
|
|
1700
|
+
topic: ORDER_SHIPPED # opcional — override del topic auto-derivado
|
|
1599
1701
|
fields:
|
|
1600
1702
|
- name: orderId
|
|
1601
1703
|
type: String
|
|
@@ -1605,11 +1707,16 @@ aggregates:
|
|
|
1605
1707
|
|
|
1606
1708
|
### Propiedades de un evento
|
|
1607
1709
|
|
|
1608
|
-
| Propiedad | Tipo | Descripción |
|
|
1609
|
-
|
|
1610
|
-
| `name` | String | Nombre de la clase del evento (PascalCase) |
|
|
1611
|
-
| `fields` | Array | Campos que transporta el evento |
|
|
1612
|
-
| `
|
|
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`.
|
|
1613
1720
|
|
|
1614
1721
|
### Archivos generados
|
|
1615
1722
|
|
|
@@ -1644,7 +1751,7 @@ public class OrderDomainEventHandler {
|
|
|
1644
1751
|
|
|
1645
1752
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
|
1646
1753
|
public void handle(OrderShippedEvent event) {
|
|
1647
|
-
messageBroker.sendOrderShippedEvent(event);
|
|
1754
|
+
messageBroker.sendOrderShippedEvent(event);
|
|
1648
1755
|
}
|
|
1649
1756
|
}
|
|
1650
1757
|
```
|
|
@@ -1665,6 +1772,316 @@ public void confirm() {
|
|
|
1665
1772
|
|
|
1666
1773
|
---
|
|
1667
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
|
+
|
|
1668
2085
|
## Relaciones
|
|
1669
2086
|
|
|
1670
2087
|
eva4j soporta relaciones JPA bidireccionales completas con generación automática del lado inverso.
|
|
@@ -2494,7 +2911,7 @@ El tipo del use case se infiere automáticamente del método HTTP:
|
|
|
2494
2911
|
|
|
2495
2912
|
| Categoría | Nombres Match | Generado |
|
|
2496
2913
|
|-----------|---------------|---------|
|
|
2497
|
-
| **Estándar** | `Create{Aggregate}`, `Update{Aggregate}`, `Delete{Aggregate}`, `Get{Aggregate}`, `FindAll{
|
|
2914
|
+
| **Estándar** | `Create{Aggregate}`, `Update{Aggregate}`, `Delete{Aggregate}`, `Get{Aggregate}`, `FindAll{PluralAggregate}` | Implementación completa con lógica de repositorio |
|
|
2498
2915
|
| **Scaffold** | Cualquier otro nombre (`ConfirmOrder`, `ActivateProduct`, etc.) | Clase con `// TODO` — el desarrollador completa la lógica |
|
|
2499
2916
|
|
|
2500
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.
|
|
@@ -2568,14 +2985,13 @@ application/
|
|
|
2568
2985
|
ConfirmOrderCommand.java ← scaffold (TODO)
|
|
2569
2986
|
queries/
|
|
2570
2987
|
GetOrderQuery.java ← estándar (completo)
|
|
2571
|
-
FindAllOrdersQuery.java ←
|
|
2572
|
-
→ scaffold (TODO)
|
|
2988
|
+
FindAllOrdersQuery.java ← estándar (completo)
|
|
2573
2989
|
usecases/
|
|
2574
2990
|
CreateOrderCommandHandler.java ← estándar
|
|
2575
2991
|
DeleteOrderCommandHandler.java ← estándar
|
|
2576
2992
|
ConfirmOrderCommandHandler.java ← scaffold
|
|
2577
2993
|
GetOrderQueryHandler.java ← estándar
|
|
2578
|
-
FindAllOrdersQueryHandler.java ←
|
|
2994
|
+
FindAllOrdersQueryHandler.java ← estándar
|
|
2579
2995
|
dtos/
|
|
2580
2996
|
OrderResponseDto.java
|
|
2581
2997
|
mappers/
|
|
@@ -3010,7 +3426,7 @@ eva4j generate entities <module-name>
|
|
|
3010
3426
|
- Control de visibilidad de campos (`readOnly`, `hidden`, `defaultValue`)
|
|
3011
3427
|
- Referencias cross-agregado (`reference:`)
|
|
3012
3428
|
- Domain Events (`events:` con soporte opcional de Kafka)
|
|
3013
|
-
- Soft delete
|
|
3429
|
+
- Soft delete por entidad raíz (`hasSoftDelete: true` en `isRoot: true`) ✅ Implementado
|
|
3014
3430
|
|
|
3015
3431
|
### 🚧 Próximamente
|
|
3016
3432
|
|