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
|
@@ -14,6 +14,10 @@ const ChecksumManager = require('../utils/checksum-manager');
|
|
|
14
14
|
const { generateFakeValue, initSeed } = require('../utils/fake-data');
|
|
15
15
|
const { getInstalledBroker, generateSingleKafkaEvent, buildKafkaEventContext, updateKafkaYml,
|
|
16
16
|
generateEventRecord, createOrUpdateMessageBroker, updateDomainEventHandler } = require('./generate-kafka-event');
|
|
17
|
+
const { generateSingleRabbitEvent, buildRabbitEventContext, updateRabbitMQYml, updateRabbitMQYmlQueue,
|
|
18
|
+
createOrUpdateRabbitMessageBroker, updateRabbitMQConfigForConsumer, updateRabbitMQYmlForConsumer } = require('./generate-rabbitmq-event');
|
|
19
|
+
const { parseSystemYaml } = require('../utils/system-yaml-parser');
|
|
20
|
+
const { computeWorkflowInputFields } = require('./generate-temporal-system');
|
|
17
21
|
|
|
18
22
|
// Maximum depth for recursive relationship traversal
|
|
19
23
|
const MAX_DEPTH = 5;
|
|
@@ -284,6 +288,33 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
284
288
|
? 'mock'
|
|
285
289
|
: installedBroker;
|
|
286
290
|
|
|
291
|
+
// Detect Temporal for auto-wiring DomainEvent → Workflow bridge
|
|
292
|
+
const hasNotifiesInModule = aggregates.some(agg =>
|
|
293
|
+
(agg.domainEvents || []).some(e => (e.notifies || []).length > 0)
|
|
294
|
+
);
|
|
295
|
+
const temporalInstalled = hasNotifiesInModule
|
|
296
|
+
? await configManager.featureExists('temporal')
|
|
297
|
+
: false;
|
|
298
|
+
|
|
299
|
+
// Build workflow → inputFields map for typed DomainEvent → Workflow bridge
|
|
300
|
+
const workflowInputMap = new Map();
|
|
301
|
+
if (temporalInstalled) {
|
|
302
|
+
try {
|
|
303
|
+
const systemDir = path.join(projectDir, 'system');
|
|
304
|
+
if (await fs.pathExists(path.join(systemDir, 'system.yaml'))) {
|
|
305
|
+
const { workflows } = await parseSystemYaml(systemDir);
|
|
306
|
+
for (const wf of workflows) {
|
|
307
|
+
const inputFields = computeWorkflowInputFields(wf.steps);
|
|
308
|
+
const flowPascal = wf.namePascal.replace(/Workflow$/, '');
|
|
309
|
+
workflowInputMap.set(wf.namePascal, { inputFields, flowPascal });
|
|
310
|
+
workflowInputMap.set(wf.name, { inputFields, flowPascal });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} catch (_) {
|
|
314
|
+
// system.yaml not required — standalone modules work without it
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
287
318
|
// Generate audit-related shared components if needed
|
|
288
319
|
if (hasAuditableEntities || hasTrackUserEntities) {
|
|
289
320
|
|
|
@@ -604,9 +635,19 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
604
635
|
aggregateName,
|
|
605
636
|
domainEvents: aggregateDomainEvents.map(e => ({
|
|
606
637
|
...e,
|
|
607
|
-
integrationEventClassName: `${e.name}IntegrationEvent
|
|
638
|
+
integrationEventClassName: `${e.name}IntegrationEvent`,
|
|
639
|
+
notifies: (e.notifies || []).map(n => {
|
|
640
|
+
const wfKey = toPascalCase(n.workflow || '');
|
|
641
|
+
const meta = workflowInputMap.get(wfKey);
|
|
642
|
+
return {
|
|
643
|
+
...n,
|
|
644
|
+
inputFields: meta ? meta.inputFields : [],
|
|
645
|
+
flowPascal: meta ? meta.flowPascal : wfKey.replace(/Workflow$/, ''),
|
|
646
|
+
};
|
|
647
|
+
}),
|
|
608
648
|
})),
|
|
609
|
-
broker
|
|
649
|
+
broker,
|
|
650
|
+
temporal: temporalInstalled
|
|
610
651
|
};
|
|
611
652
|
await renderAndWrite(
|
|
612
653
|
path.join(__dirname, '..', '..', 'templates', 'aggregate', 'DomainEventHandler.java.ejs'),
|
|
@@ -699,6 +740,52 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
699
740
|
name: 'MessageBroker (updated)',
|
|
700
741
|
path: `${moduleName}/application/ports/MessageBroker.java`
|
|
701
742
|
});
|
|
743
|
+
} else if (broker === 'rabbitmq') {
|
|
744
|
+
// ── RabbitMQ broker: RabbitTemplate adapter ──────────────────────────
|
|
745
|
+
const STANDARD_EVENT_TYPES = new Set([
|
|
746
|
+
'String','Integer','Long','Double','Float','Boolean',
|
|
747
|
+
'BigDecimal','LocalDate','LocalDateTime','LocalTime','Instant','UUID'
|
|
748
|
+
]);
|
|
749
|
+
for (const event of aggregateDomainEvents) {
|
|
750
|
+
const rabbitCtx = buildRabbitEventContext(packageName, moduleName, event);
|
|
751
|
+
await generateSingleRabbitEvent(projectDir, packagePath, rabbitCtx);
|
|
752
|
+
generatedFiles.push({
|
|
753
|
+
type: 'Integration Event',
|
|
754
|
+
name: rabbitCtx.eventClassName,
|
|
755
|
+
path: `${moduleName}/application/events/${rabbitCtx.eventClassName}.java`
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
// Generate stub records for custom collection element types
|
|
759
|
+
const customElementTypes = [...new Set(
|
|
760
|
+
(event.fields || [])
|
|
761
|
+
.filter(f => f.isCollection && f.collectionElementType && !STANDARD_EVENT_TYPES.has(f.collectionElementType))
|
|
762
|
+
.map(f => f.collectionElementType)
|
|
763
|
+
)];
|
|
764
|
+
for (const typeName of customElementTypes) {
|
|
765
|
+
const stubPath = path.join(moduleBasePath, 'domain', 'models', 'events', `${typeName}.java`);
|
|
766
|
+
await renderAndWrite(
|
|
767
|
+
path.join(__dirname, '..', '..', 'templates', 'aggregate', 'DomainEventSnapshot.java.ejs'),
|
|
768
|
+
stubPath,
|
|
769
|
+
{ packageName, moduleName, name: typeName, fields: [] },
|
|
770
|
+
{ ...writeOptions, overwrite: false }
|
|
771
|
+
);
|
|
772
|
+
generatedFiles.push({
|
|
773
|
+
type: 'Event Snapshot Type',
|
|
774
|
+
name: typeName,
|
|
775
|
+
path: `${moduleName}/domain/models/events/${typeName}.java`
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
generatedFiles.push({
|
|
780
|
+
type: 'Integration Event',
|
|
781
|
+
name: `${toPascalCase(moduleName)}RabbitMessageBroker (updated)`,
|
|
782
|
+
path: `${moduleName}/infrastructure/adapters/rabbitmqMessageBroker/${toPascalCase(moduleName)}RabbitMessageBroker.java`
|
|
783
|
+
});
|
|
784
|
+
generatedFiles.push({
|
|
785
|
+
type: 'Integration Event',
|
|
786
|
+
name: 'MessageBroker (updated)',
|
|
787
|
+
path: `${moduleName}/application/ports/MessageBroker.java`
|
|
788
|
+
});
|
|
702
789
|
}
|
|
703
790
|
}
|
|
704
791
|
}
|
|
@@ -720,10 +807,12 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
720
807
|
const topicRaw = listener.topic;
|
|
721
808
|
const topicSuffix = topicRaw.includes('.') ? topicRaw.slice(topicRaw.lastIndexOf('.') + 1) : topicRaw;
|
|
722
809
|
const topicKey = topicSuffix.toLowerCase().replace(/_/g, '-');
|
|
810
|
+
const kafkaListenerClassName = `${listener.baseName}KafkaListener`;
|
|
723
811
|
const listenerContext = {
|
|
724
812
|
packageName,
|
|
725
813
|
moduleName,
|
|
726
814
|
...listener,
|
|
815
|
+
listenerClassName: kafkaListenerClassName,
|
|
727
816
|
topicConstant: topicRaw,
|
|
728
817
|
topicSpringProperty: `\${topics.${topicKey}}`,
|
|
729
818
|
topicVariableName: toCamelCase(topicSuffix.toLowerCase())
|
|
@@ -769,7 +858,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
769
858
|
// If the file is currently a mock Spring listener, force overwrite with the real Kafka impl
|
|
770
859
|
const kafkaListenerPath = path.join(
|
|
771
860
|
moduleBasePath, 'infrastructure', 'kafkaListener',
|
|
772
|
-
`${
|
|
861
|
+
`${kafkaListenerClassName}.java`
|
|
773
862
|
);
|
|
774
863
|
let kafkaListenerWriteOpts = writeOptions;
|
|
775
864
|
if (await fs.pathExists(kafkaListenerPath)) {
|
|
@@ -786,8 +875,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
786
875
|
);
|
|
787
876
|
generatedFiles.push({
|
|
788
877
|
type: 'Kafka Listener',
|
|
789
|
-
name:
|
|
790
|
-
path: `${moduleName}/infrastructure/kafkaListener/${
|
|
878
|
+
name: kafkaListenerClassName,
|
|
879
|
+
path: `${moduleName}/infrastructure/kafkaListener/${kafkaListenerClassName}.java`
|
|
791
880
|
});
|
|
792
881
|
|
|
793
882
|
// 3. Register topic in kafka.yaml (all environments)
|
|
@@ -844,10 +933,12 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
844
933
|
const topicRaw = listener.topic;
|
|
845
934
|
const topicSuffix = topicRaw.includes('.') ? topicRaw.slice(topicRaw.lastIndexOf('.') + 1) : topicRaw;
|
|
846
935
|
const topicKey = topicSuffix.toLowerCase().replace(/_/g, '-');
|
|
936
|
+
const mockListenerClassName = `${listener.baseName}KafkaListener`;
|
|
847
937
|
const listenerContext = {
|
|
848
938
|
packageName,
|
|
849
939
|
moduleName,
|
|
850
940
|
...listener,
|
|
941
|
+
listenerClassName: mockListenerClassName,
|
|
851
942
|
topicConstant: topicRaw,
|
|
852
943
|
topicSpringProperty: `\${topics.${topicKey}}`,
|
|
853
944
|
topicVariableName: toCamelCase(topicSuffix.toLowerCase())
|
|
@@ -876,14 +967,14 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
876
967
|
generatedFiles.push({ type: 'Listener Integration Event', name: listener.integrationEventClassName, path: `${moduleName}/application/events/${listener.integrationEventClassName}.java` });
|
|
877
968
|
|
|
878
969
|
// 2. Spring @EventListener class (mock — same file path as Kafka listener)
|
|
879
|
-
const listenerPath = path.join(moduleBasePath, 'infrastructure', 'kafkaListener', `${
|
|
970
|
+
const listenerPath = path.join(moduleBasePath, 'infrastructure', 'kafkaListener', `${mockListenerClassName}.java`);
|
|
880
971
|
await renderAndWrite(
|
|
881
972
|
path.join(__dirname, '..', '..', 'templates', 'mock', 'SpringEventListener.java.ejs'),
|
|
882
973
|
listenerPath,
|
|
883
974
|
listenerContext,
|
|
884
975
|
{ ...writeOptions, force: true }
|
|
885
976
|
);
|
|
886
|
-
generatedFiles.push({ type: 'Spring Listener (mock)', name:
|
|
977
|
+
generatedFiles.push({ type: 'Spring Listener (mock)', name: mockListenerClassName, path: `${moduleName}/infrastructure/kafkaListener/${mockListenerClassName}.java` });
|
|
887
978
|
|
|
888
979
|
// 3. NO kafka.yaml update in mock mode
|
|
889
980
|
|
|
@@ -908,8 +999,146 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
908
999
|
generatedFiles.push({ type: 'Handler', name: `${listener.useCase}CommandHandler`, path: `${moduleName}/application/usecases/${listener.useCase}CommandHandler.java` });
|
|
909
1000
|
}
|
|
910
1001
|
spinner.succeed(chalk.green(`Spring Event listeners generated (mock mode)! ✨`));
|
|
1002
|
+
} else if (broker === 'rabbitmq') {
|
|
1003
|
+
// ── RabbitMQ listeners: @RabbitListener ─────────────────────────────
|
|
1004
|
+
spinner.start(`Generating ${listeners.length} RabbitMQ listener(s)...`);
|
|
1005
|
+
for (const listener of listeners) {
|
|
1006
|
+
if (!listener.topic) {
|
|
1007
|
+
spinner.warn(chalk.yellow(`⚠ listener '${listener.event}': topic is required when there is no system.yaml. Skipping.`));
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const topicRaw = listener.topic;
|
|
1012
|
+
const topicSuffix = topicRaw.includes('.') ? topicRaw.slice(topicRaw.lastIndexOf('.') + 1) : topicRaw;
|
|
1013
|
+
const topicKey = topicSuffix.toLowerCase().replace(/_/g, '-');
|
|
1014
|
+
const rabbitListenerClassName = `${listener.baseName}RabbitListener`;
|
|
1015
|
+
const consumerTopicKey = `${moduleName}-${topicKey}`;
|
|
1016
|
+
const listenerContext = {
|
|
1017
|
+
packageName,
|
|
1018
|
+
moduleName,
|
|
1019
|
+
...listener,
|
|
1020
|
+
listenerClassName: rabbitListenerClassName,
|
|
1021
|
+
topicConstant: topicRaw,
|
|
1022
|
+
topicSpringProperty: `\${queues.${consumerTopicKey}}`,
|
|
1023
|
+
topicVariableName: toCamelCase(topicSuffix.toLowerCase())
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
// 0. Nested type records (auxiliary value objects for object-typed fields)
|
|
1027
|
+
for (const nt of (listener.nestedTypes || [])) {
|
|
1028
|
+
const ntPath = path.join(
|
|
1029
|
+
moduleBasePath, 'application', 'events',
|
|
1030
|
+
`${nt.name}.java`
|
|
1031
|
+
);
|
|
1032
|
+
await renderAndWrite(
|
|
1033
|
+
path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerNestedType.java.ejs'),
|
|
1034
|
+
ntPath,
|
|
1035
|
+
{ packageName, moduleName, name: nt.name, fields: nt.fields },
|
|
1036
|
+
writeOptions
|
|
1037
|
+
);
|
|
1038
|
+
generatedFiles.push({
|
|
1039
|
+
type: 'Listener Nested Type',
|
|
1040
|
+
name: nt.name,
|
|
1041
|
+
path: `${moduleName}/application/events/${nt.name}.java`
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// 1. Integration Event record (same template — broker-agnostic)
|
|
1046
|
+
const integrationEventPath = path.join(
|
|
1047
|
+
moduleBasePath, 'application', 'events',
|
|
1048
|
+
`${listener.integrationEventClassName}.java`
|
|
1049
|
+
);
|
|
1050
|
+
await renderAndWrite(
|
|
1051
|
+
path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerIntegrationEvent.java.ejs'),
|
|
1052
|
+
integrationEventPath,
|
|
1053
|
+
listenerContext,
|
|
1054
|
+
writeOptions
|
|
1055
|
+
);
|
|
1056
|
+
generatedFiles.push({
|
|
1057
|
+
type: 'Listener Integration Event',
|
|
1058
|
+
name: listener.integrationEventClassName,
|
|
1059
|
+
path: `${moduleName}/application/events/${listener.integrationEventClassName}.java`
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
// 2. RabbitMQ listener class
|
|
1063
|
+
const rabbitListenerPath = path.join(
|
|
1064
|
+
moduleBasePath, 'infrastructure', 'rabbitListener',
|
|
1065
|
+
`${rabbitListenerClassName}.java`
|
|
1066
|
+
);
|
|
1067
|
+
let rabbitListenerWriteOpts = writeOptions;
|
|
1068
|
+
if (await fs.pathExists(rabbitListenerPath)) {
|
|
1069
|
+
const existing = await fs.readFile(rabbitListenerPath, 'utf-8');
|
|
1070
|
+
if (existing.includes('@EventListener') && !existing.includes('@RabbitListener')) {
|
|
1071
|
+
rabbitListenerWriteOpts = { ...writeOptions, force: true };
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
await renderAndWrite(
|
|
1075
|
+
path.join(__dirname, '..', '..', 'templates', 'rabbitmq-listener', 'RabbitListenerClass.java.ejs'),
|
|
1076
|
+
rabbitListenerPath,
|
|
1077
|
+
listenerContext,
|
|
1078
|
+
rabbitListenerWriteOpts
|
|
1079
|
+
);
|
|
1080
|
+
generatedFiles.push({
|
|
1081
|
+
type: 'RabbitMQ Listener',
|
|
1082
|
+
name: rabbitListenerClassName,
|
|
1083
|
+
path: `${moduleName}/infrastructure/rabbitListener/${rabbitListenerClassName}.java`
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
// 3. Register consumer infrastructure in rabbitmq.yaml + RabbitMQConfig.java
|
|
1087
|
+
// Use module-prefixed keys to avoid collision with producer-side in monolith mode
|
|
1088
|
+
const producerModule = toCamelCase(listener.producer || moduleName);
|
|
1089
|
+
const consumerExchangeName = `${producerModule}.events`;
|
|
1090
|
+
const consumerRoutingKey = topicKey.replace(/-/g, '.');
|
|
1091
|
+
const consumerQueueName = `${moduleName}.${topicKey}`;
|
|
1092
|
+
const consumerBeanMethodName = `${toCamelCase(consumerTopicKey)}Topic`;
|
|
1093
|
+
|
|
1094
|
+
await updateRabbitMQYmlForConsumer(
|
|
1095
|
+
projectDir, consumerTopicKey, consumerQueueName,
|
|
1096
|
+
producerModule, consumerExchangeName, consumerRoutingKey
|
|
1097
|
+
);
|
|
1098
|
+
await updateRabbitMQConfigForConsumer(projectDir, packagePath, {
|
|
1099
|
+
producerModule,
|
|
1100
|
+
topicKey: consumerTopicKey,
|
|
1101
|
+
beanMethodName: consumerBeanMethodName,
|
|
1102
|
+
valueFieldName: consumerBeanMethodName
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
// 4. Typed Command dispatched from the listener
|
|
1106
|
+
const commandPath = path.join(
|
|
1107
|
+
moduleBasePath, 'application', 'commands',
|
|
1108
|
+
`${listener.commandClassName}.java`
|
|
1109
|
+
);
|
|
1110
|
+
await renderAndWrite(
|
|
1111
|
+
path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerCommand.java.ejs'),
|
|
1112
|
+
commandPath,
|
|
1113
|
+
listenerContext,
|
|
1114
|
+
writeOptions
|
|
1115
|
+
);
|
|
1116
|
+
generatedFiles.push({
|
|
1117
|
+
type: 'Listener Command',
|
|
1118
|
+
name: listener.commandClassName,
|
|
1119
|
+
path: `${moduleName}/application/commands/${listener.commandClassName}.java`
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
// 5. Use case handler that processes the command
|
|
1123
|
+
const handlerPath = path.join(
|
|
1124
|
+
moduleBasePath, 'application', 'usecases',
|
|
1125
|
+
`${listener.useCase}CommandHandler.java`
|
|
1126
|
+
);
|
|
1127
|
+
await renderAndWrite(
|
|
1128
|
+
path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerCommandHandler.java.ejs'),
|
|
1129
|
+
handlerPath,
|
|
1130
|
+
listenerContext,
|
|
1131
|
+
writeOptions
|
|
1132
|
+
);
|
|
1133
|
+
generatedFiles.push({
|
|
1134
|
+
type: 'Handler',
|
|
1135
|
+
name: `${listener.useCase}CommandHandler`,
|
|
1136
|
+
path: `${moduleName}/application/usecases/${listener.useCase}CommandHandler.java`
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
spinner.succeed(chalk.green(`RabbitMQ listeners generated! ✨`));
|
|
911
1140
|
} else if (listeners.length > 0) {
|
|
912
|
-
console.log(chalk.yellow(`⚠ listeners: section found but no broker is installed. Run 'eva add kafka-client' to generate listener classes.`));
|
|
1141
|
+
console.log(chalk.yellow(`⚠ listeners: section found but no broker is installed. Run 'eva add kafka-client' or 'eva add rabbitmq-client' to generate listener classes.`));
|
|
913
1142
|
}
|
|
914
1143
|
}
|
|
915
1144
|
|
|
@@ -936,6 +1165,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
936
1165
|
|
|
937
1166
|
const adapterDir = path.join(moduleBasePath, 'infrastructure', 'adapters', adapterPackage);
|
|
938
1167
|
|
|
1168
|
+
const aclMapperClassName = `${serviceName}AclMapper`;
|
|
1169
|
+
|
|
939
1170
|
const portContext = {
|
|
940
1171
|
packageName,
|
|
941
1172
|
moduleName,
|
|
@@ -948,6 +1179,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
948
1179
|
feignClientClassName,
|
|
949
1180
|
feignAdapterClassName,
|
|
950
1181
|
feignConfigClassName,
|
|
1182
|
+
aclMapperClassName,
|
|
951
1183
|
adapterPackage,
|
|
952
1184
|
methods,
|
|
953
1185
|
nestedTypes,
|
|
@@ -988,9 +1220,9 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
988
1220
|
});
|
|
989
1221
|
}
|
|
990
1222
|
|
|
991
|
-
// 1b. Infra DTOs (one per method that has fields:) — live in infrastructure/adapters/{service}/
|
|
1223
|
+
// 1b. Infra DTOs (one per method that has fields:) — live in infrastructure/adapters/{service}/dtos/
|
|
992
1224
|
for (const method of methods.filter(m => m.hasResponse)) {
|
|
993
|
-
const infraDtoPath = path.join(adapterDir, `${method.infraDtoName}.java`);
|
|
1225
|
+
const infraDtoPath = path.join(adapterDir, 'dtos', `${method.infraDtoName}.java`);
|
|
994
1226
|
await renderAndWrite(
|
|
995
1227
|
path.join(__dirname, '..', '..', 'templates', 'ports', 'PortResponseDto.java.ejs'),
|
|
996
1228
|
infraDtoPath,
|
|
@@ -1000,7 +1232,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
1000
1232
|
generatedFiles.push({
|
|
1001
1233
|
type: 'Port Infra DTO',
|
|
1002
1234
|
name: method.infraDtoName,
|
|
1003
|
-
path: `${moduleName}/infrastructure/adapters/${adapterPackage}/${method.infraDtoName}.java`
|
|
1235
|
+
path: `${moduleName}/infrastructure/adapters/${adapterPackage}/dtos/${method.infraDtoName}.java`
|
|
1004
1236
|
});
|
|
1005
1237
|
}
|
|
1006
1238
|
|
|
@@ -1061,6 +1293,19 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
1061
1293
|
path: `${moduleName}/infrastructure/adapters/${adapterPackage}/${feignAdapterClassName}.java`
|
|
1062
1294
|
});
|
|
1063
1295
|
|
|
1296
|
+
// 5b. ACL Mapper (maps infra DTOs → domain models)
|
|
1297
|
+
await renderAndWrite(
|
|
1298
|
+
path.join(__dirname, '..', '..', 'templates', 'ports', 'PortAclMapper.java.ejs'),
|
|
1299
|
+
path.join(adapterDir, `${aclMapperClassName}.java`),
|
|
1300
|
+
portContext,
|
|
1301
|
+
writeOptions
|
|
1302
|
+
);
|
|
1303
|
+
generatedFiles.push({
|
|
1304
|
+
type: 'HTTP Port',
|
|
1305
|
+
name: aclMapperClassName,
|
|
1306
|
+
path: `${moduleName}/infrastructure/adapters/${adapterPackage}/${aclMapperClassName}.java`
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1064
1309
|
// 6. Feign Config
|
|
1065
1310
|
await renderAndWrite(
|
|
1066
1311
|
path.join(__dirname, '..', '..', 'templates', 'ports', 'PortFeignConfig.java.ejs'),
|
|
@@ -1086,7 +1331,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
1086
1331
|
|
|
1087
1332
|
// ── Generate Read Models (local projections of external data) ────────────
|
|
1088
1333
|
if (readModels && readModels.length > 0) {
|
|
1089
|
-
if (broker === 'kafka' || broker === 'mock') {
|
|
1334
|
+
if (broker === 'kafka' || broker === 'rabbitmq' || broker === 'mock') {
|
|
1090
1335
|
spinner.start(`Generating ${readModels.length} read model(s)...`);
|
|
1091
1336
|
|
|
1092
1337
|
const readModelTemplatesDir = path.join(__dirname, '..', '..', 'templates', 'read-model');
|
|
@@ -1258,13 +1503,53 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
1258
1503
|
|
|
1259
1504
|
// Register topic in kafka.yaml
|
|
1260
1505
|
await updateKafkaYml(projectDir, sync.topicKey, sync.topicConstant);
|
|
1506
|
+
} else if (broker === 'rabbitmq') {
|
|
1507
|
+
const rabbitListenerPath = path.join(
|
|
1508
|
+
moduleBasePath, 'infrastructure', 'rabbitListener',
|
|
1509
|
+
`${sync.listenerClassName}.java`
|
|
1510
|
+
);
|
|
1511
|
+
const rmConsumerTopicKey = `${moduleName}-${sync.topicKey}`;
|
|
1512
|
+
const rabbitListenerContext = {
|
|
1513
|
+
...listenerContext,
|
|
1514
|
+
topicSpringProperty: `\${queues.${rmConsumerTopicKey}}`
|
|
1515
|
+
};
|
|
1516
|
+
await renderAndWrite(
|
|
1517
|
+
path.join(readModelTemplatesDir, 'ReadModelRabbitListener.java.ejs'),
|
|
1518
|
+
rabbitListenerPath,
|
|
1519
|
+
rabbitListenerContext,
|
|
1520
|
+
writeOptions
|
|
1521
|
+
);
|
|
1522
|
+
generatedFiles.push({
|
|
1523
|
+
type: 'Read Model RabbitMQ Listener',
|
|
1524
|
+
name: sync.listenerClassName,
|
|
1525
|
+
path: `${moduleName}/infrastructure/rabbitListener/${sync.listenerClassName}.java`
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
// Register consumer infrastructure in rabbitmq.yaml + RabbitMQConfig.java
|
|
1529
|
+
// Use module-prefixed keys to avoid collision with producer-side in monolith mode
|
|
1530
|
+
const rmProducerModule = toCamelCase(rm.sourceModule || moduleName);
|
|
1531
|
+
const rmExchangeName = `${rmProducerModule}.events`;
|
|
1532
|
+
const rmRoutingKey = sync.topicKey.replace(/-/g, '.');
|
|
1533
|
+
const rmQueueName = `${moduleName}.${sync.topicKey}`;
|
|
1534
|
+
const rmBeanMethodName = `${toCamelCase(rmConsumerTopicKey)}Topic`;
|
|
1535
|
+
|
|
1536
|
+
await updateRabbitMQYmlForConsumer(
|
|
1537
|
+
projectDir, rmConsumerTopicKey, rmQueueName,
|
|
1538
|
+
rmProducerModule, rmExchangeName, rmRoutingKey
|
|
1539
|
+
);
|
|
1540
|
+
await updateRabbitMQConfigForConsumer(projectDir, packagePath, {
|
|
1541
|
+
producerModule: rmProducerModule,
|
|
1542
|
+
topicKey: rmConsumerTopicKey,
|
|
1543
|
+
beanMethodName: rmBeanMethodName,
|
|
1544
|
+
valueFieldName: rmBeanMethodName
|
|
1545
|
+
});
|
|
1261
1546
|
}
|
|
1262
1547
|
}
|
|
1263
1548
|
}
|
|
1264
1549
|
|
|
1265
1550
|
spinner.succeed(chalk.green(`Read models generated! ✨`));
|
|
1266
1551
|
} else if (readModels.length > 0) {
|
|
1267
|
-
console.log(chalk.yellow(`⚠ readModels: section found but no broker is installed. Run 'eva add kafka-client' to generate listener classes.`));
|
|
1552
|
+
console.log(chalk.yellow(`⚠ readModels: section found but no broker is installed. Run 'eva add kafka-client' or 'eva add rabbitmq-client' to generate listener classes.`));
|
|
1268
1553
|
}
|
|
1269
1554
|
}
|
|
1270
1555
|
|
|
@@ -1616,11 +1901,14 @@ function classifyUseCase(op, aggregateName, aggregate) {
|
|
|
1616
1901
|
};
|
|
1617
1902
|
}
|
|
1618
1903
|
if (op.useCase === `Remove${rel.target}`) {
|
|
1904
|
+
const removeTargetEntity = (aggregate.secondaryEntities || []).find(e => e.name === rel.target);
|
|
1905
|
+
const removeIdField = removeTargetEntity ? removeTargetEntity.fields.find(f => f.name === 'id') : null;
|
|
1619
1906
|
return {
|
|
1620
1907
|
category: 'subEntityRemove',
|
|
1621
1908
|
entityName: rel.target,
|
|
1622
1909
|
fieldName: rel.fieldName,
|
|
1623
|
-
removeMethodName: `remove${rel.target}ById
|
|
1910
|
+
removeMethodName: `remove${rel.target}ById`,
|
|
1911
|
+
itemIdType: removeIdField ? removeIdField.javaType : 'String'
|
|
1624
1912
|
};
|
|
1625
1913
|
}
|
|
1626
1914
|
}
|
|
@@ -2058,7 +2346,8 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
|
|
|
2058
2346
|
useCaseName: op.useCase,
|
|
2059
2347
|
idType,
|
|
2060
2348
|
entityName: cl.entityName,
|
|
2061
|
-
removeMethodName: cl.removeMethodName
|
|
2349
|
+
removeMethodName: cl.removeMethodName,
|
|
2350
|
+
itemIdType: cl.itemIdType || 'String'
|
|
2062
2351
|
};
|
|
2063
2352
|
await renderAndWrite(
|
|
2064
2353
|
path.join(templatesDir, 'SubEntityRemoveCommand.java.ejs'),
|
|
@@ -2102,7 +2391,10 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
|
|
|
2102
2391
|
|
|
2103
2392
|
} else {
|
|
2104
2393
|
// Scaffold: no semantic pattern matched → generate stub with TODO
|
|
2105
|
-
const
|
|
2394
|
+
const hasPathVar = Boolean(op.path && op.path.includes('{'));
|
|
2395
|
+
const pathVarMatch = hasPathVar ? op.path.match(/\{([^}]+)\}/) : null;
|
|
2396
|
+
const pathVarName = pathVarMatch ? pathVarMatch[1] : 'id';
|
|
2397
|
+
const scaffoldContext = { packageName, moduleName, aggregateName, useCaseName: op.useCase, hasPathVar, pathVarName, idType };
|
|
2106
2398
|
const scaffoldType = op.type || (op.method === 'GET' ? 'query' : 'command');
|
|
2107
2399
|
if (scaffoldType === 'command') {
|
|
2108
2400
|
await renderAndWrite(
|