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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.domain.models.events;
|
|
2
|
+
<%
|
|
3
|
+
const needsBigDecimal = fields && fields.some(f => f.javaType === 'BigDecimal');
|
|
4
|
+
const needsLocalDate = fields && fields.some(f => ['LocalDate','LocalDateTime','LocalTime'].includes(f.javaType));
|
|
5
|
+
const needsInstant = fields && fields.some(f => f.javaType === 'Instant');
|
|
6
|
+
const needsUUID = fields && fields.some(f => f.javaType === 'UUID');
|
|
7
|
+
const needsList = fields && fields.some(f => f.javaType && f.javaType.startsWith('List'));
|
|
8
|
+
%>
|
|
9
|
+
<% if (needsBigDecimal) { %>
|
|
10
|
+
import java.math.BigDecimal;
|
|
11
|
+
<% } %>
|
|
12
|
+
<% if (needsLocalDate) { %>
|
|
13
|
+
import java.time.LocalDate;
|
|
14
|
+
import java.time.LocalDateTime;
|
|
15
|
+
<% } %>
|
|
16
|
+
<% if (needsInstant) { %>
|
|
17
|
+
import java.time.Instant;
|
|
18
|
+
<% } %>
|
|
19
|
+
<% if (needsUUID) { %>
|
|
20
|
+
import java.util.UUID;
|
|
21
|
+
<% } %>
|
|
22
|
+
<% if (needsList) { %>
|
|
23
|
+
import java.util.List;
|
|
24
|
+
<% } %>
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* <%= name %> — snapshot value type carried in domain events.
|
|
28
|
+
*
|
|
29
|
+
* This is a pure domain record with no infrastructure dependencies.
|
|
30
|
+
* TODO: Define the fields this snapshot should carry.
|
|
31
|
+
* Example for an order item:
|
|
32
|
+
* String productId,
|
|
33
|
+
* String productName,
|
|
34
|
+
* Integer quantity,
|
|
35
|
+
* BigDecimal unitPrice
|
|
36
|
+
*/
|
|
37
|
+
public record <%= name %>(
|
|
38
|
+
<% if (fields && fields.length > 0) { %>
|
|
39
|
+
<% fields.forEach((field, idx) => { %>
|
|
40
|
+
<%- field.javaType %> <%= field.name %><%= idx < fields.length - 1 ? ',' : '' %>
|
|
41
|
+
<% }); %>
|
|
42
|
+
<% } else { %>
|
|
43
|
+
// TODO: Add the fields this snapshot should carry
|
|
44
|
+
<% } %>
|
|
45
|
+
) {
|
|
46
|
+
}
|
|
@@ -22,11 +22,17 @@ import <%= packageName %>.shared.domain.AuditableEntity;
|
|
|
22
22
|
<% } else if (auditable) { %>
|
|
23
23
|
import <%= packageName %>.shared.domain.AuditableEntity;
|
|
24
24
|
<% } %>
|
|
25
|
+
<% if (hasSoftDelete) { %>
|
|
26
|
+
import org.hibernate.annotations.SQLRestriction;
|
|
27
|
+
<% } %>
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
30
|
* <%= name %>Jpa - JPA Entity
|
|
28
31
|
* Persistence entity with JPA annotations
|
|
29
32
|
*/
|
|
33
|
+
<% if (hasSoftDelete) { %>
|
|
34
|
+
@SQLRestriction("deleted_at IS NULL")
|
|
35
|
+
<% } %>
|
|
30
36
|
@Entity
|
|
31
37
|
@Table(name = "<%= tableName %>")
|
|
32
38
|
@Getter
|
|
@@ -8,4 +8,9 @@ import <%= packageName %>.<%= moduleName %>.infrastructure.database.entities.<%=
|
|
|
8
8
|
* Spring Data JPA repository
|
|
9
9
|
*/
|
|
10
10
|
public interface <%= rootEntity.name %>JpaRepository extends JpaRepository<<%= rootEntity.name %>Jpa, <%= rootEntity.fields[0].javaType %>> {
|
|
11
|
+
<% if (findByOps && findByOps.length > 0) { %>
|
|
12
|
+
<% findByOps.forEach(function(op) { %>
|
|
13
|
+
org.springframework.data.domain.Page<<%= rootEntity.name %>Jpa> <%= op.jpaMethodName %>(<%= op.fieldJavaType %> <%= op.fieldName %>, org.springframework.data.domain.Pageable pageable);
|
|
14
|
+
<% }); %>
|
|
15
|
+
<% } %>
|
|
11
16
|
}
|
|
@@ -50,14 +50,15 @@ dependencies {
|
|
|
50
50
|
<% } %><% if (dependencies.includes('security')) { %> implementation 'org.springframework.boot:spring-boot-starter-security'
|
|
51
51
|
<% } %><% if (dependencies.includes('validation')) { %> implementation 'org.springframework.boot:spring-boot-starter-validation'
|
|
52
52
|
<% } %><% if (dependencies.includes('actuator')) { %> implementation 'org.springframework.boot:spring-boot-starter-aop'
|
|
53
|
-
<% }
|
|
53
|
+
<% } %> implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
|
54
|
+
<% if (features.includeDevtools) { %> developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
|
54
55
|
<% } %><% if (features.includeLombok) { %>
|
|
55
56
|
compileOnly 'org.projectlombok:lombok'
|
|
56
57
|
annotationProcessor 'org.projectlombok:lombok'
|
|
57
58
|
<% } %><% if (dependencies.includes('data-jpa')) { %>
|
|
58
59
|
runtimeOnly '<%= databaseDriver %>'
|
|
59
60
|
<% } %><% if (features.includeSwagger) { %>
|
|
60
|
-
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui
|
|
61
|
+
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:<%= springdocVersion %>'
|
|
61
62
|
<% } %>
|
|
62
63
|
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
|
63
64
|
<% if (dependencies.includes('security')) { %> testImplementation 'org.springframework.security:spring-security-test'
|
|
@@ -328,13 +328,21 @@ aggregates:
|
|
|
328
328
|
- CANCELLED
|
|
329
329
|
|
|
330
330
|
events: # Domain Events emitted by this aggregate
|
|
331
|
-
- name: OrderConfirmedEvent
|
|
332
|
-
fields:
|
|
333
|
-
- name: orderId
|
|
331
|
+
- name: OrderConfirmedEvent # Nota: kafka: true ya NO es necesario.
|
|
332
|
+
fields: # Si hay kafka-client instalado, eva g entities
|
|
333
|
+
- name: orderId # cableará automáticamente todos los eventos.
|
|
334
334
|
type: String
|
|
335
335
|
- name: confirmedAt
|
|
336
336
|
type: LocalDateTime
|
|
337
|
-
|
|
337
|
+
|
|
338
|
+
# listeners: # Sibling de aggregates: — eventos externos que CONSUME
|
|
339
|
+
# - event: PaymentApprovedEvent
|
|
340
|
+
# producer: payments
|
|
341
|
+
# topic: PAYMENT_APPROVED # obligatorio en módulos standalone
|
|
342
|
+
# useCase: ConfirmOrder
|
|
343
|
+
# fields:
|
|
344
|
+
# - name: orderId
|
|
345
|
+
# type: String
|
|
338
346
|
```
|
|
339
347
|
|
|
340
348
|
### Supported Field Types
|
|
@@ -1525,6 +1533,8 @@ public boolean canConfirm() { return this.status.canTransitionTo(OrderStatus.CO
|
|
|
1525
1533
|
|
|
1526
1534
|
### Domain Events (`events[]`)
|
|
1527
1535
|
|
|
1536
|
+
Los eventos se declaran bajo el agregado (al mismo nivel que `entities:`, `enums:`, `valueObjects:`). Opcionalmente, se conectan con transiciones de estado mediante la propiedad `triggers`.
|
|
1537
|
+
|
|
1528
1538
|
```yaml
|
|
1529
1539
|
aggregates:
|
|
1530
1540
|
- name: Order
|
|
@@ -1532,64 +1542,301 @@ aggregates:
|
|
|
1532
1542
|
- name: order
|
|
1533
1543
|
isRoot: true
|
|
1534
1544
|
# ...
|
|
1545
|
+
enums:
|
|
1546
|
+
- name: OrderStatus
|
|
1547
|
+
initialValue: DRAFT
|
|
1548
|
+
transitions:
|
|
1549
|
+
- from: DRAFT
|
|
1550
|
+
to: PLACED
|
|
1551
|
+
method: place
|
|
1552
|
+
- from: PLACED
|
|
1553
|
+
to: CANCELLED
|
|
1554
|
+
method: cancel
|
|
1555
|
+
values: [DRAFT, PLACED, CANCELLED]
|
|
1535
1556
|
events:
|
|
1536
|
-
- name:
|
|
1557
|
+
- name: OrderPlaced
|
|
1558
|
+
topic: ORDER_PLACED # opcional: sobreescribe el topic auto-derivado
|
|
1559
|
+
triggers:
|
|
1560
|
+
- place # ← nombre del método de transición
|
|
1537
1561
|
fields:
|
|
1538
|
-
- name: orderId
|
|
1562
|
+
- name: orderId # declarar para consumidores cross-módulo via Kafka
|
|
1539
1563
|
type: String
|
|
1540
|
-
- name:
|
|
1564
|
+
- name: customerId
|
|
1565
|
+
type: String
|
|
1566
|
+
- name: totalAmount
|
|
1567
|
+
type: BigDecimal
|
|
1568
|
+
- name: placedAt
|
|
1541
1569
|
type: LocalDateTime
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
kafka: true # Publishes to Kafka after commit
|
|
1570
|
+
- name: OrderCancelled
|
|
1571
|
+
triggers:
|
|
1572
|
+
- cancel
|
|
1546
1573
|
fields:
|
|
1547
|
-
- name:
|
|
1548
|
-
type: String
|
|
1549
|
-
- name: trackingNumber
|
|
1574
|
+
- name: reason # no resuelto → null /* TODO: provide reason */
|
|
1550
1575
|
type: String
|
|
1551
1576
|
```
|
|
1552
1577
|
|
|
1553
|
-
|
|
1578
|
+
#### Propiedad `topic` (opcional)
|
|
1554
1579
|
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1580
|
+
Sobreescribe el nombre del topic Kafka auto-derivado para este evento.
|
|
1581
|
+
|
|
1582
|
+
**Regla de derivación por defecto:** el generador quita el sufijo `Event` del nombre de la clase antes de convertir a SCREAMING_SNAKE_CASE:
|
|
1583
|
+
- `ProductPublishedEvent` → `PRODUCT_PUBLISHED` ✓ (no `PRODUCT_PUBLISHED_EVENT`)
|
|
1584
|
+
- `OrderCancelled` → `ORDER_CANCELLED` (sin sufijo, sin cambios)
|
|
1585
|
+
|
|
1586
|
+
**Cuándo usar `topic:` explícito:**
|
|
1587
|
+
- El producer y el consumer de otro módulo deben usar exactamente el mismo nombre.
|
|
1588
|
+
- Si el consumer en `listeners[]` declara `topic: MY_CUSTOM_TOPIC`, declara el mismo valor aquí para que el match sea garantizado.
|
|
1589
|
+
|
|
1590
|
+
```yaml
|
|
1591
|
+
events:
|
|
1592
|
+
- name: ProductPublishedEvent
|
|
1593
|
+
# topic auto-derivado: PRODUCT_PUBLISHED (sufijo 'Event' eliminado)
|
|
1594
|
+
triggers: [publish]
|
|
1595
|
+
fields: [...]
|
|
1596
|
+
|
|
1597
|
+
- name: OrderReadyEvent
|
|
1598
|
+
topic: ORDER_READY_FOR_PICKUP # override explícito
|
|
1599
|
+
triggers: [markReady]
|
|
1600
|
+
fields: [...]
|
|
1568
1601
|
```
|
|
1569
1602
|
|
|
1570
|
-
`
|
|
1571
|
-
```java
|
|
1572
|
-
@Component
|
|
1573
|
-
public class OrderDomainEventHandler {
|
|
1603
|
+
> **Nota:** el flag `kafka: true` ya no es necesario — si el proyecto tiene `kafka-client` instalado, todos los eventos se cablearán automáticamente al ejecutar `eva g entities`.
|
|
1574
1604
|
|
|
1575
|
-
|
|
1576
|
-
public void handle(OrderConfirmedEvent event) { /* lógica post-commit */ }
|
|
1605
|
+
#### Propiedad `triggers`
|
|
1577
1606
|
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1607
|
+
Lista de nombres de métodos de transición que publican este evento. El generador emite automáticamente `raise(new XEvent(...))` dentro de cada método listado.
|
|
1608
|
+
|
|
1609
|
+
**Reglas de resolución de argumentos (en orden):**
|
|
1610
|
+
|
|
1611
|
+
| Condición del campo del evento | Argumento generado |
|
|
1612
|
+
|---|---|
|
|
1613
|
+
| Siempre (primer arg, `aggregateId` del `DomainEvent` base) | `this.getId()` |
|
|
1614
|
+
| Nombre = `{entityName}Id` (ej: `orderId` en `Order`) | **Ignorado** en el Domain Event class — mapeado a `event.getAggregateId()` en el Integration Event |
|
|
1615
|
+
| Nombre coincide con un campo de la entidad | `this.get{Field}()` |
|
|
1616
|
+
| Nombre termina en `At` + tipo `LocalDateTime` | `LocalDateTime.now()` |
|
|
1617
|
+
| No resuelto | `null /* TODO: provide {fieldName} */` |
|
|
1618
|
+
|
|
1619
|
+
> **Convención:** Sí declarar `{entityName}Id` en `events[].fields` cuando el evento **cruza módulos via Kafka** — es necesario para que el id viaje en el payload del Integration Event. El generador lo mapea automáticamente a `event.getAggregateId()` en el handler, evitando la duplicación en el Domain Event class interno.
|
|
1620
|
+
|
|
1621
|
+
**Código generado:**
|
|
1622
|
+
|
|
1623
|
+
```java
|
|
1624
|
+
public void place() {
|
|
1625
|
+
this.status = this.status.transitionTo(OrderStatus.PLACED);
|
|
1626
|
+
raise(new OrderPlaced(this.getId(), this.getCustomerId(), this.getTotalAmount(), LocalDateTime.now()));
|
|
1627
|
+
// ^—aggregateId ^—customerId ^—totalAmount ^—placedAt
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
public void cancel() {
|
|
1631
|
+
this.status = this.status.transitionTo(OrderStatus.CANCELLED);
|
|
1632
|
+
raise(new OrderCancelled(this.getId(), null /* TODO: provide reason */));
|
|
1582
1633
|
}
|
|
1583
1634
|
```
|
|
1584
1635
|
|
|
1585
|
-
**
|
|
1636
|
+
Si un evento **no declara `triggers`**, el desarrollador debe llamar a `raise()` manualmente dentro del método de negocio.
|
|
1637
|
+
|
|
1638
|
+
Genera `OrderPlaced.java` (en `domain/models/events/`) que extiende `DomainEvent`, y `OrderDomainEventHandler.java` (en `application/usecases/`) con `@TransactionalEventListener(AFTER_COMMIT)`.
|
|
1639
|
+
|
|
1640
|
+
**Validaciones del generador:**
|
|
1641
|
+
|
|
1642
|
+
| Código | Severidad | Condición |
|
|
1643
|
+
|---|---|---|
|
|
1644
|
+
| C2-001 | warning | Transición sin use-case — silenciado si tiene `triggers` |
|
|
1645
|
+
| C2-004 | error | `triggers` referencia un método que no existe en ninguna transición |
|
|
1646
|
+
| C2-005 | info | Transición sin ningún evento asociado — considera declarar `triggers` |
|
|
1647
|
+
|
|
1648
|
+
**Auto-wiring de broker:** Si el proyecto tiene `eva add kafka-client` instalado, `eva g entities` genera automáticamente la capa de Integration Events para **todos** los eventos declarados:
|
|
1649
|
+
|
|
1650
|
+
| Archivo generado | Descripción |
|
|
1651
|
+
|---|---|
|
|
1652
|
+
| `application/events/OrderPlacedIntegrationEvent.java` | Record broker-facing (Integration Event) |
|
|
1653
|
+
| `application/ports/MessageBroker.java` | Puerto broker-agnóstico (creado/actualizado) |
|
|
1654
|
+
| `infrastructure/adapters/kafkaMessageBroker/…` | Adaptador Kafka (creado/actualizado) |
|
|
1655
|
+
| `shared/…/kafkaConfig/KafkaConfig.java` | Bean `NewTopic` (actualizado) |
|
|
1656
|
+
| `parameters/*/kafka.yaml` | Configuración de topic (actualizada) |
|
|
1657
|
+
|
|
1658
|
+
**Domain Event vs Integration Event:**
|
|
1659
|
+
- **Domain Event** (`domain/models/events/OrderPlaced.java`) — señal interna del bounded context. Nunca depende de infraestructura.
|
|
1660
|
+
- **Integration Event** (`application/events/OrderPlacedIntegrationEvent.java`) — proyección para el broker. Cambiar de Kafka a RabbitMQ solo requiere cambiar el adaptador `MessageBroker`.
|
|
1661
|
+
|
|
1662
|
+
El `DomainEventHandler` mapea un Domain Event a un Integration Event:
|
|
1586
1663
|
```java
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1664
|
+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
|
1665
|
+
public void onOrderPlaced(OrderPlaced event) {
|
|
1666
|
+
messageBroker.publishOrderPlacedIntegrationEvent(
|
|
1667
|
+
new OrderPlacedIntegrationEvent(event.getCustomerId(), event.getTotalAmount(), event.getPlacedAt())
|
|
1668
|
+
);
|
|
1590
1669
|
}
|
|
1591
1670
|
```
|
|
1592
1671
|
|
|
1672
|
+
**Nota:** el flag `kafka: true` por evento ya **no es necesario** — todos los eventos se cablearán automáticamente cuando haya un broker instalado.
|
|
1673
|
+
|
|
1674
|
+
### Consumo de Eventos Externos (`listeners[]`)
|
|
1675
|
+
|
|
1676
|
+
```yaml
|
|
1677
|
+
# Nivel raíz, sibling de aggregates:
|
|
1678
|
+
listeners:
|
|
1679
|
+
- event: PaymentApprovedEvent # PascalCase + sufijo Event
|
|
1680
|
+
producer: payments # Módulo que lo produce (referencia documental)
|
|
1681
|
+
topic: PAYMENT_APPROVED # Topic Kafka — obligatorio en módulos standalone
|
|
1682
|
+
useCase: ConfirmOrder # Caso de uso que maneja el evento (PascalCase)
|
|
1683
|
+
fields: # Campos del payload recibido
|
|
1684
|
+
- name: orderId
|
|
1685
|
+
type: String
|
|
1686
|
+
- name: approvedAt
|
|
1687
|
+
type: LocalDateTime
|
|
1688
|
+
- name: details # Tipo complejo → declarar en nestedTypes
|
|
1689
|
+
type: PaymentDetails
|
|
1690
|
+
nestedTypes: # Records auxiliares para campos de tipo objeto
|
|
1691
|
+
- name: paymentDetails # camelCase → normalizado a PaymentDetails
|
|
1692
|
+
fields:
|
|
1693
|
+
- name: paymentId
|
|
1694
|
+
type: String
|
|
1695
|
+
- name: method
|
|
1696
|
+
type: String
|
|
1697
|
+
- name: amount
|
|
1698
|
+
type: BigDecimal
|
|
1699
|
+
```
|
|
1700
|
+
|
|
1701
|
+
Genera por cada entrada (hasta **6 artefactos**):
|
|
1702
|
+
|
|
1703
|
+
| Artefacto | Descripción |
|
|
1704
|
+
|---|---|
|
|
1705
|
+
| `application/events/PaymentDetails.java` | Record auxiliar (uno por `nestedTypes` entry) |
|
|
1706
|
+
| `application/events/PaymentApprovedIntegrationEvent.java` | Record tipado con los `fields` declarados |
|
|
1707
|
+
| `infrastructure/kafkaListener/PaymentApprovedKafkaListener.java` | `@KafkaListener` → deserializa y despacha al `useCase` |
|
|
1708
|
+
| `parameters/*/kafka.yaml` | Registro del topic en `topics:` |
|
|
1709
|
+
| `application/commands/ConfirmOrderCommand.java` | Command tipado para el `useCase` |
|
|
1710
|
+
| `application/usecases/ConfirmOrderCommandHandler.java` | Handler stub — implementar la lógica de negocio aquí |
|
|
1711
|
+
|
|
1712
|
+
**Deserialization:** el listener usa `EventEnvelope<Map<String,Object>>` + `objectMapper.convertValue()` para deserializar cada campo del payload de forma robusta y tipada.
|
|
1713
|
+
|
|
1714
|
+
**Regla de `topic:`:**
|
|
1715
|
+
- Módulo standalone (sin `system.yaml`) → `topic:` **obligatorio**
|
|
1716
|
+
- Proyecto con `system.yaml` → puede omitirse; el generador lo infiere
|
|
1717
|
+
- Declarado explícitamente → tiene **precedencia** sobre la inferencia
|
|
1718
|
+
|
|
1719
|
+
**`nestedTypes:` — cuándo usarlo:**
|
|
1720
|
+
Declara un `nestedType` cuando un campo del payload es un **objeto anidado** (no un escalar). El generador produce un record en el mismo paquete `application/events/`, que tanto la `IntegrationEvent` como el `Command` y el `KafkaListener` usarán directamente.
|
|
1721
|
+
|
|
1722
|
+
**Contraste producidos vs. consumidos:**
|
|
1723
|
+
```
|
|
1724
|
+
aggregates:
|
|
1725
|
+
└── events: → Domain Events que PRODUCE (domain/models/events/)
|
|
1726
|
+
|
|
1727
|
+
listeners: → Integration Events que CONSUME (infrastructure/kafkaListener/)
|
|
1728
|
+
```
|
|
1729
|
+
|
|
1730
|
+
---
|
|
1731
|
+
|
|
1732
|
+
### Clientes HTTP Síncronos (`ports[]`)
|
|
1733
|
+
|
|
1734
|
+
```yaml
|
|
1735
|
+
# Nivel raíz, sibling de aggregates: y listeners:
|
|
1736
|
+
# Un método = una entrada; entries del mismo service: forman un solo FeignClient.
|
|
1737
|
+
|
|
1738
|
+
ports:
|
|
1739
|
+
- name: findScreeningById # nombre del método (camelCase)
|
|
1740
|
+
service: ScreeningService # agrupa en una interfaz/FeignClient (PascalCase)
|
|
1741
|
+
target: screenings # módulo destino (referencia documental)
|
|
1742
|
+
baseUrl: http://localhost:8081 # → parameters/*/urls.yaml (primera entrada del service)
|
|
1743
|
+
http: GET /screenings/{id} # verbo + path
|
|
1744
|
+
fields: # campos de respuesta → domain model + infra DTO
|
|
1745
|
+
- name: id
|
|
1746
|
+
type: String
|
|
1747
|
+
- name: startTime
|
|
1748
|
+
type: LocalDateTime
|
|
1749
|
+
|
|
1750
|
+
- name: findAvailableSeats
|
|
1751
|
+
service: ScreeningService # mismo service → mismo FeignClient
|
|
1752
|
+
target: screenings
|
|
1753
|
+
http: GET /screenings/{id}/seats
|
|
1754
|
+
returnList: true # → List<Seat> en la interfaz del puerto
|
|
1755
|
+
domainType: Seat # sobrescribe el tipo auto-derivado
|
|
1756
|
+
fields:
|
|
1757
|
+
- name: seatId
|
|
1758
|
+
type: String
|
|
1759
|
+
- name: seatType
|
|
1760
|
+
type: String
|
|
1761
|
+
|
|
1762
|
+
- name: processPayment
|
|
1763
|
+
service: PaymentGateway
|
|
1764
|
+
target: payment-gateway-external
|
|
1765
|
+
baseUrl: https://api.payments.example.com
|
|
1766
|
+
http: POST /payments
|
|
1767
|
+
body: # @RequestBody → ProcessPaymentRequestDto.java
|
|
1768
|
+
- name: amount
|
|
1769
|
+
type: BigDecimal
|
|
1770
|
+
- name: paymentMethod
|
|
1771
|
+
type: PaymentMethodInput # tipo objeto → declarar en nestedTypes:
|
|
1772
|
+
nestedTypes:
|
|
1773
|
+
- name: paymentMethodInput
|
|
1774
|
+
fields:
|
|
1775
|
+
- name: type
|
|
1776
|
+
type: String
|
|
1777
|
+
- name: cardToken
|
|
1778
|
+
type: String
|
|
1779
|
+
fields: # respuesta → domain model Payment + infra DTO
|
|
1780
|
+
- name: paymentId
|
|
1781
|
+
type: String
|
|
1782
|
+
- name: status
|
|
1783
|
+
type: String
|
|
1784
|
+
|
|
1785
|
+
- name: cancelPayment
|
|
1786
|
+
service: PaymentGateway
|
|
1787
|
+
target: payment-gateway-external
|
|
1788
|
+
http: DELETE /payments/{id}
|
|
1789
|
+
# fields: omitido → retorno void
|
|
1790
|
+
```
|
|
1791
|
+
|
|
1792
|
+
### Artefactos generados por `ports[]`
|
|
1793
|
+
|
|
1794
|
+
Por cada `service:` único:
|
|
1795
|
+
|
|
1796
|
+
| Archivo generado | Descripción |
|
|
1797
|
+
|---|---|
|
|
1798
|
+
| `domain/repositories/{ServiceName}.java` | Interfaz del puerto secundario (devuelve modelos de dominio) |
|
|
1799
|
+
| `infrastructure/adapters/{service}/{ServiceName}FeignClient.java` | Cliente Feign tipado (devuelve DTOs infra) |
|
|
1800
|
+
| `infrastructure/adapters/{service}/{ServiceName}FeignAdapter.java` | `@Component implements {ServiceName}` — actúa como ACL |
|
|
1801
|
+
| `infrastructure/adapters/{service}/{ServiceName}FeignConfig.java` | Timeouts Feign |
|
|
1802
|
+
| `parameters/*/urls.yaml` | Base URL parametrizada |
|
|
1803
|
+
|
|
1804
|
+
Por cada modelo de dominio único derivado de los métodos con `fields:`:
|
|
1805
|
+
|
|
1806
|
+
| Archivo generado | Descripción |
|
|
1807
|
+
|---|---|
|
|
1808
|
+
| `domain/models/{service}/{DomainType}.java` | Modelo de dominio (ACL) — abstracción interna |
|
|
1809
|
+
|
|
1810
|
+
Por cada método:
|
|
1811
|
+
|
|
1812
|
+
| Archivo generado | Condición |
|
|
1813
|
+
|---|---|
|
|
1814
|
+
| `infrastructure/adapters/{service}/{MethodPascal}Dto.java` | Cuando `fields:` presente — DTO infra (forma externa) |
|
|
1815
|
+
| `application/dtos/{MethodPascal}RequestDto.java` | Cuando `body:` presente (POST/PUT/PATCH) |
|
|
1816
|
+
| `application/dtos/{NestedTypePascal}.java` | Cuando `nestedTypes:` declarado |
|
|
1817
|
+
|
|
1818
|
+
**Patrón ACL:** Los DTOs de infraestructura (forma de la API externa) viven en `infrastructure/adapters/{service}/`. Los modelos de dominio (abstracción interna) viven en `domain/models/{service}/`. El `FeignAdapter` mapea `InfraDto → DomainModel` inline con métodos privados `to{DomainType}()`. Si la API externa cambia, solo hay que actualizar el adaptador.
|
|
1819
|
+
|
|
1820
|
+
### Reglas de `ports[]`
|
|
1821
|
+
|
|
1822
|
+
- **`service:`** — PascalCase, agrupa métodos en un mismo FeignClient
|
|
1823
|
+
- **`baseUrl:`** — declarar solo en la primera entrada de cada `service:`; si se omite en todas → warning + `http://localhost:8080`
|
|
1824
|
+
- **`body:`** — solo en POST/PUT/PATCH; en GET/DELETE emite warning y se ignora
|
|
1825
|
+
- **`domainType:`** — sobrescribe el tipo de dominio auto-derivado del nombre del método (ej: `domainType: Seat` en `findAvailableSeats`)
|
|
1826
|
+
- **`returnList: true`** — el tipo de retorno es `List<{DomainType}>` en la interfaz y `List<{InfraDto}>` en el FeignClient (default: `false`)
|
|
1827
|
+
- **`nestedTypes:`** — records auxiliares en `application/dtos/`; mismo patrón que `listeners:`
|
|
1828
|
+
- **`fields:` omitido** → retorno `void` en interfaz y FeignClient
|
|
1829
|
+
|
|
1830
|
+
**Contraste async vs sync:**
|
|
1831
|
+
```
|
|
1832
|
+
aggregates:
|
|
1833
|
+
└── events: → Domain Events que PRODUCE (async, broker)
|
|
1834
|
+
listeners: → Integration Events que CONSUME (async, broker)
|
|
1835
|
+
ports: → Servicios HTTP que LLAMA (sync, Feign)
|
|
1836
|
+
```
|
|
1837
|
+
|
|
1838
|
+
---
|
|
1839
|
+
|
|
1593
1840
|
### Estrategias de Cascade
|
|
1594
1841
|
|
|
1595
1842
|
```yaml
|
|
@@ -1725,6 +1972,8 @@ public class Product {
|
|
|
1725
1972
|
|
|
1726
1973
|
### Reglas para Agentes con Soft Delete
|
|
1727
1974
|
|
|
1975
|
+
- **SOLO** aplicar `hasSoftDelete: true` en la **entidad raíz** del agregado (`isRoot: true`)
|
|
1976
|
+
- **NUNCA** poner `hasSoftDelete: true` en entidades secundarias — el ciclo de vida lo controla la raíz mediante `cascade`; el generador emite un warning y descarta el flag
|
|
1728
1977
|
- **NUNCA** usar `repository.deleteById()` — usar `entity.softDelete()` + `repository.save(entity)`
|
|
1729
1978
|
- **NUNCA** exponer `deletedAt` en ResponseDtos (campo interno)
|
|
1730
1979
|
- **SIEMPRE** usar `@SQLRestriction("deleted_at IS NULL")` para filtrado automático en JPA
|
|
@@ -1995,8 +2244,10 @@ public record UserResponseDto(
|
|
|
1995
2244
|
2. **SI** el módulo requiere ciclo de vida → usar `transitions` + `initialValue` en el enum
|
|
1996
2245
|
3. **SI** un Value Object tiene comportamiento → declarar `methods` en `domain.yaml`
|
|
1997
2246
|
4. **SI** ocurren hechos relevantes de negocio → declarar `events[]` en el agregado
|
|
1998
|
-
5. **SI** el evento debe propagarse
|
|
1999
|
-
6. **
|
|
2247
|
+
5. **SI** el evento debe propagarse vía broker → **no** usar `kafka: true`; si `eva add kafka-client` está instalado, `eva g entities` auto-cablea todos los eventos
|
|
2248
|
+
6. **SI** el módulo consume eventos de otros servicios → declarar `listeners:` (sibling de `aggregates:`) con `topic:` obligatorio en módulos standalone
|
|
2249
|
+
7. **SI** el módulo llama servicios HTTP externos de forma síncrona → declarar `ports:` (sibling de `aggregates:`) con `baseUrl:` en la primera entrada de cada `service:`
|
|
2250
|
+
8. **DESPUÉS** de generar el `domain.yaml` → ejecutar `eva g entities <module>`
|
|
2000
2251
|
|
|
2001
2252
|
### Al Generar Código de Dominio
|
|
2002
2253
|
|
|
@@ -2089,7 +2340,17 @@ Al generar o modificar código, verificar:
|
|
|
2089
2340
|
- [ ] Enum con ciclo de vida → `transitions` + `initialValue`, no setters
|
|
2090
2341
|
- [ ] Value Object con comportamiento → `methods` en domain.yaml
|
|
2091
2342
|
- [ ] Evento de dominio → `events[]`, publicar con `raise()` en método de negocio
|
|
2092
|
-
- [ ] Evento con
|
|
2343
|
+
- [ ] Evento con broker → **no** usar `kafka: true`; si `eva add kafka-client` está instalado, `eva g entities` auto-cablea todos los eventos
|
|
2344
|
+
- [ ] Distinguir Domain Event (`domain/models/events/`) e Integration Event (`application/events/`) — cambios de broker solo afectan al adaptador `MessageBroker`
|
|
2345
|
+
- [ ] Consumo de eventos externos → declarar en `listeners[]` (nivel raíz); `topic:` obligatorio en módulos standalone
|
|
2346
|
+
- [ ] Cada `listener` genera hasta 6 artefactos: NestedType(s) → IntegrationEvent → KafkaListener → kafka.yaml → Command → CommandHandler
|
|
2347
|
+
- [ ] Campos de tipo objeto en listeners → declarar `nestedTypes:` para generar records auxiliares en `application/events/`
|
|
2348
|
+
- [ ] Clientes HTTP síncronos → declarar en `ports[]` (nivel raíz); `baseUrl:` en la primera entrada de cada `service:`
|
|
2349
|
+
- [ ] Métodos con respuesta → incluir `fields:` en la entrada del puerto; sin `fields:` = retorno `void`
|
|
2350
|
+
- [ ] Respuestas en lista → agregar `returnList: true` en el método correspondiente
|
|
2351
|
+
- [ ] Métodos con cuerpo (POST/PUT/PATCH) → incluir `body:`; campos de tipo objeto en `nestedTypes:`
|
|
2352
|
+
- [ ] Tipo de dominio auto-derivado del nombre del método — usar `domainType:` para sobrescribir si hay colisión
|
|
2353
|
+
- [ ] Cada `service:` en `ports[]` genera: interfaz (devuelve modelos de dominio en `domain/models/{service}/`), FeignClient, FeignAdapter (ACL), FeignConfig + `urls.yaml`
|
|
2093
2354
|
|
|
2094
2355
|
---
|
|
2095
2356
|
|