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.
Files changed (73) hide show
  1. package/AGENTS.md +441 -14
  2. package/DOMAIN_YAML_GUIDE.md +425 -21
  3. package/FUTURE_FEATURES.md +315 -115
  4. package/QUICK_REFERENCE.md +101 -153
  5. package/README.md +77 -70
  6. package/bin/eva4j.js +57 -1
  7. package/config/defaults.json +3 -0
  8. package/docs/commands/GENERATE_ENTITIES.md +662 -1968
  9. package/docs/commands/GENERATE_HTTP_EXCHANGE.md +274 -450
  10. package/docs/commands/GENERATE_KAFKA_EVENT.md +219 -498
  11. package/docs/commands/GENERATE_KAFKA_LISTENER.md +18 -18
  12. package/docs/commands/GENERATE_RECORD.md +335 -311
  13. package/docs/commands/GENERATE_TEMPORAL_ACTIVITY.md +174 -0
  14. package/docs/commands/GENERATE_TEMPORAL_FLOW.md +237 -0
  15. package/docs/commands/GENERATE_USECASE.md +216 -282
  16. package/docs/commands/INDEX.md +36 -7
  17. package/examples/doctor-evaluation.yaml +3 -3
  18. package/examples/domain-audit-complete.yaml +2 -2
  19. package/examples/domain-collections.yaml +2 -2
  20. package/examples/domain-ecommerce.yaml +2 -2
  21. package/examples/domain-events.yaml +201 -0
  22. package/examples/domain-field-visibility.yaml +11 -5
  23. package/examples/domain-multi-aggregate.yaml +12 -6
  24. package/examples/domain-one-to-many.yaml +1 -1
  25. package/examples/domain-one-to-one.yaml +1 -1
  26. package/examples/domain-secondary-onetomany.yaml +1 -1
  27. package/examples/domain-secondary-onetoone.yaml +1 -1
  28. package/examples/domain-simple.yaml +1 -1
  29. package/examples/domain-soft-delete.yaml +3 -3
  30. package/examples/domain-transitions.yaml +1 -1
  31. package/examples/domain-value-objects.yaml +1 -1
  32. package/package.json +2 -2
  33. package/src/commands/add-kafka-client.js +3 -1
  34. package/src/commands/add-temporal-client.js +286 -0
  35. package/src/commands/generate-entities.js +75 -4
  36. package/src/commands/generate-kafka-event.js +273 -89
  37. package/src/commands/generate-temporal-activity.js +228 -0
  38. package/src/commands/generate-temporal-flow.js +216 -0
  39. package/src/generators/module-generator.js +1 -0
  40. package/src/generators/shared-generator.js +26 -0
  41. package/src/utils/yaml-to-entity.js +93 -4
  42. package/templates/aggregate/AggregateRepository.java.ejs +3 -2
  43. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +15 -7
  44. package/templates/aggregate/AggregateRoot.java.ejs +38 -2
  45. package/templates/aggregate/DomainEntity.java.ejs +6 -2
  46. package/templates/aggregate/DomainEventHandler.java.ejs +62 -0
  47. package/templates/aggregate/DomainEventRecord.java.ejs +50 -0
  48. package/templates/aggregate/JpaAggregateRoot.java.ejs +3 -1
  49. package/templates/aggregate/JpaEntity.java.ejs +3 -1
  50. package/templates/base/docker/kafka-services.yaml.ejs +2 -2
  51. package/templates/base/docker/temporal-services.yaml.ejs +29 -0
  52. package/templates/base/resources/parameters/develop/temporal.yaml.ejs +9 -0
  53. package/templates/base/resources/parameters/local/temporal.yaml.ejs +9 -0
  54. package/templates/base/resources/parameters/production/temporal.yaml.ejs +9 -0
  55. package/templates/base/resources/parameters/test/temporal.yaml.ejs +9 -0
  56. package/templates/base/root/AGENTS.md.ejs +916 -51
  57. package/templates/crud/Controller.java.ejs +36 -6
  58. package/templates/crud/ListQuery.java.ejs +6 -2
  59. package/templates/crud/ListQueryHandler.java.ejs +24 -10
  60. package/templates/crud/UpdateCommand.java.ejs +52 -0
  61. package/templates/crud/UpdateCommandHandler.java.ejs +105 -0
  62. package/templates/kafka-event/DomainEventHandlerMethod.ejs +1 -0
  63. package/templates/kafka-event/Event.java.ejs +23 -0
  64. package/templates/shared/application/dtos/PagedResponse.java.ejs +30 -0
  65. package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +104 -0
  66. package/templates/shared/domain/DomainEvent.java.ejs +40 -0
  67. package/templates/shared/interfaces/HeavyActivity.java.ejs +4 -0
  68. package/templates/shared/interfaces/LightActivity.java.ejs +4 -0
  69. package/templates/temporal-activity/ActivityImpl.java.ejs +14 -0
  70. package/templates/temporal-activity/ActivityInterface.java.ejs +11 -0
  71. package/templates/temporal-flow/WorkFlowImpl.java.ejs +64 -0
  72. package/templates/temporal-flow/WorkFlowInterface.java.ejs +19 -0
  73. 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 java.util.List;
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
- List<<%= rootEntity.name %>> findAll();
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 List<<%= rootEntity.name %>> findAll() {
39
- return jpaRepository.findAll().stream()
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
- <% if (creationFields.length > 0 || autoInitFields.length > 0) { %>
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
- <% if (creationFields.length > 0 || autoInitFields.length > 0) { %>
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
+ }