eva4j 1.0.13 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +51 -9
- package/DOMAIN_YAML_GUIDE.md +150 -0
- package/bin/eva4j.js +31 -1
- package/design-system.md +797 -0
- package/docs/commands/EVALUATE_SYSTEM.md +542 -0
- package/docs/commands/GENERATE_ENTITIES.md +196 -0
- package/docs/commands/INDEX.md +10 -1
- package/examples/domain-endpoints-relations.yaml +353 -0
- package/examples/domain-endpoints-versioned.yaml +144 -0
- package/examples/domain-endpoints.yaml +135 -0
- package/examples/system.yaml +289 -0
- package/package.json +1 -1
- package/src/commands/create.js +6 -3
- package/src/commands/evaluate-system.js +384 -0
- package/src/commands/generate-entities.js +677 -14
- package/src/commands/generate-kafka-event.js +59 -5
- package/src/commands/generate-system.js +243 -0
- package/src/generators/base-generator.js +9 -1
- package/src/utils/naming.js +3 -2
- package/src/utils/system-validator.js +314 -0
- package/src/utils/yaml-to-entity.js +31 -2
- package/templates/aggregate/AggregateRepository.java.ejs +5 -0
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +9 -0
- package/templates/aggregate/DomainEventHandler.java.ejs +24 -20
- package/templates/aggregate/JpaRepository.java.ejs +5 -0
- package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1103 -0
- package/templates/base/root/skill-build-domain-yaml.ejs +292 -0
- package/templates/base/root/skill-build-system-yaml.ejs +252 -0
- package/templates/base/root/system.yaml.ejs +97 -0
- package/templates/crud/EndpointsController.java.ejs +178 -0
- package/templates/crud/FindByQuery.java.ejs +17 -0
- package/templates/crud/FindByQueryHandler.java.ejs +57 -0
- package/templates/crud/ScaffoldCommand.java.ejs +12 -0
- package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
- package/templates/crud/ScaffoldQuery.java.ejs +12 -0
- package/templates/crud/ScaffoldQueryHandler.java.ejs +40 -0
- package/templates/crud/SubEntityAddCommand.java.ejs +17 -0
- package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
- package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
- package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
- package/templates/crud/TransitionCommand.java.ejs +9 -0
- package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
- package/templates/evaluate/report.html.ejs +971 -0
- package/templates/kafka-event/Event.java.ejs +7 -0
|
@@ -5,11 +5,12 @@ 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, getApplicationClassName } = require('../utils/naming');
|
|
8
|
+
const { toPackagePath, toCamelCase, toKebabCase, toPascalCase, getApplicationClassName } = require('../utils/naming');
|
|
9
9
|
const { renderAndWrite } = require('../utils/template-engine');
|
|
10
10
|
const { parseDomainYaml, generateEntityImports, generateValidationImports } = require('../utils/yaml-to-entity');
|
|
11
11
|
const SharedGenerator = require('../generators/shared-generator');
|
|
12
12
|
const ChecksumManager = require('../utils/checksum-manager');
|
|
13
|
+
const { getInstalledBroker, generateSingleKafkaEvent, buildKafkaEventContext } = require('./generate-kafka-event');
|
|
13
14
|
|
|
14
15
|
// Maximum depth for recursive relationship traversal
|
|
15
16
|
const MAX_DEPTH = 5;
|
|
@@ -173,7 +174,7 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
173
174
|
|
|
174
175
|
try {
|
|
175
176
|
// Parse domain.yaml
|
|
176
|
-
const { aggregates, allEnums } = await parseDomainYaml(domainYamlPath, packageName, moduleName);
|
|
177
|
+
const { aggregates, allEnums, endpoints } = await parseDomainYaml(domainYamlPath, packageName, moduleName);
|
|
177
178
|
|
|
178
179
|
spinner.succeed(chalk.green(`Found ${aggregates.length} aggregate(s) and ${allEnums.length} enum(s)`));
|
|
179
180
|
|
|
@@ -201,6 +202,9 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
201
202
|
await sharedGenerator.generateDomainEvent(sharedBasePath);
|
|
202
203
|
}
|
|
203
204
|
|
|
205
|
+
// Detect installed message broker for auto-wiring integration events
|
|
206
|
+
const broker = hasDomainEventsInModule ? await getInstalledBroker(configManager) : null;
|
|
207
|
+
|
|
204
208
|
// Generate audit-related shared components if needed
|
|
205
209
|
if (hasAuditableEntities || hasTrackUserEntities) {
|
|
206
210
|
|
|
@@ -439,7 +443,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
439
443
|
const repoContext = {
|
|
440
444
|
packageName,
|
|
441
445
|
moduleName,
|
|
442
|
-
rootEntity
|
|
446
|
+
rootEntity,
|
|
447
|
+
findByOps: []
|
|
443
448
|
};
|
|
444
449
|
|
|
445
450
|
await renderAndWrite(
|
|
@@ -465,7 +470,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
465
470
|
moduleName,
|
|
466
471
|
aggregateName,
|
|
467
472
|
rootEntity,
|
|
468
|
-
hasDomainEvents: (aggregate.domainEvents || []).length > 0
|
|
473
|
+
hasDomainEvents: (aggregate.domainEvents || []).length > 0,
|
|
474
|
+
findByOps: []
|
|
469
475
|
};
|
|
470
476
|
|
|
471
477
|
await renderAndWrite(
|
|
@@ -503,8 +509,11 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
503
509
|
packageName,
|
|
504
510
|
moduleName,
|
|
505
511
|
aggregateName,
|
|
506
|
-
domainEvents: aggregateDomainEvents
|
|
507
|
-
|
|
512
|
+
domainEvents: aggregateDomainEvents.map(e => ({
|
|
513
|
+
...e,
|
|
514
|
+
integrationEventClassName: `${e.name}IntegrationEvent`
|
|
515
|
+
})),
|
|
516
|
+
broker
|
|
508
517
|
};
|
|
509
518
|
await renderAndWrite(
|
|
510
519
|
path.join(__dirname, '..', '..', 'templates', 'aggregate', 'DomainEventHandler.java.ejs'),
|
|
@@ -513,6 +522,33 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
513
522
|
writeOptions
|
|
514
523
|
);
|
|
515
524
|
generatedFiles.push({ type: 'Domain Event Handler', name: `${aggregateName}DomainEventHandler`, path: `${moduleName}/application/usecases/${aggregateName}DomainEventHandler.java` });
|
|
525
|
+
|
|
526
|
+
// ── Auto-wire broker integration events ────────────────────────────────
|
|
527
|
+
// When a message broker is installed, generate the complete integration
|
|
528
|
+
// event layer (XIntegrationEvent record, MessageBroker port method,
|
|
529
|
+
// KafkaMessageBroker impl method, topic config, KafkaConfig bean) for
|
|
530
|
+
// every domain event declared in this aggregate.
|
|
531
|
+
if (broker === 'kafka') {
|
|
532
|
+
for (const event of aggregateDomainEvents) {
|
|
533
|
+
const kafkaCtx = buildKafkaEventContext(packageName, moduleName, event);
|
|
534
|
+
await generateSingleKafkaEvent(projectDir, packagePath, kafkaCtx);
|
|
535
|
+
generatedFiles.push({
|
|
536
|
+
type: 'Integration Event',
|
|
537
|
+
name: kafkaCtx.eventClassName,
|
|
538
|
+
path: `${moduleName}/application/events/${kafkaCtx.eventClassName}.java`
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
generatedFiles.push({
|
|
542
|
+
type: 'Integration Event',
|
|
543
|
+
name: `${toPascalCase(moduleName)}KafkaMessageBroker (updated)`,
|
|
544
|
+
path: `${moduleName}/infrastructure/adapters/kafkaMessageBroker/${toPascalCase(moduleName)}KafkaMessageBroker.java`
|
|
545
|
+
});
|
|
546
|
+
generatedFiles.push({
|
|
547
|
+
type: 'Integration Event',
|
|
548
|
+
name: 'MessageBroker (updated)',
|
|
549
|
+
path: `${moduleName}/application/ports/MessageBroker.java`
|
|
550
|
+
});
|
|
551
|
+
}
|
|
516
552
|
}
|
|
517
553
|
}
|
|
518
554
|
|
|
@@ -541,15 +577,60 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
541
577
|
// Persist checksums to disk before asking about CRUD
|
|
542
578
|
await checksumManager.save();
|
|
543
579
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
580
|
+
if (endpoints) {
|
|
581
|
+
// ── endpoints: section declared → skip CRUD prompt, auto-generate ──
|
|
582
|
+
spinner.start('Generating endpoint-driven resources...');
|
|
583
|
+
|
|
584
|
+
for (const aggregate of aggregates) {
|
|
585
|
+
await generateEndpointsResources(
|
|
586
|
+
aggregate,
|
|
587
|
+
endpoints,
|
|
588
|
+
moduleName,
|
|
589
|
+
moduleBasePath,
|
|
590
|
+
packageName,
|
|
591
|
+
generatedFiles,
|
|
592
|
+
writeOptions
|
|
593
|
+
);
|
|
551
594
|
}
|
|
552
|
-
|
|
595
|
+
|
|
596
|
+
spinner.succeed(chalk.green('Endpoint-driven resources generated! ✨'));
|
|
597
|
+
|
|
598
|
+
const epFiles = generatedFiles.filter(f =>
|
|
599
|
+
f.type.includes('Command') || f.type.includes('Query') ||
|
|
600
|
+
f.type.includes('Handler') || f.type.includes('DTO') ||
|
|
601
|
+
f.type.includes('Controller') || f.type.includes('Mapper')
|
|
602
|
+
);
|
|
603
|
+
console.log(chalk.blue('\n📄 Generated endpoint files:'));
|
|
604
|
+
const groupedEp = epFiles.reduce((acc, file) => {
|
|
605
|
+
if (!acc[file.type]) acc[file.type] = [];
|
|
606
|
+
acc[file.type].push(file);
|
|
607
|
+
return acc;
|
|
608
|
+
}, {});
|
|
609
|
+
Object.keys(groupedEp).forEach(type => {
|
|
610
|
+
console.log(chalk.gray(`\n ${type}:`));
|
|
611
|
+
groupedEp[type].forEach(file => {
|
|
612
|
+
console.log(chalk.gray(` ├── ${file.name}`));
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// Collect versions for summary
|
|
617
|
+
const versionList = endpoints.versions.map(v => v.version).join(', ');
|
|
618
|
+
const totalOps = endpoints.versions.reduce((sum, v) => sum + v.operations.length, 0);
|
|
619
|
+
console.log(chalk.blue(`\n✅ Generated ${totalOps} endpoint(s) across version(s): ${versionList}`));
|
|
620
|
+
|
|
621
|
+
await checksumManager.save();
|
|
622
|
+
|
|
623
|
+
} else {
|
|
624
|
+
// ── No endpoints section → original interactive CRUD flow ────────
|
|
625
|
+
// Ask user if they want to generate CRUD resources
|
|
626
|
+
const { generateCrud } = await inquirer.prompt([
|
|
627
|
+
{
|
|
628
|
+
type: 'confirm',
|
|
629
|
+
name: 'generateCrud',
|
|
630
|
+
message: 'Do you want to generate CRUD resources for aggregate roots?',
|
|
631
|
+
default: true
|
|
632
|
+
}
|
|
633
|
+
]);
|
|
553
634
|
|
|
554
635
|
if (generateCrud) {
|
|
555
636
|
// Ask for API version
|
|
@@ -641,6 +722,8 @@ async function generateEntitiesCommand(moduleName, options = {}) {
|
|
|
641
722
|
await checksumManager.save();
|
|
642
723
|
}
|
|
643
724
|
|
|
725
|
+
} // end else (no endpoints section)
|
|
726
|
+
|
|
644
727
|
console.log();
|
|
645
728
|
|
|
646
729
|
} catch (error) {
|
|
@@ -686,6 +769,586 @@ function transformRelsForApp(rels, validatedVoNames) {
|
|
|
686
769
|
}));
|
|
687
770
|
}
|
|
688
771
|
|
|
772
|
+
/**
|
|
773
|
+
* Classify an endpoint operation into a semantic category.
|
|
774
|
+
* Returns { category, ...metadata } where category is one of:
|
|
775
|
+
* 'standard' → matches the 5 CRUD patterns exactly
|
|
776
|
+
* 'transition' → matches {MethodPascal}{Aggregate} for an enum transition
|
|
777
|
+
* 'subEntityAdd' → matches Add{EntityName} for a OneToMany secondary entity
|
|
778
|
+
* 'subEntityRemove' → matches Remove{EntityName} for a OneToMany secondary entity
|
|
779
|
+
* 'findBy' → matches FindAll{Aggregate}sBy{FieldPascal} for a root field
|
|
780
|
+
* 'scaffold' → no semantic pattern matched
|
|
781
|
+
*/
|
|
782
|
+
function classifyUseCase(op, aggregateName, aggregate) {
|
|
783
|
+
// 1. Standard CRUD
|
|
784
|
+
const standardMap = {
|
|
785
|
+
[`Create${aggregateName}`]: 'create',
|
|
786
|
+
[`Update${aggregateName}`]: 'update',
|
|
787
|
+
[`Delete${aggregateName}`]: 'delete',
|
|
788
|
+
[`Get${aggregateName}`]: 'getById',
|
|
789
|
+
[`FindAll${aggregateName}s`]: 'findAll'
|
|
790
|
+
};
|
|
791
|
+
if (standardMap[op.useCase]) {
|
|
792
|
+
return { category: 'standard', variant: standardMap[op.useCase] };
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const rootEntity = aggregate.rootEntity;
|
|
796
|
+
const enums = aggregate.enums || [];
|
|
797
|
+
|
|
798
|
+
// 2. Enum transitions — pattern: {MethodPascal}{Aggregate}
|
|
799
|
+
for (const enumDef of enums) {
|
|
800
|
+
for (const transition of (enumDef.transitions || [])) {
|
|
801
|
+
const methodPascal = toPascalCase(transition.method);
|
|
802
|
+
if (op.useCase === `${methodPascal}${aggregateName}`) {
|
|
803
|
+
return {
|
|
804
|
+
category: 'transition',
|
|
805
|
+
domainMethod: transition.method,
|
|
806
|
+
enumName: enumDef.name,
|
|
807
|
+
targetStatus: Array.isArray(transition.to) ? transition.to[0] : transition.to
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// 3. Sub-entity operations — pattern: Add{Entity} / Remove{Entity} (OneToMany only)
|
|
814
|
+
const oneToManyRels = (rootEntity.relationships || []).filter(r =>
|
|
815
|
+
r.type === 'OneToMany' && !r.isInverse
|
|
816
|
+
);
|
|
817
|
+
for (const rel of oneToManyRels) {
|
|
818
|
+
if (op.useCase === `Add${rel.target}`) {
|
|
819
|
+
const targetEntity = (aggregate.secondaryEntities || []).find(e => e.name === rel.target);
|
|
820
|
+
const entityFields = targetEntity
|
|
821
|
+
? targetEntity.fields.filter(f =>
|
|
822
|
+
f.name !== 'id' && f.name !== 'createdAt' && f.name !== 'updatedAt' &&
|
|
823
|
+
f.name !== 'createdBy' && f.name !== 'updatedBy' && !f.readOnly
|
|
824
|
+
)
|
|
825
|
+
: [];
|
|
826
|
+
return {
|
|
827
|
+
category: 'subEntityAdd',
|
|
828
|
+
entityName: rel.target,
|
|
829
|
+
fieldName: rel.fieldName,
|
|
830
|
+
addMethodName: `add${rel.target}`,
|
|
831
|
+
entityFields,
|
|
832
|
+
entityImports: targetEntity ? (targetEntity.imports || []) : []
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
if (op.useCase === `Remove${rel.target}`) {
|
|
836
|
+
return {
|
|
837
|
+
category: 'subEntityRemove',
|
|
838
|
+
entityName: rel.target,
|
|
839
|
+
fieldName: rel.fieldName,
|
|
840
|
+
removeMethodName: `remove${rel.target}ById`
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// 4. FindBy field — pattern: FindAll{Aggregate}sBy{FieldPascal}
|
|
846
|
+
for (const field of (rootEntity.fields || [])) {
|
|
847
|
+
const fieldPascal = toPascalCase(field.name);
|
|
848
|
+
if (op.useCase === `FindAll${aggregateName}sBy${fieldPascal}`) {
|
|
849
|
+
return {
|
|
850
|
+
category: 'findBy',
|
|
851
|
+
fieldName: field.name,
|
|
852
|
+
fieldPascal,
|
|
853
|
+
fieldJavaType: field.javaType,
|
|
854
|
+
jpaMethodName: `findBy${fieldPascal}`
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return { category: 'scaffold' };
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Enrich a single endpoint operation with derived properties for template rendering.
|
|
864
|
+
* Expects op._classification to be set by classifyUseCase() before this call.
|
|
865
|
+
*/
|
|
866
|
+
function enrichEndpointOperation(op, aggregateName, idType) {
|
|
867
|
+
const httpAnnotationMap = {
|
|
868
|
+
GET: 'GetMapping', POST: 'PostMapping',
|
|
869
|
+
PUT: 'PutMapping', PATCH: 'PatchMapping', DELETE: 'DeleteMapping'
|
|
870
|
+
};
|
|
871
|
+
const hasPathVar = Boolean(op.path && op.path.includes('{'));
|
|
872
|
+
const pathVarMatch = hasPathVar ? op.path.match(/\{([^}]+)\}/) : null;
|
|
873
|
+
const pathVarName = pathVarMatch ? pathVarMatch[1] : 'id';
|
|
874
|
+
|
|
875
|
+
const cl = op._classification || { category: 'scaffold' };
|
|
876
|
+
const isStandard = cl.category === 'standard';
|
|
877
|
+
const standardType = isStandard ? cl.variant : null;
|
|
878
|
+
|
|
879
|
+
// Infer type from HTTP method when not explicitly declared: GET → query, everything else → command
|
|
880
|
+
const resolvedType = op.type || (op.method === 'GET' ? 'query' : 'command');
|
|
881
|
+
|
|
882
|
+
let returnType = 'void';
|
|
883
|
+
if (standardType === 'getById') returnType = `${aggregateName}ResponseDto`;
|
|
884
|
+
else if (standardType === 'findAll') returnType = `PagedResponse<${aggregateName}ResponseDto>`;
|
|
885
|
+
else if (cl.category === 'findBy') returnType = `PagedResponse<${aggregateName}ResponseDto>`;
|
|
886
|
+
else if (cl.category === 'scaffold' && resolvedType === 'query') returnType = 'Object';
|
|
887
|
+
|
|
888
|
+
let httpStatus = 'HttpStatus.OK';
|
|
889
|
+
if (standardType === 'create') httpStatus = 'HttpStatus.CREATED';
|
|
890
|
+
else if (standardType === 'update') httpStatus = 'HttpStatus.NO_CONTENT';
|
|
891
|
+
else if (cl.category === 'transition') httpStatus = 'HttpStatus.NO_CONTENT';
|
|
892
|
+
else if (cl.category === 'subEntityAdd') httpStatus = 'HttpStatus.CREATED';
|
|
893
|
+
else if (cl.category === 'subEntityRemove') httpStatus = 'HttpStatus.NO_CONTENT';
|
|
894
|
+
|
|
895
|
+
return {
|
|
896
|
+
...op,
|
|
897
|
+
type: resolvedType,
|
|
898
|
+
httpAnnotation: httpAnnotationMap[op.method] || 'PostMapping',
|
|
899
|
+
methodName: toCamelCase(op.useCase),
|
|
900
|
+
hasPathVar,
|
|
901
|
+
pathVarName,
|
|
902
|
+
isStandard,
|
|
903
|
+
standardType,
|
|
904
|
+
classifiedType: cl.category,
|
|
905
|
+
classification: cl,
|
|
906
|
+
returnType,
|
|
907
|
+
httpStatus,
|
|
908
|
+
idType
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Generate endpoint-driven resources (use cases + versioned controllers)
|
|
914
|
+
* for an aggregate when domain.yaml declares an `endpoints:` section.
|
|
915
|
+
*/
|
|
916
|
+
async function generateEndpointsResources(aggregate, endpoints, moduleName, moduleBasePath, packageName, generatedFiles, writeOptions = {}) {
|
|
917
|
+
const { name: aggregateName, rootEntity, secondaryEntities, valueObjects = [] } = aggregate;
|
|
918
|
+
const templatesDir = path.join(__dirname, '..', '..', 'templates', 'crud');
|
|
919
|
+
|
|
920
|
+
const idField = rootEntity.fields[0];
|
|
921
|
+
const idType = idField.javaType;
|
|
922
|
+
|
|
923
|
+
const commandFields = rootEntity.fields.filter(f =>
|
|
924
|
+
f.name !== 'id' && f.name !== 'createdAt' && f.name !== 'updatedAt' &&
|
|
925
|
+
f.name !== 'createdBy' && f.name !== 'updatedBy' && !f.readOnly
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
const validatedVos = valueObjects.filter(vo =>
|
|
929
|
+
vo.fields.some(f => f.validationAnnotations && f.validationAnnotations.length > 0)
|
|
930
|
+
);
|
|
931
|
+
const validatedVoNames = new Set(validatedVos.map(vo => vo.name));
|
|
932
|
+
|
|
933
|
+
const oneToManyRelationships = enrichRelationshipsRecursively(rootEntity, secondaryEntities, 0, new Set());
|
|
934
|
+
const oneToOneRels = rootEntity.relationships?.filter(r => r.type === 'OneToOne' && !r.isInverse) || [];
|
|
935
|
+
const oneToOneRelationships = oneToOneRels.map(rel => {
|
|
936
|
+
const targetEntity = secondaryEntities.find(e => e.name === rel.target);
|
|
937
|
+
if (!targetEntity) return { targetEntityName: rel.target, fieldName: rel.fieldName, type: rel.type, fields: [] };
|
|
938
|
+
const targetFields = targetEntity.fields.filter(f =>
|
|
939
|
+
f.name !== 'id' && f.name !== 'createdAt' && f.name !== 'updatedAt' &&
|
|
940
|
+
f.name !== 'createdBy' && f.name !== 'updatedBy'
|
|
941
|
+
);
|
|
942
|
+
return { targetEntityName: rel.target, fieldName: rel.fieldName, type: rel.type, fields: targetFields, entity: targetEntity };
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
const hasValueObjects = rootEntity.fields.some(f => f.isValueObject);
|
|
946
|
+
const hasEnums = rootEntity.enums && rootEntity.enums.length > 0;
|
|
947
|
+
const resourceNameCamel = toCamelCase(aggregateName);
|
|
948
|
+
const resourceNameKebab = toKebabCase(aggregateName);
|
|
949
|
+
|
|
950
|
+
const responseFields = rootEntity.fields.filter(f =>
|
|
951
|
+
f.name !== 'createdBy' && f.name !== 'updatedBy' && !f.hidden
|
|
952
|
+
);
|
|
953
|
+
const responseSecondaryEntities = secondaryEntities.map(entity => ({
|
|
954
|
+
...entity,
|
|
955
|
+
responseFields: entity.fields.filter(f => f.name !== 'createdBy' && f.name !== 'updatedBy' && !f.hidden),
|
|
956
|
+
nestedRelationships: enrichRelationshipsRecursively(entity, secondaryEntities, 0, new Set()),
|
|
957
|
+
forwardOneToOneRels: (entity.relationships || [])
|
|
958
|
+
.filter(r => r.type === 'OneToOne' && !r.isInverse)
|
|
959
|
+
.map(r => ({ targetEntityName: r.target, fieldName: r.fieldName }))
|
|
960
|
+
}));
|
|
961
|
+
|
|
962
|
+
const commandFieldsApp = transformFieldsForApp(commandFields, validatedVoNames);
|
|
963
|
+
const oneToOneRelationshipsApp = oneToOneRelationships.map(rel => ({
|
|
964
|
+
...rel,
|
|
965
|
+
fields: transformFieldsForApp(rel.fields || [], validatedVoNames)
|
|
966
|
+
}));
|
|
967
|
+
const oneToManyRelationshipsApp = transformRelsForApp(oneToManyRelationships, validatedVoNames);
|
|
968
|
+
|
|
969
|
+
const baseContext = {
|
|
970
|
+
packageName, moduleName, aggregateName, rootEntity, secondaryEntities,
|
|
971
|
+
responseFields, responseSecondaryEntities, idType,
|
|
972
|
+
commandFields: commandFieldsApp, oneToManyRelationships, oneToOneRelationships,
|
|
973
|
+
hasValueObjects, hasEnums, imports: rootEntity.imports,
|
|
974
|
+
resourceNameCamel, resourceNameKebab
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
// ── Step 1: Validated VO Dtos ────────────────────────────────────────
|
|
978
|
+
for (const vo of validatedVos) {
|
|
979
|
+
const voDtoContext = {
|
|
980
|
+
packageName, moduleName, voName: vo.name, fields: vo.fields,
|
|
981
|
+
hasEnums: (vo.imports || []).some(i => i.includes('.enums.')),
|
|
982
|
+
imports: [...(vo.imports || []), ...generateValidationImports(vo.fields)]
|
|
983
|
+
};
|
|
984
|
+
await renderAndWrite(
|
|
985
|
+
path.join(templatesDir, 'CreateValueObjectDto.java.ejs'),
|
|
986
|
+
path.join(moduleBasePath, 'application', 'dtos', `Create${vo.name}Dto.java`),
|
|
987
|
+
voDtoContext, writeOptions
|
|
988
|
+
);
|
|
989
|
+
generatedFiles.push({ type: 'DTO', name: `Create${vo.name}Dto`, path: `${moduleName}/application/dtos/Create${vo.name}Dto.java` });
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// ── Step 2: ApplicationMapper ────────────────────────────────────────
|
|
993
|
+
await renderAndWrite(
|
|
994
|
+
path.join(templatesDir, 'ApplicationMapper.java.ejs'),
|
|
995
|
+
path.join(moduleBasePath, 'application', 'mappers', `${aggregateName}ApplicationMapper.java`),
|
|
996
|
+
{ ...baseContext, commandFields: commandFieldsApp, oneToOneRelationships: oneToOneRelationshipsApp, oneToManyRelationships: oneToManyRelationshipsApp, validatedVos },
|
|
997
|
+
writeOptions
|
|
998
|
+
);
|
|
999
|
+
generatedFiles.push({ type: 'Application Mapper', name: `${aggregateName}ApplicationMapper`, path: `${moduleName}/application/mappers/${aggregateName}ApplicationMapper.java` });
|
|
1000
|
+
|
|
1001
|
+
// ── Step 3: ResponseDto ──────────────────────────────────────────────
|
|
1002
|
+
const responseDtoContext = {
|
|
1003
|
+
...baseContext,
|
|
1004
|
+
allFields: rootEntity.fields.filter(f => f.name !== 'createdBy' && f.name !== 'updatedBy' && !f.hidden),
|
|
1005
|
+
relationships: rootEntity.relationships.filter(r => (r.type === 'OneToMany' || r.type === 'OneToOne') && !r.isInverse)
|
|
1006
|
+
};
|
|
1007
|
+
await renderAndWrite(
|
|
1008
|
+
path.join(templatesDir, 'ResponseDto.java.ejs'),
|
|
1009
|
+
path.join(moduleBasePath, 'application', 'dtos', `${aggregateName}ResponseDto.java`),
|
|
1010
|
+
responseDtoContext, writeOptions
|
|
1011
|
+
);
|
|
1012
|
+
generatedFiles.push({ type: 'DTO', name: `${aggregateName}ResponseDto`, path: `${moduleName}/application/dtos/${aggregateName}ResponseDto.java` });
|
|
1013
|
+
|
|
1014
|
+
// ── Step 4: Secondary entity DTOs ───────────────────────────────────
|
|
1015
|
+
for (const entity of secondaryEntities) {
|
|
1016
|
+
const nestedRelationships = enrichRelationshipsRecursively(entity, secondaryEntities, 0, new Set());
|
|
1017
|
+
const forwardOoO = (entity.relationships || []).filter(r => r.type === 'OneToOne' && !r.isInverse)
|
|
1018
|
+
.map(r => ({ targetEntityName: r.target, fieldName: r.fieldName }));
|
|
1019
|
+
|
|
1020
|
+
await renderAndWrite(
|
|
1021
|
+
path.join(templatesDir, 'SecondaryEntityDto.java.ejs'),
|
|
1022
|
+
path.join(moduleBasePath, 'application', 'dtos', `${entity.name}Dto.java`),
|
|
1023
|
+
{ packageName, moduleName, entityName: entity.name,
|
|
1024
|
+
fields: entity.fields.filter(f => f.name !== 'createdBy' && f.name !== 'updatedBy' && !f.hidden),
|
|
1025
|
+
nestedRelationships, hasNestedRelationships: nestedRelationships.length > 0,
|
|
1026
|
+
forwardOneToOneRels: forwardOoO,
|
|
1027
|
+
hasValueObjects: entity.fields.some(f => f.isValueObject),
|
|
1028
|
+
hasEnums: entity.enums && entity.enums.length > 0, imports: entity.imports },
|
|
1029
|
+
writeOptions
|
|
1030
|
+
);
|
|
1031
|
+
generatedFiles.push({ type: 'DTO', name: `${entity.name}Dto`, path: `${moduleName}/application/dtos/${entity.name}Dto.java` });
|
|
1032
|
+
|
|
1033
|
+
const createFields = entity.fields.filter(f =>
|
|
1034
|
+
f.name !== 'id' && f.name !== 'createdAt' && f.name !== 'updatedAt' &&
|
|
1035
|
+
f.name !== 'createdBy' && f.name !== 'updatedBy' && !f.readOnly
|
|
1036
|
+
);
|
|
1037
|
+
const entityNestedRels = enrichRelationshipsRecursively(entity, secondaryEntities, 0, new Set());
|
|
1038
|
+
const createFieldsApp = transformFieldsForApp(createFields, validatedVoNames);
|
|
1039
|
+
const dtoVoDtoImports = validatedVos.filter(vo => createFieldsApp.some(f => f.originalVoType === vo.name))
|
|
1040
|
+
.map(vo => `import ${packageName}.${moduleName}.application.dtos.Create${vo.name}Dto;`);
|
|
1041
|
+
const createDtoImports = [...new Set([
|
|
1042
|
+
...(entity.imports || []), ...generateValidationImports(createFieldsApp), ...dtoVoDtoImports,
|
|
1043
|
+
...(createFieldsApp.some(f => f.originalVoType) ? ['import jakarta.validation.Valid;'] : [])
|
|
1044
|
+
])];
|
|
1045
|
+
const fwdOoO = (entity.relationships || []).filter(r => r.type === 'OneToOne' && !r.isInverse)
|
|
1046
|
+
.map(r => ({ targetEntityName: r.target, fieldName: r.fieldName }));
|
|
1047
|
+
|
|
1048
|
+
await renderAndWrite(
|
|
1049
|
+
path.join(templatesDir, 'CreateItemDto.java.ejs'),
|
|
1050
|
+
path.join(moduleBasePath, 'application', 'dtos', `Create${entity.name}Dto.java`),
|
|
1051
|
+
{ packageName, moduleName, entityName: entity.name, fields: createFieldsApp,
|
|
1052
|
+
nestedRelationships: entityNestedRels, hasNestedRelationships: entityNestedRels.length > 0,
|
|
1053
|
+
forwardOneToOneRels: fwdOoO,
|
|
1054
|
+
hasValueObjects: entity.fields.some(f => f.isValueObject),
|
|
1055
|
+
hasEnums: entity.enums && entity.enums.length > 0, imports: createDtoImports },
|
|
1056
|
+
writeOptions
|
|
1057
|
+
);
|
|
1058
|
+
generatedFiles.push({ type: 'DTO', name: `Create${entity.name}Dto`, path: `${moduleName}/application/dtos/Create${entity.name}Dto.java` });
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// ── Step 5: Generate declared use cases (anti-duplicate across versions) ─
|
|
1062
|
+
const commandVoDtoImports = validatedVos
|
|
1063
|
+
.filter(vo => commandFieldsApp.some(f => f.originalVoType === vo.name))
|
|
1064
|
+
.map(vo => `import ${packageName}.${moduleName}.application.dtos.Create${vo.name}Dto;`);
|
|
1065
|
+
const commandAppImports = [...new Set([
|
|
1066
|
+
...(rootEntity.imports || []), ...generateValidationImports(commandFieldsApp), ...commandVoDtoImports,
|
|
1067
|
+
...(commandFieldsApp.some(f => f.originalVoType) ? ['import jakarta.validation.Valid;'] : [])
|
|
1068
|
+
])];
|
|
1069
|
+
|
|
1070
|
+
// Pre-classify ALL operations (including cross-version duplicates) so that
|
|
1071
|
+
// enrichEndpointOperation() can read op._classification in step 6.
|
|
1072
|
+
for (const version of endpoints.versions) {
|
|
1073
|
+
for (const op of version.operations) {
|
|
1074
|
+
op._classification = classifyUseCase(op, aggregateName, aggregate);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const generatedUseCases = new Set();
|
|
1079
|
+
const findByOps = []; // collect FindBy ops for repository re-generation
|
|
1080
|
+
|
|
1081
|
+
for (const version of endpoints.versions) {
|
|
1082
|
+
for (const op of version.operations) {
|
|
1083
|
+
if (generatedUseCases.has(op.useCase)) continue; // anti-duplicate
|
|
1084
|
+
generatedUseCases.add(op.useCase);
|
|
1085
|
+
|
|
1086
|
+
const cl = op._classification;
|
|
1087
|
+
|
|
1088
|
+
if (cl.category === 'standard') {
|
|
1089
|
+
const isStandard = true;
|
|
1090
|
+
if (cl.variant === 'create') {
|
|
1091
|
+
await renderAndWrite(
|
|
1092
|
+
path.join(templatesDir, 'CreateCommand.java.ejs'),
|
|
1093
|
+
path.join(moduleBasePath, 'application', 'commands', `Create${aggregateName}Command.java`),
|
|
1094
|
+
{ ...baseContext, imports: commandAppImports }, writeOptions
|
|
1095
|
+
);
|
|
1096
|
+
generatedFiles.push({ type: 'Command', name: `Create${aggregateName}Command`, path: `${moduleName}/application/commands/Create${aggregateName}Command.java` });
|
|
1097
|
+
|
|
1098
|
+
await renderAndWrite(
|
|
1099
|
+
path.join(templatesDir, 'CreateCommandHandler.java.ejs'),
|
|
1100
|
+
path.join(moduleBasePath, 'application', 'usecases', `Create${aggregateName}CommandHandler.java`),
|
|
1101
|
+
{ ...baseContext, commandFields: commandFieldsApp, oneToOneRelationships: oneToOneRelationshipsApp, oneToManyRelationships: oneToManyRelationshipsApp, validatedVos }, writeOptions
|
|
1102
|
+
);
|
|
1103
|
+
generatedFiles.push({ type: 'Handler', name: `Create${aggregateName}CommandHandler`, path: `${moduleName}/application/usecases/Create${aggregateName}CommandHandler.java` });
|
|
1104
|
+
|
|
1105
|
+
} else if (cl.variant === 'update') {
|
|
1106
|
+
await renderAndWrite(
|
|
1107
|
+
path.join(templatesDir, 'UpdateCommand.java.ejs'),
|
|
1108
|
+
path.join(moduleBasePath, 'application', 'commands', `Update${aggregateName}Command.java`),
|
|
1109
|
+
{ ...baseContext, imports: commandAppImports }, writeOptions
|
|
1110
|
+
);
|
|
1111
|
+
generatedFiles.push({ type: 'Command', name: `Update${aggregateName}Command`, path: `${moduleName}/application/commands/Update${aggregateName}Command.java` });
|
|
1112
|
+
|
|
1113
|
+
await renderAndWrite(
|
|
1114
|
+
path.join(templatesDir, 'UpdateCommandHandler.java.ejs'),
|
|
1115
|
+
path.join(moduleBasePath, 'application', 'usecases', `Update${aggregateName}CommandHandler.java`),
|
|
1116
|
+
baseContext, writeOptions
|
|
1117
|
+
);
|
|
1118
|
+
generatedFiles.push({ type: 'Handler', name: `Update${aggregateName}CommandHandler`, path: `${moduleName}/application/usecases/Update${aggregateName}CommandHandler.java` });
|
|
1119
|
+
|
|
1120
|
+
} else if (cl.variant === 'delete') {
|
|
1121
|
+
await renderAndWrite(
|
|
1122
|
+
path.join(templatesDir, 'DeleteCommand.java.ejs'),
|
|
1123
|
+
path.join(moduleBasePath, 'application', 'commands', `Delete${aggregateName}Command.java`),
|
|
1124
|
+
baseContext, writeOptions
|
|
1125
|
+
);
|
|
1126
|
+
generatedFiles.push({ type: 'Command', name: `Delete${aggregateName}Command`, path: `${moduleName}/application/commands/Delete${aggregateName}Command.java` });
|
|
1127
|
+
|
|
1128
|
+
await renderAndWrite(
|
|
1129
|
+
path.join(templatesDir, 'DeleteCommandHandler.java.ejs'),
|
|
1130
|
+
path.join(moduleBasePath, 'application', 'usecases', `Delete${aggregateName}CommandHandler.java`),
|
|
1131
|
+
baseContext, writeOptions
|
|
1132
|
+
);
|
|
1133
|
+
generatedFiles.push({ type: 'Handler', name: `Delete${aggregateName}CommandHandler`, path: `${moduleName}/application/usecases/Delete${aggregateName}CommandHandler.java` });
|
|
1134
|
+
|
|
1135
|
+
} else if (cl.variant === 'getById') {
|
|
1136
|
+
await renderAndWrite(
|
|
1137
|
+
path.join(templatesDir, 'GetQuery.java.ejs'),
|
|
1138
|
+
path.join(moduleBasePath, 'application', 'queries', `Get${aggregateName}Query.java`),
|
|
1139
|
+
baseContext, writeOptions
|
|
1140
|
+
);
|
|
1141
|
+
generatedFiles.push({ type: 'Query', name: `Get${aggregateName}Query`, path: `${moduleName}/application/queries/Get${aggregateName}Query.java` });
|
|
1142
|
+
|
|
1143
|
+
await renderAndWrite(
|
|
1144
|
+
path.join(templatesDir, 'GetQueryHandler.java.ejs'),
|
|
1145
|
+
path.join(moduleBasePath, 'application', 'usecases', `Get${aggregateName}QueryHandler.java`),
|
|
1146
|
+
baseContext, writeOptions
|
|
1147
|
+
);
|
|
1148
|
+
generatedFiles.push({ type: 'Handler', name: `Get${aggregateName}QueryHandler`, path: `${moduleName}/application/usecases/Get${aggregateName}QueryHandler.java` });
|
|
1149
|
+
|
|
1150
|
+
} else if (cl.variant === 'findAll') {
|
|
1151
|
+
await renderAndWrite(
|
|
1152
|
+
path.join(templatesDir, 'ListQuery.java.ejs'),
|
|
1153
|
+
path.join(moduleBasePath, 'application', 'queries', `FindAll${aggregateName}sQuery.java`),
|
|
1154
|
+
baseContext, writeOptions
|
|
1155
|
+
);
|
|
1156
|
+
generatedFiles.push({ type: 'Query', name: `FindAll${aggregateName}sQuery`, path: `${moduleName}/application/queries/FindAll${aggregateName}sQuery.java` });
|
|
1157
|
+
|
|
1158
|
+
await renderAndWrite(
|
|
1159
|
+
path.join(templatesDir, 'ListQueryHandler.java.ejs'),
|
|
1160
|
+
path.join(moduleBasePath, 'application', 'usecases', `FindAll${aggregateName}sQueryHandler.java`),
|
|
1161
|
+
baseContext, writeOptions
|
|
1162
|
+
);
|
|
1163
|
+
generatedFiles.push({ type: 'Handler', name: `FindAll${aggregateName}sQueryHandler`, path: `${moduleName}/application/usecases/FindAll${aggregateName}sQueryHandler.java` });
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
} else if (cl.category === 'transition') {
|
|
1167
|
+
// Transition: {MethodPascal}{Aggregate} → findById → entity.{method}() → save
|
|
1168
|
+
const transitionContext = {
|
|
1169
|
+
packageName, moduleName, aggregateName,
|
|
1170
|
+
useCaseName: op.useCase,
|
|
1171
|
+
idType,
|
|
1172
|
+
domainMethod: cl.domainMethod
|
|
1173
|
+
};
|
|
1174
|
+
await renderAndWrite(
|
|
1175
|
+
path.join(templatesDir, 'TransitionCommand.java.ejs'),
|
|
1176
|
+
path.join(moduleBasePath, 'application', 'commands', `${op.useCase}Command.java`),
|
|
1177
|
+
transitionContext, writeOptions
|
|
1178
|
+
);
|
|
1179
|
+
generatedFiles.push({ type: 'Command', name: `${op.useCase}Command`, path: `${moduleName}/application/commands/${op.useCase}Command.java` });
|
|
1180
|
+
|
|
1181
|
+
await renderAndWrite(
|
|
1182
|
+
path.join(templatesDir, 'TransitionCommandHandler.java.ejs'),
|
|
1183
|
+
path.join(moduleBasePath, 'application', 'usecases', `${op.useCase}CommandHandler.java`),
|
|
1184
|
+
transitionContext, writeOptions
|
|
1185
|
+
);
|
|
1186
|
+
generatedFiles.push({ type: 'Handler', name: `${op.useCase}CommandHandler`, path: `${moduleName}/application/usecases/${op.useCase}CommandHandler.java` });
|
|
1187
|
+
|
|
1188
|
+
} else if (cl.category === 'subEntityAdd') {
|
|
1189
|
+
// SubEntityAdd: Add{EntityName} → findById → entity.add{Entity}(...) → save
|
|
1190
|
+
const addContext = {
|
|
1191
|
+
packageName, moduleName, aggregateName,
|
|
1192
|
+
useCaseName: op.useCase,
|
|
1193
|
+
idType,
|
|
1194
|
+
entityName: cl.entityName,
|
|
1195
|
+
entityFields: cl.entityFields,
|
|
1196
|
+
addMethodName: cl.addMethodName,
|
|
1197
|
+
imports: cl.entityImports
|
|
1198
|
+
};
|
|
1199
|
+
await renderAndWrite(
|
|
1200
|
+
path.join(templatesDir, 'SubEntityAddCommand.java.ejs'),
|
|
1201
|
+
path.join(moduleBasePath, 'application', 'commands', `${op.useCase}Command.java`),
|
|
1202
|
+
addContext, writeOptions
|
|
1203
|
+
);
|
|
1204
|
+
generatedFiles.push({ type: 'Command', name: `${op.useCase}Command`, path: `${moduleName}/application/commands/${op.useCase}Command.java` });
|
|
1205
|
+
|
|
1206
|
+
await renderAndWrite(
|
|
1207
|
+
path.join(templatesDir, 'SubEntityAddCommandHandler.java.ejs'),
|
|
1208
|
+
path.join(moduleBasePath, 'application', 'usecases', `${op.useCase}CommandHandler.java`),
|
|
1209
|
+
addContext, writeOptions
|
|
1210
|
+
);
|
|
1211
|
+
generatedFiles.push({ type: 'Handler', name: `${op.useCase}CommandHandler`, path: `${moduleName}/application/usecases/${op.useCase}CommandHandler.java` });
|
|
1212
|
+
|
|
1213
|
+
} else if (cl.category === 'subEntityRemove') {
|
|
1214
|
+
// SubEntityRemove: Remove{EntityName} → findById → entity.remove{Entity}ById(itemId) → save
|
|
1215
|
+
const removeContext = {
|
|
1216
|
+
packageName, moduleName, aggregateName,
|
|
1217
|
+
useCaseName: op.useCase,
|
|
1218
|
+
idType,
|
|
1219
|
+
entityName: cl.entityName,
|
|
1220
|
+
removeMethodName: cl.removeMethodName
|
|
1221
|
+
};
|
|
1222
|
+
await renderAndWrite(
|
|
1223
|
+
path.join(templatesDir, 'SubEntityRemoveCommand.java.ejs'),
|
|
1224
|
+
path.join(moduleBasePath, 'application', 'commands', `${op.useCase}Command.java`),
|
|
1225
|
+
removeContext, writeOptions
|
|
1226
|
+
);
|
|
1227
|
+
generatedFiles.push({ type: 'Command', name: `${op.useCase}Command`, path: `${moduleName}/application/commands/${op.useCase}Command.java` });
|
|
1228
|
+
|
|
1229
|
+
await renderAndWrite(
|
|
1230
|
+
path.join(templatesDir, 'SubEntityRemoveCommandHandler.java.ejs'),
|
|
1231
|
+
path.join(moduleBasePath, 'application', 'usecases', `${op.useCase}CommandHandler.java`),
|
|
1232
|
+
removeContext, writeOptions
|
|
1233
|
+
);
|
|
1234
|
+
generatedFiles.push({ type: 'Handler', name: `${op.useCase}CommandHandler`, path: `${moduleName}/application/usecases/${op.useCase}CommandHandler.java` });
|
|
1235
|
+
|
|
1236
|
+
} else if (cl.category === 'findBy') {
|
|
1237
|
+
// FindBy: FindAll{Aggregate}sBy{Field} → paginated query on a root field
|
|
1238
|
+
findByOps.push(cl); // collected for repository re-generation after the loop
|
|
1239
|
+
const findByContext = {
|
|
1240
|
+
packageName, moduleName, aggregateName,
|
|
1241
|
+
useCaseName: op.useCase,
|
|
1242
|
+
idType,
|
|
1243
|
+
fieldName: cl.fieldName,
|
|
1244
|
+
fieldPascal: cl.fieldPascal,
|
|
1245
|
+
fieldJavaType: cl.fieldJavaType,
|
|
1246
|
+
jpaMethodName: cl.jpaMethodName
|
|
1247
|
+
};
|
|
1248
|
+
await renderAndWrite(
|
|
1249
|
+
path.join(templatesDir, 'FindByQuery.java.ejs'),
|
|
1250
|
+
path.join(moduleBasePath, 'application', 'queries', `${op.useCase}Query.java`),
|
|
1251
|
+
findByContext, writeOptions
|
|
1252
|
+
);
|
|
1253
|
+
generatedFiles.push({ type: 'Query', name: `${op.useCase}Query`, path: `${moduleName}/application/queries/${op.useCase}Query.java` });
|
|
1254
|
+
|
|
1255
|
+
await renderAndWrite(
|
|
1256
|
+
path.join(templatesDir, 'FindByQueryHandler.java.ejs'),
|
|
1257
|
+
path.join(moduleBasePath, 'application', 'usecases', `${op.useCase}QueryHandler.java`),
|
|
1258
|
+
findByContext, writeOptions
|
|
1259
|
+
);
|
|
1260
|
+
generatedFiles.push({ type: 'Handler', name: `${op.useCase}QueryHandler`, path: `${moduleName}/application/usecases/${op.useCase}QueryHandler.java` });
|
|
1261
|
+
|
|
1262
|
+
} else {
|
|
1263
|
+
// Scaffold: no semantic pattern matched → generate stub with TODO
|
|
1264
|
+
const scaffoldContext = { packageName, moduleName, aggregateName, useCaseName: op.useCase };
|
|
1265
|
+
const scaffoldType = op.type || (op.method === 'GET' ? 'query' : 'command');
|
|
1266
|
+
if (scaffoldType === 'command') {
|
|
1267
|
+
await renderAndWrite(
|
|
1268
|
+
path.join(templatesDir, 'ScaffoldCommand.java.ejs'),
|
|
1269
|
+
path.join(moduleBasePath, 'application', 'commands', `${op.useCase}Command.java`),
|
|
1270
|
+
scaffoldContext, writeOptions
|
|
1271
|
+
);
|
|
1272
|
+
generatedFiles.push({ type: 'Command', name: `${op.useCase}Command`, path: `${moduleName}/application/commands/${op.useCase}Command.java` });
|
|
1273
|
+
|
|
1274
|
+
await renderAndWrite(
|
|
1275
|
+
path.join(templatesDir, 'ScaffoldCommandHandler.java.ejs'),
|
|
1276
|
+
path.join(moduleBasePath, 'application', 'usecases', `${op.useCase}CommandHandler.java`),
|
|
1277
|
+
scaffoldContext, writeOptions
|
|
1278
|
+
);
|
|
1279
|
+
generatedFiles.push({ type: 'Handler', name: `${op.useCase}CommandHandler`, path: `${moduleName}/application/usecases/${op.useCase}CommandHandler.java` });
|
|
1280
|
+
} else {
|
|
1281
|
+
await renderAndWrite(
|
|
1282
|
+
path.join(templatesDir, 'ScaffoldQuery.java.ejs'),
|
|
1283
|
+
path.join(moduleBasePath, 'application', 'queries', `${op.useCase}Query.java`),
|
|
1284
|
+
scaffoldContext, writeOptions
|
|
1285
|
+
);
|
|
1286
|
+
generatedFiles.push({ type: 'Query', name: `${op.useCase}Query`, path: `${moduleName}/application/queries/${op.useCase}Query.java` });
|
|
1287
|
+
|
|
1288
|
+
await renderAndWrite(
|
|
1289
|
+
path.join(templatesDir, 'ScaffoldQueryHandler.java.ejs'),
|
|
1290
|
+
path.join(moduleBasePath, 'application', 'usecases', `${op.useCase}QueryHandler.java`),
|
|
1291
|
+
scaffoldContext, writeOptions
|
|
1292
|
+
);
|
|
1293
|
+
generatedFiles.push({ type: 'Handler', name: `${op.useCase}QueryHandler`, path: `${moduleName}/application/usecases/${op.useCase}QueryHandler.java` });
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// ── Step 5b: Re-generate repository files when FindBy ops are present ────
|
|
1300
|
+
// Checksum protection still applies: manually modified files are skipped.
|
|
1301
|
+
if (findByOps.length > 0) {
|
|
1302
|
+
const aggregateTemplatesDir = path.join(__dirname, '..', '..', 'templates', 'aggregate');
|
|
1303
|
+
const repoContext = { packageName, moduleName, rootEntity, findByOps };
|
|
1304
|
+
const repoImplContext = {
|
|
1305
|
+
packageName, moduleName, aggregateName, rootEntity,
|
|
1306
|
+
hasDomainEvents: (aggregate.domainEvents || []).length > 0,
|
|
1307
|
+
findByOps
|
|
1308
|
+
};
|
|
1309
|
+
await renderAndWrite(
|
|
1310
|
+
path.join(aggregateTemplatesDir, 'AggregateRepository.java.ejs'),
|
|
1311
|
+
path.join(moduleBasePath, 'domain', 'repositories', `${rootEntity.name}Repository.java`),
|
|
1312
|
+
repoContext, writeOptions
|
|
1313
|
+
);
|
|
1314
|
+
await renderAndWrite(
|
|
1315
|
+
path.join(aggregateTemplatesDir, 'JpaRepository.java.ejs'),
|
|
1316
|
+
path.join(moduleBasePath, 'infrastructure', 'database', 'repositories', `${rootEntity.name}JpaRepository.java`),
|
|
1317
|
+
repoContext, writeOptions
|
|
1318
|
+
);
|
|
1319
|
+
await renderAndWrite(
|
|
1320
|
+
path.join(aggregateTemplatesDir, 'AggregateRepositoryImpl.java.ejs'),
|
|
1321
|
+
path.join(moduleBasePath, 'infrastructure', 'database', 'repositories', `${rootEntity.name}RepositoryImpl.java`),
|
|
1322
|
+
repoImplContext, writeOptions
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// ── Step 6: Versioned controllers ────────────────────────────────────
|
|
1327
|
+
for (const version of endpoints.versions) {
|
|
1328
|
+
const versionCap = version.version.charAt(0).toUpperCase() + version.version.slice(1);
|
|
1329
|
+
const controllerName = `${aggregateName}${versionCap}Controller`;
|
|
1330
|
+
const enrichedOps = version.operations.map(op => enrichEndpointOperation(op, aggregateName, idType));
|
|
1331
|
+
|
|
1332
|
+
const controllerContext = {
|
|
1333
|
+
...baseContext,
|
|
1334
|
+
apiVersion: version.version,
|
|
1335
|
+
controllerName,
|
|
1336
|
+
operations: enrichedOps,
|
|
1337
|
+
basePath: endpoints.basePath,
|
|
1338
|
+
commandFields: commandFieldsApp,
|
|
1339
|
+
oneToManyRelationships,
|
|
1340
|
+
oneToOneRelationships
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
await renderAndWrite(
|
|
1344
|
+
path.join(templatesDir, 'EndpointsController.java.ejs'),
|
|
1345
|
+
path.join(moduleBasePath, 'infrastructure', 'rest', 'controllers', resourceNameCamel, version.version, `${controllerName}.java`),
|
|
1346
|
+
controllerContext, writeOptions
|
|
1347
|
+
);
|
|
1348
|
+
generatedFiles.push({ type: 'Controller', name: controllerName, path: `${moduleName}/infrastructure/rest/controllers/${resourceNameCamel}/${version.version}/${controllerName}.java` });
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
689
1352
|
/**
|
|
690
1353
|
* Generate CRUD resources for an aggregate root
|
|
691
1354
|
*/
|