eva4j 1.0.17 → 1.0.18
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 +2 -0
- package/DOMAIN_YAML_GUIDE.md +3 -1
- package/QUICK_REFERENCE.md +8 -4
- package/bin/eva4j.js +70 -2
- package/config/defaults.json +1 -0
- package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
- package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
- package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
- package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
- package/docs/commands/EVALUATE_SYSTEM.md +272 -8
- package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
- package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
- package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
- package/docs/commands/INDEX.md +27 -3
- package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
- package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
- package/docs/prototype/system/RISKS.md +277 -0
- package/docs/prototype/system/customers.yaml +133 -0
- package/docs/prototype/system/inventory.yaml +109 -0
- package/docs/prototype/system/notifications.yaml +131 -0
- package/docs/prototype/system/orders.yaml +241 -0
- package/docs/prototype/system/payments.yaml +256 -0
- package/docs/prototype/system/products.yaml +168 -0
- package/docs/prototype/system/system.yaml +269 -0
- package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
- package/examples/domain-read-models.yaml +2 -2
- package/examples/system/customer.yaml +89 -0
- package/examples/system/orders.yaml +119 -0
- package/examples/system/product.yaml +27 -0
- package/examples/system/system.yaml +80 -0
- package/package.json +1 -1
- package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
- package/src/agents/design-gap-analyst.agent.md +383 -0
- package/src/agents/design-reviewer-temporal.agent.md +412 -0
- package/src/agents/design-reviewer.agent.md +31 -5
- package/src/agents/implement-use-cases.prompt.md +179 -0
- package/src/agents/ux-gap-analyst.agent.md +412 -0
- package/src/commands/add-rabbitmq-client.js +261 -0
- package/src/commands/add-temporal-client.js +22 -2
- package/src/commands/build.js +267 -11
- package/src/commands/evaluate-system.js +700 -13
- package/src/commands/generate-entities.js +308 -16
- package/src/commands/generate-rabbitmq-event.js +665 -0
- package/src/commands/generate-rabbitmq-listener.js +205 -0
- package/src/commands/generate-temporal-activity.js +968 -34
- package/src/commands/generate-temporal-flow.js +95 -38
- package/src/commands/generate-temporal-system.js +708 -0
- package/src/skills/build-system-yaml/SKILL.md +222 -2
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +50 -4
- package/src/skills/build-system-yaml/references/module-spec.md +57 -8
- package/src/skills/build-temporal-system/SKILL.md +752 -0
- package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
- package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
- package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
- package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
- package/src/skills/implement-use-case/SKILL.md +350 -0
- package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
- package/src/skills/requirements-elicitation/SKILL.md +228 -0
- package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
- package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
- package/src/utils/bounded-context-diagram.js +844 -0
- package/src/utils/domain-validator.js +266 -4
- package/src/utils/naming.js +10 -0
- package/src/utils/system-validator.js +169 -11
- package/src/utils/system-yaml-parser.js +318 -0
- package/src/utils/temporal-validator.js +497 -0
- package/src/utils/yaml-to-entity.js +10 -7
- package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
- package/templates/aggregate/JpaAggregateRoot.java.ejs +2 -2
- package/templates/aggregate/JpaEntity.java.ejs +2 -2
- package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
- package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
- package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
- package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
- package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
- package/templates/base/root/AGENTS.md.ejs +1 -1
- package/templates/crud/EndpointsController.java.ejs +1 -1
- package/templates/crud/ScaffoldCommand.java.ejs +5 -2
- package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
- package/templates/crud/ScaffoldQuery.java.ejs +5 -2
- package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
- package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
- package/templates/evaluate/report.html.ejs +1447 -90
- package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
- package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
- package/templates/ports/PortAclMapper.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +7 -22
- package/templates/ports/PortFeignClient.java.ejs +4 -0
- package/templates/ports/PortResponseDto.java.ejs +1 -1
- package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
- package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
- package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
- package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
- package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
- package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
- package/templates/read-model/ReadModelJpa.java.ejs +1 -1
- package/templates/read-model/ReadModelJpaRepository.java.ejs +2 -0
- package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
- package/templates/read-model/ReadModelRepositoryImpl.java.ejs +9 -5
- package/templates/read-model/ReadModelSyncHandler.java.ejs +2 -0
- package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
- package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
- package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
- package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
- package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
- package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
- package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/NestedType.java.ejs +12 -0
- package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
- package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
- package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
- package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
- package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
- package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
- package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
- package/COMMAND_EVALUATION.md +0 -911
- package/test-c2010.js +0 -49
- package/test-update-compat.js +0 -109
- package/test-update-lifecycle.js +0 -121
|
@@ -12,7 +12,7 @@ import org.slf4j.MDC;
|
|
|
12
12
|
public class <%= modulePascalCase %>KafkaMessageBroker implements MessageBroker {
|
|
13
13
|
|
|
14
14
|
@Value("<%= topicSpringProperty %>")
|
|
15
|
-
private String <%=
|
|
15
|
+
private String <%= topicNameCamel %>Topic;
|
|
16
16
|
|
|
17
17
|
private final KafkaTemplate<String, Object> kafkaTemplate;
|
|
18
18
|
|
|
@@ -23,10 +23,10 @@ public class <%= modulePascalCase %>KafkaMessageBroker implements MessageBroker
|
|
|
23
23
|
@Override
|
|
24
24
|
public void publish<%= eventClassName %>(<%= eventClassName %> event) {
|
|
25
25
|
EventEnvelope<<%= eventClassName %>> envelope = EventEnvelope.of(
|
|
26
|
-
<%=
|
|
26
|
+
<%= topicNameCamel %>Topic,
|
|
27
27
|
event,
|
|
28
28
|
MDC.get("correlationId")
|
|
29
29
|
);
|
|
30
|
-
kafkaTemplate.send(<%=
|
|
30
|
+
kafkaTemplate.send(<%= topicNameCamel %>Topic, envelope);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.infrastructure.adapters.<%= adapterPackage %>;
|
|
2
|
+
|
|
3
|
+
import org.springframework.stereotype.Component;
|
|
4
|
+
<% const allMethods = methods || []; %>
|
|
5
|
+
<% const responseMethods = allMethods.filter(m => m.hasResponse); %>
|
|
6
|
+
<% /* ACL: import domain models */ %>
|
|
7
|
+
<% const uniqueDomainTypes = [...new Set(responseMethods.map(m => m.domainType))]; %>
|
|
8
|
+
<% uniqueDomainTypes.forEach(dt => { %>import <%= packageName %>.<%= moduleName %>.domain.models.<%= adapterPackage %>.<%= dt %>;
|
|
9
|
+
<% }); %>
|
|
10
|
+
<% /* Import infra DTOs from dtos sub-package */ %>
|
|
11
|
+
<% const uniqueInfraDtos = [...new Set(responseMethods.map(m => m.infraDtoName))]; %>
|
|
12
|
+
<% uniqueInfraDtos.forEach(dto => { %>import <%= packageName %>.<%= moduleName %>.infrastructure.adapters.<%= adapterPackage %>.dtos.<%= dto %>;
|
|
13
|
+
<% }); %>
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ACL (Anti-Corruption Layer) mapper for {@link <%= serviceName %>}.
|
|
17
|
+
*
|
|
18
|
+
* <p>Maps infrastructure DTOs (shaped by the external service) to domain models.
|
|
19
|
+
* If the external API changes, only these methods need to be updated;
|
|
20
|
+
* domain logic using the domain models remains untouched.
|
|
21
|
+
*/
|
|
22
|
+
@Component("<%= moduleName %>.<%= aclMapperClassName %>")
|
|
23
|
+
public class <%= aclMapperClassName %> {
|
|
24
|
+
<% responseMethods.forEach(m => { %>
|
|
25
|
+
|
|
26
|
+
public <%- m.domainType %> to<%= m.domainType %>(<%= m.infraDtoName %> dto) {
|
|
27
|
+
if (dto == null) return null;
|
|
28
|
+
return new <%= m.domainType %>(
|
|
29
|
+
<% m.fields.forEach((f, i) => { %>
|
|
30
|
+
dto.<%= f.name %>()<%= i < m.fields.length - 1 ? ',' : '' %>
|
|
31
|
+
<% }); %>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
<% }); %>
|
|
35
|
+
}
|
|
@@ -20,9 +20,11 @@ import java.util.stream.Collectors;
|
|
|
20
20
|
public class <%= feignAdapterClassName %> implements <%= serviceName %> {
|
|
21
21
|
|
|
22
22
|
private final <%= feignClientClassName %> feignClient;
|
|
23
|
+
private final <%= aclMapperClassName %> aclMapper;
|
|
23
24
|
|
|
24
|
-
public <%= feignAdapterClassName %>(<%= feignClientClassName %> feignClient) {
|
|
25
|
+
public <%= feignAdapterClassName %>(<%= feignClientClassName %> feignClient, <%= aclMapperClassName %> aclMapper) {
|
|
25
26
|
this.feignClient = feignClient;
|
|
27
|
+
this.aclMapper = aclMapper;
|
|
26
28
|
}
|
|
27
29
|
<% allMethods.forEach(m => { %>
|
|
28
30
|
<% /* Public override returns domain model; FeignClient returns infra DTO */ %>
|
|
@@ -39,29 +41,12 @@ public class <%= feignAdapterClassName %> implements <%= serviceName %> {
|
|
|
39
41
|
<% if (!m.hasResponse) { %>
|
|
40
42
|
feignClient.<%= m.name %>(<%= args.join(', ') %>);
|
|
41
43
|
<% } else if (m.returnList) { %>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
return feignClient.<%= m.name %>(<%= args.join(', ') %>).stream()
|
|
45
|
+
.map(aclMapper::to<%= m.domainType %>)
|
|
46
|
+
.collect(Collectors.toList());
|
|
44
47
|
<% } else { %>
|
|
45
|
-
return to<%= m.domainType %>(feignClient.<%= m.name %>(<%= args.join(', ') %>));
|
|
48
|
+
return aclMapper.to<%= m.domainType %>(feignClient.<%= m.name %>(<%= args.join(', ') %>));
|
|
46
49
|
<% } %>
|
|
47
50
|
}
|
|
48
51
|
<% }); %>
|
|
49
|
-
|
|
50
|
-
// ── ACL Mappers ─────────────────────────────────────────────────────────
|
|
51
|
-
// Maps infra DTOs (shaped by the external service) to domain models.
|
|
52
|
-
// If the external API changes, only these methods need to be updated.
|
|
53
|
-
<%
|
|
54
|
-
// Build one mapper per method that has a response
|
|
55
|
-
const responseMethods = allMethods.filter(m => m.hasResponse);
|
|
56
|
-
%>
|
|
57
|
-
<% responseMethods.forEach(m => { %>
|
|
58
|
-
private <%- m.domainType %> to<%= m.domainType %>(<%= m.infraDtoName %> dto) {
|
|
59
|
-
if (dto == null) return null;
|
|
60
|
-
return new <%= m.domainType %>(
|
|
61
|
-
<% m.fields.forEach((f, i) => { %>
|
|
62
|
-
dto.<%= f.name %>()<%= i < m.fields.length - 1 ? ',' : '' %>
|
|
63
|
-
<% }); %>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
<% }); %>
|
|
67
52
|
}
|
|
@@ -23,6 +23,10 @@ import java.time.LocalTime;
|
|
|
23
23
|
<% }); %>
|
|
24
24
|
<% (nestedTypes || []).forEach(nt => { %>import <%= packageName %>.<%= moduleName %>.application.dtos.<%= nt.name %>;
|
|
25
25
|
<% }); %>
|
|
26
|
+
<% /* Infra response DTOs live in infrastructure.adapters.{service}.dtos */ %>
|
|
27
|
+
<% const uniqueInfraDtos = [...new Set(allMethods.filter(m => m.hasResponse).map(m => m.infraDtoName))]; %>
|
|
28
|
+
<% uniqueInfraDtos.forEach(dto => { %>import <%= packageName %>.<%= moduleName %>.infrastructure.adapters.<%= adapterPackage %>.dtos.<%= dto %>;
|
|
29
|
+
<% }); %>
|
|
26
30
|
@FeignClient(
|
|
27
31
|
name = "<%= feignClientName %>",
|
|
28
32
|
url = "${<%= baseUrlProperty %>}",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
package <%= packageName %>.<%= moduleName %>.infrastructure.adapters.<%= adapterPackage
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.infrastructure.adapters.<%= adapterPackage %>.dtos;
|
|
2
2
|
<% const needsBigDecimal = fields && fields.some(f => f.javaType === 'BigDecimal'); %>
|
|
3
3
|
<% const needsLocalDate = fields && fields.some(f => ['LocalDate','LocalDateTime','LocalTime'].includes(f.javaType)); %>
|
|
4
4
|
<% const needsInstant = fields && fields.some(f => f.javaType === 'Instant'); %>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
@Value("${queues.<%= topicNameKebab %>}")
|
|
2
|
+
private String <%= valueFieldName %>QueueName;
|
|
3
|
+
|
|
4
|
+
@Value("${routing-keys.<%= topicNameKebab %>}")
|
|
5
|
+
private String <%= valueFieldName %>RoutingKeyValue;
|
|
6
|
+
|
|
7
|
+
@Bean
|
|
8
|
+
public Queue <%= beanMethodName %>Queue() {
|
|
9
|
+
return QueueBuilder.durable(<%= valueFieldName %>QueueName)
|
|
10
|
+
.withArgument("x-dead-letter-exchange", <%= moduleName %>ExchangeName + ".dlx")
|
|
11
|
+
.build();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@Bean
|
|
15
|
+
public Binding <%= beanMethodName %>Binding() {
|
|
16
|
+
return BindingBuilder
|
|
17
|
+
.bind(<%= beanMethodName %>Queue())
|
|
18
|
+
.to(<%= moduleName %>Exchange())
|
|
19
|
+
.with(<%= valueFieldName %>RoutingKeyValue);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Bean
|
|
23
|
+
public Queue <%= beanMethodName %>Dlq() {
|
|
24
|
+
return QueueBuilder.durable(<%= valueFieldName %>QueueName + ".dlq").build();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Bean
|
|
28
|
+
public Binding <%= beanMethodName %>DlqBinding() {
|
|
29
|
+
return BindingBuilder
|
|
30
|
+
.bind(<%= beanMethodName %>Dlq())
|
|
31
|
+
.to(<%= moduleName %>DlxExchange())
|
|
32
|
+
.with(<%= valueFieldName %>RoutingKeyValue);
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
@Value("${exchanges.<%= moduleName %>}")
|
|
2
|
+
private String <%= moduleName %>ExchangeName;
|
|
3
|
+
|
|
4
|
+
@Bean
|
|
5
|
+
public TopicExchange <%= moduleName %>Exchange() {
|
|
6
|
+
return new TopicExchange(<%= moduleName %>ExchangeName, true, false);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@Bean
|
|
10
|
+
public TopicExchange <%= moduleName %>DlxExchange() {
|
|
11
|
+
return new TopicExchange(<%= moduleName %>ExchangeName + ".dlx", true, false);
|
|
12
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.infrastructure.adapters.rabbitmqMessageBroker;
|
|
2
|
+
|
|
3
|
+
import <%= packageName %>.<%= moduleName %>.application.events.<%= eventClassName %>;
|
|
4
|
+
import <%= packageName %>.<%= moduleName %>.application.ports.MessageBroker;
|
|
5
|
+
import <%= packageName %>.shared.infrastructure.eventEnvelope.EventEnvelope;
|
|
6
|
+
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
|
7
|
+
import org.springframework.beans.factory.annotation.Value;
|
|
8
|
+
import org.springframework.stereotype.Component;
|
|
9
|
+
import org.slf4j.MDC;
|
|
10
|
+
|
|
11
|
+
@Component("<%= moduleCamelCase %>RabbitMessageBroker")
|
|
12
|
+
public class <%= modulePascalCase %>RabbitMessageBroker implements MessageBroker {
|
|
13
|
+
|
|
14
|
+
@Value("${exchanges.<%= moduleName %>}")
|
|
15
|
+
private String exchange;
|
|
16
|
+
|
|
17
|
+
@Value("${routing-keys.<%= topicNameKebab %>}")
|
|
18
|
+
private String <%= topicNameCamel %>RoutingKey;
|
|
19
|
+
|
|
20
|
+
private final RabbitTemplate rabbitTemplate;
|
|
21
|
+
|
|
22
|
+
public <%= modulePascalCase %>RabbitMessageBroker(RabbitTemplate rabbitTemplate) {
|
|
23
|
+
this.rabbitTemplate = rabbitTemplate;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Override
|
|
27
|
+
public void publish<%= eventClassName %>(<%= eventClassName %> event) {
|
|
28
|
+
EventEnvelope<<%= eventClassName %>> envelope = EventEnvelope.of(
|
|
29
|
+
<%= topicNameCamel %>RoutingKey,
|
|
30
|
+
event,
|
|
31
|
+
MDC.get("correlationId")
|
|
32
|
+
);
|
|
33
|
+
rabbitTemplate.convertAndSend(exchange, <%= topicNameCamel %>RoutingKey, envelope);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
@Override
|
|
2
|
+
public void publish<%= eventClassName %>(<%= eventClassName %> event) {
|
|
3
|
+
EventEnvelope<<%= eventClassName %>> envelope = EventEnvelope.of(
|
|
4
|
+
<%= valueFieldName %>,
|
|
5
|
+
event,
|
|
6
|
+
MDC.get("correlationId")
|
|
7
|
+
);
|
|
8
|
+
rabbitTemplate.convertAndSend(exchange, <%= valueFieldName %>, envelope);
|
|
9
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
@Value("${queues.<%= topicKey %>}")
|
|
2
|
+
private String <%= valueFieldName %>QueueName;
|
|
3
|
+
|
|
4
|
+
@Value("${routing-keys.<%= topicKey %>}")
|
|
5
|
+
private String <%= valueFieldName %>RoutingKeyValue;
|
|
6
|
+
|
|
7
|
+
@Bean
|
|
8
|
+
public Queue <%= beanMethodName %>Queue() {
|
|
9
|
+
return QueueBuilder.durable(<%= valueFieldName %>QueueName)
|
|
10
|
+
.withArgument("x-dead-letter-exchange", <%= producerModule %>ExchangeName + ".dlx")
|
|
11
|
+
.build();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@Bean
|
|
15
|
+
public Binding <%= beanMethodName %>Binding() {
|
|
16
|
+
return BindingBuilder
|
|
17
|
+
.bind(<%= beanMethodName %>Queue())
|
|
18
|
+
.to(<%= producerModule %>Exchange())
|
|
19
|
+
.with(<%= valueFieldName %>RoutingKeyValue);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Bean
|
|
23
|
+
public Queue <%= beanMethodName %>Dlq() {
|
|
24
|
+
return QueueBuilder.durable(<%= valueFieldName %>QueueName + ".dlq").build();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Bean
|
|
28
|
+
public Binding <%= beanMethodName %>DlqBinding() {
|
|
29
|
+
return BindingBuilder
|
|
30
|
+
.bind(<%= beanMethodName %>Dlq())
|
|
31
|
+
.to(<%= producerModule %>DlxExchange())
|
|
32
|
+
.with(<%= valueFieldName %>RoutingKeyValue);
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
@Value("${exchanges.<%= producerModule %>}")
|
|
2
|
+
private String <%= producerModule %>ExchangeName;
|
|
3
|
+
|
|
4
|
+
@Bean
|
|
5
|
+
public TopicExchange <%= producerModule %>Exchange() {
|
|
6
|
+
return new TopicExchange(<%= producerModule %>ExchangeName, true, false);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@Bean
|
|
10
|
+
public TopicExchange <%= producerModule %>DlxExchange() {
|
|
11
|
+
return new TopicExchange(<%= producerModule %>ExchangeName + ".dlx", true, false);
|
|
12
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.infrastructure.rabbitListener;
|
|
2
|
+
|
|
3
|
+
import <%= packageName %>.<%= moduleName %>.application.commands.<%= commandClassName %>;
|
|
4
|
+
import <%= packageName %>.shared.infrastructure.configurations.useCaseConfig.UseCaseMediator;
|
|
5
|
+
import <%= packageName %>.shared.infrastructure.eventEnvelope.EventEnvelope;
|
|
6
|
+
|
|
7
|
+
import com.fasterxml.jackson.core.type.TypeReference;
|
|
8
|
+
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
9
|
+
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
10
|
+
import com.rabbitmq.client.Channel;
|
|
11
|
+
import org.springframework.amqp.core.Message;
|
|
12
|
+
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
|
13
|
+
import org.springframework.stereotype.Component;
|
|
14
|
+
import org.slf4j.Logger;
|
|
15
|
+
import org.slf4j.LoggerFactory;
|
|
16
|
+
|
|
17
|
+
import java.io.IOException;
|
|
18
|
+
import java.util.Map;
|
|
19
|
+
<% const hasLists = fields && fields.some(f => f.javaType && f.javaType.startsWith('List')); %>
|
|
20
|
+
<% if (hasLists) { %>import java.util.List;
|
|
21
|
+
<% } %><% const needsBigDecimal = fields && fields.some(f => f.javaType === 'BigDecimal'); %>
|
|
22
|
+
<% const needsLocalDate = fields && fields.some(f => ['LocalDate','LocalDateTime','LocalTime'].includes(f.javaType)); %>
|
|
23
|
+
<% const needsInstant = fields && fields.some(f => f.javaType === 'Instant'); %>
|
|
24
|
+
<% const needsUUID = fields && fields.some(f => f.javaType === 'UUID'); %>
|
|
25
|
+
<% if (needsBigDecimal) { %>import java.math.BigDecimal;
|
|
26
|
+
<% } %><% if (needsLocalDate) { %>import java.time.LocalDate;
|
|
27
|
+
import java.time.LocalDateTime;
|
|
28
|
+
import java.time.LocalTime;
|
|
29
|
+
<% } %><% if (needsInstant) { %>import java.time.Instant;
|
|
30
|
+
<% } %><% if (needsUUID) { %>import java.util.UUID;
|
|
31
|
+
<% } %><% (nestedTypes || []).forEach(nt => { %>import <%= packageName %>.<%= moduleName %>.application.events.<%= nt.name %>;
|
|
32
|
+
<% }); %>
|
|
33
|
+
/**
|
|
34
|
+
* RabbitMQ listener for queue <%= topicConstant %>.
|
|
35
|
+
* Consumes events produced by: <%= producer %>.
|
|
36
|
+
* Dispatches to use case: <%= useCase %>.
|
|
37
|
+
*/
|
|
38
|
+
@Component("<%= moduleName %>.<%= listenerClassName %>")
|
|
39
|
+
public class <%= listenerClassName %> {
|
|
40
|
+
|
|
41
|
+
private static final Logger log = LoggerFactory.getLogger(<%= listenerClassName %>.class);
|
|
42
|
+
|
|
43
|
+
private final UseCaseMediator useCaseMediator;
|
|
44
|
+
private final ObjectMapper objectMapper;
|
|
45
|
+
|
|
46
|
+
public <%= listenerClassName %>(UseCaseMediator useCaseMediator, ObjectMapper objectMapper) {
|
|
47
|
+
this.useCaseMediator = useCaseMediator;
|
|
48
|
+
this.objectMapper = objectMapper;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@RabbitListener(queues = "<%= topicSpringProperty %>")
|
|
52
|
+
public void handle(Message message, Channel channel) throws IOException {
|
|
53
|
+
long deliveryTag = message.getMessageProperties().getDeliveryTag();
|
|
54
|
+
|
|
55
|
+
EventEnvelope<Map<String, Object>> event;
|
|
56
|
+
try {
|
|
57
|
+
event = objectMapper.readValue(
|
|
58
|
+
message.getBody(),
|
|
59
|
+
new TypeReference<EventEnvelope<Map<String, Object>>>() {});
|
|
60
|
+
} catch (JsonProcessingException e) {
|
|
61
|
+
log.error("Fatal deserialization error — sending to DLQ: {}", e.getMessage());
|
|
62
|
+
channel.basicNack(deliveryTag, false, false);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
<% (fields || []).forEach(f => { %><%
|
|
67
|
+
const listMatch = f.javaType.match(/^List<(.+)>$/);
|
|
68
|
+
if (listMatch) { %>
|
|
69
|
+
<%- f.javaType %> <%= f.name %> = objectMapper.convertValue(
|
|
70
|
+
event.data().get("<%= f.name %>"),
|
|
71
|
+
objectMapper.getTypeFactory().constructCollectionType(List.class, <%= listMatch[1] %>.class));
|
|
72
|
+
<% } else { %>
|
|
73
|
+
<%- f.javaType %> <%= f.name %> = objectMapper.convertValue(event.data().get("<%= f.name %>"), <%- f.javaType %>.class);
|
|
74
|
+
<% } %><% }); %>
|
|
75
|
+
useCaseMediator.dispatch(new <%= commandClassName %>(
|
|
76
|
+
<% (fields || []).forEach((f, i) => { %>
|
|
77
|
+
<%= f.name %><%= i < fields.length - 1 ? ',' : '' %>
|
|
78
|
+
<% }); %>
|
|
79
|
+
));
|
|
80
|
+
channel.basicAck(deliveryTag, false);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.infrastructure.rabbitListener;
|
|
2
|
+
|
|
3
|
+
import <%= packageName %>.shared.infrastructure.configurations.useCaseConfig.UseCaseMediator;
|
|
4
|
+
import <%= packageName %>.shared.infrastructure.eventEnvelope.EventEnvelope;
|
|
5
|
+
|
|
6
|
+
import com.fasterxml.jackson.core.type.TypeReference;
|
|
7
|
+
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
8
|
+
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
9
|
+
import com.rabbitmq.client.Channel;
|
|
10
|
+
import org.springframework.amqp.core.Message;
|
|
11
|
+
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
|
12
|
+
import org.springframework.stereotype.Component;
|
|
13
|
+
import org.slf4j.Logger;
|
|
14
|
+
import org.slf4j.LoggerFactory;
|
|
15
|
+
|
|
16
|
+
import java.io.IOException;
|
|
17
|
+
import java.util.Map;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* RabbitMQ listener for queue <%= topicValue %>
|
|
21
|
+
*/
|
|
22
|
+
@Component("<%= listenerBeanName %>")
|
|
23
|
+
public class <%= listenerClassName %> {
|
|
24
|
+
|
|
25
|
+
private static final Logger log = LoggerFactory.getLogger(<%= listenerClassName %>.class);
|
|
26
|
+
|
|
27
|
+
private final UseCaseMediator useCaseMediator;
|
|
28
|
+
private final ObjectMapper objectMapper;
|
|
29
|
+
|
|
30
|
+
public <%= listenerClassName %>(UseCaseMediator useCaseMediator, ObjectMapper objectMapper) {
|
|
31
|
+
this.useCaseMediator = useCaseMediator;
|
|
32
|
+
this.objectMapper = objectMapper;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@RabbitListener(queues = "<%= topicSpringProperty %>")
|
|
36
|
+
public void handle(Message message, Channel channel) throws IOException {
|
|
37
|
+
long deliveryTag = message.getMessageProperties().getDeliveryTag();
|
|
38
|
+
|
|
39
|
+
EventEnvelope<Map<String, Object>> event;
|
|
40
|
+
try {
|
|
41
|
+
event = objectMapper.readValue(
|
|
42
|
+
message.getBody(),
|
|
43
|
+
new TypeReference<EventEnvelope<Map<String, Object>>>() {});
|
|
44
|
+
} catch (JsonProcessingException e) {
|
|
45
|
+
log.error("Fatal deserialization error — sending to DLQ: {}", e.getMessage());
|
|
46
|
+
channel.basicNack(deliveryTag, false, false);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// TODO: Implement event processing logic
|
|
51
|
+
// Example: useCaseMediator.dispatch(new YourCommand(event.data()));
|
|
52
|
+
|
|
53
|
+
channel.basicAck(deliveryTag, false);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
}
|
|
@@ -2,10 +2,12 @@ package <%= packageName %>.<%= moduleName %>.infrastructure.database.repositorie
|
|
|
2
2
|
|
|
3
3
|
import <%= packageName %>.<%= moduleName %>.infrastructure.database.entities.<%= jpaEntityName %>;
|
|
4
4
|
import org.springframework.data.jpa.repository.JpaRepository;
|
|
5
|
+
import org.springframework.stereotype.Repository;
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Spring Data JPA repository for the <%= domainClassName %> read model.
|
|
8
9
|
* Table: <%= tableName %>.
|
|
9
10
|
*/
|
|
11
|
+
@Repository("<%= moduleName %>.<%= jpaRepositoryName %>")
|
|
10
12
|
public interface <%= jpaRepositoryName %> extends JpaRepository<<%= jpaEntityName %>, String> {
|
|
11
13
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.infrastructure.rabbitListener;
|
|
2
|
+
<% const isUpsert = action === 'UPSERT'; %>
|
|
3
|
+
<% if (isUpsert) { %>
|
|
4
|
+
|
|
5
|
+
import <%= packageName %>.<%= moduleName %>.application.events.<%= integrationEventClassName %>;
|
|
6
|
+
<% } %>
|
|
7
|
+
import <%= packageName %>.<%= moduleName %>.application.usecases.<%= syncHandlerName %>;
|
|
8
|
+
import <%= packageName %>.shared.infrastructure.eventEnvelope.EventEnvelope;
|
|
9
|
+
|
|
10
|
+
import com.fasterxml.jackson.core.type.TypeReference;
|
|
11
|
+
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
12
|
+
import com.rabbitmq.client.Channel;
|
|
13
|
+
import org.springframework.amqp.core.Message;
|
|
14
|
+
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
|
15
|
+
import org.springframework.stereotype.Component;
|
|
16
|
+
|
|
17
|
+
import java.io.IOException;
|
|
18
|
+
import java.util.Map;
|
|
19
|
+
<% if (isUpsert) { %>
|
|
20
|
+
<% const needsBigDecimal = fields && fields.some(f => f.javaType === 'BigDecimal'); %>
|
|
21
|
+
<% const needsLocalDate = fields && fields.some(f => ['LocalDate','LocalDateTime','LocalTime'].includes(f.javaType)); %>
|
|
22
|
+
<% const needsInstant = fields && fields.some(f => f.javaType === 'Instant'); %>
|
|
23
|
+
<% const needsUUID = fields && fields.some(f => f.javaType === 'UUID'); %>
|
|
24
|
+
<% if (needsBigDecimal) { %>import java.math.BigDecimal;
|
|
25
|
+
<% } %><% if (needsLocalDate) { %>import java.time.LocalDate;
|
|
26
|
+
import java.time.LocalDateTime;
|
|
27
|
+
import java.time.LocalTime;
|
|
28
|
+
<% } %><% if (needsInstant) { %>import java.time.Instant;
|
|
29
|
+
<% } %><% if (needsUUID) { %>import java.util.UUID;
|
|
30
|
+
<% } %>
|
|
31
|
+
<% } %>
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* RabbitMQ listener for read model sync — queue: <%= topicConstant %>.
|
|
35
|
+
* Delegates to <%= syncHandlerName %>.on<%= eventBaseName %>().
|
|
36
|
+
*/
|
|
37
|
+
@Component("<%= moduleName %>.<%= listenerClassName %>")
|
|
38
|
+
public class <%= listenerClassName %> {
|
|
39
|
+
|
|
40
|
+
private final <%= syncHandlerName %> syncHandler;
|
|
41
|
+
private final ObjectMapper objectMapper;
|
|
42
|
+
|
|
43
|
+
public <%= listenerClassName %>(<%= syncHandlerName %> syncHandler, ObjectMapper objectMapper) {
|
|
44
|
+
this.syncHandler = syncHandler;
|
|
45
|
+
this.objectMapper = objectMapper;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@RabbitListener(queues = "<%= topicSpringProperty %>")
|
|
49
|
+
public void handle(Message message, Channel channel) throws IOException {
|
|
50
|
+
EventEnvelope<Map<String, Object>> event = objectMapper.readValue(
|
|
51
|
+
message.getBody(),
|
|
52
|
+
new TypeReference<EventEnvelope<Map<String, Object>>>() {});
|
|
53
|
+
|
|
54
|
+
<% if (isUpsert) { %>
|
|
55
|
+
<% (fields || []).forEach(f => { %>
|
|
56
|
+
<%- f.javaType %> <%= f.name %> = objectMapper.convertValue(event.data().get("<%= f.payloadKey || f.name %>"), <%- f.javaType %>.class);
|
|
57
|
+
<% }); %>
|
|
58
|
+
|
|
59
|
+
syncHandler.on<%= eventBaseName %>(new <%= integrationEventClassName %>(
|
|
60
|
+
<% (fields || []).forEach((f, i) => { %>
|
|
61
|
+
<%= f.name %><%= i < fields.length - 1 ? ',' : '' %>
|
|
62
|
+
<% }); %>
|
|
63
|
+
));
|
|
64
|
+
<% } else { %>
|
|
65
|
+
String id = objectMapper.convertValue(event.data().get("<%= (fields[0] && fields[0].payloadKey) || 'id' %>"), String.class);
|
|
66
|
+
syncHandler.on<%= eventBaseName %>(id);
|
|
67
|
+
<% } %>
|
|
68
|
+
|
|
69
|
+
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -27,7 +27,7 @@ import java.util.Optional;
|
|
|
27
27
|
* Repository implementation for the <%= domainClassName %> read model.
|
|
28
28
|
* Maps between domain read model and JPA entity.
|
|
29
29
|
*/
|
|
30
|
-
@Repository
|
|
30
|
+
@Repository("<%= moduleName %>.<%= repositoryImplName %>")
|
|
31
31
|
public class <%= repositoryImplName %> implements <%= repositoryName %> {
|
|
32
32
|
|
|
33
33
|
private final <%= jpaRepositoryName %> jpaRepository;
|
|
@@ -38,11 +38,15 @@ public class <%= repositoryImplName %> implements <%= repositoryName %> {
|
|
|
38
38
|
|
|
39
39
|
@Override
|
|
40
40
|
public void upsert(<%= domainClassName %> readModel) {
|
|
41
|
-
<%= jpaEntityName %> entity =
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
<%= jpaEntityName %> entity = jpaRepository.findById(readModel.getId())
|
|
42
|
+
.orElseGet(() -> {
|
|
43
|
+
<%= jpaEntityName %> newEntity = new <%= jpaEntityName %>();
|
|
44
|
+
newEntity.setId(readModel.getId());
|
|
45
|
+
return newEntity;
|
|
46
|
+
});
|
|
47
|
+
<% fields.filter(f => f.name !== 'id').forEach(field => { %>
|
|
48
|
+
entity.set<%= field.name.charAt(0).toUpperCase() + field.name.slice(1) %>(readModel.get<%= field.name.charAt(0).toUpperCase() + field.name.slice(1) %>());
|
|
44
49
|
<% }); %>
|
|
45
|
-
.build();
|
|
46
50
|
jpaRepository.save(entity);
|
|
47
51
|
}
|
|
48
52
|
|
|
@@ -8,6 +8,7 @@ import <%= packageName %>.<%= moduleName %>.domain.models.readmodels.<%= domainC
|
|
|
8
8
|
import <%= packageName %>.<%= moduleName %>.domain.repositories.<%= repositoryName %>;
|
|
9
9
|
import <%= packageName %>.shared.domain.annotations.ApplicationComponent;
|
|
10
10
|
import <%= packageName %>.shared.domain.annotations.LogExceptions;
|
|
11
|
+
import org.springframework.stereotype.Component;
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Sync handler for the <%= domainClassName %> read model.
|
|
@@ -16,6 +17,7 @@ import <%= packageName %>.shared.domain.annotations.LogExceptions;
|
|
|
16
17
|
* One method per syncedBy event — Kafka listeners delegate directly to these methods.
|
|
17
18
|
*/
|
|
18
19
|
@ApplicationComponent
|
|
20
|
+
@Component("<%= moduleName %>.<%= syncHandlerName %>")
|
|
19
21
|
public class <%= syncHandlerName %> {
|
|
20
22
|
|
|
21
23
|
private final <%= repositoryName %> repository;
|
|
@@ -12,7 +12,7 @@ import org.springframework.kafka.core.KafkaTemplate;
|
|
|
12
12
|
import org.springframework.kafka.listener.ContainerProperties;
|
|
13
13
|
import org.springframework.kafka.listener.DeadLetterPublishingRecoverer;
|
|
14
14
|
import org.springframework.kafka.listener.DefaultErrorHandler;
|
|
15
|
-
import org.springframework.util.backoff.
|
|
15
|
+
import org.springframework.util.backoff.ExponentialBackOff;
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
@Configuration
|
|
@@ -20,11 +20,23 @@ import org.springframework.util.backoff.FixedBackOff;
|
|
|
20
20
|
public class KafkaConfig {
|
|
21
21
|
|
|
22
22
|
@Value("${spring.kafka.listener.retry.max-attempts}")
|
|
23
|
-
private
|
|
23
|
+
private int maxAttempts;
|
|
24
24
|
|
|
25
25
|
@Value("${spring.kafka.listener.retry.backoff-delay}")
|
|
26
26
|
private long backoffDelay;
|
|
27
27
|
|
|
28
|
+
@Value("${spring.kafka.listener.retry.backoff-multiplier:1.0}")
|
|
29
|
+
private double backoffMultiplier;
|
|
30
|
+
|
|
31
|
+
@Value("${spring.kafka.listener.retry.backoff-max-delay:30000}")
|
|
32
|
+
private long backoffMaxDelay;
|
|
33
|
+
|
|
34
|
+
@Value("${kafka.topic-defaults.partitions:3}")
|
|
35
|
+
private int defaultPartitions;
|
|
36
|
+
|
|
37
|
+
@Value("${kafka.topic-defaults.replicas:1}")
|
|
38
|
+
private short defaultReplicas;
|
|
39
|
+
|
|
28
40
|
@Bean
|
|
29
41
|
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory(
|
|
30
42
|
ConsumerFactory<String, String> consumerFactory,
|
|
@@ -39,8 +51,10 @@ public class KafkaConfig {
|
|
|
39
51
|
// Redirige al topic original + ".DLT"
|
|
40
52
|
(record, ex) -> new TopicPartition(record.topic() + ".DLT", record.partition())
|
|
41
53
|
);
|
|
42
|
-
// Configura reintentos
|
|
43
|
-
|
|
54
|
+
// Configura reintentos con backoff exponencial
|
|
55
|
+
ExponentialBackOff backOff = new ExponentialBackOff(backoffDelay, backoffMultiplier);
|
|
56
|
+
backOff.setMaxInterval(backoffMaxDelay);
|
|
57
|
+
backOff.setMaxAttempts(maxAttempts);
|
|
44
58
|
// Manejador de errores
|
|
45
59
|
DefaultErrorHandler errorHandler = new DefaultErrorHandler(recoverer, backOff);
|
|
46
60
|
factory.setCommonErrorHandler(errorHandler);
|