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.
- package/AGENTS.md +220 -5
- package/DOMAIN_YAML_GUIDE.md +188 -3
- package/FUTURE_FEATURES.md +33 -52
- package/QUICK_REFERENCE.md +8 -4
- package/bin/eva4j.js +70 -2
- package/config/defaults.json +1 -0
- package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
- package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
- package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
- package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
- package/docs/commands/EVALUATE_SYSTEM.md +290 -10
- package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
- package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
- package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
- package/docs/commands/INDEX.md +27 -3
- package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
- package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
- package/docs/prototype/system/RISKS.md +277 -0
- package/docs/prototype/system/customers.yaml +133 -0
- package/docs/prototype/system/inventory.yaml +109 -0
- package/docs/prototype/system/notifications.yaml +131 -0
- package/docs/prototype/system/orders.yaml +241 -0
- package/docs/prototype/system/payments.yaml +256 -0
- package/docs/prototype/system/products.yaml +168 -0
- package/docs/prototype/system/system.yaml +269 -0
- package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
- package/examples/domain-events.yaml +26 -0
- package/examples/domain-read-models.yaml +113 -0
- package/examples/system/customer.yaml +89 -0
- package/examples/system/orders.yaml +119 -0
- package/examples/system/product.yaml +27 -0
- package/examples/system/system.yaml +80 -0
- package/package.json +1 -1
- package/read-model-spec.md +664 -0
- package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
- package/src/agents/design-gap-analyst.agent.md +383 -0
- package/src/agents/design-reviewer-temporal.agent.md +412 -0
- package/src/agents/design-reviewer.agent.md +34 -5
- package/src/agents/implement-use-cases.prompt.md +179 -0
- package/src/agents/ux-gap-analyst.agent.md +412 -0
- package/src/commands/add-rabbitmq-client.js +261 -0
- package/src/commands/add-temporal-client.js +22 -2
- package/src/commands/build.js +267 -11
- package/src/commands/evaluate-system.js +700 -13
- package/src/commands/generate-entities.js +560 -24
- package/src/commands/generate-http-exchange.js +3 -0
- package/src/commands/generate-kafka-event.js +3 -0
- package/src/commands/generate-kafka-listener.js +3 -0
- package/src/commands/generate-rabbitmq-event.js +665 -0
- package/src/commands/generate-rabbitmq-listener.js +205 -0
- package/src/commands/generate-record.js +2 -2
- package/src/commands/generate-resource.js +4 -1
- package/src/commands/generate-temporal-activity.js +970 -33
- package/src/commands/generate-temporal-flow.js +98 -38
- package/src/commands/generate-temporal-system.js +708 -0
- package/src/commands/generate-usecase.js +4 -1
- package/src/skills/build-system-yaml/SKILL.md +343 -2
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +253 -26
- package/src/skills/build-system-yaml/references/module-spec.md +90 -9
- package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
- package/src/skills/build-temporal-system/SKILL.md +752 -0
- package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
- package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
- package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
- package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
- package/src/skills/implement-use-case/SKILL.md +350 -0
- package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
- package/src/skills/requirements-elicitation/SKILL.md +228 -0
- package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
- package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
- package/src/utils/bounded-context-diagram.js +844 -0
- package/src/utils/config-manager.js +4 -2
- package/src/utils/domain-validator.js +495 -17
- package/src/utils/naming.js +20 -0
- package/src/utils/system-validator.js +169 -11
- package/src/utils/system-yaml-parser.js +318 -0
- package/src/utils/temporal-validator.js +497 -0
- package/src/utils/validator.js +3 -1
- package/src/utils/yaml-to-entity.js +281 -9
- package/templates/aggregate/AggregateRepository.java.ejs +4 -0
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +8 -0
- package/templates/aggregate/AggregateRoot.java.ejs +38 -4
- package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
- package/templates/aggregate/JpaAggregateRoot.java.ejs +4 -4
- package/templates/aggregate/JpaEntity.java.ejs +2 -2
- package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
- package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
- package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
- package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
- package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
- package/templates/base/root/AGENTS.md.ejs +1 -1
- package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
- package/templates/crud/EndpointsController.java.ejs +1 -1
- package/templates/crud/ScaffoldCommand.java.ejs +5 -2
- package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
- package/templates/crud/ScaffoldQuery.java.ejs +5 -2
- package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
- package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
- package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
- package/templates/evaluate/report.html.ejs +1447 -90
- package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
- package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
- package/templates/ports/PortAclMapper.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +7 -22
- package/templates/ports/PortFeignClient.java.ejs +4 -0
- package/templates/ports/PortResponseDto.java.ejs +1 -1
- package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
- package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
- package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
- package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
- package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
- package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
- package/templates/read-model/ReadModelDomain.java.ejs +46 -0
- package/templates/read-model/ReadModelJpa.java.ejs +58 -0
- package/templates/read-model/ReadModelJpaRepository.java.ejs +13 -0
- package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
- package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
- package/templates/read-model/ReadModelRepository.java.ejs +42 -0
- package/templates/read-model/ReadModelRepositoryImpl.java.ejs +85 -0
- package/templates/read-model/ReadModelSyncHandler.java.ejs +54 -0
- package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
- package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
- package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
- package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
- package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
- package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
- package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/NestedType.java.ejs +12 -0
- package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
- package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
- package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
- package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
- package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
- package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
- package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
- 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;
|