eva4j 1.0.16 → 1.0.18

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 (151) hide show
  1. package/AGENTS.md +220 -5
  2. package/DOMAIN_YAML_GUIDE.md +188 -3
  3. package/FUTURE_FEATURES.md +33 -52
  4. package/QUICK_REFERENCE.md +8 -4
  5. package/bin/eva4j.js +70 -2
  6. package/config/defaults.json +1 -0
  7. package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
  8. package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
  9. package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
  10. package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
  11. package/docs/commands/EVALUATE_SYSTEM.md +290 -10
  12. package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
  13. package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
  14. package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
  15. package/docs/commands/INDEX.md +27 -3
  16. package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
  17. package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
  18. package/docs/prototype/system/RISKS.md +277 -0
  19. package/docs/prototype/system/customers.yaml +133 -0
  20. package/docs/prototype/system/inventory.yaml +109 -0
  21. package/docs/prototype/system/notifications.yaml +131 -0
  22. package/docs/prototype/system/orders.yaml +241 -0
  23. package/docs/prototype/system/payments.yaml +256 -0
  24. package/docs/prototype/system/products.yaml +168 -0
  25. package/docs/prototype/system/system.yaml +269 -0
  26. package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
  27. package/examples/domain-events.yaml +26 -0
  28. package/examples/domain-read-models.yaml +113 -0
  29. package/examples/system/customer.yaml +89 -0
  30. package/examples/system/orders.yaml +119 -0
  31. package/examples/system/product.yaml +27 -0
  32. package/examples/system/system.yaml +80 -0
  33. package/package.json +1 -1
  34. package/read-model-spec.md +664 -0
  35. package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
  36. package/src/agents/design-gap-analyst.agent.md +383 -0
  37. package/src/agents/design-reviewer-temporal.agent.md +412 -0
  38. package/src/agents/design-reviewer.agent.md +34 -5
  39. package/src/agents/implement-use-cases.prompt.md +179 -0
  40. package/src/agents/ux-gap-analyst.agent.md +412 -0
  41. package/src/commands/add-rabbitmq-client.js +261 -0
  42. package/src/commands/add-temporal-client.js +22 -2
  43. package/src/commands/build.js +267 -11
  44. package/src/commands/evaluate-system.js +700 -13
  45. package/src/commands/generate-entities.js +560 -24
  46. package/src/commands/generate-http-exchange.js +3 -0
  47. package/src/commands/generate-kafka-event.js +3 -0
  48. package/src/commands/generate-kafka-listener.js +3 -0
  49. package/src/commands/generate-rabbitmq-event.js +665 -0
  50. package/src/commands/generate-rabbitmq-listener.js +205 -0
  51. package/src/commands/generate-record.js +2 -2
  52. package/src/commands/generate-resource.js +4 -1
  53. package/src/commands/generate-temporal-activity.js +970 -33
  54. package/src/commands/generate-temporal-flow.js +98 -38
  55. package/src/commands/generate-temporal-system.js +708 -0
  56. package/src/commands/generate-usecase.js +4 -1
  57. package/src/skills/build-system-yaml/SKILL.md +343 -2
  58. package/src/skills/build-system-yaml/references/domain-yaml-spec.md +253 -26
  59. package/src/skills/build-system-yaml/references/module-spec.md +90 -9
  60. package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
  61. package/src/skills/build-temporal-system/SKILL.md +752 -0
  62. package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
  63. package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
  64. package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
  65. package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
  66. package/src/skills/implement-use-case/SKILL.md +350 -0
  67. package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
  68. package/src/skills/requirements-elicitation/SKILL.md +228 -0
  69. package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
  70. package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
  71. package/src/utils/bounded-context-diagram.js +844 -0
  72. package/src/utils/config-manager.js +4 -2
  73. package/src/utils/domain-validator.js +495 -17
  74. package/src/utils/naming.js +20 -0
  75. package/src/utils/system-validator.js +169 -11
  76. package/src/utils/system-yaml-parser.js +318 -0
  77. package/src/utils/temporal-validator.js +497 -0
  78. package/src/utils/validator.js +3 -1
  79. package/src/utils/yaml-to-entity.js +281 -9
  80. package/templates/aggregate/AggregateRepository.java.ejs +4 -0
  81. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +8 -0
  82. package/templates/aggregate/AggregateRoot.java.ejs +38 -4
  83. package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
  84. package/templates/aggregate/JpaAggregateRoot.java.ejs +4 -4
  85. package/templates/aggregate/JpaEntity.java.ejs +2 -2
  86. package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
  87. package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
  88. package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
  89. package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
  90. package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
  91. package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
  92. package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
  93. package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
  94. package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
  95. package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
  96. package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
  97. package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
  98. package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
  99. package/templates/base/root/AGENTS.md.ejs +1 -1
  100. package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
  101. package/templates/crud/EndpointsController.java.ejs +1 -1
  102. package/templates/crud/ScaffoldCommand.java.ejs +5 -2
  103. package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
  104. package/templates/crud/ScaffoldQuery.java.ejs +5 -2
  105. package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
  106. package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
  107. package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
  108. package/templates/evaluate/report.html.ejs +1447 -90
  109. package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
  110. package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
  111. package/templates/ports/PortAclMapper.java.ejs +35 -0
  112. package/templates/ports/PortFeignAdapter.java.ejs +7 -22
  113. package/templates/ports/PortFeignClient.java.ejs +4 -0
  114. package/templates/ports/PortResponseDto.java.ejs +1 -1
  115. package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
  116. package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
  117. package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
  118. package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
  119. package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
  120. package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
  121. package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
  122. package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
  123. package/templates/read-model/ReadModelDomain.java.ejs +46 -0
  124. package/templates/read-model/ReadModelJpa.java.ejs +58 -0
  125. package/templates/read-model/ReadModelJpaRepository.java.ejs +13 -0
  126. package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
  127. package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
  128. package/templates/read-model/ReadModelRepository.java.ejs +42 -0
  129. package/templates/read-model/ReadModelRepositoryImpl.java.ejs +85 -0
  130. package/templates/read-model/ReadModelSyncHandler.java.ejs +54 -0
  131. package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
  132. package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
  133. package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
  134. package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
  135. package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
  136. package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
  137. package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
  138. package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
  139. package/templates/temporal-activity/NestedType.java.ejs +12 -0
  140. package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
  141. package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
  142. package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
  143. package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
  144. package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
  145. package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
  146. package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
  147. package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
  148. package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
  149. package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
  150. package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
  151. package/COMMAND_EVALUATION.md +0 -911
@@ -0,0 +1,665 @@
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 yaml = require('js-yaml');
7
+ const ConfigManager = require('../utils/config-manager');
8
+ const { isEva4jProject, moduleExists } = require('../utils/validator');
9
+ const { toPackagePath, toPascalCase, toCamelCase, toSnakeCase, toKebabCase } = require('../utils/naming');
10
+ const { renderAndWrite, renderTemplate } = require('../utils/template-engine');
11
+ const { parseDomainYaml } = require('../utils/yaml-to-entity');
12
+ const { generateEventRecord, createOrUpdateMessageBroker, updateDomainEventHandler } = require('./generate-kafka-event');
13
+
14
+ async function generateRabbitMQEventCommand(moduleName, eventName) {
15
+ const projectDir = process.cwd();
16
+
17
+ // Validate we're in an eva4j project
18
+ if (!(await isEva4jProject(projectDir))) {
19
+ console.error(chalk.red('❌ Not in an eva4j project directory'));
20
+ console.error(chalk.gray('Run this command inside a project created with eva4j'));
21
+ process.exit(1);
22
+ }
23
+
24
+ // Check if RabbitMQ is installed
25
+ const configManager = new ConfigManager(projectDir);
26
+ if (!(await configManager.featureExists('rabbitmq'))) {
27
+ console.error(chalk.red('❌ RabbitMQ client is not installed in this project'));
28
+ console.error(chalk.gray('Install RabbitMQ first using: eva4j add rabbitmq-client'));
29
+ process.exit(1);
30
+ }
31
+
32
+ // Load project configuration
33
+ const projectConfig = await configManager.loadProjectConfig();
34
+
35
+ if (!projectConfig) {
36
+ console.error(chalk.red('❌ Could not load project configuration'));
37
+ console.error(chalk.gray('Make sure .eva4j.json exists in the project root'));
38
+ process.exit(1);
39
+ }
40
+
41
+ const { packageName } = projectConfig;
42
+ const packagePath = toPackagePath(packageName);
43
+
44
+ // Normalise module name to camelCase
45
+ moduleName = toCamelCase(moduleName);
46
+
47
+ // Validate module exists
48
+ if (!(await configManager.moduleExists(moduleName))) {
49
+ console.error(chalk.red(`❌ Module '${moduleName}' not found in project configuration`));
50
+ console.error(chalk.gray('Create the module first using: eva4j add module <name>'));
51
+ process.exit(1);
52
+ }
53
+
54
+ if (!(await moduleExists(projectDir, packagePath, moduleName))) {
55
+ console.error(chalk.red(`❌ Module '${moduleName}' does not exist in filesystem`));
56
+ process.exit(1);
57
+ }
58
+
59
+ // Try to read domain events declared in domain.yaml for the module
60
+ let domainEventChoices = [];
61
+ let domainEventMap = {};
62
+ const domainYamlPath = path.join(projectDir, 'src', 'main', 'java', packagePath, moduleName, 'domain.yaml');
63
+ if (await fs.pathExists(domainYamlPath)) {
64
+ try {
65
+ const parsed = await parseDomainYaml(domainYamlPath, packageName, moduleName);
66
+ parsed.aggregates.forEach(agg => {
67
+ (agg.domainEvents || []).forEach(event => {
68
+ domainEventChoices.push({ name: `${event.name} (from ${agg.name} aggregate)`, value: event.name });
69
+ domainEventMap[event.name] = event;
70
+ });
71
+ });
72
+ } catch (_) {
73
+ // domain.yaml may not be parseable yet — silently fall back to free text
74
+ }
75
+ }
76
+
77
+ // ── Resolve list of event names to process ─────────────────────────────
78
+ let eventNames = [];
79
+
80
+ if (eventName) {
81
+ eventNames = [eventName];
82
+ } else if (domainEventChoices.length > 0) {
83
+ const choicesWithAll = [
84
+ { name: chalk.bold('★ All events'), value: '__all__' },
85
+ new inquirer.Separator(),
86
+ ...domainEventChoices,
87
+ new inquirer.Separator(),
88
+ { name: 'Custom name (free text)...', value: '__custom__' }
89
+ ];
90
+
91
+ const { selectedEvents } = await inquirer.prompt([
92
+ {
93
+ type: 'checkbox',
94
+ name: 'selectedEvents',
95
+ message: 'Select domain events to publish via RabbitMQ (space to select, enter to confirm):',
96
+ choices: choicesWithAll,
97
+ validate: (input) => input.length > 0 ? true : 'Select at least one event'
98
+ }
99
+ ]);
100
+
101
+ if (selectedEvents.includes('__all__')) {
102
+ eventNames = domainEventChoices.map(c => c.value);
103
+ } else if (selectedEvents.includes('__custom__')) {
104
+ const { customName } = await inquirer.prompt([
105
+ {
106
+ type: 'input',
107
+ name: 'customName',
108
+ message: 'Enter event name:',
109
+ validate: (input) => input && input.trim() !== '' ? true : 'Event name cannot be empty'
110
+ }
111
+ ]);
112
+ eventNames = [customName];
113
+ } else {
114
+ eventNames = selectedEvents;
115
+ }
116
+ } else {
117
+ const nameAnswer = await inquirer.prompt([
118
+ {
119
+ type: 'input',
120
+ name: 'eventName',
121
+ message: 'Enter event name:',
122
+ validate: (input) => input && input.trim() !== '' ? true : 'Event name cannot be empty'
123
+ }
124
+ ]);
125
+ eventNames = [nameAnswer.eventName];
126
+ }
127
+
128
+ const isBatch = eventNames.length > 1;
129
+ const spinner = ora(`Generating ${isBatch ? `${eventNames.length} RabbitMQ events` : 'RabbitMQ event'}...`).start();
130
+ const results = [];
131
+
132
+ try {
133
+ for (const name of eventNames) {
134
+ const normalizedName = toPascalCase(name);
135
+ const evtClassName = normalizedName.endsWith('IntegrationEvent') ? normalizedName : `${normalizedName}IntegrationEvent`;
136
+
137
+ // In batch mode skip already-existing events; in single mode abort
138
+ const evtPath = path.join(projectDir, 'src', 'main', 'java', packagePath, moduleName, 'application', 'events', `${evtClassName}.java`);
139
+ if (await fs.pathExists(evtPath)) {
140
+ if (isBatch) {
141
+ results.push({ name: evtClassName, skipped: true });
142
+ continue;
143
+ } else {
144
+ spinner.fail();
145
+ console.error(chalk.red(`❌ Event '${evtClassName}' already exists in module '${moduleName}'`));
146
+ process.exit(1);
147
+ }
148
+ }
149
+
150
+ const selectedDomainEvent = domainEventMap[normalizedName] || null;
151
+ const context = buildRabbitEventContext(packageName, moduleName, { name, fields: selectedDomainEvent ? selectedDomainEvent.fields : null });
152
+
153
+ if (isBatch) spinner.text = `[${results.length + 1}/${eventNames.length}] Generating ${evtClassName}...`;
154
+
155
+ await generateSingleRabbitEvent(projectDir, packagePath, context);
156
+ results.push({ name: evtClassName, skipped: false, handlerUpdated: context._handlerUpdated });
157
+ }
158
+
159
+ const generated = results.filter(r => !r.skipped);
160
+ spinner.succeed(chalk.green(`${isBatch ? `${generated.length} RabbitMQ events` : 'RabbitMQ event'} generated successfully! ✨`));
161
+
162
+ const rabbitMessageBrokerClass = `${toPascalCase(moduleName)}RabbitMessageBroker`;
163
+ console.log(chalk.blue('\n📦 Generated/Updated components:'));
164
+ results.forEach((r) => {
165
+ if (r.skipped) {
166
+ console.log(chalk.yellow(` ├── ${moduleName}/application/events/${r.name}.java (skipped — already exists)`));
167
+ } else {
168
+ console.log(chalk.gray(` ├── ${moduleName}/application/events/${r.name}.java`));
169
+ }
170
+ });
171
+ console.log(chalk.gray(` ├── ${moduleName}/application/ports/MessageBroker.java`));
172
+ console.log(chalk.gray(` ├── ${moduleName}/infrastructure/adapters/rabbitmqMessageBroker/${rabbitMessageBrokerClass}.java`));
173
+ console.log(chalk.gray(` ├── shared/configurations/rabbitmqConfig/RabbitMQConfig.java`));
174
+ if (results.some(r => r.handlerUpdated)) {
175
+ console.log(chalk.gray(` ├── ${moduleName}/application/usecases/*DomainEventHandler.java`));
176
+ }
177
+ console.log(chalk.gray(' └── parameters/*/rabbitmq.yaml (all environments)'));
178
+
179
+ if (!isBatch && generated.length === 1) {
180
+ const r = generated[0];
181
+ const routingKey = toKebabCase(stripEventSuffix(eventNames[0])).replace(/-/g, '.');
182
+ console.log(chalk.blue('\n✅ RabbitMQ event configured successfully!'));
183
+ console.log(chalk.white(`\n Event: ${r.name}`));
184
+ console.log(chalk.white(` Exchange: ${moduleName}.events`));
185
+ console.log(chalk.white(` Routing Key: ${routingKey}`));
186
+ console.log(chalk.gray('\n You can now inject MessageBroker in your services and call:'));
187
+ console.log(chalk.gray(` messageBroker.publish${r.name}(event);\n`));
188
+ } else {
189
+ console.log(chalk.blue('\n✅ All RabbitMQ events configured successfully!'));
190
+ if (generated.length > 0) {
191
+ console.log(chalk.gray('\n Available MessageBroker methods:'));
192
+ generated.forEach(r => console.log(chalk.gray(` messageBroker.publish${r.name}(event);`)));
193
+ }
194
+ console.log('');
195
+ }
196
+
197
+ } catch (error) {
198
+ spinner.fail(chalk.red('Failed to generate RabbitMQ event'));
199
+ console.error(chalk.red('\n❌ Error:'), error.message);
200
+ if (error.stack) {
201
+ console.error(chalk.gray(error.stack));
202
+ }
203
+ process.exit(1);
204
+ }
205
+ }
206
+
207
+ // ── Helper Functions ─────────────────────────────────────────────────────────
208
+
209
+ /**
210
+ * Strip the conventional Java 'Event' suffix from an event class name
211
+ * before deriving the routing key / queue name.
212
+ */
213
+ function stripEventSuffix(name) {
214
+ return name.endsWith('Event') ? name.slice(0, -'Event'.length) : name;
215
+ }
216
+
217
+ /**
218
+ * Build a RabbitMQ event generation context for a given domain event.
219
+ * Parallel to buildKafkaEventContext in generate-kafka-event.js.
220
+ *
221
+ * @param {string} packageName
222
+ * @param {string} moduleName
223
+ * @param {{ name: string, topic?: string, fields: Array }} domainEvent
224
+ * @returns {Object} context ready for generateSingleRabbitEvent()
225
+ */
226
+ function buildRabbitEventContext(packageName, moduleName, domainEvent) {
227
+ const normalizedName = toPascalCase(domainEvent.name);
228
+ const integrationEventClassName = normalizedName.endsWith('IntegrationEvent')
229
+ ? normalizedName
230
+ : `${normalizedName}IntegrationEvent`;
231
+
232
+ // Derive routing key and queue names from event name
233
+ // OrderPlacedEvent → order-placed (kebab) → order.placed (routing key)
234
+ const topicBase = domainEvent.topic
235
+ ? domainEvent.topic.trim().toUpperCase().replace(/-/g, '_')
236
+ : toSnakeCase(stripEventSuffix(domainEvent.name)).toUpperCase();
237
+ const topicNameSnake = topicBase;
238
+ const topicNameKebab = topicBase.toLowerCase().replace(/_/g, '-');
239
+ const topicNameCamel = toCamelCase(topicNameKebab);
240
+ const routingKey = topicNameKebab.replace(/-/g, '.');
241
+
242
+ return {
243
+ packageName,
244
+ moduleName,
245
+ modulePascalCase: toPascalCase(moduleName),
246
+ moduleCamelCase: toCamelCase(moduleName),
247
+ rabbitMessageBrokerClassName: `${toPascalCase(moduleName)}RabbitMessageBroker`,
248
+ // Reuse kafkaMessageBrokerClassName for DomainEventHandler.ejs compatibility
249
+ kafkaMessageBrokerClassName: `${toPascalCase(moduleName)}RabbitMessageBroker`,
250
+ eventClassName: integrationEventClassName,
251
+ topicNameSnake,
252
+ topicNameKebab,
253
+ topicNameCamel,
254
+ topicPropertyKey: topicNameKebab,
255
+ topicPropertyValue: topicNameSnake,
256
+ topicSpringProperty: `\${routing-keys.${topicNameKebab}}`,
257
+ routingKey,
258
+ exchangeName: `${moduleName}.events`,
259
+ queueName: `${moduleName}.${topicNameKebab}`,
260
+ eventFields: domainEvent.fields || null
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Run the full generation pipeline for a single RabbitMQ event.
266
+ * Mutates context._handlerUpdated to signal whether the DomainEventHandler was wired.
267
+ */
268
+ async function generateSingleRabbitEvent(projectDir, packagePath, context) {
269
+ // 1. Integration Event record (reuse kafka-event template — broker-agnostic)
270
+ await generateEventRecord(projectDir, packagePath, context);
271
+ // 2. Update rabbitmq.yaml with exchange, queue, routing-key entries
272
+ await updateRabbitMQYml(projectDir, context);
273
+ // 3. MessageBroker port interface (reuse kafka-event — broker-agnostic)
274
+ await createOrUpdateMessageBroker(projectDir, packagePath, context);
275
+ // 4. RabbitMessageBroker adapter
276
+ await createOrUpdateRabbitMessageBroker(projectDir, packagePath, context);
277
+ // 5. RabbitMQConfig beans (exchange + queue + binding)
278
+ await updateRabbitMQConfig(projectDir, packagePath, context);
279
+ // 6. DomainEventHandler wiring (reuse kafka-event — broker-agnostic)
280
+ context._handlerUpdated = await updateDomainEventHandler(projectDir, packagePath, context);
281
+ }
282
+
283
+ /**
284
+ * Create or update the RabbitMQ MessageBroker adapter.
285
+ * Creates a new file if not present; appends publish method otherwise.
286
+ */
287
+ async function createOrUpdateRabbitMessageBroker(projectDir, packagePath, context) {
288
+ const adapterPath = path.join(
289
+ projectDir, 'src', 'main', 'java', packagePath,
290
+ context.moduleName, 'infrastructure', 'adapters', 'rabbitmqMessageBroker',
291
+ `${context.rabbitMessageBrokerClassName}.java`
292
+ );
293
+
294
+ const methodName = `publish${context.eventClassName}`;
295
+ const rabbitTemplatesDir = path.join(__dirname, '..', '..', 'templates', 'rabbitmq-event');
296
+
297
+ if (await fs.pathExists(adapterPath)) {
298
+ let content = await fs.readFile(adapterPath, 'utf-8');
299
+
300
+ // If this file is a mock implementation (from eva build --mock), replace it wholesale
301
+ const isMockImpl = content.includes('ApplicationEventPublisher') && !content.includes('RabbitTemplate');
302
+ if (isMockImpl) {
303
+ await renderAndWrite(
304
+ path.join(rabbitTemplatesDir, 'RabbitMessageBroker.java.ejs'),
305
+ adapterPath,
306
+ context
307
+ );
308
+ return;
309
+ }
310
+
311
+ // Check if method already exists
312
+ if (content.includes(methodName)) {
313
+ return;
314
+ }
315
+
316
+ // Inject event import if missing
317
+ const eventImport = `import ${context.packageName}.${context.moduleName}.application.events.${context.eventClassName};`;
318
+ if (!content.includes(eventImport)) {
319
+ content = injectImportIntoFile(content, eventImport);
320
+ }
321
+
322
+ // Inject EventEnvelope import if missing
323
+ const envelopeImport = `import ${context.packageName}.shared.infrastructure.eventEnvelope.EventEnvelope;`;
324
+ if (!content.includes(envelopeImport)) {
325
+ content = injectImportIntoFile(content, envelopeImport);
326
+ }
327
+
328
+ // Inject @Value import if missing
329
+ const valueImport = 'import org.springframework.beans.factory.annotation.Value;';
330
+ if (!content.includes(valueImport)) {
331
+ content = injectImportIntoFile(content, valueImport);
332
+ }
333
+
334
+ // Check if @Value routing-key field exists for this event
335
+ const valueFieldName = `${context.topicNameCamel}RoutingKey`;
336
+ if (!content.includes(`private String ${valueFieldName};`)) {
337
+ const valueFieldPattern = /(@Value\([^)]+\)\s*\n\s*private\s+String\s+\w+;\s*\n)/g;
338
+ const valueFields = [...content.matchAll(valueFieldPattern)];
339
+
340
+ if (valueFields.length > 0) {
341
+ const lastField = valueFields[valueFields.length - 1];
342
+ const insertPos = lastField.index + lastField[0].length;
343
+ content = content.slice(0, insertPos) +
344
+ `\n @Value("\${routing-keys.${context.topicNameKebab}}")\n private String ${valueFieldName};\n\n` +
345
+ content.slice(insertPos);
346
+ } else {
347
+ const classPattern = /(public\s+class\s+\w+RabbitMessageBroker\s+implements\s+MessageBroker\s*\{\s*\n)/;
348
+ const classMatch = content.match(classPattern);
349
+ if (classMatch) {
350
+ const insertPos = classMatch.index + classMatch[0].length;
351
+ content = content.slice(0, insertPos) +
352
+ `\n @Value("\${routing-keys.${context.topicNameKebab}}")\n private String ${valueFieldName};\n` +
353
+ content.slice(insertPos);
354
+ }
355
+ }
356
+ }
357
+
358
+ // Generate method implementation
359
+ const templatePath = path.join(rabbitTemplatesDir, 'RabbitMessageBrokerMethod.java.ejs');
360
+ const methodImpl = await renderTemplate(templatePath, { ...context, valueFieldName });
361
+
362
+ const lastBraceIndex = content.lastIndexOf('}');
363
+ if (lastBraceIndex === -1) {
364
+ throw new Error(`Could not find closing brace in ${context.rabbitMessageBrokerClassName} class`);
365
+ }
366
+
367
+ content = content.slice(0, lastBraceIndex) + '\n' + methodImpl + '\n}\n';
368
+ await fs.writeFile(adapterPath, content, 'utf-8');
369
+ } else {
370
+ // Create new implementation
371
+ await renderAndWrite(
372
+ path.join(rabbitTemplatesDir, 'RabbitMessageBroker.java.ejs'),
373
+ adapterPath,
374
+ context
375
+ );
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Update RabbitMQConfig.java with exchange + queue + binding beans.
381
+ * Exchange bean is emitted once per module; queue/binding beans are emitted per event.
382
+ */
383
+ async function updateRabbitMQConfig(projectDir, packagePath, context) {
384
+ const configPath = path.join(
385
+ projectDir, 'src', 'main', 'java', packagePath,
386
+ 'shared', 'infrastructure', 'configurations', 'rabbitmqConfig', 'RabbitMQConfig.java'
387
+ );
388
+
389
+ if (!(await fs.pathExists(configPath))) {
390
+ throw new Error('RabbitMQConfig.java not found. Please install RabbitMQ first using: eva4j add rabbitmq-client');
391
+ }
392
+
393
+ let content = await fs.readFile(configPath, 'utf-8');
394
+
395
+ const beanMethodName = `${context.topicNameCamel}Topic`;
396
+
397
+ // Check if queue bean already exists for this event
398
+ if (content.includes(`public Queue ${beanMethodName}Queue(`)) {
399
+ return; // Beans already exist
400
+ }
401
+
402
+ const templatesDir = path.join(__dirname, '..', '..', 'templates', 'rabbitmq-event');
403
+
404
+ // ── Exchange bean — emit only once per module ──────────────────────────────
405
+ const exchangeBeanName = `${context.moduleName}Exchange`;
406
+ if (!content.includes(`public TopicExchange ${exchangeBeanName}(`)) {
407
+ const exchangeSnippet = await renderTemplate(
408
+ path.join(templatesDir, 'RabbitConfigExchange.java.ejs'),
409
+ { moduleName: context.moduleName }
410
+ );
411
+ const lastBrace = content.lastIndexOf('}');
412
+ if (lastBrace === -1) throw new Error('Could not find closing brace in RabbitMQConfig class');
413
+ content = content.slice(0, lastBrace) + '\n' + exchangeSnippet + '\n}\n';
414
+ }
415
+
416
+ // ── Queue + Binding beans — emit per event ─────────────────────────────────
417
+ const valueFieldName = `${context.topicNameCamel}Topic`;
418
+ const queueBindingSnippet = await renderTemplate(
419
+ path.join(templatesDir, 'RabbitConfigBean.java.ejs'),
420
+ { ...context, beanMethodName, valueFieldName, moduleName: context.moduleName }
421
+ );
422
+
423
+ const lastBraceIndex = content.lastIndexOf('}');
424
+ if (lastBraceIndex === -1) throw new Error('Could not find closing brace in RabbitMQConfig class');
425
+ content = content.slice(0, lastBraceIndex) + '\n' + queueBindingSnippet + '\n}\n';
426
+
427
+ await fs.writeFile(configPath, content, 'utf-8');
428
+ }
429
+
430
+ /**
431
+ * Update rabbitmq.yaml files in all environments with exchange, queue, and routing-key entries.
432
+ */
433
+ async function updateRabbitMQYml(projectDir, context) {
434
+ const environments = ['local', 'develop', 'test', 'production'];
435
+
436
+ for (const env of environments) {
437
+ const rabbitYmlPath = path.join(projectDir, 'src', 'main', 'resources', 'parameters', env, 'rabbitmq.yaml');
438
+
439
+ if (!(await fs.pathExists(rabbitYmlPath))) {
440
+ continue;
441
+ }
442
+
443
+ const existingContent = await fs.readFile(rabbitYmlPath, 'utf8');
444
+ let rabbitContent = yaml.load(existingContent) || {};
445
+
446
+ let changed = false;
447
+
448
+ // Initialize sections if they don't exist
449
+ if (!rabbitContent.exchanges) rabbitContent.exchanges = {};
450
+ if (!rabbitContent.queues) rabbitContent.queues = {};
451
+ if (!rabbitContent['routing-keys']) rabbitContent['routing-keys'] = {};
452
+
453
+ // Add exchange if it doesn't exist
454
+ if (!rabbitContent.exchanges[context.moduleName]) {
455
+ rabbitContent.exchanges[context.moduleName] = context.exchangeName;
456
+ changed = true;
457
+ }
458
+
459
+ // Add queue if it doesn't exist
460
+ if (!rabbitContent.queues[context.topicPropertyKey]) {
461
+ rabbitContent.queues[context.topicPropertyKey] = context.queueName;
462
+ changed = true;
463
+ }
464
+
465
+ // Add routing key if it doesn't exist
466
+ if (!rabbitContent['routing-keys'][context.topicPropertyKey]) {
467
+ rabbitContent['routing-keys'][context.topicPropertyKey] = context.routingKey;
468
+ changed = true;
469
+ }
470
+
471
+ if (changed) {
472
+ const yamlContent = yaml.dump(rabbitContent, {
473
+ indent: 2,
474
+ lineWidth: -1,
475
+ quotingType: '"',
476
+ forceQuotes: false
477
+ });
478
+ await fs.writeFile(rabbitYmlPath, yamlContent, 'utf8');
479
+ }
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Update rabbitmq.yaml with a specific queue entry (for listeners/read models).
485
+ * Simplified version that only registers a queue.
486
+ */
487
+ async function updateRabbitMQYmlQueue(projectDir, queueKey, queueValue) {
488
+ const environments = ['local', 'develop', 'test', 'production'];
489
+
490
+ for (const env of environments) {
491
+ const rabbitYmlPath = path.join(projectDir, 'src', 'main', 'resources', 'parameters', env, 'rabbitmq.yaml');
492
+
493
+ if (!(await fs.pathExists(rabbitYmlPath))) {
494
+ continue;
495
+ }
496
+
497
+ const existingContent = await fs.readFile(rabbitYmlPath, 'utf8');
498
+ let rabbitContent = yaml.load(existingContent) || {};
499
+
500
+ if (!rabbitContent.queues) rabbitContent.queues = {};
501
+
502
+ if (!rabbitContent.queues[queueKey]) {
503
+ rabbitContent.queues[queueKey] = queueValue;
504
+
505
+ const yamlContent = yaml.dump(rabbitContent, {
506
+ indent: 2,
507
+ lineWidth: -1,
508
+ quotingType: '"',
509
+ forceQuotes: false
510
+ });
511
+ await fs.writeFile(rabbitYmlPath, yamlContent, 'utf8');
512
+ }
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Injects an import statement after the last existing import, or after the package declaration.
518
+ */
519
+ function injectImportIntoFile(content, importStatement) {
520
+ const packageMatch = content.match(/(package\s+[\w.]+;\s*\n)/);
521
+ if (!packageMatch) return content;
522
+
523
+ const hasImports = /import\s+[\w.]+;/.test(content);
524
+ if (hasImports) {
525
+ const imports = content.matchAll(/import\s+[\w.]+;\s*\n/g);
526
+ let lastImportEnd = packageMatch.index + packageMatch[0].length;
527
+ for (const match of imports) {
528
+ lastImportEnd = match.index + match[0].length;
529
+ }
530
+ return content.slice(0, lastImportEnd) + importStatement + '\n' + content.slice(lastImportEnd);
531
+ } else {
532
+ const insertPos = packageMatch.index + packageMatch[0].length;
533
+ return content.slice(0, insertPos) + '\n' + importStatement + '\n' + content.slice(insertPos);
534
+ }
535
+ }
536
+
537
+ /**
538
+ * Update RabbitMQConfig.java with consumer-side beans: producer exchange + queue + binding.
539
+ * Used by listeners and read-model consumers that need to declare infrastructure
540
+ * for binding to a remote producer's exchange.
541
+ *
542
+ * @param {string} projectDir - Project root
543
+ * @param {string} packagePath - Java package path (e.g. com/myapp)
544
+ * @param {Object} ctx - Consumer context
545
+ * @param {string} ctx.producerModule - Producer module name (camelCase, e.g. "orders")
546
+ * @param {string} ctx.topicKey - Kebab-case topic key (e.g. "order-confirmed")
547
+ * @param {string} ctx.beanMethodName - Bean method prefix (e.g. "orderConfirmedTopic")
548
+ * @param {string} ctx.valueFieldName - Field name prefix (e.g. "orderConfirmedTopic")
549
+ */
550
+ async function updateRabbitMQConfigForConsumer(projectDir, packagePath, ctx) {
551
+ const configPath = path.join(
552
+ projectDir, 'src', 'main', 'java', packagePath,
553
+ 'shared', 'infrastructure', 'configurations', 'rabbitmqConfig', 'RabbitMQConfig.java'
554
+ );
555
+
556
+ if (!(await fs.pathExists(configPath))) {
557
+ throw new Error('RabbitMQConfig.java not found. Please install RabbitMQ first using: eva4j add rabbitmq-client');
558
+ }
559
+
560
+ let content = await fs.readFile(configPath, 'utf-8');
561
+
562
+ // Skip if queue bean already exists
563
+ if (content.includes(`public Queue ${ctx.beanMethodName}Queue(`)) {
564
+ return;
565
+ }
566
+
567
+ const templatesDir = path.join(__dirname, '..', '..', 'templates', 'rabbitmq-listener');
568
+
569
+ // ── Producer exchange bean — emit only once per unique producer ─────────────
570
+ const exchangeBeanName = `${ctx.producerModule}Exchange`;
571
+ if (!content.includes(`public TopicExchange ${exchangeBeanName}(`)) {
572
+ const exchangeSnippet = await renderTemplate(
573
+ path.join(templatesDir, 'RabbitConfigConsumerExchange.java.ejs'),
574
+ { producerModule: ctx.producerModule }
575
+ );
576
+ const lastBrace = content.lastIndexOf('}');
577
+ if (lastBrace === -1) throw new Error('Could not find closing brace in RabbitMQConfig class');
578
+ content = content.slice(0, lastBrace) + '\n' + exchangeSnippet + '\n}\n';
579
+ }
580
+
581
+ // ── Queue + Binding beans — emit per consumer listener ─────────────────────
582
+ const queueBindingSnippet = await renderTemplate(
583
+ path.join(templatesDir, 'RabbitConfigConsumerBean.java.ejs'),
584
+ {
585
+ topicKey: ctx.topicKey,
586
+ beanMethodName: ctx.beanMethodName,
587
+ valueFieldName: ctx.valueFieldName,
588
+ producerModule: ctx.producerModule
589
+ }
590
+ );
591
+
592
+ const lastBraceIndex = content.lastIndexOf('}');
593
+ if (lastBraceIndex === -1) throw new Error('Could not find closing brace in RabbitMQConfig class');
594
+ content = content.slice(0, lastBraceIndex) + '\n' + queueBindingSnippet + '\n}\n';
595
+
596
+ await fs.writeFile(configPath, content, 'utf-8');
597
+ }
598
+
599
+ /**
600
+ * Update rabbitmq.yaml for consumer-side: adds exchange (producer), queue, and routing-key entries.
601
+ *
602
+ * @param {string} projectDir - Project root
603
+ * @param {string} topicKey - Kebab-case key (e.g. "order-confirmed")
604
+ * @param {string} queueName - Full queue name (e.g. "notifications.order-confirmed")
605
+ * @param {string} producerModule - Producer module name (e.g. "orders")
606
+ * @param {string} exchangeName - Exchange name (e.g. "orders.events")
607
+ * @param {string} routingKey - Routing key (e.g. "order.confirmed")
608
+ */
609
+ async function updateRabbitMQYmlForConsumer(projectDir, topicKey, queueName, producerModule, exchangeName, routingKey) {
610
+ const environments = ['local', 'develop', 'test', 'production'];
611
+
612
+ for (const env of environments) {
613
+ const rabbitYmlPath = path.join(projectDir, 'src', 'main', 'resources', 'parameters', env, 'rabbitmq.yaml');
614
+
615
+ if (!(await fs.pathExists(rabbitYmlPath))) {
616
+ continue;
617
+ }
618
+
619
+ const existingContent = await fs.readFile(rabbitYmlPath, 'utf8');
620
+ let rabbitContent = yaml.load(existingContent) || {};
621
+ let changed = false;
622
+
623
+ if (!rabbitContent.exchanges) rabbitContent.exchanges = {};
624
+ if (!rabbitContent.queues) rabbitContent.queues = {};
625
+ if (!rabbitContent['routing-keys']) rabbitContent['routing-keys'] = {};
626
+
627
+ // Producer exchange (once per unique producer)
628
+ if (!rabbitContent.exchanges[producerModule]) {
629
+ rabbitContent.exchanges[producerModule] = exchangeName;
630
+ changed = true;
631
+ }
632
+
633
+ // Queue
634
+ if (!rabbitContent.queues[topicKey]) {
635
+ rabbitContent.queues[topicKey] = queueName;
636
+ changed = true;
637
+ }
638
+
639
+ // Routing key
640
+ if (!rabbitContent['routing-keys'][topicKey]) {
641
+ rabbitContent['routing-keys'][topicKey] = routingKey;
642
+ changed = true;
643
+ }
644
+
645
+ if (changed) {
646
+ const yamlContent = yaml.dump(rabbitContent, {
647
+ indent: 2,
648
+ lineWidth: -1,
649
+ quotingType: '"',
650
+ forceQuotes: false
651
+ });
652
+ await fs.writeFile(rabbitYmlPath, yamlContent, 'utf8');
653
+ }
654
+ }
655
+ }
656
+
657
+ module.exports = generateRabbitMQEventCommand;
658
+ module.exports.generateSingleRabbitEvent = generateSingleRabbitEvent;
659
+ module.exports.buildRabbitEventContext = buildRabbitEventContext;
660
+ module.exports.updateRabbitMQYml = updateRabbitMQYml;
661
+ module.exports.updateRabbitMQYmlQueue = updateRabbitMQYmlQueue;
662
+ module.exports.createOrUpdateRabbitMessageBroker = createOrUpdateRabbitMessageBroker;
663
+ module.exports.updateRabbitMQConfig = updateRabbitMQConfig;
664
+ module.exports.updateRabbitMQConfigForConsumer = updateRabbitMQConfigForConsumer;
665
+ module.exports.updateRabbitMQYmlForConsumer = updateRabbitMQYmlForConsumer;