eva4j 1.0.16 → 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 +220 -5
- package/DOMAIN_YAML_GUIDE.md +188 -3
- package/FUTURE_FEATURES.md +33 -52
- 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 +290 -10
- 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-events.yaml +26 -0
- package/examples/domain-read-models.yaml +113 -0
- 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/read-model-spec.md +664 -0
- 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 +34 -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 +560 -24
- package/src/commands/generate-http-exchange.js +3 -0
- package/src/commands/generate-kafka-event.js +3 -0
- package/src/commands/generate-kafka-listener.js +3 -0
- package/src/commands/generate-rabbitmq-event.js +665 -0
- package/src/commands/generate-rabbitmq-listener.js +205 -0
- package/src/commands/generate-record.js +2 -2
- package/src/commands/generate-resource.js +4 -1
- package/src/commands/generate-temporal-activity.js +970 -33
- package/src/commands/generate-temporal-flow.js +98 -38
- package/src/commands/generate-temporal-system.js +708 -0
- package/src/commands/generate-usecase.js +4 -1
- package/src/skills/build-system-yaml/SKILL.md +343 -2
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +253 -26
- package/src/skills/build-system-yaml/references/module-spec.md +90 -9
- package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
- 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/config-manager.js +4 -2
- package/src/utils/domain-validator.js +495 -17
- package/src/utils/naming.js +20 -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/validator.js +3 -1
- package/src/utils/yaml-to-entity.js +281 -9
- package/templates/aggregate/AggregateRepository.java.ejs +4 -0
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +8 -0
- package/templates/aggregate/AggregateRoot.java.ejs +38 -4
- package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
- package/templates/aggregate/JpaAggregateRoot.java.ejs +4 -4
- 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/DeleteCommandHandler.java.ejs +19 -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/crud/UpdateCommandHandler.java.ejs +53 -2
- 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/ReadModelDomain.java.ejs +46 -0
- package/templates/read-model/ReadModelJpa.java.ejs +58 -0
- package/templates/read-model/ReadModelJpaRepository.java.ejs +13 -0
- package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
- package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
- package/templates/read-model/ReadModelRepository.java.ejs +42 -0
- package/templates/read-model/ReadModelRepositoryImpl.java.ejs +85 -0
- package/templates/read-model/ReadModelSyncHandler.java.ejs +54 -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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.domain.models.readmodels;
|
|
2
|
+
<% const needsBigDecimal = fields && fields.some(f => f.javaType === 'BigDecimal'); %>
|
|
3
|
+
<% const needsLocalDate = fields && fields.some(f => ['LocalDate','LocalDateTime','LocalTime'].includes(f.javaType)); %>
|
|
4
|
+
<% const needsInstant = fields && fields.some(f => f.javaType === 'Instant'); %>
|
|
5
|
+
<% const needsUUID = fields && fields.some(f => f.javaType === 'UUID'); %>
|
|
6
|
+
<% if (needsBigDecimal) { %>
|
|
7
|
+
import java.math.BigDecimal;
|
|
8
|
+
<% } %>
|
|
9
|
+
<% if (needsLocalDate) { %>
|
|
10
|
+
import java.time.LocalDate;
|
|
11
|
+
import java.time.LocalDateTime;
|
|
12
|
+
import java.time.LocalTime;
|
|
13
|
+
<% } %>
|
|
14
|
+
<% if (needsInstant) { %>
|
|
15
|
+
import java.time.Instant;
|
|
16
|
+
<% } %>
|
|
17
|
+
<% if (needsUUID) { %>
|
|
18
|
+
import java.util.UUID;
|
|
19
|
+
<% } %>
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Local Read Model — projection of data from <%= sourceModule %>/<%= sourceName %>.
|
|
23
|
+
* <p>
|
|
24
|
+
* This class is read-only from this module's perspective.
|
|
25
|
+
* The source module owns mutations; this projection is maintained
|
|
26
|
+
* via domain events (see syncedBy configuration in domain.yaml).
|
|
27
|
+
* <p>
|
|
28
|
+
* No business logic, no setters, no empty constructor.
|
|
29
|
+
*/
|
|
30
|
+
public class <%= domainClassName %> {
|
|
31
|
+
<% fields.forEach(field => { %>
|
|
32
|
+
private final <%- field.javaType %> <%= field.name %>;
|
|
33
|
+
<% }); %>
|
|
34
|
+
|
|
35
|
+
public <%= domainClassName %>(<%= fields.map(f => f.javaType + ' ' + f.name).join(', ') %>) {
|
|
36
|
+
<% fields.forEach(field => { %>
|
|
37
|
+
this.<%= field.name %> = <%= field.name %>;
|
|
38
|
+
<% }); %>
|
|
39
|
+
}
|
|
40
|
+
<% fields.forEach(field => { %>
|
|
41
|
+
|
|
42
|
+
public <%- field.javaType %> get<%= field.name.charAt(0).toUpperCase() + field.name.slice(1) %>() {
|
|
43
|
+
return this.<%= field.name %>;
|
|
44
|
+
}
|
|
45
|
+
<% }); %>
|
|
46
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.infrastructure.database.entities;
|
|
2
|
+
<% const needsBigDecimal = fields && fields.some(f => f.javaType === 'BigDecimal'); %>
|
|
3
|
+
<% const needsLocalDate = fields && fields.some(f => ['LocalDate','LocalDateTime','LocalTime'].includes(f.javaType)); %>
|
|
4
|
+
<% const needsInstant = fields && fields.some(f => f.javaType === 'Instant'); %>
|
|
5
|
+
<% const needsUUID = fields && fields.some(f => f.javaType === 'UUID'); %>
|
|
6
|
+
<% if (needsBigDecimal) { %>
|
|
7
|
+
import java.math.BigDecimal;
|
|
8
|
+
<% } %>
|
|
9
|
+
<% if (needsLocalDate || hasSoftDelete) { %>
|
|
10
|
+
import java.time.LocalDate;
|
|
11
|
+
import java.time.LocalDateTime;
|
|
12
|
+
import java.time.LocalTime;
|
|
13
|
+
<% } %>
|
|
14
|
+
<% if (needsInstant) { %>
|
|
15
|
+
import java.time.Instant;
|
|
16
|
+
<% } %>
|
|
17
|
+
<% if (needsUUID) { %>
|
|
18
|
+
import java.util.UUID;
|
|
19
|
+
<% } %>
|
|
20
|
+
|
|
21
|
+
import jakarta.persistence.Column;
|
|
22
|
+
import jakarta.persistence.Entity;
|
|
23
|
+
import jakarta.persistence.Id;
|
|
24
|
+
import jakarta.persistence.Table;
|
|
25
|
+
import lombok.AllArgsConstructor;
|
|
26
|
+
import lombok.Builder;
|
|
27
|
+
import lombok.Getter;
|
|
28
|
+
import lombok.NoArgsConstructor;
|
|
29
|
+
import lombok.Setter;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* JPA entity for the local read model — table: <%= tableName %>.
|
|
33
|
+
* Projection of data from <%= sourceModule %>/<%= sourceName %>.
|
|
34
|
+
* <p>
|
|
35
|
+
* No audit fields. The @Id mirrors the source entity's ID (not auto-generated).
|
|
36
|
+
*/
|
|
37
|
+
@Entity(name = "<%= moduleName %>_<%= jpaEntityName %>")
|
|
38
|
+
@Table(name = "<%= tableName %>")
|
|
39
|
+
@Getter
|
|
40
|
+
@Setter
|
|
41
|
+
@NoArgsConstructor
|
|
42
|
+
@AllArgsConstructor
|
|
43
|
+
@Builder
|
|
44
|
+
public class <%= jpaEntityName %> {
|
|
45
|
+
|
|
46
|
+
@Id
|
|
47
|
+
private String id;
|
|
48
|
+
<% fields.filter(f => f.name !== 'id').forEach(field => { %>
|
|
49
|
+
|
|
50
|
+
@Column(name = "<%= field.name.replace(/([A-Z])/g, '_$1').toLowerCase() %>")
|
|
51
|
+
private <%- field.javaType %> <%= field.name %>;
|
|
52
|
+
<% }); %>
|
|
53
|
+
<% if (hasSoftDelete) { %>
|
|
54
|
+
|
|
55
|
+
@Column(name = "deleted_at")
|
|
56
|
+
private LocalDateTime deletedAt;
|
|
57
|
+
<% } %>
|
|
58
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.infrastructure.database.repositories;
|
|
2
|
+
|
|
3
|
+
import <%= packageName %>.<%= moduleName %>.infrastructure.database.entities.<%= jpaEntityName %>;
|
|
4
|
+
import org.springframework.data.jpa.repository.JpaRepository;
|
|
5
|
+
import org.springframework.stereotype.Repository;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Spring Data JPA repository for the <%= domainClassName %> read model.
|
|
9
|
+
* Table: <%= tableName %>.
|
|
10
|
+
*/
|
|
11
|
+
@Repository("<%= moduleName %>.<%= jpaRepositoryName %>")
|
|
12
|
+
public interface <%= jpaRepositoryName %> extends JpaRepository<<%= jpaEntityName %>, String> {
|
|
13
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.infrastructure.kafkaListener;
|
|
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.databind.ObjectMapper;
|
|
11
|
+
import org.springframework.kafka.annotation.KafkaListener;
|
|
12
|
+
import org.springframework.kafka.support.Acknowledgment;
|
|
13
|
+
import org.springframework.stereotype.Component;
|
|
14
|
+
|
|
15
|
+
import java.util.Map;
|
|
16
|
+
<% if (isUpsert) { %>
|
|
17
|
+
<% const needsBigDecimal = fields && fields.some(f => f.javaType === 'BigDecimal'); %>
|
|
18
|
+
<% const needsLocalDate = fields && fields.some(f => ['LocalDate','LocalDateTime','LocalTime'].includes(f.javaType)); %>
|
|
19
|
+
<% const needsInstant = fields && fields.some(f => f.javaType === 'Instant'); %>
|
|
20
|
+
<% const needsUUID = fields && fields.some(f => f.javaType === 'UUID'); %>
|
|
21
|
+
<% if (needsBigDecimal) { %>import java.math.BigDecimal;
|
|
22
|
+
<% } %><% if (needsLocalDate) { %>import java.time.LocalDate;
|
|
23
|
+
import java.time.LocalDateTime;
|
|
24
|
+
import java.time.LocalTime;
|
|
25
|
+
<% } %><% if (needsInstant) { %>import java.time.Instant;
|
|
26
|
+
<% } %><% if (needsUUID) { %>import java.util.UUID;
|
|
27
|
+
<% } %>
|
|
28
|
+
<% } %>
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Kafka listener for read model sync — topic: <%= topicConstant %>.
|
|
32
|
+
* Delegates to <%= syncHandlerName %>.on<%= eventBaseName %>().
|
|
33
|
+
*/
|
|
34
|
+
@Component("<%= moduleName %>.<%= listenerClassName %>")
|
|
35
|
+
public class <%= listenerClassName %> {
|
|
36
|
+
|
|
37
|
+
private final <%= syncHandlerName %> syncHandler;
|
|
38
|
+
private final ObjectMapper objectMapper;
|
|
39
|
+
|
|
40
|
+
public <%= listenerClassName %>(<%= syncHandlerName %> syncHandler, ObjectMapper objectMapper) {
|
|
41
|
+
this.syncHandler = syncHandler;
|
|
42
|
+
this.objectMapper = objectMapper;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@KafkaListener(topics = "<%= topicSpringProperty %>", groupId = "${spring.application.name}-<%= moduleName %>-group")
|
|
46
|
+
public void handle(EventEnvelope<Map<String, Object>> event, Acknowledgment ack) {
|
|
47
|
+
<% if (isUpsert) { %>
|
|
48
|
+
<% (fields || []).forEach(f => { %>
|
|
49
|
+
<%- f.javaType %> <%= f.name %> = objectMapper.convertValue(event.data().get("<%= f.payloadKey || f.name %>"), <%- f.javaType %>.class);
|
|
50
|
+
<% }); %>
|
|
51
|
+
|
|
52
|
+
syncHandler.on<%= eventBaseName %>(new <%= integrationEventClassName %>(
|
|
53
|
+
<% (fields || []).forEach((f, i) => { %>
|
|
54
|
+
<%= f.name %><%= i < fields.length - 1 ? ',' : '' %>
|
|
55
|
+
<% }); %>
|
|
56
|
+
));
|
|
57
|
+
<% } else { %>
|
|
58
|
+
String id = objectMapper.convertValue(event.data().get("<%= (fields[0] && fields[0].payloadKey) || 'id' %>"), String.class);
|
|
59
|
+
syncHandler.on<%= eventBaseName %>(id);
|
|
60
|
+
<% } %>
|
|
61
|
+
|
|
62
|
+
ack.acknowledge();
|
|
63
|
+
}
|
|
64
|
+
}
|