eva4j 1.0.14 → 1.0.16

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.
Files changed (139) hide show
  1. package/.agents/skills/skill-creator/LICENSE.txt +202 -0
  2. package/.agents/skills/skill-creator/SKILL.md +485 -0
  3. package/.agents/skills/skill-creator/agents/analyzer.md +274 -0
  4. package/.agents/skills/skill-creator/agents/comparator.md +202 -0
  5. package/.agents/skills/skill-creator/agents/grader.md +223 -0
  6. package/.agents/skills/skill-creator/assets/eval_review.html +146 -0
  7. package/.agents/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  8. package/.agents/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  9. package/.agents/skills/skill-creator/references/schemas.md +430 -0
  10. package/.agents/skills/skill-creator/scripts/__init__.py +0 -0
  11. package/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  12. package/.agents/skills/skill-creator/scripts/generate_report.py +326 -0
  13. package/.agents/skills/skill-creator/scripts/improve_description.py +247 -0
  14. package/.agents/skills/skill-creator/scripts/package_skill.py +136 -0
  15. package/.agents/skills/skill-creator/scripts/quick_validate.py +103 -0
  16. package/.agents/skills/skill-creator/scripts/run_eval.py +310 -0
  17. package/.agents/skills/skill-creator/scripts/run_loop.py +328 -0
  18. package/.agents/skills/skill-creator/scripts/utils.py +47 -0
  19. package/AGENTS.md +268 -6
  20. package/COMMAND_EVALUATION.md +15 -16
  21. package/DOMAIN_YAML_GUIDE.md +430 -14
  22. package/FUTURE_FEATURES.md +1627 -1168
  23. package/README.md +461 -13
  24. package/bin/eva4j.js +32 -14
  25. package/config/defaults.json +1 -0
  26. package/docs/commands/EVALUATE_SYSTEM.md +746 -261
  27. package/docs/commands/EXPORT_DIAGRAM.md +153 -0
  28. package/docs/commands/GENERATE_ENTITIES.md +599 -6
  29. package/docs/commands/INDEX.md +7 -0
  30. package/examples/domain-events.yaml +166 -20
  31. package/examples/domain-listeners.yaml +212 -0
  32. package/examples/domain-one-to-many.yaml +1 -0
  33. package/examples/domain-one-to-one.yaml +1 -0
  34. package/examples/domain-ports.yaml +414 -0
  35. package/examples/domain-soft-delete.yaml +47 -44
  36. package/examples/system/notification.yaml +147 -0
  37. package/examples/system/product.yaml +185 -0
  38. package/examples/system/system.yaml +112 -0
  39. package/examples/system-report.html +971 -0
  40. package/examples/system.yaml +46 -3
  41. package/package.json +2 -1
  42. package/src/agents/design-reviewer.agent.md +163 -0
  43. package/src/commands/build.js +714 -0
  44. package/src/commands/create.js +1 -0
  45. package/src/commands/detach.js +149 -108
  46. package/src/commands/evaluate-system.js +234 -8
  47. package/src/commands/export-diagram.js +522 -0
  48. package/src/commands/generate-entities.js +685 -66
  49. package/src/commands/generate-http-exchange.js +2 -0
  50. package/src/commands/generate-kafka-event.js +43 -10
  51. package/src/generators/base-generator.js +18 -6
  52. package/src/generators/postman-generator.js +188 -0
  53. package/src/generators/shared-generator.js +12 -2
  54. package/src/skills/build-system-yaml/SKILL.md +323 -0
  55. package/src/skills/build-system-yaml/references/domain-yaml-spec.md +410 -0
  56. package/src/skills/build-system-yaml/references/module-spec.md +367 -0
  57. package/src/skills/build-system-yaml/references/system-yaml-spec.md +178 -0
  58. package/src/utils/config-manager.js +54 -0
  59. package/src/utils/context-builder.js +1 -0
  60. package/src/utils/domain-diagram.js +192 -0
  61. package/src/utils/domain-validator.js +1020 -0
  62. package/src/utils/fake-data.js +376 -0
  63. package/src/utils/system-validator.js +319 -199
  64. package/src/utils/yaml-to-entity.js +272 -7
  65. package/templates/aggregate/AggregateMapper.java.ejs +3 -2
  66. package/templates/aggregate/AggregateRepository.java.ejs +3 -2
  67. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +6 -5
  68. package/templates/aggregate/AggregateRoot.java.ejs +60 -2
  69. package/templates/aggregate/DomainEventHandler.java.ejs +4 -1
  70. package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
  71. package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
  72. package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
  73. package/templates/base/docker/Dockerfile.ejs +21 -0
  74. package/templates/base/gradle/build.gradle.ejs +3 -2
  75. package/templates/base/root/AGENTS.md.ejs +306 -45
  76. package/templates/crud/ApplicationMapper.java.ejs +4 -0
  77. package/templates/crud/Controller.java.ejs +4 -4
  78. package/templates/crud/CreateCommand.java.ejs +4 -0
  79. package/templates/crud/CreateCommandHandler.java.ejs +3 -6
  80. package/templates/crud/CreateItemDto.java.ejs +4 -0
  81. package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
  82. package/templates/crud/DeleteCommandHandler.java.ejs +12 -6
  83. package/templates/crud/EndpointsController.java.ejs +6 -6
  84. package/templates/crud/FindByQuery.java.ejs +1 -1
  85. package/templates/crud/FindByQueryHandler.java.ejs +3 -9
  86. package/templates/crud/GetQueryHandler.java.ejs +2 -5
  87. package/templates/crud/ListQuery.java.ejs +1 -1
  88. package/templates/crud/ListQueryHandler.java.ejs +8 -14
  89. package/templates/crud/ScaffoldCommandHandler.java.ejs +2 -4
  90. package/templates/crud/ScaffoldQuery.java.ejs +3 -2
  91. package/templates/crud/ScaffoldQueryHandler.java.ejs +5 -6
  92. package/templates/crud/SubEntityAddCommand.java.ejs +4 -0
  93. package/templates/crud/SubEntityAddCommandHandler.java.ejs +2 -6
  94. package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +2 -7
  95. package/templates/crud/TransitionCommandHandler.java.ejs +2 -6
  96. package/templates/crud/UpdateCommand.java.ejs +4 -0
  97. package/templates/crud/UpdateCommandHandler.java.ejs +2 -5
  98. package/templates/evaluate/report.html.ejs +394 -2
  99. package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
  100. package/templates/kafka-event/Event.java.ejs +9 -0
  101. package/templates/kafka-listener/KafkaController.java.ejs +1 -1
  102. package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
  103. package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
  104. package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
  105. package/templates/kafka-listener/ListenerCommandHandler.java.ejs +25 -0
  106. package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
  107. package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
  108. package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
  109. package/templates/mock/MockEvent.java.ejs +10 -0
  110. package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
  111. package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
  112. package/templates/mock/SpringEventListener.java.ejs +61 -0
  113. package/templates/ports/PortDomainModel.java.ejs +35 -0
  114. package/templates/ports/PortFeignAdapter.java.ejs +67 -0
  115. package/templates/ports/PortFeignClient.java.ejs +45 -0
  116. package/templates/ports/PortFeignConfig.java.ejs +24 -0
  117. package/templates/ports/PortInterface.java.ejs +45 -0
  118. package/templates/ports/PortNestedType.java.ejs +28 -0
  119. package/templates/ports/PortRequestDto.java.ejs +30 -0
  120. package/templates/ports/PortResponseDto.java.ejs +28 -0
  121. package/templates/postman/Collection.json.ejs +1 -1
  122. package/templates/postman/UnifiedCollection.json.ejs +185 -0
  123. package/templates/shared/annotations/LogAfter.java.ejs +2 -0
  124. package/templates/shared/annotations/LogBefore.java.ejs +2 -0
  125. package/templates/shared/annotations/LogExceptions.java.ejs +2 -0
  126. package/templates/shared/annotations/LogLevel.java.ejs +8 -0
  127. package/templates/shared/annotations/LogTimer.java.ejs +1 -0
  128. package/templates/shared/annotations/Loggable.java.ejs +17 -0
  129. package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
  130. package/templates/shared/configurations/loggerConfig/HandlerLogs.java.ejs +120 -32
  131. package/templates/shared/errorMessage/ErrorResponse.java.ejs +23 -0
  132. package/templates/shared/handlerException/HandlerExceptions.java.ejs +99 -79
  133. package/src/commands/generate-system.js +0 -243
  134. package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +0 -1103
  135. package/templates/base/root/skill-build-domain-yaml.ejs +0 -292
  136. package/templates/base/root/skill-build-system-yaml.ejs +0 -252
  137. package/templates/shared/errorMessage/ErrorMessage.java.ejs +0 -5
  138. package/templates/shared/errorMessage/FullErrorMessage.java.ejs +0 -9
  139. package/templates/shared/errorMessage/ShortErrorMessage.java.ejs +0 -6
@@ -5,16 +5,86 @@ 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 } = require('../utils/naming');
9
- const { renderAndWrite } = require('../utils/template-engine');
8
+ const { toPackagePath, toCamelCase, toKebabCase, toPascalCase, getApplicationClassName, pluralizeWord } = require('../utils/naming');
9
+ const { renderAndWrite, renderTemplate } = require('../utils/template-engine');
10
10
  const { parseDomainYaml, generateEntityImports, generateValidationImports } = require('../utils/yaml-to-entity');
11
+ const { createOrUpdateUrlsConfig, ensureUrlsImport } = require('./generate-http-exchange');
11
12
  const SharedGenerator = require('../generators/shared-generator');
12
13
  const ChecksumManager = require('../utils/checksum-manager');
13
- const { getInstalledBroker, generateSingleKafkaEvent, buildKafkaEventContext } = require('./generate-kafka-event');
14
+ const { generateFakeValue, initSeed } = require('../utils/fake-data');
15
+ const { getInstalledBroker, generateSingleKafkaEvent, buildKafkaEventContext, updateKafkaYml,
16
+ generateEventRecord, createOrUpdateMessageBroker, updateDomainEventHandler } = require('./generate-kafka-event');
14
17
 
15
18
  // Maximum depth for recursive relationship traversal
16
19
  const MAX_DEPTH = 5;
17
20
 
21
+ /**
22
+ * Create or update {Module}KafkaMessageBroker with a mock implementation that
23
+ * uses ApplicationEventPublisher + MockEvent instead of Kafka.
24
+ * Mirrors the logic of createOrUpdateKafkaMessageBroker but uses mock templates.
25
+ */
26
+ async function createOrUpdateMockMessageBroker(projectDir, packagePath, context) {
27
+ const adapterPath = path.join(
28
+ projectDir, 'src', 'main', 'java', packagePath,
29
+ context.moduleName, 'infrastructure', 'adapters', 'kafkaMessageBroker',
30
+ `${context.kafkaMessageBrokerClassName}.java`
31
+ );
32
+
33
+ const methodName = `publish${context.eventClassName}`;
34
+ const mockTemplatesDir = path.join(__dirname, '..', '..', 'templates', 'mock');
35
+
36
+ if (await fs.pathExists(adapterPath)) {
37
+ let content = await fs.readFile(adapterPath, 'utf-8');
38
+
39
+ // If this file is still a Kafka implementation, replace it wholesale
40
+ const isKafkaImpl = content.includes('KafkaTemplate') || content.includes('kafkaTemplate');
41
+ if (isKafkaImpl) {
42
+ // Re-create from mock template (overwrite)
43
+ await renderAndWrite(
44
+ path.join(mockTemplatesDir, 'MockMessageBrokerImpl.java.ejs'),
45
+ adapterPath,
46
+ { ...context, topicName: context.topicNameSnake },
47
+ { force: true }
48
+ );
49
+ return;
50
+ }
51
+
52
+ // Already a mock impl — check if method already exists
53
+ if (content.includes(methodName)) {
54
+ return;
55
+ }
56
+
57
+ // Inject event import if missing
58
+ const eventImport = `import ${context.packageName}.${context.moduleName}.application.events.${context.eventClassName};`;
59
+ if (!content.includes(eventImport)) {
60
+ const pkgMatch = content.match(/(package\s+[\w.]+;\s*\n)/);
61
+ if (pkgMatch) {
62
+ const imports = [...content.matchAll(/import\s+[\w.]+;\s*\n/g)];
63
+ const insertPos = imports.length
64
+ ? imports[imports.length - 1].index + imports[imports.length - 1][0].length
65
+ : pkgMatch.index + pkgMatch[0].length;
66
+ content = content.slice(0, insertPos) + eventImport + '\n' + content.slice(insertPos);
67
+ }
68
+ }
69
+
70
+ // Append mock method before the last closing brace
71
+ const methodSnippet = await renderTemplate(
72
+ path.join(mockTemplatesDir, 'MockMessageBrokerImplMethod.java.ejs'),
73
+ { ...context, topicName: context.topicNameSnake }
74
+ );
75
+ const lastBrace = content.lastIndexOf('}');
76
+ content = content.slice(0, lastBrace) + '\n' + methodSnippet + '\n}\n';
77
+ await fs.writeFile(adapterPath, content, 'utf-8');
78
+ } else {
79
+ // Create new mock broker from template
80
+ await renderAndWrite(
81
+ path.join(mockTemplatesDir, 'MockMessageBrokerImpl.java.ejs'),
82
+ adapterPath,
83
+ { ...context, topicName: context.topicNameSnake }
84
+ );
85
+ }
86
+ }
87
+
18
88
  /**
19
89
  * Build a relationship graph for secondary entities
20
90
  * @param {Array} secondaryEntities - Array of secondary entities
@@ -174,7 +244,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
174
244
 
175
245
  try {
176
246
  // Parse domain.yaml
177
- const { aggregates, allEnums, endpoints } = await parseDomainYaml(domainYamlPath, packageName, moduleName);
247
+ const { aggregates, allEnums, endpoints, listeners, ports } = await parseDomainYaml(domainYamlPath, packageName, moduleName);
178
248
 
179
249
  spinner.succeed(chalk.green(`Found ${aggregates.length} aggregate(s) and ${allEnums.length} enum(s)`));
180
250
 
@@ -203,7 +273,13 @@ async function generateEntitiesCommand(moduleName, options = {}) {
203
273
  }
204
274
 
205
275
  // Detect installed message broker for auto-wiring integration events
206
- const broker = hasDomainEventsInModule ? await getInstalledBroker(configManager) : null;
276
+ const installedBroker = (hasDomainEventsInModule || (listeners && listeners.length > 0))
277
+ ? await getInstalledBroker(configManager)
278
+ : null;
279
+ // When brokerMode:'mock' is requested AND a broker is installed, use mock Spring-Events adapter
280
+ const broker = (options.brokerMode === 'mock' && installedBroker)
281
+ ? 'mock'
282
+ : installedBroker;
207
283
 
208
284
  // Generate audit-related shared components if needed
209
285
  if (hasAuditableEntities || hasTrackUserEntities) {
@@ -303,7 +379,9 @@ async function generateEntitiesCommand(moduleName, options = {}) {
303
379
  valueObjects,
304
380
  aggregateMethods: aggregate.aggregateMethods,
305
381
  auditable: rootEntity.auditable,
306
- domainEvents: aggregate.domainEvents || []
382
+ hasSoftDelete: rootEntity.hasSoftDelete || false,
383
+ domainEvents: aggregate.domainEvents || [],
384
+ triggeredEventsMap: aggregate.triggeredEventsMap || {}
307
385
  };
308
386
 
309
387
  await renderAndWrite(
@@ -326,7 +404,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
326
404
  valueObjects,
327
405
  enums: allEnums,
328
406
  auditable: rootEntity.auditable,
329
- audit: rootEntity.audit
407
+ audit: rootEntity.audit,
408
+ hasSoftDelete: rootEntity.hasSoftDelete || false
330
409
  };
331
410
 
332
411
  await renderAndWrite(
@@ -444,6 +523,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
444
523
  packageName,
445
524
  moduleName,
446
525
  rootEntity,
526
+ hasSoftDelete: rootEntity.hasSoftDelete || false,
447
527
  findByOps: []
448
528
  };
449
529
 
@@ -471,6 +551,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
471
551
  aggregateName,
472
552
  rootEntity,
473
553
  hasDomainEvents: (aggregate.domainEvents || []).length > 0,
554
+ hasSoftDelete: rootEntity.hasSoftDelete || false,
474
555
  findByOps: []
475
556
  };
476
557
 
@@ -529,6 +610,10 @@ async function generateEntitiesCommand(moduleName, options = {}) {
529
610
  // KafkaMessageBroker impl method, topic config, KafkaConfig bean) for
530
611
  // every domain event declared in this aggregate.
531
612
  if (broker === 'kafka') {
613
+ const STANDARD_EVENT_TYPES = new Set([
614
+ 'String','Integer','Long','Double','Float','Boolean',
615
+ 'BigDecimal','LocalDate','LocalDateTime','LocalTime','Instant','UUID'
616
+ ]);
532
617
  for (const event of aggregateDomainEvents) {
533
618
  const kafkaCtx = buildKafkaEventContext(packageName, moduleName, event);
534
619
  await generateSingleKafkaEvent(projectDir, packagePath, kafkaCtx);
@@ -537,6 +622,28 @@ async function generateEntitiesCommand(moduleName, options = {}) {
537
622
  name: kafkaCtx.eventClassName,
538
623
  path: `${moduleName}/application/events/${kafkaCtx.eventClassName}.java`
539
624
  });
625
+
626
+ // Generate stub records for custom collection element types
627
+ // e.g. List<OrderItemSnapshot> → generate OrderItemSnapshot.java
628
+ const customElementTypes = [...new Set(
629
+ (event.fields || [])
630
+ .filter(f => f.isCollection && f.collectionElementType && !STANDARD_EVENT_TYPES.has(f.collectionElementType))
631
+ .map(f => f.collectionElementType)
632
+ )];
633
+ for (const typeName of customElementTypes) {
634
+ const stubPath = path.join(moduleBasePath, 'domain', 'models', 'events', `${typeName}.java`);
635
+ await renderAndWrite(
636
+ path.join(__dirname, '..', '..', 'templates', 'aggregate', 'DomainEventSnapshot.java.ejs'),
637
+ stubPath,
638
+ { packageName, moduleName, name: typeName, fields: [] },
639
+ { ...writeOptions, overwrite: false }
640
+ );
641
+ generatedFiles.push({
642
+ type: 'Event Snapshot Type',
643
+ name: typeName,
644
+ path: `${moduleName}/domain/models/events/${typeName}.java`
645
+ });
646
+ }
540
647
  }
541
648
  generatedFiles.push({
542
649
  type: 'Integration Event',
@@ -548,12 +655,423 @@ async function generateEntitiesCommand(moduleName, options = {}) {
548
655
  name: 'MessageBroker (updated)',
549
656
  path: `${moduleName}/application/ports/MessageBroker.java`
550
657
  });
658
+ } else if (broker === 'mock') {
659
+ // ── Mock broker: Spring ApplicationEventPublisher instead of Kafka ──
660
+ // Generate MockEvent record in shared once (idempotent)
661
+ await sharedGenerator.generateMockEvent(sharedBasePath);
662
+
663
+ for (const event of aggregateDomainEvents) {
664
+ const kafkaCtx = buildKafkaEventContext(packageName, moduleName, event);
665
+ // 1. Integration Event record (same as Kafka — only the adapter differs)
666
+ await generateEventRecord(projectDir, packagePath, kafkaCtx);
667
+ generatedFiles.push({
668
+ type: 'Integration Event',
669
+ name: kafkaCtx.eventClassName,
670
+ path: `${moduleName}/application/events/${kafkaCtx.eventClassName}.java`
671
+ });
672
+ // 2. MessageBroker port interface (same as Kafka)
673
+ await createOrUpdateMessageBroker(projectDir, packagePath, kafkaCtx);
674
+ // 3. Mock KafkaMessageBroker impl (uses ApplicationEventPublisher)
675
+ await createOrUpdateMockMessageBroker(projectDir, packagePath, kafkaCtx);
676
+ // 4. DomainEventHandler (same as Kafka — calls messageBroker.publishX)
677
+ await updateDomainEventHandler(projectDir, packagePath, kafkaCtx);
678
+ // 5. NO kafka.yaml update and NO KafkaConfig update in mock mode
679
+ }
680
+ generatedFiles.push({
681
+ type: 'Integration Event',
682
+ name: `${toPascalCase(moduleName)}KafkaMessageBroker (mock)`,
683
+ path: `${moduleName}/infrastructure/adapters/kafkaMessageBroker/${toPascalCase(moduleName)}KafkaMessageBroker.java`
684
+ });
685
+ generatedFiles.push({
686
+ type: 'Integration Event',
687
+ name: 'MessageBroker (updated)',
688
+ path: `${moduleName}/application/ports/MessageBroker.java`
689
+ });
551
690
  }
552
691
  }
553
692
  }
554
693
 
555
694
  spinner.succeed(chalk.green(`Generated ${generatedFiles.length} files! ✨`));
556
695
 
696
+ // ── Generate listeners (integration events CONSUMED from external producers) ──
697
+ if (listeners && listeners.length > 0) {
698
+ if (broker === 'kafka') {
699
+ spinner.start(`Generating ${listeners.length} Kafka listener(s)...`);
700
+ for (const listener of listeners) {
701
+ // Validate topic presence (mandatory for standalone modules)
702
+ if (!listener.topic) {
703
+ spinner.warn(chalk.yellow(`⚠ listener '${listener.event}': topic is required when there is no system.yaml. Skipping.`));
704
+ continue;
705
+ }
706
+
707
+ // Strip any prefix before the last dot (e.g. "test-eva.ORDER_PLACED" → "ORDER_PLACED")
708
+ const topicRaw = listener.topic;
709
+ const topicSuffix = topicRaw.includes('.') ? topicRaw.slice(topicRaw.lastIndexOf('.') + 1) : topicRaw;
710
+ const topicKey = topicSuffix.toLowerCase().replace(/_/g, '-');
711
+ const listenerContext = {
712
+ packageName,
713
+ moduleName,
714
+ ...listener,
715
+ topicConstant: topicRaw,
716
+ topicSpringProperty: `\${topics.${topicKey}}`,
717
+ topicVariableName: toCamelCase(topicSuffix.toLowerCase())
718
+ };
719
+
720
+ // 0. Nested type records (auxiliary value objects for object-typed fields)
721
+ for (const nt of (listener.nestedTypes || [])) {
722
+ const ntPath = path.join(
723
+ moduleBasePath, 'application', 'events',
724
+ `${nt.name}.java`
725
+ );
726
+ await renderAndWrite(
727
+ path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerNestedType.java.ejs'),
728
+ ntPath,
729
+ { packageName, moduleName, name: nt.name, fields: nt.fields },
730
+ writeOptions
731
+ );
732
+ generatedFiles.push({
733
+ type: 'Listener Nested Type',
734
+ name: nt.name,
735
+ path: `${moduleName}/application/events/${nt.name}.java`
736
+ });
737
+ }
738
+
739
+ // 1. Integration Event record
740
+ const integrationEventPath = path.join(
741
+ moduleBasePath, 'application', 'events',
742
+ `${listener.integrationEventClassName}.java`
743
+ );
744
+ await renderAndWrite(
745
+ path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerIntegrationEvent.java.ejs'),
746
+ integrationEventPath,
747
+ listenerContext,
748
+ writeOptions
749
+ );
750
+ generatedFiles.push({
751
+ type: 'Listener Integration Event',
752
+ name: listener.integrationEventClassName,
753
+ path: `${moduleName}/application/events/${listener.integrationEventClassName}.java`
754
+ });
755
+
756
+ // 2. Kafka listener class
757
+ // If the file is currently a mock Spring listener, force overwrite with the real Kafka impl
758
+ const kafkaListenerPath = path.join(
759
+ moduleBasePath, 'infrastructure', 'kafkaListener',
760
+ `${listener.listenerClassName}.java`
761
+ );
762
+ let kafkaListenerWriteOpts = writeOptions;
763
+ if (await fs.pathExists(kafkaListenerPath)) {
764
+ const existing = await fs.readFile(kafkaListenerPath, 'utf-8');
765
+ if (existing.includes('@EventListener') && !existing.includes('@KafkaListener')) {
766
+ kafkaListenerWriteOpts = { ...writeOptions, force: true };
767
+ }
768
+ }
769
+ await renderAndWrite(
770
+ path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerClass.java.ejs'),
771
+ kafkaListenerPath,
772
+ listenerContext,
773
+ kafkaListenerWriteOpts
774
+ );
775
+ generatedFiles.push({
776
+ type: 'Kafka Listener',
777
+ name: listener.listenerClassName,
778
+ path: `${moduleName}/infrastructure/kafkaListener/${listener.listenerClassName}.java`
779
+ });
780
+
781
+ // 3. Register topic in kafka.yaml (all environments)
782
+ await updateKafkaYml(projectDir, topicKey, listener.topic);
783
+
784
+ // 4. Typed Command dispatched from the listener
785
+ const commandPath = path.join(
786
+ moduleBasePath, 'application', 'commands',
787
+ `${listener.commandClassName}.java`
788
+ );
789
+ await renderAndWrite(
790
+ path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerCommand.java.ejs'),
791
+ commandPath,
792
+ listenerContext,
793
+ writeOptions
794
+ );
795
+ generatedFiles.push({
796
+ type: 'Listener Command',
797
+ name: listener.commandClassName,
798
+ path: `${moduleName}/application/commands/${listener.commandClassName}.java`
799
+ });
800
+
801
+ // 5. Use case handler that processes the command
802
+ const handlerPath = path.join(
803
+ moduleBasePath, 'application', 'usecases',
804
+ `${listener.useCase}CommandHandler.java`
805
+ );
806
+ await renderAndWrite(
807
+ path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerCommandHandler.java.ejs'),
808
+ handlerPath,
809
+ listenerContext,
810
+ writeOptions
811
+ );
812
+ generatedFiles.push({
813
+ type: 'Handler',
814
+ name: `${listener.useCase}CommandHandler`,
815
+ path: `${moduleName}/application/usecases/${listener.useCase}CommandHandler.java`
816
+ });
817
+ }
818
+ spinner.succeed(chalk.green(`Kafka listeners generated! ✨`));
819
+ } else if (broker === 'mock') {
820
+ // ── Mock listeners: @EventListener instead of @KafkaListener ──────────
821
+ spinner.start(`Generating ${listeners.length} Spring Event listener(s) (mock mode)...`);
822
+
823
+ // Ensure MockEvent is available in shared
824
+ await sharedGenerator.generateMockEvent(sharedBasePath);
825
+
826
+ for (const listener of listeners) {
827
+ if (!listener.topic) {
828
+ spinner.warn(chalk.yellow(`⚠ listener '${listener.event}': topic is required. Skipping.`));
829
+ continue;
830
+ }
831
+
832
+ const topicRaw = listener.topic;
833
+ const topicSuffix = topicRaw.includes('.') ? topicRaw.slice(topicRaw.lastIndexOf('.') + 1) : topicRaw;
834
+ const topicKey = topicSuffix.toLowerCase().replace(/_/g, '-');
835
+ const listenerContext = {
836
+ packageName,
837
+ moduleName,
838
+ ...listener,
839
+ topicConstant: topicRaw,
840
+ topicSpringProperty: `\${topics.${topicKey}}`,
841
+ topicVariableName: toCamelCase(topicSuffix.toLowerCase())
842
+ };
843
+
844
+ // 0. Nested type records
845
+ for (const nt of (listener.nestedTypes || [])) {
846
+ const ntPath = path.join(moduleBasePath, 'application', 'events', `${nt.name}.java`);
847
+ await renderAndWrite(
848
+ path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerNestedType.java.ejs'),
849
+ ntPath,
850
+ { packageName, moduleName, name: nt.name, fields: nt.fields },
851
+ writeOptions
852
+ );
853
+ generatedFiles.push({ type: 'Listener Nested Type', name: nt.name, path: `${moduleName}/application/events/${nt.name}.java` });
854
+ }
855
+
856
+ // 1. Integration Event record (same as Kafka)
857
+ const integrationEventPath = path.join(moduleBasePath, 'application', 'events', `${listener.integrationEventClassName}.java`);
858
+ await renderAndWrite(
859
+ path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerIntegrationEvent.java.ejs'),
860
+ integrationEventPath,
861
+ listenerContext,
862
+ writeOptions
863
+ );
864
+ generatedFiles.push({ type: 'Listener Integration Event', name: listener.integrationEventClassName, path: `${moduleName}/application/events/${listener.integrationEventClassName}.java` });
865
+
866
+ // 2. Spring @EventListener class (mock — same file path as Kafka listener)
867
+ const listenerPath = path.join(moduleBasePath, 'infrastructure', 'kafkaListener', `${listener.listenerClassName}.java`);
868
+ await renderAndWrite(
869
+ path.join(__dirname, '..', '..', 'templates', 'mock', 'SpringEventListener.java.ejs'),
870
+ listenerPath,
871
+ listenerContext,
872
+ { ...writeOptions, force: true }
873
+ );
874
+ generatedFiles.push({ type: 'Spring Listener (mock)', name: listener.listenerClassName, path: `${moduleName}/infrastructure/kafkaListener/${listener.listenerClassName}.java` });
875
+
876
+ // 3. NO kafka.yaml update in mock mode
877
+
878
+ // 4. Typed Command dispatched from the listener
879
+ const mockCommandPath = path.join(moduleBasePath, 'application', 'commands', `${listener.commandClassName}.java`);
880
+ await renderAndWrite(
881
+ path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerCommand.java.ejs'),
882
+ mockCommandPath,
883
+ listenerContext,
884
+ writeOptions
885
+ );
886
+ generatedFiles.push({ type: 'Listener Command', name: listener.commandClassName, path: `${moduleName}/application/commands/${listener.commandClassName}.java` });
887
+
888
+ // 5. Use case handler stub
889
+ const mockHandlerPath = path.join(moduleBasePath, 'application', 'usecases', `${listener.useCase}CommandHandler.java`);
890
+ await renderAndWrite(
891
+ path.join(__dirname, '..', '..', 'templates', 'kafka-listener', 'ListenerCommandHandler.java.ejs'),
892
+ mockHandlerPath,
893
+ listenerContext,
894
+ writeOptions
895
+ );
896
+ generatedFiles.push({ type: 'Handler', name: `${listener.useCase}CommandHandler`, path: `${moduleName}/application/usecases/${listener.useCase}CommandHandler.java` });
897
+ }
898
+ spinner.succeed(chalk.green(`Spring Event listeners generated (mock mode)! ✨`));
899
+ } 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.`));
901
+ }
902
+ }
903
+
904
+ // ── Generate ports (HTTP clients for synchronous communication) ──────────
905
+ if (ports && ports.length > 0) {
906
+ spinner.start(`Generating ${ports.length} HTTP port(s)...`);
907
+
908
+ for (const portGroup of ports) {
909
+ const {
910
+ serviceName,
911
+ serviceNameCamelCase,
912
+ target,
913
+ baseUrl,
914
+ baseUrlProperty,
915
+ feignClientName,
916
+ feignClientClassName,
917
+ feignAdapterClassName,
918
+ feignConfigClassName,
919
+ adapterPackage,
920
+ methods,
921
+ nestedTypes,
922
+ domainModels
923
+ } = portGroup;
924
+
925
+ const adapterDir = path.join(moduleBasePath, 'infrastructure', 'adapters', adapterPackage);
926
+
927
+ const portContext = {
928
+ packageName,
929
+ moduleName,
930
+ serviceName,
931
+ serviceNameCamelCase,
932
+ target,
933
+ baseUrl,
934
+ baseUrlProperty,
935
+ feignClientName,
936
+ feignClientClassName,
937
+ feignAdapterClassName,
938
+ feignConfigClassName,
939
+ adapterPackage,
940
+ methods,
941
+ nestedTypes,
942
+ domainModels
943
+ };
944
+
945
+ // 0. Nested type records (shared across methods in the same service)
946
+ for (const nt of nestedTypes) {
947
+ const ntPath = path.join(
948
+ moduleBasePath, 'application', 'dtos', `${nt.name}.java`
949
+ );
950
+ await renderAndWrite(
951
+ path.join(__dirname, '..', '..', 'templates', 'ports', 'PortNestedType.java.ejs'),
952
+ ntPath,
953
+ { packageName, moduleName, name: nt.name, fields: nt.fields },
954
+ writeOptions
955
+ );
956
+ generatedFiles.push({
957
+ type: 'Port DTO',
958
+ name: nt.name,
959
+ path: `${moduleName}/application/dtos/${nt.name}.java`
960
+ });
961
+ }
962
+
963
+ // 1a. Domain models in domain/models/{adapterPackage}/ (ACL: domain-side abstraction)
964
+ for (const dm of (domainModels || [])) {
965
+ const dmPath = path.join(moduleBasePath, 'domain', 'models', adapterPackage, `${dm.name}.java`);
966
+ await renderAndWrite(
967
+ path.join(__dirname, '..', '..', 'templates', 'ports', 'PortDomainModel.java.ejs'),
968
+ dmPath,
969
+ { packageName, moduleName, name: dm.name, fields: dm.fields, target, serviceName, adapterPackage },
970
+ writeOptions
971
+ );
972
+ generatedFiles.push({
973
+ type: 'Port Domain Model',
974
+ name: dm.name,
975
+ path: `${moduleName}/domain/models/${adapterPackage}/${dm.name}.java`
976
+ });
977
+ }
978
+
979
+ // 1b. Infra DTOs (one per method that has fields:) — live in infrastructure/adapters/{service}/
980
+ for (const method of methods.filter(m => m.hasResponse)) {
981
+ const infraDtoPath = path.join(adapterDir, `${method.infraDtoName}.java`);
982
+ await renderAndWrite(
983
+ path.join(__dirname, '..', '..', 'templates', 'ports', 'PortResponseDto.java.ejs'),
984
+ infraDtoPath,
985
+ { packageName, moduleName, dtoName: method.infraDtoName, fields: method.fields, adapterPackage },
986
+ writeOptions
987
+ );
988
+ generatedFiles.push({
989
+ type: 'Port Infra DTO',
990
+ name: method.infraDtoName,
991
+ path: `${moduleName}/infrastructure/adapters/${adapterPackage}/${method.infraDtoName}.java`
992
+ });
993
+ }
994
+
995
+ // 2. Request DTOs (one per method that has body:)
996
+ for (const method of methods.filter(m => m.hasBody)) {
997
+ const reqPath = path.join(
998
+ moduleBasePath, 'application', 'dtos', `${method.requestDtoName}.java`
999
+ );
1000
+ await renderAndWrite(
1001
+ path.join(__dirname, '..', '..', 'templates', 'ports', 'PortRequestDto.java.ejs'),
1002
+ reqPath,
1003
+ { packageName, moduleName, dtoName: method.requestDtoName, bodyFields: method.bodyFields, nestedTypes: method.nestedTypes },
1004
+ writeOptions
1005
+ );
1006
+ generatedFiles.push({
1007
+ type: 'Port DTO',
1008
+ name: method.requestDtoName,
1009
+ path: `${moduleName}/application/dtos/${method.requestDtoName}.java`
1010
+ });
1011
+ }
1012
+
1013
+ // 3. Port interface (domain/repositories/)
1014
+ await renderAndWrite(
1015
+ path.join(__dirname, '..', '..', 'templates', 'ports', 'PortInterface.java.ejs'),
1016
+ path.join(moduleBasePath, 'domain', 'repositories', `${serviceName}.java`),
1017
+ portContext,
1018
+ writeOptions
1019
+ );
1020
+ generatedFiles.push({
1021
+ type: 'HTTP Port',
1022
+ name: serviceName,
1023
+ path: `${moduleName}/domain/repositories/${serviceName}.java`
1024
+ });
1025
+
1026
+ // 4. Feign Client interface
1027
+ await renderAndWrite(
1028
+ path.join(__dirname, '..', '..', 'templates', 'ports', 'PortFeignClient.java.ejs'),
1029
+ path.join(adapterDir, `${feignClientClassName}.java`),
1030
+ portContext,
1031
+ writeOptions
1032
+ );
1033
+ generatedFiles.push({
1034
+ type: 'HTTP Port',
1035
+ name: feignClientClassName,
1036
+ path: `${moduleName}/infrastructure/adapters/${adapterPackage}/${feignClientClassName}.java`
1037
+ });
1038
+
1039
+ // 5. Feign Adapter (@Component implementation)
1040
+ await renderAndWrite(
1041
+ path.join(__dirname, '..', '..', 'templates', 'ports', 'PortFeignAdapter.java.ejs'),
1042
+ path.join(adapterDir, `${feignAdapterClassName}.java`),
1043
+ portContext,
1044
+ writeOptions
1045
+ );
1046
+ generatedFiles.push({
1047
+ type: 'HTTP Port',
1048
+ name: feignAdapterClassName,
1049
+ path: `${moduleName}/infrastructure/adapters/${adapterPackage}/${feignAdapterClassName}.java`
1050
+ });
1051
+
1052
+ // 6. Feign Config
1053
+ await renderAndWrite(
1054
+ path.join(__dirname, '..', '..', 'templates', 'ports', 'PortFeignConfig.java.ejs'),
1055
+ path.join(adapterDir, `${feignConfigClassName}.java`),
1056
+ portContext,
1057
+ writeOptions
1058
+ );
1059
+ generatedFiles.push({
1060
+ type: 'HTTP Port',
1061
+ name: feignConfigClassName,
1062
+ path: `${moduleName}/infrastructure/adapters/${adapterPackage}/${feignConfigClassName}.java`
1063
+ });
1064
+
1065
+ // 7. Register base URL in parameters/*/urls.yaml
1066
+ await createOrUpdateUrlsConfig(projectDir, baseUrlProperty, baseUrl);
1067
+ }
1068
+
1069
+ // Ensure urls.yaml is imported in all application-*.yaml files
1070
+ await ensureUrlsImport(projectDir);
1071
+
1072
+ spinner.succeed(chalk.green(`HTTP ports generated! ✨`));
1073
+ }
1074
+
557
1075
  console.log(chalk.blue('\n📦 Generated files:'));
558
1076
  const groupedFiles = generatedFiles.reduce((acc, file) => {
559
1077
  if (!acc[file.type]) acc[file.type] = [];
@@ -581,6 +1099,32 @@ async function generateEntitiesCommand(moduleName, options = {}) {
581
1099
  // ── endpoints: section declared → skip CRUD prompt, auto-generate ──
582
1100
  spinner.start('Generating endpoint-driven resources...');
583
1101
 
1102
+ // Pre-classify all operations ONCE against all aggregates so that
1103
+ // each per-aggregate pass never overwrites a classification that
1104
+ // belongs to a different aggregate (multi-aggregate modules).
1105
+ for (const version of endpoints.versions) {
1106
+ for (const op of version.operations) {
1107
+ for (const agg of aggregates) {
1108
+ const cl = classifyUseCase(op, agg.name, agg);
1109
+ if (cl.category !== 'scaffold') {
1110
+ op._ownerAggregate = agg.name;
1111
+ op._classification = cl;
1112
+ break;
1113
+ }
1114
+ }
1115
+ if (!op._ownerAggregate) {
1116
+ // True scaffold: heuristic — find aggregate whose name appears inside the use case name
1117
+ const matched = aggregates.find(agg =>
1118
+ op.useCase.toLowerCase().includes(agg.name.toLowerCase())
1119
+ );
1120
+ const owner = matched || aggregates[0];
1121
+ op._ownerAggregate = owner.name;
1122
+ op._classification = { category: 'scaffold' };
1123
+ }
1124
+ }
1125
+ }
1126
+
1127
+ const sharedGeneratedUseCases = new Set();
584
1128
  for (const aggregate of aggregates) {
585
1129
  await generateEndpointsResources(
586
1130
  aggregate,
@@ -589,7 +1133,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
589
1133
  moduleBasePath,
590
1134
  packageName,
591
1135
  generatedFiles,
592
- writeOptions
1136
+ writeOptions,
1137
+ sharedGeneratedUseCases
593
1138
  );
594
1139
  }
595
1140
 
@@ -664,22 +1209,24 @@ async function generateEntitiesCommand(moduleName, options = {}) {
664
1209
  writeOptions
665
1210
  );
666
1211
 
667
- // Generate Postman Collection for this aggregate
668
- const collectionPath = await generatePostmanCollection(
669
- aggregate,
670
- moduleName,
671
- moduleBasePath,
672
- projectDir,
673
- packageName,
674
- apiVersion,
675
- projectConfig,
676
- allEnums,
677
- writeOptions
678
- );
679
- postmanCollections.push({
680
- name: aggregate.name,
681
- path: path.relative(projectDir, collectionPath)
682
- });
1212
+ // Generate Postman Collection for this aggregate (skip when called from eva build)
1213
+ if (!options.skipPostman) {
1214
+ const collectionPath = await generatePostmanCollection(
1215
+ aggregate,
1216
+ moduleName,
1217
+ moduleBasePath,
1218
+ projectDir,
1219
+ packageName,
1220
+ apiVersion,
1221
+ projectConfig,
1222
+ allEnums,
1223
+ writeOptions
1224
+ );
1225
+ postmanCollections.push({
1226
+ name: aggregate.name,
1227
+ path: path.relative(projectDir, collectionPath)
1228
+ });
1229
+ }
683
1230
  }
684
1231
 
685
1232
  spinner.succeed(chalk.green('CRUD resources generated! ✨'));
@@ -769,6 +1316,35 @@ function transformRelsForApp(rels, validatedVoNames) {
769
1316
  }));
770
1317
  }
771
1318
 
1319
+ /**
1320
+ * Enrich fields with @Schema(example) values for Swagger UI.
1321
+ * Uses the same fake-data heuristics as the Postman collection generator.
1322
+ */
1323
+ function enrichFieldsWithSchemaExamples(fields, allEnums, valueObjects) {
1324
+ return fields.map(f => {
1325
+ // Skip VOs (nested DTOs describe themselves), collections, and already-enriched
1326
+ if (f.originalVoType || f.isValueObject || f.isCollection) return f;
1327
+ const example = generateFakeValue(f, allEnums, valueObjects);
1328
+ if (example === null || example === undefined || typeof example === 'object') return f;
1329
+ return { ...f, schemaExample: String(example) };
1330
+ });
1331
+ }
1332
+
1333
+ /**
1334
+ * Recursively enrich relationship fields with schema examples.
1335
+ */
1336
+ function enrichRelsWithSchemaExamples(rels, allEnums, valueObjects) {
1337
+ return (rels || []).map(rel => ({
1338
+ ...rel,
1339
+ fields: enrichFieldsWithSchemaExamples(rel.fields || [], allEnums, valueObjects),
1340
+ nestedRelationships: enrichRelsWithSchemaExamples(rel.nestedRelationships, allEnums, valueObjects),
1341
+ nestedOneToOneRelationships: (rel.nestedOneToOneRelationships || []).map(otoRel => ({
1342
+ ...otoRel,
1343
+ fields: enrichFieldsWithSchemaExamples(otoRel.fields || [], allEnums, valueObjects)
1344
+ }))
1345
+ }));
1346
+ }
1347
+
772
1348
  /**
773
1349
  * Classify an endpoint operation into a semantic category.
774
1350
  * Returns { category, ...metadata } where category is one of:
@@ -781,12 +1357,13 @@ function transformRelsForApp(rels, validatedVoNames) {
781
1357
  */
782
1358
  function classifyUseCase(op, aggregateName, aggregate) {
783
1359
  // 1. Standard CRUD
1360
+ const aggregateNamePlural = pluralizeWord(aggregateName);
784
1361
  const standardMap = {
785
1362
  [`Create${aggregateName}`]: 'create',
786
1363
  [`Update${aggregateName}`]: 'update',
787
1364
  [`Delete${aggregateName}`]: 'delete',
788
1365
  [`Get${aggregateName}`]: 'getById',
789
- [`FindAll${aggregateName}s`]: 'findAll'
1366
+ [`FindAll${aggregateNamePlural}`]: 'findAll'
790
1367
  };
791
1368
  if (standardMap[op.useCase]) {
792
1369
  return { category: 'standard', variant: standardMap[op.useCase] };
@@ -845,7 +1422,8 @@ function classifyUseCase(op, aggregateName, aggregate) {
845
1422
  // 4. FindBy field — pattern: FindAll{Aggregate}sBy{FieldPascal}
846
1423
  for (const field of (rootEntity.fields || [])) {
847
1424
  const fieldPascal = toPascalCase(field.name);
848
- if (op.useCase === `FindAll${aggregateName}sBy${fieldPascal}`) {
1425
+ const aggregateNamePlural = pluralizeWord(aggregateName);
1426
+ if (op.useCase === `FindAll${aggregateNamePlural}By${fieldPascal}`) {
849
1427
  return {
850
1428
  category: 'findBy',
851
1429
  fieldName: field.name,
@@ -883,7 +1461,7 @@ function enrichEndpointOperation(op, aggregateName, idType) {
883
1461
  if (standardType === 'getById') returnType = `${aggregateName}ResponseDto`;
884
1462
  else if (standardType === 'findAll') returnType = `PagedResponse<${aggregateName}ResponseDto>`;
885
1463
  else if (cl.category === 'findBy') returnType = `PagedResponse<${aggregateName}ResponseDto>`;
886
- else if (cl.category === 'scaffold' && resolvedType === 'query') returnType = 'Object';
1464
+ else if (cl.category === 'scaffold' && resolvedType === 'query') returnType = `${aggregateName}ResponseDto`;
887
1465
 
888
1466
  let httpStatus = 'HttpStatus.OK';
889
1467
  if (standardType === 'create') httpStatus = 'HttpStatus.CREATED';
@@ -913,8 +1491,9 @@ function enrichEndpointOperation(op, aggregateName, idType) {
913
1491
  * Generate endpoint-driven resources (use cases + versioned controllers)
914
1492
  * for an aggregate when domain.yaml declares an `endpoints:` section.
915
1493
  */
916
- async function generateEndpointsResources(aggregate, endpoints, moduleName, moduleBasePath, packageName, generatedFiles, writeOptions = {}) {
1494
+ async function generateEndpointsResources(aggregate, endpoints, moduleName, moduleBasePath, packageName, generatedFiles, writeOptions = {}, sharedGeneratedUseCases = null) {
917
1495
  const { name: aggregateName, rootEntity, secondaryEntities, valueObjects = [] } = aggregate;
1496
+ const aggregateNamePlural = pluralizeWord(aggregateName);
918
1497
  const templatesDir = path.join(__dirname, '..', '..', 'templates', 'crud');
919
1498
 
920
1499
  const idField = rootEntity.fields[0];
@@ -922,7 +1501,7 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
922
1501
 
923
1502
  const commandFields = rootEntity.fields.filter(f =>
924
1503
  f.name !== 'id' && f.name !== 'createdAt' && f.name !== 'updatedAt' &&
925
- f.name !== 'createdBy' && f.name !== 'updatedBy' && !f.readOnly
1504
+ f.name !== 'createdBy' && f.name !== 'updatedBy' && f.name !== 'deletedAt' && !f.readOnly
926
1505
  );
927
1506
 
928
1507
  const validatedVos = valueObjects.filter(vo =>
@@ -948,7 +1527,7 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
948
1527
  const resourceNameKebab = toKebabCase(aggregateName);
949
1528
 
950
1529
  const responseFields = rootEntity.fields.filter(f =>
951
- f.name !== 'createdBy' && f.name !== 'updatedBy' && !f.hidden
1530
+ f.name !== 'createdBy' && f.name !== 'updatedBy' && f.name !== 'deletedAt' && !f.hidden
952
1531
  );
953
1532
  const responseSecondaryEntities = secondaryEntities.map(entity => ({
954
1533
  ...entity,
@@ -959,25 +1538,34 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
959
1538
  .map(r => ({ targetEntityName: r.target, fieldName: r.fieldName }))
960
1539
  }));
961
1540
 
962
- const commandFieldsApp = transformFieldsForApp(commandFields, validatedVoNames);
1541
+ // Schema examples for Swagger UI (same heuristics as Postman generator)
1542
+ const localAllEnums = [...(aggregate.enums || []), ...(rootEntity.enums || [])];
1543
+ initSeed(42);
1544
+
1545
+ const commandFieldsApp = enrichFieldsWithSchemaExamples(
1546
+ transformFieldsForApp(commandFields, validatedVoNames), localAllEnums, valueObjects);
963
1547
  const oneToOneRelationshipsApp = oneToOneRelationships.map(rel => ({
964
1548
  ...rel,
965
- fields: transformFieldsForApp(rel.fields || [], validatedVoNames)
1549
+ fields: enrichFieldsWithSchemaExamples(
1550
+ transformFieldsForApp(rel.fields || [], validatedVoNames), localAllEnums, valueObjects)
966
1551
  }));
967
- const oneToManyRelationshipsApp = transformRelsForApp(oneToManyRelationships, validatedVoNames);
1552
+ const oneToManyRelationshipsApp = enrichRelsWithSchemaExamples(
1553
+ transformRelsForApp(oneToManyRelationships, validatedVoNames), localAllEnums, valueObjects);
968
1554
 
969
1555
  const baseContext = {
970
- packageName, moduleName, aggregateName, rootEntity, secondaryEntities,
1556
+ packageName, moduleName, aggregateName, aggregateNamePlural, rootEntity, secondaryEntities,
971
1557
  responseFields, responseSecondaryEntities, idType,
972
1558
  commandFields: commandFieldsApp, oneToManyRelationships, oneToOneRelationships,
973
1559
  hasValueObjects, hasEnums, imports: rootEntity.imports,
974
- resourceNameCamel, resourceNameKebab
1560
+ resourceNameCamel, resourceNameKebab,
1561
+ hasSoftDelete: rootEntity.hasSoftDelete || false
975
1562
  };
976
1563
 
977
1564
  // ── Step 1: Validated VO Dtos ────────────────────────────────────────
978
1565
  for (const vo of validatedVos) {
979
1566
  const voDtoContext = {
980
- packageName, moduleName, voName: vo.name, fields: vo.fields,
1567
+ packageName, moduleName, voName: vo.name,
1568
+ fields: enrichFieldsWithSchemaExamples(vo.fields, localAllEnums, valueObjects),
981
1569
  hasEnums: (vo.imports || []).some(i => i.includes('.enums.')),
982
1570
  imports: [...(vo.imports || []), ...generateValidationImports(vo.fields)]
983
1571
  };
@@ -990,10 +1578,16 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
990
1578
  }
991
1579
 
992
1580
  // ── Step 2: ApplicationMapper ────────────────────────────────────────
1581
+ // Only emit Create{Aggregate}Command import and fromCommand() when a
1582
+ // standard CreateOrder operation is declared — other POST use cases
1583
+ // (e.g. PlaceOrder) are scaffolds and never produce that command class.
1584
+ const hasCreateOperation = endpoints.versions.some(v =>
1585
+ v.operations.some(op => op.useCase === `Create${aggregateName}`)
1586
+ );
993
1587
  await renderAndWrite(
994
1588
  path.join(templatesDir, 'ApplicationMapper.java.ejs'),
995
1589
  path.join(moduleBasePath, 'application', 'mappers', `${aggregateName}ApplicationMapper.java`),
996
- { ...baseContext, commandFields: commandFieldsApp, oneToOneRelationships: oneToOneRelationshipsApp, oneToManyRelationships: oneToManyRelationshipsApp, validatedVos },
1590
+ { ...baseContext, commandFields: commandFieldsApp, oneToOneRelationships: oneToOneRelationshipsApp, oneToManyRelationships: oneToManyRelationshipsApp, validatedVos, hasCreateOperation },
997
1591
  writeOptions
998
1592
  );
999
1593
  generatedFiles.push({ type: 'Application Mapper', name: `${aggregateName}ApplicationMapper`, path: `${moduleName}/application/mappers/${aggregateName}ApplicationMapper.java` });
@@ -1001,7 +1595,7 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
1001
1595
  // ── Step 3: ResponseDto ──────────────────────────────────────────────
1002
1596
  const responseDtoContext = {
1003
1597
  ...baseContext,
1004
- allFields: rootEntity.fields.filter(f => f.name !== 'createdBy' && f.name !== 'updatedBy' && !f.hidden),
1598
+ allFields: rootEntity.fields.filter(f => f.name !== 'createdBy' && f.name !== 'updatedBy' && f.name !== 'deletedAt' && !f.hidden),
1005
1599
  relationships: rootEntity.relationships.filter(r => (r.type === 'OneToMany' || r.type === 'OneToOne') && !r.isInverse)
1006
1600
  };
1007
1601
  await renderAndWrite(
@@ -1035,7 +1629,8 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
1035
1629
  f.name !== 'createdBy' && f.name !== 'updatedBy' && !f.readOnly
1036
1630
  );
1037
1631
  const entityNestedRels = enrichRelationshipsRecursively(entity, secondaryEntities, 0, new Set());
1038
- const createFieldsApp = transformFieldsForApp(createFields, validatedVoNames);
1632
+ const createFieldsApp = enrichFieldsWithSchemaExamples(
1633
+ transformFieldsForApp(createFields, validatedVoNames), localAllEnums, valueObjects);
1039
1634
  const dtoVoDtoImports = validatedVos.filter(vo => createFieldsApp.some(f => f.originalVoType === vo.name))
1040
1635
  .map(vo => `import ${packageName}.${moduleName}.application.dtos.Create${vo.name}Dto;`);
1041
1636
  const createDtoImports = [...new Set([
@@ -1067,20 +1662,27 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
1067
1662
  ...(commandFieldsApp.some(f => f.originalVoType) ? ['import jakarta.validation.Valid;'] : [])
1068
1663
  ])];
1069
1664
 
1070
- // Pre-classify ALL operations (including cross-version duplicates) so that
1071
- // enrichEndpointOperation() can read op._classification in step 6.
1665
+ // Defensive: classify ops not yet assigned by the outer pre-pass
1666
+ // (single-aggregate modules or direct calls without a shared set).
1072
1667
  for (const version of endpoints.versions) {
1073
1668
  for (const op of version.operations) {
1074
- op._classification = classifyUseCase(op, aggregateName, aggregate);
1669
+ if (!op._classification) {
1670
+ op._classification = classifyUseCase(op, aggregateName, aggregate);
1671
+ op._ownerAggregate = op._ownerAggregate || aggregateName;
1672
+ }
1075
1673
  }
1076
1674
  }
1077
1675
 
1078
- const generatedUseCases = new Set();
1676
+ const generatedUseCases = sharedGeneratedUseCases ?? new Set();
1079
1677
  const findByOps = []; // collect FindBy ops for repository re-generation
1080
1678
 
1081
1679
  for (const version of endpoints.versions) {
1082
1680
  for (const op of version.operations) {
1083
- if (generatedUseCases.has(op.useCase)) continue; // anti-duplicate
1681
+ // Skip operations owned by a different aggregate (multi-aggregate endpoints section)
1682
+ // MUST come before the shared-set check so foreign ops don't poison the set.
1683
+ if (op._ownerAggregate !== aggregateName) continue;
1684
+
1685
+ if (generatedUseCases.has(op.useCase)) continue; // anti-duplicate (same use case across versions)
1084
1686
  generatedUseCases.add(op.useCase);
1085
1687
 
1086
1688
  const cl = op._classification;
@@ -1150,17 +1752,17 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
1150
1752
  } else if (cl.variant === 'findAll') {
1151
1753
  await renderAndWrite(
1152
1754
  path.join(templatesDir, 'ListQuery.java.ejs'),
1153
- path.join(moduleBasePath, 'application', 'queries', `FindAll${aggregateName}sQuery.java`),
1755
+ path.join(moduleBasePath, 'application', 'queries', `FindAll${aggregateNamePlural}Query.java`),
1154
1756
  baseContext, writeOptions
1155
1757
  );
1156
- generatedFiles.push({ type: 'Query', name: `FindAll${aggregateName}sQuery`, path: `${moduleName}/application/queries/FindAll${aggregateName}sQuery.java` });
1758
+ generatedFiles.push({ type: 'Query', name: `FindAll${aggregateNamePlural}Query`, path: `${moduleName}/application/queries/FindAll${aggregateNamePlural}Query.java` });
1157
1759
 
1158
1760
  await renderAndWrite(
1159
1761
  path.join(templatesDir, 'ListQueryHandler.java.ejs'),
1160
- path.join(moduleBasePath, 'application', 'usecases', `FindAll${aggregateName}sQueryHandler.java`),
1762
+ path.join(moduleBasePath, 'application', 'usecases', `FindAll${aggregateNamePlural}QueryHandler.java`),
1161
1763
  baseContext, writeOptions
1162
1764
  );
1163
- generatedFiles.push({ type: 'Handler', name: `FindAll${aggregateName}sQueryHandler`, path: `${moduleName}/application/usecases/FindAll${aggregateName}sQueryHandler.java` });
1765
+ generatedFiles.push({ type: 'Handler', name: `FindAll${aggregateNamePlural}QueryHandler`, path: `${moduleName}/application/usecases/FindAll${aggregateNamePlural}QueryHandler.java` });
1164
1766
  }
1165
1767
 
1166
1768
  } else if (cl.category === 'transition') {
@@ -1192,7 +1794,7 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
1192
1794
  useCaseName: op.useCase,
1193
1795
  idType,
1194
1796
  entityName: cl.entityName,
1195
- entityFields: cl.entityFields,
1797
+ entityFields: enrichFieldsWithSchemaExamples(cl.entityFields || [], localAllEnums, valueObjects),
1196
1798
  addMethodName: cl.addMethodName,
1197
1799
  imports: cl.entityImports
1198
1800
  };
@@ -1327,7 +1929,9 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
1327
1929
  for (const version of endpoints.versions) {
1328
1930
  const versionCap = version.version.charAt(0).toUpperCase() + version.version.slice(1);
1329
1931
  const controllerName = `${aggregateName}${versionCap}Controller`;
1330
- const enrichedOps = version.operations.map(op => enrichEndpointOperation(op, aggregateName, idType));
1932
+ const enrichedOps = version.operations
1933
+ .filter(op => op._ownerAggregate === aggregateName)
1934
+ .map(op => enrichEndpointOperation(op, aggregateName, idType));
1331
1935
 
1332
1936
  const controllerContext = {
1333
1937
  ...baseContext,
@@ -1354,15 +1958,17 @@ async function generateEndpointsResources(aggregate, endpoints, moduleName, modu
1354
1958
  */
1355
1959
  async function generateCrudResources(aggregate, moduleName, moduleBasePath, packageName, apiVersion, generatedFiles, writeOptions = {}) {
1356
1960
  const { name: aggregateName, rootEntity, secondaryEntities, valueObjects = [] } = aggregate;
1961
+ const aggregateNamePlural = pluralizeWord(aggregateName);
1357
1962
  const templatesDir = path.join(__dirname, '..', '..', 'templates', 'crud');
1358
1963
 
1359
1964
  // Get ID field and type
1360
1965
  const idField = rootEntity.fields[0];
1361
1966
  const idType = idField.javaType;
1362
1967
 
1363
- // Filter command fields (exclude id, audit fields, and readOnly fields)
1968
+ // Filter command fields (exclude id, audit fields, soft-delete fields, and readOnly fields)
1364
1969
  const commandFields = rootEntity.fields.filter(f =>
1365
- f.name !== 'id' && f.name !== 'createdAt' && f.name !== 'updatedAt' && f.name !== 'createdBy' && f.name !== 'updatedBy' && !f.readOnly
1970
+ f.name !== 'id' && f.name !== 'createdAt' && f.name !== 'updatedAt' &&
1971
+ f.name !== 'createdBy' && f.name !== 'updatedBy' && f.name !== 'deletedAt' && !f.readOnly
1366
1972
  );
1367
1973
 
1368
1974
  // Validated VOs: VOs where any field has validation annotations
@@ -1427,9 +2033,9 @@ async function generateCrudResources(aggregate, moduleName, moduleBasePath, pack
1427
2033
  const resourceNameCamel = toCamelCase(aggregateName);
1428
2034
  const resourceNameKebab = toKebabCase(aggregateName);
1429
2035
 
1430
- // Filter audit user fields and hidden fields from response DTOs
2036
+ // Filter audit user fields, soft-delete fields, and hidden fields from response DTOs
1431
2037
  const responseFields = rootEntity.fields.filter(f =>
1432
- f.name !== 'createdBy' && f.name !== 'updatedBy' && !f.hidden
2038
+ f.name !== 'createdBy' && f.name !== 'updatedBy' && f.name !== 'deletedAt' && !f.hidden
1433
2039
  );
1434
2040
 
1435
2041
  const responseSecondaryEntities = secondaryEntities.map(entity => ({
@@ -1444,18 +2050,26 @@ async function generateCrudResources(aggregate, moduleName, moduleBasePath, pack
1444
2050
  }));
1445
2051
 
1446
2052
  // Apply app-layer field transformation (VO fields become Create<Vo>Dto types)
1447
- const commandFieldsApp = transformFieldsForApp(commandFields, validatedVoNames);
2053
+ // Schema examples for Swagger UI (same heuristics as Postman generator)
2054
+ const localAllEnums = [...(aggregate.enums || []), ...(rootEntity.enums || [])];
2055
+ initSeed(42);
2056
+
2057
+ const commandFieldsApp = enrichFieldsWithSchemaExamples(
2058
+ transformFieldsForApp(commandFields, validatedVoNames), localAllEnums, valueObjects);
1448
2059
  const oneToOneRelationshipsApp = oneToOneRelationships.map(rel => ({
1449
2060
  ...rel,
1450
- fields: transformFieldsForApp(rel.fields || [], validatedVoNames)
2061
+ fields: enrichFieldsWithSchemaExamples(
2062
+ transformFieldsForApp(rel.fields || [], validatedVoNames), localAllEnums, valueObjects)
1451
2063
  }));
1452
- const oneToManyRelationshipsApp = transformRelsForApp(oneToManyRelationships, validatedVoNames);
2064
+ const oneToManyRelationshipsApp = enrichRelsWithSchemaExamples(
2065
+ transformRelsForApp(oneToManyRelationships, validatedVoNames), localAllEnums, valueObjects);
1453
2066
 
1454
2067
  // Base context for all templates
1455
2068
  const baseContext = {
1456
2069
  packageName,
1457
2070
  moduleName,
1458
2071
  aggregateName,
2072
+ aggregateNamePlural,
1459
2073
  rootEntity,
1460
2074
  secondaryEntities,
1461
2075
  responseFields,
@@ -1469,7 +2083,9 @@ async function generateCrudResources(aggregate, moduleName, moduleBasePath, pack
1469
2083
  imports: rootEntity.imports,
1470
2084
  apiVersion,
1471
2085
  resourceNameCamel,
1472
- resourceNameKebab
2086
+ resourceNameKebab,
2087
+ hasSoftDelete: rootEntity.hasSoftDelete || false,
2088
+ hasCreateOperation: true // In interactive CRUD flow, Create is always generated
1473
2089
  };
1474
2090
 
1475
2091
  // 0. Generate Create<VoName>Dto for validated Value Objects
@@ -1478,7 +2094,7 @@ async function generateCrudResources(aggregate, moduleName, moduleBasePath, pack
1478
2094
  packageName,
1479
2095
  moduleName,
1480
2096
  voName: vo.name,
1481
- fields: vo.fields,
2097
+ fields: enrichFieldsWithSchemaExamples(vo.fields, localAllEnums, valueObjects),
1482
2098
  hasEnums: (vo.imports || []).some(i => i.includes('.enums.')),
1483
2099
  imports: [...(vo.imports || []), ...generateValidationImports(vo.fields)]
1484
2100
  };
@@ -1500,7 +2116,8 @@ async function generateCrudResources(aggregate, moduleName, moduleBasePath, pack
1500
2116
  commandFields: commandFieldsApp,
1501
2117
  oneToOneRelationships: oneToOneRelationshipsApp,
1502
2118
  oneToManyRelationships: oneToManyRelationshipsApp,
1503
- validatedVos
2119
+ validatedVos,
2120
+ hasCreateOperation: true
1504
2121
  },
1505
2122
  writeOptions
1506
2123
  );
@@ -1551,11 +2168,11 @@ async function generateCrudResources(aggregate, moduleName, moduleBasePath, pack
1551
2168
 
1552
2169
  await renderAndWrite(
1553
2170
  path.join(templatesDir, 'ListQuery.java.ejs'),
1554
- path.join(moduleBasePath, 'application', 'queries', `FindAll${aggregateName}sQuery.java`),
2171
+ path.join(moduleBasePath, 'application', 'queries', `FindAll${aggregateNamePlural}Query.java`),
1555
2172
  baseContext,
1556
2173
  writeOptions
1557
2174
  );
1558
- generatedFiles.push({ type: 'Query', name: `FindAll${aggregateName}sQuery`, path: `${moduleName}/application/queries/FindAll${aggregateName}sQuery.java` });
2175
+ generatedFiles.push({ type: 'Query', name: `FindAll${aggregateNamePlural}Query`, path: `${moduleName}/application/queries/FindAll${aggregateNamePlural}Query.java` });
1559
2176
 
1560
2177
  // 4. Generate Handlers
1561
2178
  await renderAndWrite(
@@ -1582,11 +2199,11 @@ async function generateCrudResources(aggregate, moduleName, moduleBasePath, pack
1582
2199
 
1583
2200
  await renderAndWrite(
1584
2201
  path.join(templatesDir, 'ListQueryHandler.java.ejs'),
1585
- path.join(moduleBasePath, 'application', 'usecases', `FindAll${aggregateName}sQueryHandler.java`),
2202
+ path.join(moduleBasePath, 'application', 'usecases', `FindAll${aggregateNamePlural}QueryHandler.java`),
1586
2203
  baseContext,
1587
2204
  writeOptions
1588
2205
  );
1589
- generatedFiles.push({ type: 'Handler', name: `FindAll${aggregateName}sQueryHandler`, path: `${moduleName}/application/usecases/FindAll${aggregateName}sQueryHandler.java` });
2206
+ generatedFiles.push({ type: 'Handler', name: `FindAll${aggregateNamePlural}QueryHandler`, path: `${moduleName}/application/usecases/FindAll${aggregateNamePlural}QueryHandler.java` });
1590
2207
 
1591
2208
  await renderAndWrite(
1592
2209
  path.join(templatesDir, 'DeleteCommandHandler.java.ejs'),
@@ -1607,7 +2224,7 @@ async function generateCrudResources(aggregate, moduleName, moduleBasePath, pack
1607
2224
  // 5. Generate DTOs
1608
2225
  const responseDtoContext = {
1609
2226
  ...baseContext,
1610
- allFields: rootEntity.fields.filter(f => f.name !== 'createdBy' && f.name !== 'updatedBy' && !f.hidden),
2227
+ allFields: rootEntity.fields.filter(f => f.name !== 'createdBy' && f.name !== 'updatedBy' && f.name !== 'deletedAt' && !f.hidden),
1611
2228
  relationships: rootEntity.relationships.filter(r => (r.type === 'OneToMany' || r.type === 'OneToOne') && !r.isInverse)
1612
2229
  };
1613
2230
 
@@ -1671,7 +2288,8 @@ async function generateCrudResources(aggregate, moduleName, moduleBasePath, pack
1671
2288
  new Set()
1672
2289
  );
1673
2290
 
1674
- const createFieldsApp = transformFieldsForApp(createFields, validatedVoNames);
2291
+ const createFieldsApp = enrichFieldsWithSchemaExamples(
2292
+ transformFieldsForApp(createFields, validatedVoNames), localAllEnums, valueObjects);
1675
2293
  const dtoVoDtoImports = validatedVos
1676
2294
  .filter(vo => createFieldsApp.some(f => f.originalVoType === vo.name))
1677
2295
  .map(vo => `import ${packageName}.${moduleName}.application.dtos.Create${vo.name}Dto;`);
@@ -1762,6 +2380,7 @@ async function generatePostmanCollection(
1762
2380
 
1763
2381
  const context = {
1764
2382
  aggregateName,
2383
+ aggregateNamePlural: pluralizeWord(aggregateName),
1765
2384
  moduleName,
1766
2385
  resourceNameKebab,
1767
2386
  apiVersion,