eva4j 1.0.11 → 1.0.13
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 +441 -14
- package/DOMAIN_YAML_GUIDE.md +425 -21
- package/FUTURE_FEATURES.md +315 -115
- package/QUICK_REFERENCE.md +101 -153
- package/README.md +77 -70
- package/bin/eva4j.js +57 -1
- package/config/defaults.json +3 -0
- package/docs/commands/GENERATE_ENTITIES.md +662 -1968
- package/docs/commands/GENERATE_HTTP_EXCHANGE.md +274 -450
- package/docs/commands/GENERATE_KAFKA_EVENT.md +219 -498
- package/docs/commands/GENERATE_KAFKA_LISTENER.md +18 -18
- package/docs/commands/GENERATE_RECORD.md +335 -311
- package/docs/commands/GENERATE_TEMPORAL_ACTIVITY.md +174 -0
- package/docs/commands/GENERATE_TEMPORAL_FLOW.md +237 -0
- package/docs/commands/GENERATE_USECASE.md +216 -282
- package/docs/commands/INDEX.md +36 -7
- package/examples/doctor-evaluation.yaml +3 -3
- package/examples/domain-audit-complete.yaml +2 -2
- package/examples/domain-collections.yaml +2 -2
- package/examples/domain-ecommerce.yaml +2 -2
- package/examples/domain-events.yaml +201 -0
- package/examples/domain-field-visibility.yaml +11 -5
- package/examples/domain-multi-aggregate.yaml +12 -6
- package/examples/domain-one-to-many.yaml +1 -1
- package/examples/domain-one-to-one.yaml +1 -1
- package/examples/domain-secondary-onetomany.yaml +1 -1
- package/examples/domain-secondary-onetoone.yaml +1 -1
- package/examples/domain-simple.yaml +1 -1
- package/examples/domain-soft-delete.yaml +3 -3
- package/examples/domain-transitions.yaml +1 -1
- package/examples/domain-value-objects.yaml +1 -1
- package/package.json +2 -2
- package/src/commands/add-kafka-client.js +3 -1
- package/src/commands/add-temporal-client.js +286 -0
- package/src/commands/generate-entities.js +75 -4
- package/src/commands/generate-kafka-event.js +273 -89
- package/src/commands/generate-temporal-activity.js +228 -0
- package/src/commands/generate-temporal-flow.js +216 -0
- package/src/generators/module-generator.js +1 -0
- package/src/generators/shared-generator.js +26 -0
- package/src/utils/yaml-to-entity.js +93 -4
- package/templates/aggregate/AggregateRepository.java.ejs +3 -2
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +15 -7
- package/templates/aggregate/AggregateRoot.java.ejs +38 -2
- package/templates/aggregate/DomainEntity.java.ejs +6 -2
- package/templates/aggregate/DomainEventHandler.java.ejs +62 -0
- package/templates/aggregate/DomainEventRecord.java.ejs +50 -0
- package/templates/aggregate/JpaAggregateRoot.java.ejs +3 -1
- package/templates/aggregate/JpaEntity.java.ejs +3 -1
- package/templates/base/docker/kafka-services.yaml.ejs +2 -2
- package/templates/base/docker/temporal-services.yaml.ejs +29 -0
- package/templates/base/resources/parameters/develop/temporal.yaml.ejs +9 -0
- package/templates/base/resources/parameters/local/temporal.yaml.ejs +9 -0
- package/templates/base/resources/parameters/production/temporal.yaml.ejs +9 -0
- package/templates/base/resources/parameters/test/temporal.yaml.ejs +9 -0
- package/templates/base/root/AGENTS.md.ejs +916 -51
- package/templates/crud/Controller.java.ejs +36 -6
- package/templates/crud/ListQuery.java.ejs +6 -2
- package/templates/crud/ListQueryHandler.java.ejs +24 -10
- package/templates/crud/UpdateCommand.java.ejs +52 -0
- package/templates/crud/UpdateCommandHandler.java.ejs +105 -0
- package/templates/kafka-event/DomainEventHandlerMethod.ejs +1 -0
- package/templates/kafka-event/Event.java.ejs +23 -0
- package/templates/shared/application/dtos/PagedResponse.java.ejs +30 -0
- package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +104 -0
- package/templates/shared/domain/DomainEvent.java.ejs +40 -0
- package/templates/shared/interfaces/HeavyActivity.java.ejs +4 -0
- package/templates/shared/interfaces/LightActivity.java.ejs +4 -0
- package/templates/temporal-activity/ActivityImpl.java.ejs +14 -0
- package/templates/temporal-activity/ActivityInterface.java.ejs +11 -0
- package/templates/temporal-flow/WorkFlowImpl.java.ejs +64 -0
- package/templates/temporal-flow/WorkFlowInterface.java.ejs +19 -0
- package/templates/temporal-flow/WorkFlowService.java.ejs +49 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
const inquirer = require('inquirer');
|
|
2
|
+
const ora = require('ora');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
|
+
const ConfigManager = require('../utils/config-manager');
|
|
7
|
+
const { isEva4jProject, moduleExists } = require('../utils/validator');
|
|
8
|
+
const { toPackagePath, toPascalCase } = require('../utils/naming');
|
|
9
|
+
const { renderAndWrite } = require('../utils/template-engine');
|
|
10
|
+
const ChecksumManager = require('../utils/checksum-manager');
|
|
11
|
+
|
|
12
|
+
async function generateTemporalFlowCommand(moduleName, flowName, options = {}) {
|
|
13
|
+
const projectDir = process.cwd();
|
|
14
|
+
|
|
15
|
+
if (!(await isEva4jProject(projectDir))) {
|
|
16
|
+
console.error(chalk.red('❌ Not in an eva4j project directory'));
|
|
17
|
+
console.error(chalk.gray('Run this command inside a project created with eva4j'));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const configManager = new ConfigManager(projectDir);
|
|
22
|
+
|
|
23
|
+
if (!(await configManager.featureExists('temporal'))) {
|
|
24
|
+
console.error(chalk.red('❌ Temporal client is not installed in this project'));
|
|
25
|
+
console.error(chalk.gray('Install Temporal first using: eva add temporal-client'));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!moduleName) {
|
|
30
|
+
const answer = await inquirer.prompt([
|
|
31
|
+
{
|
|
32
|
+
type: 'input',
|
|
33
|
+
name: 'moduleName',
|
|
34
|
+
message: 'Module name:',
|
|
35
|
+
validate: (v) => (v.trim() ? true : 'Module name is required'),
|
|
36
|
+
},
|
|
37
|
+
]);
|
|
38
|
+
moduleName = answer.moduleName.trim();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const projectConfig = await configManager.loadProjectConfig();
|
|
42
|
+
if (!projectConfig) {
|
|
43
|
+
console.error(chalk.red('❌ Could not load project configuration'));
|
|
44
|
+
console.error(chalk.gray('Make sure .eva4j.json exists in the project root'));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { packageName } = projectConfig;
|
|
49
|
+
const packagePath = toPackagePath(packageName);
|
|
50
|
+
|
|
51
|
+
if (!(await configManager.moduleExists(moduleName))) {
|
|
52
|
+
console.error(chalk.red(`❌ Module '${moduleName}' does not exist`));
|
|
53
|
+
console.error(chalk.gray(`Create it first using: eva add module ${moduleName}`));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!(await moduleExists(projectDir, packagePath, moduleName))) {
|
|
58
|
+
console.error(chalk.red(`❌ Module directory for '${moduleName}' not found`));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!flowName) {
|
|
63
|
+
const answer = await inquirer.prompt([
|
|
64
|
+
{
|
|
65
|
+
type: 'input',
|
|
66
|
+
name: 'flowName',
|
|
67
|
+
message: 'Workflow name (e.g. process-order, Payment):',
|
|
68
|
+
validate: (v) => (v.trim() ? true : 'Workflow name is required'),
|
|
69
|
+
},
|
|
70
|
+
]);
|
|
71
|
+
flowName = answer.flowName.trim();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const flowPascalCase = toPascalCase(flowName);
|
|
75
|
+
|
|
76
|
+
const moduleBasePath = path.join(
|
|
77
|
+
projectDir,
|
|
78
|
+
'src',
|
|
79
|
+
'main',
|
|
80
|
+
'java',
|
|
81
|
+
packagePath,
|
|
82
|
+
moduleName
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const checksumManager = new ChecksumManager(moduleBasePath);
|
|
86
|
+
await checksumManager.load();
|
|
87
|
+
|
|
88
|
+
const spinner = ora(`Generating ${flowPascalCase}WorkFlow...`).start();
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const context = {
|
|
92
|
+
packageName,
|
|
93
|
+
moduleName,
|
|
94
|
+
flowPascalCase,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const templatesDir = path.join(__dirname, '..', '..', 'templates', 'temporal-flow');
|
|
98
|
+
const usecasesDir = path.join(moduleBasePath, 'application', 'usecases');
|
|
99
|
+
const writeOptions = { force: options.force, checksumManager };
|
|
100
|
+
|
|
101
|
+
// 1. Generate WorkFlow interface
|
|
102
|
+
spinner.text = `Generating ${flowPascalCase}WorkFlow interface...`;
|
|
103
|
+
await renderAndWrite(
|
|
104
|
+
path.join(templatesDir, 'WorkFlowInterface.java.ejs'),
|
|
105
|
+
path.join(usecasesDir, `${flowPascalCase}WorkFlow.java`),
|
|
106
|
+
context,
|
|
107
|
+
writeOptions
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// 2. Generate WorkFlow implementation
|
|
111
|
+
spinner.text = `Generating ${flowPascalCase}WorkFlowImpl...`;
|
|
112
|
+
await renderAndWrite(
|
|
113
|
+
path.join(templatesDir, 'WorkFlowImpl.java.ejs'),
|
|
114
|
+
path.join(usecasesDir, `${flowPascalCase}WorkFlowImpl.java`),
|
|
115
|
+
context,
|
|
116
|
+
writeOptions
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// 3. Generate WorkFlow service facade
|
|
120
|
+
spinner.text = `Generating ${flowPascalCase}WorkFlowService...`;
|
|
121
|
+
await renderAndWrite(
|
|
122
|
+
path.join(templatesDir, 'WorkFlowService.java.ejs'),
|
|
123
|
+
path.join(usecasesDir, `${flowPascalCase}WorkFlowService.java`),
|
|
124
|
+
context,
|
|
125
|
+
writeOptions
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// 4. Register workflow implementation in TemporalConfig.java
|
|
129
|
+
spinner.text = 'Registering workflow in TemporalConfig...';
|
|
130
|
+
await registerWorkflowInTemporalConfig(projectDir, packagePath, packageName, moduleName, flowPascalCase);
|
|
131
|
+
|
|
132
|
+
spinner.succeed(chalk.green(`✅ ${flowPascalCase}WorkFlow generated successfully`));
|
|
133
|
+
|
|
134
|
+
console.log(chalk.blue('\n📁 Generated files:'));
|
|
135
|
+
console.log(chalk.gray(` ${moduleName}/application/usecases/${flowPascalCase}WorkFlow.java`));
|
|
136
|
+
console.log(chalk.gray(` ${moduleName}/application/usecases/${flowPascalCase}WorkFlowImpl.java`));
|
|
137
|
+
console.log(chalk.gray(` ${moduleName}/application/usecases/${flowPascalCase}WorkFlowService.java`));
|
|
138
|
+
console.log(chalk.blue('\n📝 Updated files:'));
|
|
139
|
+
console.log(chalk.gray(' shared/infrastructure/configurations/temporalConfig/TemporalConfig.java'));
|
|
140
|
+
|
|
141
|
+
await checksumManager.save();
|
|
142
|
+
} catch (error) {
|
|
143
|
+
spinner.fail(chalk.red('Failed to generate temporal flow'));
|
|
144
|
+
console.error(chalk.red(error.message));
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function registerWorkflowInTemporalConfig(projectDir, packagePath, packageName, moduleName, flowPascalCase) {
|
|
150
|
+
const configPath = path.join(
|
|
151
|
+
projectDir,
|
|
152
|
+
'src',
|
|
153
|
+
'main',
|
|
154
|
+
'java',
|
|
155
|
+
packagePath,
|
|
156
|
+
'shared',
|
|
157
|
+
'infrastructure',
|
|
158
|
+
'configurations',
|
|
159
|
+
'temporalConfig',
|
|
160
|
+
'TemporalConfig.java'
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
if (!(await fs.pathExists(configPath))) {
|
|
164
|
+
console.warn(chalk.yellow('\n⚠️ TemporalConfig.java not found — skipping auto-registration'));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let content = await fs.readFile(configPath, 'utf-8');
|
|
169
|
+
|
|
170
|
+
const implClass = `${flowPascalCase}WorkFlowImpl`;
|
|
171
|
+
const importLine = `import ${packageName}.${moduleName}.application.usecases.${implClass};`;
|
|
172
|
+
|
|
173
|
+
// Add import if not already present
|
|
174
|
+
if (!content.includes(importLine)) {
|
|
175
|
+
const importBlockMatch = content.match(/^import .+;(\r?\n)/m);
|
|
176
|
+
if (importBlockMatch) {
|
|
177
|
+
// Find position after last import line
|
|
178
|
+
const allImports = [...content.matchAll(/^import .+;/gm)];
|
|
179
|
+
if (allImports.length > 0) {
|
|
180
|
+
const lastImport = allImports[allImports.length - 1];
|
|
181
|
+
const insertPos = lastImport.index + lastImport[0].length;
|
|
182
|
+
content = content.slice(0, insertPos) + '\n' + importLine + content.slice(insertPos);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Remove TODO comment and commented-out example placeholder
|
|
188
|
+
content = content.replace(/[ \t]*\/\/ TODO: register your workflow implementation types here\r?\n/g, '');
|
|
189
|
+
content = content.replace(/[ \t]*\/\/ workflowWorker\.registerWorkflowImplementationTypes\(MyWorkflowImpl\.class\);\r?\n?/g, '');
|
|
190
|
+
|
|
191
|
+
// Check if there is already an active registerWorkflowImplementationTypes call
|
|
192
|
+
const activeRegisterRegex = /workflowWorker\.registerWorkflowImplementationTypes\(([^)]+)\);/;
|
|
193
|
+
if (activeRegisterRegex.test(content)) {
|
|
194
|
+
// Add the new class, filtering out the stale MyWorkflowImpl example if still present
|
|
195
|
+
content = content.replace(activeRegisterRegex, (match, classes) => {
|
|
196
|
+
const classList = classes
|
|
197
|
+
.split(',')
|
|
198
|
+
.map((c) => c.trim())
|
|
199
|
+
.filter((c) => c !== 'MyWorkflowImpl.class');
|
|
200
|
+
if (!classList.includes(`${implClass}.class`)) {
|
|
201
|
+
classList.push(`${implClass}.class`);
|
|
202
|
+
}
|
|
203
|
+
return `workflowWorker.registerWorkflowImplementationTypes(${classList.join(', ')});`;
|
|
204
|
+
});
|
|
205
|
+
} else {
|
|
206
|
+
// Insert active registration after the workflowWorker declaration line
|
|
207
|
+
content = content.replace(
|
|
208
|
+
/(Worker workflowWorker = workerFactory\.newWorker\([^;]+;\r?\n)/,
|
|
209
|
+
`$1 workflowWorker.registerWorkflowImplementationTypes(${implClass}.class);\n`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
await fs.writeFile(configPath, content, 'utf-8');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = generateTemporalFlowCommand;
|
|
@@ -25,6 +25,7 @@ class ModuleGenerator {
|
|
|
25
25
|
// Create domain subdirectories
|
|
26
26
|
await fs.ensureDir(path.join(moduleBasePath, 'domain', 'models', 'entities'));
|
|
27
27
|
await fs.ensureDir(path.join(moduleBasePath, 'domain', 'models', 'valueObjects'));
|
|
28
|
+
await fs.ensureDir(path.join(moduleBasePath, 'domain', 'models', 'events'));
|
|
28
29
|
await fs.ensureDir(path.join(moduleBasePath, 'domain', 'repositories'));
|
|
29
30
|
await fs.ensureDir(path.join(moduleBasePath, 'domain', 'services'));
|
|
30
31
|
|
|
@@ -67,6 +67,32 @@ class SharedGenerator {
|
|
|
67
67
|
await fs.ensureDir(eventsPath);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
async generatePagedResponse(basePath) {
|
|
71
|
+
const dtosPath = path.join(basePath, 'application', 'dtos');
|
|
72
|
+
const destPath = path.join(dtosPath, 'PagedResponse.java');
|
|
73
|
+
|
|
74
|
+
// Only generate if it doesn't exist yet
|
|
75
|
+
if (await fs.pathExists(destPath)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await fs.ensureDir(dtosPath);
|
|
80
|
+
await this.generateFile('application/dtos/PagedResponse.java.ejs', destPath);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async generateDomainEvent(basePath) {
|
|
84
|
+
const domainPath = path.join(basePath, 'domain');
|
|
85
|
+
const destPath = path.join(domainPath, 'DomainEvent.java');
|
|
86
|
+
|
|
87
|
+
// Only generate if it doesn't exist yet
|
|
88
|
+
if (await fs.pathExists(destPath)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await fs.ensureDir(domainPath);
|
|
93
|
+
await this.generateFile('domain/DomainEvent.java.ejs', destPath);
|
|
94
|
+
}
|
|
95
|
+
|
|
70
96
|
async generatePackageInfo(basePath) {
|
|
71
97
|
await this.generateFile('package-info.java.ejs',
|
|
72
98
|
path.join(basePath, 'package-info.java'));
|
|
@@ -36,7 +36,7 @@ async function parseDomainYaml(yamlPath, packageName = '', moduleName = '') {
|
|
|
36
36
|
* @returns {Object} Parsed aggregate with entities and value objects
|
|
37
37
|
*/
|
|
38
38
|
function parseAggregate(aggregateData) {
|
|
39
|
-
const { name, entities = [], valueObjects = [], enums = [], packageName = '', moduleName = '' } = aggregateData;
|
|
39
|
+
const { name, entities = [], valueObjects = [], enums = [], events = [], packageName = '', moduleName = '' } = aggregateData;
|
|
40
40
|
|
|
41
41
|
// Find the aggregate root
|
|
42
42
|
const rootEntity = entities.find(e => e.isRoot === true);
|
|
@@ -72,6 +72,17 @@ function parseAggregate(aggregateData) {
|
|
|
72
72
|
const allRootImports = new Set([...parsedRoot.imports, ...methodImports]);
|
|
73
73
|
parsedRoot.imports = Array.from(allRootImports).sort();
|
|
74
74
|
|
|
75
|
+
// Parse domain events declared at aggregate level
|
|
76
|
+
const domainEvents = events.map(event => {
|
|
77
|
+
const eventName = toPascalCase(event.name);
|
|
78
|
+
const eventFields = (event.fields || []).map(f => parseProperty(f, aggregateEnums, valueObjectNames));
|
|
79
|
+
return {
|
|
80
|
+
name: eventName,
|
|
81
|
+
fieldName: toCamelCase(eventName),
|
|
82
|
+
fields: eventFields
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
|
|
75
86
|
return {
|
|
76
87
|
name: toPascalCase(name),
|
|
77
88
|
packageName: aggregateData.package || '',
|
|
@@ -79,7 +90,8 @@ function parseAggregate(aggregateData) {
|
|
|
79
90
|
secondaryEntities,
|
|
80
91
|
valueObjects: parsedValueObjects,
|
|
81
92
|
aggregateMethods,
|
|
82
|
-
allEntities: parsedEntities
|
|
93
|
+
allEntities: parsedEntities,
|
|
94
|
+
domainEvents
|
|
83
95
|
};
|
|
84
96
|
}
|
|
85
97
|
|
|
@@ -230,8 +242,80 @@ function buildAnnotationString(validation) {
|
|
|
230
242
|
return params.length === 0 ? `@${type}` : `@${type}(${params.join(', ')})`;
|
|
231
243
|
}
|
|
232
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Compute the Java literal for a defaultValue declared in domain.yaml.
|
|
247
|
+
* Returns a string ready to be emitted in a template (e.g. `0`, `"foo"`, `Status.ACTIVE`).
|
|
248
|
+
* Returns null for unsupported combinations — callers should guard before emitting.
|
|
249
|
+
*
|
|
250
|
+
* @param {*} rawValue - The raw value from YAML (string, number, boolean)
|
|
251
|
+
* @param {string} javaType - The resolved Java type (e.g. "String", "BigDecimal")
|
|
252
|
+
* @param {boolean} isEnum - Whether the field type is an enum
|
|
253
|
+
* @returns {string|null}
|
|
254
|
+
*/
|
|
255
|
+
function computeJavaDefaultValue(rawValue, javaType, isEnum) {
|
|
256
|
+
if (rawValue === null || rawValue === undefined) return null;
|
|
257
|
+
|
|
258
|
+
const strValue = String(rawValue);
|
|
259
|
+
|
|
260
|
+
// Enum type: emit EnumType.VALUE
|
|
261
|
+
if (isEnum) {
|
|
262
|
+
return `${javaType}.${strValue}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
switch (javaType) {
|
|
266
|
+
case 'String':
|
|
267
|
+
return `"${strValue}"`;
|
|
268
|
+
case 'Boolean':
|
|
269
|
+
return strValue.toLowerCase() === 'true' ? 'true' : 'false';
|
|
270
|
+
case 'Integer':
|
|
271
|
+
case 'int':
|
|
272
|
+
return strValue;
|
|
273
|
+
case 'Long':
|
|
274
|
+
case 'long':
|
|
275
|
+
return `${strValue}L`;
|
|
276
|
+
case 'Double':
|
|
277
|
+
case 'double':
|
|
278
|
+
case 'Float':
|
|
279
|
+
case 'float':
|
|
280
|
+
return strValue;
|
|
281
|
+
case 'BigDecimal':
|
|
282
|
+
return `new BigDecimal("${strValue}")`;
|
|
283
|
+
case 'LocalDateTime':
|
|
284
|
+
if (strValue === 'now') return 'LocalDateTime.now()';
|
|
285
|
+
return null; // arbitrary datetime strings not supported
|
|
286
|
+
case 'LocalDate':
|
|
287
|
+
if (strValue === 'now') return 'LocalDate.now()';
|
|
288
|
+
return null;
|
|
289
|
+
case 'LocalTime':
|
|
290
|
+
if (strValue === 'now') return 'LocalTime.now()';
|
|
291
|
+
return null;
|
|
292
|
+
case 'Instant':
|
|
293
|
+
if (strValue === 'now') return 'Instant.now()';
|
|
294
|
+
return null;
|
|
295
|
+
case 'UUID':
|
|
296
|
+
if (strValue === 'random') return 'UUID.randomUUID()';
|
|
297
|
+
return null;
|
|
298
|
+
default:
|
|
299
|
+
// Unknown type — cannot safely emit a literal
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
233
304
|
function parseProperty(propData, valueObjectNames = [], aggregateEnums = []) {
|
|
234
|
-
const { name, type, annotations = [], isValueObject = false, isEmbedded = false, enumValues, readOnly = false, hidden = false, validations = [] } = propData;
|
|
305
|
+
const { name, type, annotations = [], isValueObject = false, isEmbedded = false, enumValues, readOnly = false, hidden = false, validations = [], reference = null, defaultValue = null } = propData;
|
|
306
|
+
|
|
307
|
+
if (defaultValue !== null && !readOnly) {
|
|
308
|
+
console.warn(`⚠️ Field "${name}": "defaultValue" is only meaningful for readOnly fields. It will be ignored since readOnly is not set.`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (reference !== null) {
|
|
312
|
+
if (typeof reference !== 'object' || !reference.aggregate || typeof reference.aggregate !== 'string') {
|
|
313
|
+
throw new Error(`Field "${name}": "reference" must be an object with at least "aggregate" (string). Example:\n reference:\n aggregate: Customer\n module: customers`);
|
|
314
|
+
}
|
|
315
|
+
if (reference.module !== undefined && typeof reference.module !== 'string') {
|
|
316
|
+
throw new Error(`Field "${name}": "reference.module" must be a string.`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
235
319
|
|
|
236
320
|
const javaType = mapYamlTypeToJava(type, enumValues);
|
|
237
321
|
const fieldName = toCamelCase(name);
|
|
@@ -286,7 +370,12 @@ function parseProperty(propData, valueObjectNames = [], aggregateEnums = []) {
|
|
|
286
370
|
validationAnnotations: validations.map(v => buildAnnotationString(v)),
|
|
287
371
|
transitionMeta,
|
|
288
372
|
autoInit,
|
|
289
|
-
autoInitValue
|
|
373
|
+
autoInitValue,
|
|
374
|
+
reference,
|
|
375
|
+
defaultValue,
|
|
376
|
+
javaDefaultValue: (readOnly && !autoInit && defaultValue !== null)
|
|
377
|
+
? computeJavaDefaultValue(defaultValue, javaType, !!enumValues || isEnumType)
|
|
378
|
+
: null
|
|
290
379
|
};
|
|
291
380
|
}
|
|
292
381
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
package <%= packageName %>.<%= moduleName %>.domain.repositories;
|
|
2
2
|
|
|
3
3
|
import <%= packageName %>.<%= moduleName %>.domain.models.entities.<%= rootEntity.name %>;
|
|
4
|
-
import
|
|
4
|
+
import org.springframework.data.domain.Page;
|
|
5
|
+
import org.springframework.data.domain.Pageable;
|
|
5
6
|
import java.util.Optional;
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -14,7 +15,7 @@ public interface <%= rootEntity.name %>Repository {
|
|
|
14
15
|
|
|
15
16
|
Optional<<%= rootEntity.name %>> findById(<%= rootEntity.fields[0].javaType %> id);
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
Page<<%= rootEntity.name %>> findAll(Pageable pageable);
|
|
18
19
|
|
|
19
20
|
void deleteById(<%= rootEntity.fields[0].javaType %> id);
|
|
20
21
|
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
package <%= packageName %>.<%= moduleName %>.infrastructure.database.repositories;
|
|
2
2
|
|
|
3
|
+
import org.springframework.data.domain.Page;
|
|
4
|
+
import org.springframework.data.domain.Pageable;
|
|
3
5
|
import org.springframework.stereotype.Repository;
|
|
6
|
+
<% if (hasDomainEvents) { %>
|
|
7
|
+
import org.springframework.context.ApplicationEventPublisher;
|
|
8
|
+
<% } %>
|
|
4
9
|
import lombok.RequiredArgsConstructor;
|
|
5
10
|
import <%= packageName %>.<%= moduleName %>.domain.models.entities.<%= rootEntity.name %>;
|
|
6
11
|
import <%= packageName %>.<%= moduleName %>.domain.repositories.<%= rootEntity.name %>Repository;
|
|
7
12
|
import <%= packageName %>.<%= moduleName %>.infrastructure.database.entities.<%= rootEntity.name %>Jpa;
|
|
8
13
|
import <%= packageName %>.<%= moduleName %>.infrastructure.database.mappers.<%= aggregateName %>Mapper;
|
|
9
|
-
import java.util.List;
|
|
10
14
|
import java.util.Optional;
|
|
11
|
-
import java.util.stream.Collectors;
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* <%= rootEntity.name %>RepositoryImpl
|
|
@@ -20,11 +23,17 @@ public class <%= rootEntity.name %>RepositoryImpl implements <%= rootEntity.name
|
|
|
20
23
|
|
|
21
24
|
private final <%= rootEntity.name %>JpaRepository jpaRepository;
|
|
22
25
|
private final <%= aggregateName %>Mapper mapper;
|
|
26
|
+
<% if (hasDomainEvents) { %>
|
|
27
|
+
private final ApplicationEventPublisher eventPublisher;
|
|
28
|
+
<% } %>
|
|
23
29
|
|
|
24
30
|
@Override
|
|
25
31
|
public <%= rootEntity.name %> save(<%= rootEntity.name %> <%= rootEntity.fieldName %>) {
|
|
26
32
|
<%= rootEntity.name %>Jpa jpa = mapper.toJpa(<%= rootEntity.fieldName %>);
|
|
27
33
|
<%= rootEntity.name %>Jpa saved = jpaRepository.save(jpa);
|
|
34
|
+
<% if (hasDomainEvents) { %>
|
|
35
|
+
<%= rootEntity.fieldName %>.pullDomainEvents().forEach(eventPublisher::publishEvent);
|
|
36
|
+
<% } %>
|
|
28
37
|
return mapper.toDomain(saved);
|
|
29
38
|
}
|
|
30
39
|
|
|
@@ -33,12 +42,11 @@ public class <%= rootEntity.name %>RepositoryImpl implements <%= rootEntity.name
|
|
|
33
42
|
return jpaRepository.findById(id)
|
|
34
43
|
.map(mapper::toDomain);
|
|
35
44
|
}
|
|
36
|
-
|
|
45
|
+
|
|
37
46
|
@Override
|
|
38
|
-
public
|
|
39
|
-
return jpaRepository.findAll()
|
|
40
|
-
.map(mapper::toDomain)
|
|
41
|
-
.collect(Collectors.toList());
|
|
47
|
+
public Page<<%= rootEntity.name %>> findAll(Pageable pageable) {
|
|
48
|
+
return jpaRepository.findAll(pageable)
|
|
49
|
+
.map(mapper::toDomain);
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
@Override
|
|
@@ -12,6 +12,12 @@ import <%= packageName %>.<%= moduleName %>.domain.models.enums.*;
|
|
|
12
12
|
<% if (fields.some(f => f.transitionMeta && f.transitionMeta.transitions.some(t => t.guard))) { %>
|
|
13
13
|
import <%= packageName %>.shared.domain.customExceptions.BusinessException;
|
|
14
14
|
<% } %>
|
|
15
|
+
<% if (domainEvents && domainEvents.length > 0) { %>
|
|
16
|
+
import <%= packageName %>.shared.domain.DomainEvent;
|
|
17
|
+
<% domainEvents.forEach(event => { %>
|
|
18
|
+
import <%= packageName %>.<%= moduleName %>.domain.models.events.<%= event.name %>;
|
|
19
|
+
<% }); %>
|
|
20
|
+
<% } %>
|
|
15
21
|
import java.util.ArrayList;
|
|
16
22
|
import java.util.Collections;
|
|
17
23
|
import java.util.List;
|
|
@@ -21,11 +27,37 @@ import java.util.List;
|
|
|
21
27
|
* Domain entity (pure Java class without Lombok)
|
|
22
28
|
*/
|
|
23
29
|
public class <%= name %> {
|
|
30
|
+
<% if (domainEvents && domainEvents.length > 0) { %>
|
|
31
|
+
|
|
32
|
+
// ─── Domain Events ──────────────────────────────────────────────────────────
|
|
33
|
+
private final List<DomainEvent> _domainEvents = new ArrayList<>();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Registers a domain event to be published after successful persistence.
|
|
37
|
+
* Call this inside business methods when something significant happens.
|
|
38
|
+
*/
|
|
39
|
+
protected void raise(DomainEvent event) {
|
|
40
|
+
_domainEvents.add(event);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns all pending domain events and clears the internal list (atomic).
|
|
45
|
+
* Called by the repository implementation after jpaRepository.save().
|
|
46
|
+
*/
|
|
47
|
+
public List<DomainEvent> pullDomainEvents() {
|
|
48
|
+
List<DomainEvent> events = Collections.unmodifiableList(new ArrayList<>(_domainEvents));
|
|
49
|
+
_domainEvents.clear();
|
|
50
|
+
return events;
|
|
51
|
+
}
|
|
52
|
+
<% } %>
|
|
24
53
|
|
|
25
54
|
<% fields.forEach(field => { %>
|
|
26
55
|
<% if (field.isCollection) { %>
|
|
27
56
|
private <%- field.javaType %> <%= field.name %> = new ArrayList<>();
|
|
28
57
|
<% } else { %>
|
|
58
|
+
<% if (field.reference) { %>
|
|
59
|
+
/** Cross-aggregate reference → <%= field.reference.aggregate %><% if (field.reference.module) { %> (module: <%= field.reference.module %>)<% } %> */
|
|
60
|
+
<% } %>
|
|
29
61
|
private <%- field.javaType %> <%= field.name %>;
|
|
30
62
|
<% } %>
|
|
31
63
|
<% }); %>
|
|
@@ -52,7 +84,8 @@ public class <%= name %> {
|
|
|
52
84
|
|
|
53
85
|
<% const creationFields = fields.filter(f => f.name !== 'id' && f.name !== 'createdAt' && f.name !== 'updatedAt' && f.name !== 'createdBy' && f.name !== 'updatedBy' && !f.readOnly && !f.autoInit); %>
|
|
54
86
|
<% const autoInitFields = fields.filter(f => f.autoInit); %>
|
|
55
|
-
<%
|
|
87
|
+
<% const defaultValueFields = fields.filter(f => f.readOnly && !f.autoInit && f.javaDefaultValue); %>
|
|
88
|
+
<% if (creationFields.length > 0 || autoInitFields.length > 0 || defaultValueFields.length > 0) { %>
|
|
56
89
|
// Constructor for new entity creation (without id, audit fields, readOnly and auto-initialized fields)
|
|
57
90
|
public <%= name %>(<% let paramIdx = 0; %><% creationFields.forEach((field, idx) => { %><% if (paramIdx > 0) { %>, <% } %><%- field.javaType %> <%= field.name %><% paramIdx++; %><% }); %>) {
|
|
58
91
|
<% creationFields.forEach(field => { %>
|
|
@@ -60,6 +93,9 @@ public class <%= name %> {
|
|
|
60
93
|
<% }); %>
|
|
61
94
|
<% autoInitFields.forEach(field => { %>
|
|
62
95
|
this.<%= field.name %> = <%- field.javaType %>.<%= field.autoInitValue %>;
|
|
96
|
+
<% }); %>
|
|
97
|
+
<% defaultValueFields.forEach(field => { %>
|
|
98
|
+
this.<%= field.name %> = <%- field.javaDefaultValue %>;
|
|
63
99
|
<% }); %>
|
|
64
100
|
}
|
|
65
101
|
<% } %>
|
|
@@ -134,7 +170,7 @@ public class <%= name %> {
|
|
|
134
170
|
<% transitionFields.forEach(field => { %>
|
|
135
171
|
<% const meta = field.transitionMeta; %>
|
|
136
172
|
<% const methodMap = new Map(); %>
|
|
137
|
-
<% meta.transitions.forEach(t => { if (!methodMap.has(t.method)) methodMap.set(t.method, t); }); %>
|
|
173
|
+
<% meta.transitions.forEach(t => { if (t.method && !methodMap.has(t.method)) methodMap.set(t.method, t); }); %>
|
|
138
174
|
<% methodMap.forEach((transition, methodName) => { %>
|
|
139
175
|
<% const froms = Array.isArray(transition.from) ? transition.from : [transition.from]; %>
|
|
140
176
|
|
|
@@ -51,7 +51,8 @@ public class <%= name %> {
|
|
|
51
51
|
|
|
52
52
|
<% const creationFields = fields.filter(f => f.name !== 'id' && f.name !== 'createdAt' && f.name !== 'updatedAt' && f.name !== 'createdBy' && f.name !== 'updatedBy' && !f.readOnly && !f.autoInit); %>
|
|
53
53
|
<% const autoInitFields = fields.filter(f => f.autoInit); %>
|
|
54
|
-
<%
|
|
54
|
+
<% const defaultValueFields = fields.filter(f => f.readOnly && !f.autoInit && f.javaDefaultValue); %>
|
|
55
|
+
<% if (creationFields.length > 0 || autoInitFields.length > 0 || defaultValueFields.length > 0) { %>
|
|
55
56
|
// Constructor for new entity creation (without id, audit fields, readOnly and auto-initialized fields)
|
|
56
57
|
public <%= name %>(<% let paramIdx = 0; %><% creationFields.forEach((field, idx) => { %><% if (paramIdx > 0) { %>, <% } %><%- field.javaType %> <%= field.name %><% paramIdx++; %><% }); %>) {
|
|
57
58
|
<% creationFields.forEach(field => { %>
|
|
@@ -59,6 +60,9 @@ public class <%= name %> {
|
|
|
59
60
|
<% }); %>
|
|
60
61
|
<% autoInitFields.forEach(field => { %>
|
|
61
62
|
this.<%= field.name %> = <%- field.javaType %>.<%= field.autoInitValue %>;
|
|
63
|
+
<% }); %>
|
|
64
|
+
<% defaultValueFields.forEach(field => { %>
|
|
65
|
+
this.<%= field.name %> = <%- field.javaDefaultValue %>;
|
|
62
66
|
<% }); %>
|
|
63
67
|
}
|
|
64
68
|
<% } %>
|
|
@@ -136,7 +140,7 @@ public class <%= name %> {
|
|
|
136
140
|
<% transitionFields.forEach(field => { %>
|
|
137
141
|
<% const meta = field.transitionMeta; %>
|
|
138
142
|
<% const methodMap = new Map(); %>
|
|
139
|
-
<% meta.transitions.forEach(t => { if (!methodMap.has(t.method)) methodMap.set(t.method, t); }); %>
|
|
143
|
+
<% meta.transitions.forEach(t => { if (t.method && !methodMap.has(t.method)) methodMap.set(t.method, t); }); %>
|
|
140
144
|
<% methodMap.forEach((transition, methodName) => { %>
|
|
141
145
|
<% const froms = Array.isArray(transition.from) ? transition.from : [transition.from]; %>
|
|
142
146
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
package <%= packageName %>.<%= moduleName %>.application.usecases;
|
|
2
|
+
|
|
3
|
+
import <%= packageName %>.shared.domain.annotations.ApplicationComponent;
|
|
4
|
+
import org.springframework.transaction.event.TransactionPhase;
|
|
5
|
+
import org.springframework.transaction.event.TransactionalEventListener;
|
|
6
|
+
<% domainEvents.forEach(event => { %>
|
|
7
|
+
import <%= packageName %>.<%= moduleName %>.domain.models.events.<%= event.name %>;
|
|
8
|
+
<% }); %>
|
|
9
|
+
<% if (hasKafkaEvents) { %>
|
|
10
|
+
import <%= packageName %>.<%= moduleName %>.domain.repositories.MessageBroker;
|
|
11
|
+
<% } %>
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* <%= aggregateName %>DomainEventHandler — Domain Event Bridge
|
|
15
|
+
*
|
|
16
|
+
* This handler connects the internal Spring event bus (ApplicationEventPublisher)
|
|
17
|
+
* with the external messaging port (MessageBroker).
|
|
18
|
+
*
|
|
19
|
+
* Architecture:
|
|
20
|
+
* AggregateRepositoryImpl.save()
|
|
21
|
+
* → eventPublisher.publishEvent(domainEvent) [internal Spring bus]
|
|
22
|
+
* → @TransactionalEventListener(AFTER_COMMIT) [this class]
|
|
23
|
+
* → messageBroker.send*(event) [port — broker-agnostic]
|
|
24
|
+
*
|
|
25
|
+
* AFTER_COMMIT ensures the external event is only published if the
|
|
26
|
+
* database transaction committed successfully, preventing ghost events
|
|
27
|
+
* from rolled-back operations.
|
|
28
|
+
*
|
|
29
|
+
* To switch brokers (Kafka → RabbitMQ → SNS): change only the MessageBroker
|
|
30
|
+
* adapter in infrastructure/. This class never needs modification.
|
|
31
|
+
*/
|
|
32
|
+
@ApplicationComponent
|
|
33
|
+
public class <%= aggregateName %>DomainEventHandler {
|
|
34
|
+
<% if (hasKafkaEvents) { %>
|
|
35
|
+
|
|
36
|
+
private final MessageBroker messageBroker;
|
|
37
|
+
|
|
38
|
+
public <%= aggregateName %>DomainEventHandler(MessageBroker messageBroker) {
|
|
39
|
+
this.messageBroker = messageBroker;
|
|
40
|
+
}
|
|
41
|
+
<% } %>
|
|
42
|
+
<% domainEvents.forEach(event => { %>
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Handles {@link <%= event.name %>} after the wrapping transaction commits.
|
|
46
|
+
* <% if (!event.kafka) { %>
|
|
47
|
+
* TODO: Implement the side effect for this event (e.g., send notification,
|
|
48
|
+
* update a read model, trigger a saga step, etc.).
|
|
49
|
+
* <% } %>
|
|
50
|
+
*/
|
|
51
|
+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
|
52
|
+
public void on<%= event.name %>(<%= event.name %> event) {
|
|
53
|
+
<% if (event.kafka) { %>
|
|
54
|
+
messageBroker.send<%= event.name %>(event);
|
|
55
|
+
<% } else { %>
|
|
56
|
+
// TODO: handle <%= event.name %> — add your side-effect logic here
|
|
57
|
+
// e.g.: notificationService.notify(event);
|
|
58
|
+
// readModelUpdater.on(event);
|
|
59
|
+
<% } %>
|
|
60
|
+
}
|
|
61
|
+
<% }); %>
|
|
62
|
+
}
|