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.
- package/.agents/skills/skill-creator/LICENSE.txt +202 -0
- package/.agents/skills/skill-creator/SKILL.md +485 -0
- package/.agents/skills/skill-creator/agents/analyzer.md +274 -0
- package/.agents/skills/skill-creator/agents/comparator.md +202 -0
- package/.agents/skills/skill-creator/agents/grader.md +223 -0
- package/.agents/skills/skill-creator/assets/eval_review.html +146 -0
- package/.agents/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/.agents/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/.agents/skills/skill-creator/references/schemas.md +430 -0
- package/.agents/skills/skill-creator/scripts/__init__.py +0 -0
- package/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/.agents/skills/skill-creator/scripts/generate_report.py +326 -0
- package/.agents/skills/skill-creator/scripts/improve_description.py +247 -0
- package/.agents/skills/skill-creator/scripts/package_skill.py +136 -0
- package/.agents/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/.agents/skills/skill-creator/scripts/run_eval.py +310 -0
- package/.agents/skills/skill-creator/scripts/run_loop.py +328 -0
- package/.agents/skills/skill-creator/scripts/utils.py +47 -0
- package/AGENTS.md +268 -6
- package/COMMAND_EVALUATION.md +15 -16
- package/DOMAIN_YAML_GUIDE.md +430 -14
- package/FUTURE_FEATURES.md +1627 -1168
- package/README.md +461 -13
- package/bin/eva4j.js +32 -14
- package/config/defaults.json +1 -0
- package/docs/commands/EVALUATE_SYSTEM.md +746 -261
- package/docs/commands/EXPORT_DIAGRAM.md +153 -0
- package/docs/commands/GENERATE_ENTITIES.md +599 -6
- package/docs/commands/INDEX.md +7 -0
- package/examples/domain-events.yaml +166 -20
- package/examples/domain-listeners.yaml +212 -0
- package/examples/domain-one-to-many.yaml +1 -0
- package/examples/domain-one-to-one.yaml +1 -0
- package/examples/domain-ports.yaml +414 -0
- package/examples/domain-soft-delete.yaml +47 -44
- package/examples/system/notification.yaml +147 -0
- package/examples/system/product.yaml +185 -0
- package/examples/system/system.yaml +112 -0
- package/examples/system-report.html +971 -0
- package/examples/system.yaml +46 -3
- package/package.json +2 -1
- package/src/agents/design-reviewer.agent.md +163 -0
- package/src/commands/build.js +714 -0
- package/src/commands/create.js +1 -0
- package/src/commands/detach.js +149 -108
- package/src/commands/evaluate-system.js +234 -8
- package/src/commands/export-diagram.js +522 -0
- package/src/commands/generate-entities.js +685 -66
- package/src/commands/generate-http-exchange.js +2 -0
- package/src/commands/generate-kafka-event.js +43 -10
- package/src/generators/base-generator.js +18 -6
- package/src/generators/postman-generator.js +188 -0
- package/src/generators/shared-generator.js +12 -2
- package/src/skills/build-system-yaml/SKILL.md +323 -0
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +410 -0
- package/src/skills/build-system-yaml/references/module-spec.md +367 -0
- package/src/skills/build-system-yaml/references/system-yaml-spec.md +178 -0
- package/src/utils/config-manager.js +54 -0
- package/src/utils/context-builder.js +1 -0
- package/src/utils/domain-diagram.js +192 -0
- package/src/utils/domain-validator.js +1020 -0
- package/src/utils/fake-data.js +376 -0
- package/src/utils/system-validator.js +319 -199
- package/src/utils/yaml-to-entity.js +272 -7
- package/templates/aggregate/AggregateMapper.java.ejs +3 -2
- package/templates/aggregate/AggregateRepository.java.ejs +3 -2
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +6 -5
- package/templates/aggregate/AggregateRoot.java.ejs +60 -2
- package/templates/aggregate/DomainEventHandler.java.ejs +4 -1
- package/templates/aggregate/DomainEventRecord.java.ejs +24 -8
- package/templates/aggregate/DomainEventSnapshot.java.ejs +46 -0
- package/templates/aggregate/JpaAggregateRoot.java.ejs +6 -0
- package/templates/base/docker/Dockerfile.ejs +21 -0
- package/templates/base/gradle/build.gradle.ejs +3 -2
- package/templates/base/root/AGENTS.md.ejs +306 -45
- package/templates/crud/ApplicationMapper.java.ejs +4 -0
- package/templates/crud/Controller.java.ejs +4 -4
- package/templates/crud/CreateCommand.java.ejs +4 -0
- package/templates/crud/CreateCommandHandler.java.ejs +3 -6
- package/templates/crud/CreateItemDto.java.ejs +4 -0
- package/templates/crud/CreateValueObjectDto.java.ejs +4 -0
- package/templates/crud/DeleteCommandHandler.java.ejs +12 -6
- package/templates/crud/EndpointsController.java.ejs +6 -6
- package/templates/crud/FindByQuery.java.ejs +1 -1
- package/templates/crud/FindByQueryHandler.java.ejs +3 -9
- package/templates/crud/GetQueryHandler.java.ejs +2 -5
- package/templates/crud/ListQuery.java.ejs +1 -1
- package/templates/crud/ListQueryHandler.java.ejs +8 -14
- package/templates/crud/ScaffoldCommandHandler.java.ejs +2 -4
- package/templates/crud/ScaffoldQuery.java.ejs +3 -2
- package/templates/crud/ScaffoldQueryHandler.java.ejs +5 -6
- package/templates/crud/SubEntityAddCommand.java.ejs +4 -0
- package/templates/crud/SubEntityAddCommandHandler.java.ejs +2 -6
- package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +2 -7
- package/templates/crud/TransitionCommandHandler.java.ejs +2 -6
- package/templates/crud/UpdateCommand.java.ejs +4 -0
- package/templates/crud/UpdateCommandHandler.java.ejs +2 -5
- package/templates/evaluate/report.html.ejs +394 -2
- package/templates/kafka-event/DomainEventHandlerMethod.ejs +3 -1
- package/templates/kafka-event/Event.java.ejs +9 -0
- package/templates/kafka-listener/KafkaController.java.ejs +1 -1
- package/templates/kafka-listener/KafkaListenerClass.java.ejs +1 -1
- package/templates/kafka-listener/ListenerClass.java.ejs +65 -0
- package/templates/kafka-listener/ListenerCommand.java.ejs +31 -0
- package/templates/kafka-listener/ListenerCommandHandler.java.ejs +25 -0
- package/templates/kafka-listener/ListenerIntegrationEvent.java.ejs +37 -0
- package/templates/kafka-listener/ListenerMethod.java.ejs +1 -1
- package/templates/kafka-listener/ListenerNestedType.java.ejs +28 -0
- package/templates/mock/MockEvent.java.ejs +10 -0
- package/templates/mock/MockMessageBrokerImpl.java.ejs +35 -0
- package/templates/mock/MockMessageBrokerImplMethod.java.ejs +6 -0
- package/templates/mock/SpringEventListener.java.ejs +61 -0
- package/templates/ports/PortDomainModel.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +67 -0
- package/templates/ports/PortFeignClient.java.ejs +45 -0
- package/templates/ports/PortFeignConfig.java.ejs +24 -0
- package/templates/ports/PortInterface.java.ejs +45 -0
- package/templates/ports/PortNestedType.java.ejs +28 -0
- package/templates/ports/PortRequestDto.java.ejs +30 -0
- package/templates/ports/PortResponseDto.java.ejs +28 -0
- package/templates/postman/Collection.json.ejs +1 -1
- package/templates/postman/UnifiedCollection.json.ejs +185 -0
- package/templates/shared/annotations/LogAfter.java.ejs +2 -0
- package/templates/shared/annotations/LogBefore.java.ejs +2 -0
- package/templates/shared/annotations/LogExceptions.java.ejs +2 -0
- package/templates/shared/annotations/LogLevel.java.ejs +8 -0
- package/templates/shared/annotations/LogTimer.java.ejs +1 -0
- package/templates/shared/annotations/Loggable.java.ejs +17 -0
- package/templates/shared/configurations/eventPublicationConfig/EventPublicationSchemaConfig.java.ejs +109 -0
- package/templates/shared/configurations/loggerConfig/HandlerLogs.java.ejs +120 -32
- package/templates/shared/errorMessage/ErrorResponse.java.ejs +23 -0
- package/templates/shared/handlerException/HandlerExceptions.java.ejs +99 -79
- package/src/commands/generate-system.js +0 -243
- package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +0 -1103
- package/templates/base/root/skill-build-domain-yaml.ejs +0 -292
- package/templates/base/root/skill-build-system-yaml.ejs +0 -252
- package/templates/shared/errorMessage/ErrorMessage.java.ejs +0 -5
- package/templates/shared/errorMessage/FullErrorMessage.java.ejs +0 -9
- 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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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${
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
1549
|
+
fields: enrichFieldsWithSchemaExamples(
|
|
1550
|
+
transformFieldsForApp(rel.fields || [], validatedVoNames), localAllEnums, valueObjects)
|
|
966
1551
|
}));
|
|
967
|
-
const oneToManyRelationshipsApp =
|
|
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,
|
|
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 =
|
|
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
|
-
//
|
|
1071
|
-
//
|
|
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
|
|
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
|
-
|
|
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${
|
|
1755
|
+
path.join(moduleBasePath, 'application', 'queries', `FindAll${aggregateNamePlural}Query.java`),
|
|
1154
1756
|
baseContext, writeOptions
|
|
1155
1757
|
);
|
|
1156
|
-
generatedFiles.push({ type: 'Query', name: `FindAll${
|
|
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${
|
|
1762
|
+
path.join(moduleBasePath, 'application', 'usecases', `FindAll${aggregateNamePlural}QueryHandler.java`),
|
|
1161
1763
|
baseContext, writeOptions
|
|
1162
1764
|
);
|
|
1163
|
-
generatedFiles.push({ type: 'Handler', name: `FindAll${
|
|
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
|
|
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' &&
|
|
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
|
-
|
|
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:
|
|
2061
|
+
fields: enrichFieldsWithSchemaExamples(
|
|
2062
|
+
transformFieldsForApp(rel.fields || [], validatedVoNames), localAllEnums, valueObjects)
|
|
1451
2063
|
}));
|
|
1452
|
-
const oneToManyRelationshipsApp =
|
|
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${
|
|
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${
|
|
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${
|
|
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${
|
|
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 =
|
|
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,
|