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
|
@@ -5,15 +5,19 @@ const fs = require('fs-extra');
|
|
|
5
5
|
const inquirer = require('inquirer');
|
|
6
6
|
const ConfigManager = require('../utils/config-manager');
|
|
7
7
|
const { isEva4jProject } = require('../utils/validator');
|
|
8
|
-
const { toPackagePath, toCamelCase, toKebabCase, toPascalCase, getApplicationClassName, pluralizeWord } = require('../utils/naming');
|
|
8
|
+
const { toPackagePath, toCamelCase, toKebabCase, toPascalCase, getApplicationClassName, pluralizeWord, singularizeWord } = require('../utils/naming');
|
|
9
9
|
const { renderAndWrite, renderTemplate } = require('../utils/template-engine');
|
|
10
|
-
const { parseDomainYaml, generateEntityImports, generateValidationImports } = require('../utils/yaml-to-entity');
|
|
10
|
+
const { parseDomainYaml, generateEntityImports, generateValidationImports, resolveLifecycleEventArgs, resolveEventArgs } = require('../utils/yaml-to-entity');
|
|
11
11
|
const { createOrUpdateUrlsConfig, ensureUrlsImport } = require('./generate-http-exchange');
|
|
12
12
|
const SharedGenerator = require('../generators/shared-generator');
|
|
13
13
|
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;
|
|
@@ -216,6 +220,9 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
216
220
|
const { packageName, artifactId } = projectConfig;
|
|
217
221
|
const packagePath = toPackagePath(packageName);
|
|
218
222
|
|
|
223
|
+
// Normalise module name to camelCase (system.yaml uses kebab-case, .eva4j.json stores camelCase)
|
|
224
|
+
moduleName = toCamelCase(moduleName);
|
|
225
|
+
|
|
219
226
|
// Validate module exists
|
|
220
227
|
if (!(await configManager.moduleExists(moduleName))) {
|
|
221
228
|
console.error(chalk.red(`❌ Module '${moduleName}' not found`));
|
|
@@ -244,7 +251,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
244
251
|
|
|
245
252
|
try {
|
|
246
253
|
// Parse domain.yaml
|
|
247
|
-
const { aggregates, allEnums, endpoints, listeners, ports } = await parseDomainYaml(domainYamlPath, packageName, moduleName);
|
|
254
|
+
const { aggregates, allEnums, endpoints, listeners, ports, readModels } = await parseDomainYaml(domainYamlPath, packageName, moduleName);
|
|
248
255
|
|
|
249
256
|
spinner.succeed(chalk.green(`Found ${aggregates.length} aggregate(s) and ${allEnums.length} enum(s)`));
|
|
250
257
|
|
|
@@ -273,7 +280,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
273
280
|
}
|
|
274
281
|
|
|
275
282
|
// Detect installed message broker for auto-wiring integration events
|
|
276
|
-
const installedBroker = (hasDomainEventsInModule || (listeners && listeners.length > 0))
|
|
283
|
+
const installedBroker = (hasDomainEventsInModule || (listeners && listeners.length > 0) || (readModels && readModels.length > 0))
|
|
277
284
|
? await getInstalledBroker(configManager)
|
|
278
285
|
: null;
|
|
279
286
|
// When brokerMode:'mock' is requested AND a broker is installed, use mock Spring-Events adapter
|
|
@@ -281,6 +288,33 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
281
288
|
? 'mock'
|
|
282
289
|
: installedBroker;
|
|
283
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
|
+
|
|
284
318
|
// Generate audit-related shared components if needed
|
|
285
319
|
if (hasAuditableEntities || hasTrackUserEntities) {
|
|
286
320
|
|
|
@@ -369,6 +403,9 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
369
403
|
const { name: aggregateName, rootEntity, secondaryEntities, valueObjects } = aggregate;
|
|
370
404
|
|
|
371
405
|
// 1. Generate Domain Aggregate Root
|
|
406
|
+
const resolvedLifecycle = resolveLifecycleEventArgs(
|
|
407
|
+
aggregate.lifecycleEventsMap || {}, rootEntity.name, rootEntity.fields, valueObjects
|
|
408
|
+
);
|
|
372
409
|
const rootDomainContext = {
|
|
373
410
|
packageName,
|
|
374
411
|
moduleName,
|
|
@@ -381,7 +418,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
381
418
|
auditable: rootEntity.auditable,
|
|
382
419
|
hasSoftDelete: rootEntity.hasSoftDelete || false,
|
|
383
420
|
domainEvents: aggregate.domainEvents || [],
|
|
384
|
-
triggeredEventsMap: aggregate.triggeredEventsMap || {}
|
|
421
|
+
triggeredEventsMap: aggregate.triggeredEventsMap || {},
|
|
422
|
+
lifecycleEventsMap: resolvedLifecycle
|
|
385
423
|
};
|
|
386
424
|
|
|
387
425
|
await renderAndWrite(
|
|
@@ -393,6 +431,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
393
431
|
generatedFiles.push({ type: 'Domain Entity', name: rootEntity.name, path: `${moduleName}/domain/models/entities/${rootEntity.name}.java` });
|
|
394
432
|
|
|
395
433
|
// 2. Generate JPA Aggregate Root
|
|
434
|
+
const hasCreateLifecycle = !!(aggregate.lifecycleEventsMap && aggregate.lifecycleEventsMap.create && aggregate.lifecycleEventsMap.create.length > 0);
|
|
396
435
|
const rootJpaContext = {
|
|
397
436
|
packageName,
|
|
398
437
|
moduleName,
|
|
@@ -405,7 +444,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
405
444
|
enums: allEnums,
|
|
406
445
|
auditable: rootEntity.auditable,
|
|
407
446
|
audit: rootEntity.audit,
|
|
408
|
-
hasSoftDelete: rootEntity.hasSoftDelete || false
|
|
447
|
+
hasSoftDelete: rootEntity.hasSoftDelete || false,
|
|
448
|
+
hasCreateLifecycle
|
|
409
449
|
};
|
|
410
450
|
|
|
411
451
|
await renderAndWrite(
|
|
@@ -524,6 +564,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
524
564
|
moduleName,
|
|
525
565
|
rootEntity,
|
|
526
566
|
hasSoftDelete: rootEntity.hasSoftDelete || false,
|
|
567
|
+
hasDomainEvents: (aggregate.domainEvents || []).length > 0,
|
|
568
|
+
hasDeleteLifecycle: !!(aggregate.lifecycleEventsMap || {}).delete,
|
|
527
569
|
findByOps: []
|
|
528
570
|
};
|
|
529
571
|
|
|
@@ -551,6 +593,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
551
593
|
aggregateName,
|
|
552
594
|
rootEntity,
|
|
553
595
|
hasDomainEvents: (aggregate.domainEvents || []).length > 0,
|
|
596
|
+
hasDeleteLifecycle: !!(aggregate.lifecycleEventsMap || {}).delete,
|
|
554
597
|
hasSoftDelete: rootEntity.hasSoftDelete || false,
|
|
555
598
|
findByOps: []
|
|
556
599
|
};
|
|
@@ -592,9 +635,19 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
592
635
|
aggregateName,
|
|
593
636
|
domainEvents: aggregateDomainEvents.map(e => ({
|
|
594
637
|
...e,
|
|
595
|
-
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
|
+
}),
|
|
596
648
|
})),
|
|
597
|
-
broker
|
|
649
|
+
broker,
|
|
650
|
+
temporal: temporalInstalled
|
|
598
651
|
};
|
|
599
652
|
await renderAndWrite(
|
|
600
653
|
path.join(__dirname, '..', '..', 'templates', 'aggregate', 'DomainEventHandler.java.ejs'),
|
|
@@ -687,6 +740,52 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
687
740
|
name: 'MessageBroker (updated)',
|
|
688
741
|
path: `${moduleName}/application/ports/MessageBroker.java`
|
|
689
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
|
+
});
|
|
690
789
|
}
|
|
691
790
|
}
|
|
692
791
|
}
|
|
@@ -708,10 +807,12 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
708
807
|
const topicRaw = listener.topic;
|
|
709
808
|
const topicSuffix = topicRaw.includes('.') ? topicRaw.slice(topicRaw.lastIndexOf('.') + 1) : topicRaw;
|
|
710
809
|
const topicKey = topicSuffix.toLowerCase().replace(/_/g, '-');
|
|
810
|
+
const kafkaListenerClassName = `${listener.baseName}KafkaListener`;
|
|
711
811
|
const listenerContext = {
|
|
712
812
|
packageName,
|
|
713
813
|
moduleName,
|
|
714
814
|
...listener,
|
|
815
|
+
listenerClassName: kafkaListenerClassName,
|
|
715
816
|
topicConstant: topicRaw,
|
|
716
817
|
topicSpringProperty: `\${topics.${topicKey}}`,
|
|
717
818
|
topicVariableName: toCamelCase(topicSuffix.toLowerCase())
|
|
@@ -757,7 +858,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
757
858
|
// If the file is currently a mock Spring listener, force overwrite with the real Kafka impl
|
|
758
859
|
const kafkaListenerPath = path.join(
|
|
759
860
|
moduleBasePath, 'infrastructure', 'kafkaListener',
|
|
760
|
-
`${
|
|
861
|
+
`${kafkaListenerClassName}.java`
|
|
761
862
|
);
|
|
762
863
|
let kafkaListenerWriteOpts = writeOptions;
|
|
763
864
|
if (await fs.pathExists(kafkaListenerPath)) {
|
|
@@ -774,8 +875,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
774
875
|
);
|
|
775
876
|
generatedFiles.push({
|
|
776
877
|
type: 'Kafka Listener',
|
|
777
|
-
name:
|
|
778
|
-
path: `${moduleName}/infrastructure/kafkaListener/${
|
|
878
|
+
name: kafkaListenerClassName,
|
|
879
|
+
path: `${moduleName}/infrastructure/kafkaListener/${kafkaListenerClassName}.java`
|
|
779
880
|
});
|
|
780
881
|
|
|
781
882
|
// 3. Register topic in kafka.yaml (all environments)
|
|
@@ -832,10 +933,12 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
832
933
|
const topicRaw = listener.topic;
|
|
833
934
|
const topicSuffix = topicRaw.includes('.') ? topicRaw.slice(topicRaw.lastIndexOf('.') + 1) : topicRaw;
|
|
834
935
|
const topicKey = topicSuffix.toLowerCase().replace(/_/g, '-');
|
|
936
|
+
const mockListenerClassName = `${listener.baseName}KafkaListener`;
|
|
835
937
|
const listenerContext = {
|
|
836
938
|
packageName,
|
|
837
939
|
moduleName,
|
|
838
940
|
...listener,
|
|
941
|
+
listenerClassName: mockListenerClassName,
|
|
839
942
|
topicConstant: topicRaw,
|
|
840
943
|
topicSpringProperty: `\${topics.${topicKey}}`,
|
|
841
944
|
topicVariableName: toCamelCase(topicSuffix.toLowerCase())
|
|
@@ -864,14 +967,14 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
864
967
|
generatedFiles.push({ type: 'Listener Integration Event', name: listener.integrationEventClassName, path: `${moduleName}/application/events/${listener.integrationEventClassName}.java` });
|
|
865
968
|
|
|
866
969
|
// 2. Spring @EventListener class (mock — same file path as Kafka listener)
|
|
867
|
-
const listenerPath = path.join(moduleBasePath, 'infrastructure', 'kafkaListener', `${
|
|
970
|
+
const listenerPath = path.join(moduleBasePath, 'infrastructure', 'kafkaListener', `${mockListenerClassName}.java`);
|
|
868
971
|
await renderAndWrite(
|
|
869
972
|
path.join(__dirname, '..', '..', 'templates', 'mock', 'SpringEventListener.java.ejs'),
|
|
870
973
|
listenerPath,
|
|
871
974
|
listenerContext,
|
|
872
975
|
{ ...writeOptions, force: true }
|
|
873
976
|
);
|
|
874
|
-
generatedFiles.push({ type: 'Spring Listener (mock)', name:
|
|
977
|
+
generatedFiles.push({ type: 'Spring Listener (mock)', name: mockListenerClassName, path: `${moduleName}/infrastructure/kafkaListener/${mockListenerClassName}.java` });
|
|
875
978
|
|
|
876
979
|
// 3. NO kafka.yaml update in mock mode
|
|
877
980
|
|
|
@@ -896,8 +999,146 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
896
999
|
generatedFiles.push({ type: 'Handler', name: `${listener.useCase}CommandHandler`, path: `${moduleName}/application/usecases/${listener.useCase}CommandHandler.java` });
|
|
897
1000
|
}
|
|
898
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! ✨`));
|
|
899
1140
|
} else if (listeners.length > 0) {
|
|
900
|
-
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.`));
|
|
901
1142
|
}
|
|
902
1143
|
}
|
|
903
1144
|
|
|
@@ -924,6 +1165,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
924
1165
|
|
|
925
1166
|
const adapterDir = path.join(moduleBasePath, 'infrastructure', 'adapters', adapterPackage);
|
|
926
1167
|
|
|
1168
|
+
const aclMapperClassName = `${serviceName}AclMapper`;
|
|
1169
|
+
|
|
927
1170
|
const portContext = {
|
|
928
1171
|
packageName,
|
|
929
1172
|
moduleName,
|
|
@@ -936,6 +1179,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
936
1179
|
feignClientClassName,
|
|
937
1180
|
feignAdapterClassName,
|
|
938
1181
|
feignConfigClassName,
|
|
1182
|
+
aclMapperClassName,
|
|
939
1183
|
adapterPackage,
|
|
940
1184
|
methods,
|
|
941
1185
|
nestedTypes,
|
|
@@ -976,9 +1220,9 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
976
1220
|
});
|
|
977
1221
|
}
|
|
978
1222
|
|
|
979
|
-
// 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/
|
|
980
1224
|
for (const method of methods.filter(m => m.hasResponse)) {
|
|
981
|
-
const infraDtoPath = path.join(adapterDir, `${method.infraDtoName}.java`);
|
|
1225
|
+
const infraDtoPath = path.join(adapterDir, 'dtos', `${method.infraDtoName}.java`);
|
|
982
1226
|
await renderAndWrite(
|
|
983
1227
|
path.join(__dirname, '..', '..', 'templates', 'ports', 'PortResponseDto.java.ejs'),
|
|
984
1228
|
infraDtoPath,
|
|
@@ -988,7 +1232,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
988
1232
|
generatedFiles.push({
|
|
989
1233
|
type: 'Port Infra DTO',
|
|
990
1234
|
name: method.infraDtoName,
|
|
991
|
-
path: `${moduleName}/infrastructure/adapters/${adapterPackage}/${method.infraDtoName}.java`
|
|
1235
|
+
path: `${moduleName}/infrastructure/adapters/${adapterPackage}/dtos/${method.infraDtoName}.java`
|
|
992
1236
|
});
|
|
993
1237
|
}
|
|
994
1238
|
|
|
@@ -1049,6 +1293,19 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
1049
1293
|
path: `${moduleName}/infrastructure/adapters/${adapterPackage}/${feignAdapterClassName}.java`
|
|
1050
1294
|
});
|
|
1051
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
|
+
|
|
1052
1309
|
// 6. Feign Config
|
|
1053
1310
|
await renderAndWrite(
|
|
1054
1311
|
path.join(__dirname, '..', '..', 'templates', 'ports', 'PortFeignConfig.java.ejs'),
|
|
@@ -1072,6 +1329,230 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
1072
1329
|
spinner.succeed(chalk.green(`HTTP ports generated! ✨`));
|
|
1073
1330
|
}
|
|
1074
1331
|
|
|
1332
|
+
// ── Generate Read Models (local projections of external data) ────────────
|
|
1333
|
+
if (readModels && readModels.length > 0) {
|
|
1334
|
+
if (broker === 'kafka' || broker === 'rabbitmq' || broker === 'mock') {
|
|
1335
|
+
spinner.start(`Generating ${readModels.length} read model(s)...`);
|
|
1336
|
+
|
|
1337
|
+
const readModelTemplatesDir = path.join(__dirname, '..', '..', 'templates', 'read-model');
|
|
1338
|
+
|
|
1339
|
+
for (const rm of readModels) {
|
|
1340
|
+
const rmContext = {
|
|
1341
|
+
packageName,
|
|
1342
|
+
moduleName,
|
|
1343
|
+
...rm
|
|
1344
|
+
};
|
|
1345
|
+
|
|
1346
|
+
// 1. Domain read model class
|
|
1347
|
+
const domainPath = path.join(
|
|
1348
|
+
moduleBasePath, 'domain', 'models', 'readmodels',
|
|
1349
|
+
`${rm.domainClassName}.java`
|
|
1350
|
+
);
|
|
1351
|
+
await renderAndWrite(
|
|
1352
|
+
path.join(readModelTemplatesDir, 'ReadModelDomain.java.ejs'),
|
|
1353
|
+
domainPath,
|
|
1354
|
+
rmContext,
|
|
1355
|
+
writeOptions
|
|
1356
|
+
);
|
|
1357
|
+
generatedFiles.push({
|
|
1358
|
+
type: 'Read Model',
|
|
1359
|
+
name: rm.domainClassName,
|
|
1360
|
+
path: `${moduleName}/domain/models/readmodels/${rm.domainClassName}.java`
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
// 2. JPA entity
|
|
1364
|
+
const jpaPath = path.join(
|
|
1365
|
+
moduleBasePath, 'infrastructure', 'database', 'entities',
|
|
1366
|
+
`${rm.jpaEntityName}.java`
|
|
1367
|
+
);
|
|
1368
|
+
await renderAndWrite(
|
|
1369
|
+
path.join(readModelTemplatesDir, 'ReadModelJpa.java.ejs'),
|
|
1370
|
+
jpaPath,
|
|
1371
|
+
rmContext,
|
|
1372
|
+
writeOptions
|
|
1373
|
+
);
|
|
1374
|
+
generatedFiles.push({
|
|
1375
|
+
type: 'Read Model',
|
|
1376
|
+
name: rm.jpaEntityName,
|
|
1377
|
+
path: `${moduleName}/infrastructure/database/entities/${rm.jpaEntityName}.java`
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
// 3. JPA repository
|
|
1381
|
+
const jpaRepoPath = path.join(
|
|
1382
|
+
moduleBasePath, 'infrastructure', 'database', 'repositories',
|
|
1383
|
+
`${rm.jpaRepositoryName}.java`
|
|
1384
|
+
);
|
|
1385
|
+
await renderAndWrite(
|
|
1386
|
+
path.join(readModelTemplatesDir, 'ReadModelJpaRepository.java.ejs'),
|
|
1387
|
+
jpaRepoPath,
|
|
1388
|
+
rmContext,
|
|
1389
|
+
writeOptions
|
|
1390
|
+
);
|
|
1391
|
+
generatedFiles.push({
|
|
1392
|
+
type: 'Read Model',
|
|
1393
|
+
name: rm.jpaRepositoryName,
|
|
1394
|
+
path: `${moduleName}/infrastructure/database/repositories/${rm.jpaRepositoryName}.java`
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
// 4. Domain repository interface
|
|
1398
|
+
const repoPath = path.join(
|
|
1399
|
+
moduleBasePath, 'domain', 'repositories',
|
|
1400
|
+
`${rm.repositoryName}.java`
|
|
1401
|
+
);
|
|
1402
|
+
await renderAndWrite(
|
|
1403
|
+
path.join(readModelTemplatesDir, 'ReadModelRepository.java.ejs'),
|
|
1404
|
+
repoPath,
|
|
1405
|
+
rmContext,
|
|
1406
|
+
writeOptions
|
|
1407
|
+
);
|
|
1408
|
+
generatedFiles.push({
|
|
1409
|
+
type: 'Read Model',
|
|
1410
|
+
name: rm.repositoryName,
|
|
1411
|
+
path: `${moduleName}/domain/repositories/${rm.repositoryName}.java`
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
// 5. Repository implementation
|
|
1415
|
+
const repoImplPath = path.join(
|
|
1416
|
+
moduleBasePath, 'infrastructure', 'database', 'repositories',
|
|
1417
|
+
`${rm.repositoryImplName}.java`
|
|
1418
|
+
);
|
|
1419
|
+
await renderAndWrite(
|
|
1420
|
+
path.join(readModelTemplatesDir, 'ReadModelRepositoryImpl.java.ejs'),
|
|
1421
|
+
repoImplPath,
|
|
1422
|
+
rmContext,
|
|
1423
|
+
writeOptions
|
|
1424
|
+
);
|
|
1425
|
+
generatedFiles.push({
|
|
1426
|
+
type: 'Read Model',
|
|
1427
|
+
name: rm.repositoryImplName,
|
|
1428
|
+
path: `${moduleName}/infrastructure/database/repositories/${rm.repositoryImplName}.java`
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
// 6. Sync handler
|
|
1432
|
+
const handlerPath = path.join(
|
|
1433
|
+
moduleBasePath, 'application', 'usecases',
|
|
1434
|
+
`${rm.syncHandlerName}.java`
|
|
1435
|
+
);
|
|
1436
|
+
await renderAndWrite(
|
|
1437
|
+
path.join(readModelTemplatesDir, 'ReadModelSyncHandler.java.ejs'),
|
|
1438
|
+
handlerPath,
|
|
1439
|
+
rmContext,
|
|
1440
|
+
writeOptions
|
|
1441
|
+
);
|
|
1442
|
+
generatedFiles.push({
|
|
1443
|
+
type: 'Read Model',
|
|
1444
|
+
name: rm.syncHandlerName,
|
|
1445
|
+
path: `${moduleName}/application/usecases/${rm.syncHandlerName}.java`
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
// 7. Per syncedBy event: integration event + Kafka listener + topic registration
|
|
1449
|
+
for (const sync of rm.syncedBy) {
|
|
1450
|
+
const listenerContext = {
|
|
1451
|
+
packageName,
|
|
1452
|
+
moduleName,
|
|
1453
|
+
...sync,
|
|
1454
|
+
syncHandlerName: rm.syncHandlerName,
|
|
1455
|
+
domainClassName: rm.domainClassName,
|
|
1456
|
+
repositoryName: rm.repositoryName
|
|
1457
|
+
};
|
|
1458
|
+
|
|
1459
|
+
// Integration event (reuse if already exists from listeners: section)
|
|
1460
|
+
// Only generate for UPSERT — DELETE/SOFT_DELETE bypass IntegrationEvent entirely
|
|
1461
|
+
const integrationEventPath = path.join(
|
|
1462
|
+
moduleBasePath, 'application', 'events',
|
|
1463
|
+
`${sync.integrationEventClassName}.java`
|
|
1464
|
+
);
|
|
1465
|
+
if (sync.action === 'UPSERT' && !(await fs.pathExists(integrationEventPath))) {
|
|
1466
|
+
await renderAndWrite(
|
|
1467
|
+
path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerIntegrationEvent.java.ejs'),
|
|
1468
|
+
integrationEventPath,
|
|
1469
|
+
{
|
|
1470
|
+
packageName,
|
|
1471
|
+
moduleName,
|
|
1472
|
+
integrationEventClassName: sync.integrationEventClassName,
|
|
1473
|
+
topicConstant: sync.topicConstant,
|
|
1474
|
+
producer: rm.sourceModule,
|
|
1475
|
+
fields: sync.fields
|
|
1476
|
+
},
|
|
1477
|
+
writeOptions
|
|
1478
|
+
);
|
|
1479
|
+
generatedFiles.push({
|
|
1480
|
+
type: 'Read Model Integration Event',
|
|
1481
|
+
name: sync.integrationEventClassName,
|
|
1482
|
+
path: `${moduleName}/application/events/${sync.integrationEventClassName}.java`
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// Kafka listener
|
|
1487
|
+
if (broker === 'kafka') {
|
|
1488
|
+
const kafkaListenerPath = path.join(
|
|
1489
|
+
moduleBasePath, 'infrastructure', 'kafkaListener',
|
|
1490
|
+
`${sync.listenerClassName}.java`
|
|
1491
|
+
);
|
|
1492
|
+
await renderAndWrite(
|
|
1493
|
+
path.join(readModelTemplatesDir, 'ReadModelKafkaListener.java.ejs'),
|
|
1494
|
+
kafkaListenerPath,
|
|
1495
|
+
listenerContext,
|
|
1496
|
+
writeOptions
|
|
1497
|
+
);
|
|
1498
|
+
generatedFiles.push({
|
|
1499
|
+
type: 'Read Model Kafka Listener',
|
|
1500
|
+
name: sync.listenerClassName,
|
|
1501
|
+
path: `${moduleName}/infrastructure/kafkaListener/${sync.listenerClassName}.java`
|
|
1502
|
+
});
|
|
1503
|
+
|
|
1504
|
+
// Register topic in kafka.yaml
|
|
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
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
spinner.succeed(chalk.green(`Read models generated! ✨`));
|
|
1551
|
+
} else if (readModels.length > 0) {
|
|
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.`));
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1075
1556
|
console.log(chalk.blue('\n📦 Generated files:'));
|
|
1076
1557
|
const groupedFiles = generatedFiles.reduce((acc, file) => {
|
|
1077
1558
|
if (!acc[file.type]) acc[file.type] = [];
|
|
@@ -1113,8 +1594,18 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
1113
1594
|
}
|
|
1114
1595
|
}
|
|
1115
1596
|
if (!op._ownerAggregate) {
|
|
1116
|
-
// True scaffold: heuristic —
|
|
1117
|
-
|
|
1597
|
+
// True scaffold: heuristic — strip CRUD verb prefix and singularize to find
|
|
1598
|
+
// the best aggregate match via prefix comparison.
|
|
1599
|
+
// e.g. "FindAllGuarantees" → suffix "Guarantees" → singular "Guarantee"
|
|
1600
|
+
// → "GuaranteeCatalog".startsWith("guarantee") ✓
|
|
1601
|
+
const ucSuffix = op.useCase.replace(/^(FindAll|GetAll|Get|Create|Update|Delete|Add|Remove)/, '');
|
|
1602
|
+
const singularSuffix = ucSuffix ? singularizeWord(ucSuffix).toLowerCase() : '';
|
|
1603
|
+
const matched = (singularSuffix &&
|
|
1604
|
+
aggregates.find(agg => {
|
|
1605
|
+
const aggLower = agg.name.toLowerCase();
|
|
1606
|
+
return aggLower.startsWith(singularSuffix) || singularSuffix.startsWith(aggLower);
|
|
1607
|
+
})
|
|
1608
|
+
) || aggregates.find(agg =>
|
|
1118
1609
|
op.useCase.toLowerCase().includes(agg.name.toLowerCase())
|
|
1119
1610
|
);
|
|
1120
1611
|
const owner = matched || aggregates[0];
|
|
@@ -1410,11 +1901,14 @@ function classifyUseCase(op, aggregateName, aggregate) {
|
|
|
1410
1901
|
};
|
|
1411
1902
|
}
|
|
1412
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;
|
|
1413
1906
|
return {
|
|
1414
1907
|
category: 'subEntityRemove',
|
|
1415
1908
|
entityName: rel.target,
|
|
1416
1909
|
fieldName: rel.fieldName,
|
|
1417
|
-
removeMethodName: `remove${rel.target}ById
|
|
1910
|
+
removeMethodName: `remove${rel.target}ById`,
|
|
1911
|
+
itemIdType: removeIdField ? removeIdField.javaType : 'String'
|
|
1418
1912
|
};
|
|
1419
1913
|
}
|
|
1420
1914
|
}
|
|
@@ -1434,6 +1928,34 @@ function classifyUseCase(op, aggregateName, aggregate) {
|
|
|
1434
1928
|
}
|
|
1435
1929
|
}
|
|
1436
1930
|
|
|
1931
|
+
// 5. Fuzzy FindAll — useCase starts with "FindAll" and the singular of the
|
|
1932
|
+
// suffix is a prefix of the aggregate name (or vice-versa).
|
|
1933
|
+
// e.g. FindAllGuarantees → singular "Guarantee" → "GuaranteeCatalog" starts with it ✓
|
|
1934
|
+
if (op.useCase.startsWith('FindAll')) {
|
|
1935
|
+
const suffix = op.useCase.slice(7);
|
|
1936
|
+
if (suffix) {
|
|
1937
|
+
const singularSuffix = singularizeWord(suffix).toLowerCase();
|
|
1938
|
+
const aggLower = aggregateName.toLowerCase();
|
|
1939
|
+
if (aggLower.startsWith(singularSuffix) || singularSuffix.startsWith(aggLower)) {
|
|
1940
|
+
return { category: 'standard', variant: 'findAll' };
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// 6. Fuzzy Get — useCase starts with "Get" and the suffix matches the
|
|
1946
|
+
// aggregate name as a prefix (or vice-versa).
|
|
1947
|
+
// e.g. GetCatTariff → "CatTariff" matches aggregate "CatTariff" ✓
|
|
1948
|
+
if (op.useCase.startsWith('Get') && !op.useCase.startsWith('GetAll')) {
|
|
1949
|
+
const suffix = op.useCase.slice(3);
|
|
1950
|
+
if (suffix) {
|
|
1951
|
+
const suffixLower = suffix.toLowerCase();
|
|
1952
|
+
const aggLower = aggregateName.toLowerCase();
|
|
1953
|
+
if (aggLower.startsWith(suffixLower) || suffixLower.startsWith(aggLower)) {
|
|
1954
|
+
return { category: 'standard', variant: 'getById' };
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1437
1959
|
return { category: 'scaffold' };
|
|
1438
1960
|
}
|
|
1439
1961
|
|
|
@@ -1552,13 +2074,18 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
|
|
|
1552
2074
|
const oneToManyRelationshipsApp = enrichRelsWithSchemaExamples(
|
|
1553
2075
|
transformRelsForApp(oneToManyRelationships, validatedVoNames), localAllEnums, valueObjects);
|
|
1554
2076
|
|
|
2077
|
+
const resolvedLifecycleEndpoints = resolveLifecycleEventArgs(
|
|
2078
|
+
aggregate.lifecycleEventsMap || {}, aggregateName, rootEntity.fields, valueObjects
|
|
2079
|
+
);
|
|
1555
2080
|
const baseContext = {
|
|
1556
2081
|
packageName, moduleName, aggregateName, aggregateNamePlural, rootEntity, secondaryEntities,
|
|
1557
2082
|
responseFields, responseSecondaryEntities, idType,
|
|
1558
2083
|
commandFields: commandFieldsApp, oneToManyRelationships, oneToOneRelationships,
|
|
1559
2084
|
hasValueObjects, hasEnums, imports: rootEntity.imports,
|
|
1560
2085
|
resourceNameCamel, resourceNameKebab,
|
|
1561
|
-
hasSoftDelete: rootEntity.hasSoftDelete || false
|
|
2086
|
+
hasSoftDelete: rootEntity.hasSoftDelete || false,
|
|
2087
|
+
domainEvents: aggregate.domainEvents || [],
|
|
2088
|
+
lifecycleEventsMap: resolvedLifecycleEndpoints
|
|
1562
2089
|
};
|
|
1563
2090
|
|
|
1564
2091
|
// ── Step 1: Validated VO Dtos ────────────────────────────────────────
|
|
@@ -1819,7 +2346,8 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
|
|
|
1819
2346
|
useCaseName: op.useCase,
|
|
1820
2347
|
idType,
|
|
1821
2348
|
entityName: cl.entityName,
|
|
1822
|
-
removeMethodName: cl.removeMethodName
|
|
2349
|
+
removeMethodName: cl.removeMethodName,
|
|
2350
|
+
itemIdType: cl.itemIdType || 'String'
|
|
1823
2351
|
};
|
|
1824
2352
|
await renderAndWrite(
|
|
1825
2353
|
path.join(templatesDir, 'SubEntityRemoveCommand.java.ejs'),
|
|
@@ -1863,7 +2391,10 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
|
|
|
1863
2391
|
|
|
1864
2392
|
} else {
|
|
1865
2393
|
// Scaffold: no semantic pattern matched → generate stub with TODO
|
|
1866
|
-
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 };
|
|
1867
2398
|
const scaffoldType = op.type || (op.method === 'GET' ? 'query' : 'command');
|
|
1868
2399
|
if (scaffoldType === 'command') {
|
|
1869
2400
|
await renderAndWrite(
|
|
@@ -2065,6 +2596,9 @@ async function generateCrudResources(aggregate, moduleName, moduleBasePath, pack
|
|
|
2065
2596
|
transformRelsForApp(oneToManyRelationships, validatedVoNames), localAllEnums, valueObjects);
|
|
2066
2597
|
|
|
2067
2598
|
// Base context for all templates
|
|
2599
|
+
const resolvedLifecycleCrud = resolveLifecycleEventArgs(
|
|
2600
|
+
aggregate.lifecycleEventsMap || {}, aggregateName, rootEntity.fields, valueObjects
|
|
2601
|
+
);
|
|
2068
2602
|
const baseContext = {
|
|
2069
2603
|
packageName,
|
|
2070
2604
|
moduleName,
|
|
@@ -2085,7 +2619,9 @@ async function generateCrudResources(aggregate, moduleName, moduleBasePath, pack
|
|
|
2085
2619
|
resourceNameCamel,
|
|
2086
2620
|
resourceNameKebab,
|
|
2087
2621
|
hasSoftDelete: rootEntity.hasSoftDelete || false,
|
|
2088
|
-
hasCreateOperation: true // In interactive CRUD flow, Create is always generated
|
|
2622
|
+
hasCreateOperation: true, // In interactive CRUD flow, Create is always generated
|
|
2623
|
+
domainEvents: aggregate.domainEvents || [],
|
|
2624
|
+
lifecycleEventsMap: resolvedLifecycleCrud
|
|
2089
2625
|
};
|
|
2090
2626
|
|
|
2091
2627
|
// 0. Generate Create<VoName>Dto for validated Value Objects
|