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
|
@@ -80,6 +80,10 @@ async function addTemporalClientCommand() {
|
|
|
80
80
|
spinner.text = 'Generating TemporalConfig class...';
|
|
81
81
|
await generateTemporalConfigClass(projectDir, context);
|
|
82
82
|
|
|
83
|
+
// 5b. Generate TemporalWorkerFactoryLifecycle.java
|
|
84
|
+
spinner.text = 'Generating TemporalWorkerFactoryLifecycle class...';
|
|
85
|
+
await generateTemporalLifecycleClass(projectDir, context);
|
|
86
|
+
|
|
83
87
|
// 6. Update docker-compose.yaml if it exists
|
|
84
88
|
spinner.text = 'Updating docker-compose.yaml...';
|
|
85
89
|
await updateDockerCompose(projectDir, context);
|
|
@@ -99,13 +103,14 @@ async function addTemporalClientCommand() {
|
|
|
99
103
|
console.log(chalk.gray(' │ └── production/temporal.yaml'));
|
|
100
104
|
console.log(chalk.gray(' ├── shared/domain/interfaces/HeavyActivity.java'));
|
|
101
105
|
console.log(chalk.gray(' ├── shared/domain/interfaces/LightActivity.java'));
|
|
102
|
-
console.log(chalk.gray('
|
|
106
|
+
console.log(chalk.gray(' ├── shared/infrastructure/configurations/temporalConfig/TemporalConfig.java'));
|
|
107
|
+
console.log(chalk.gray(' └── shared/infrastructure/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java'));
|
|
103
108
|
|
|
104
109
|
console.log(chalk.blue('\n✅ Temporal client configured successfully!'));
|
|
105
110
|
console.log(chalk.white('\n Service URL: localhost:7233'));
|
|
106
111
|
console.log(chalk.white(' Namespace: default'));
|
|
107
112
|
console.log(chalk.white(' Temporal UI: http://localhost:8088'));
|
|
108
|
-
console.log(chalk.yellow('\n ⚠️
|
|
113
|
+
console.log(chalk.yellow('\n ⚠️ Run "eva g temporal-flow <module>" to create workflows with module-scoped queues'));
|
|
109
114
|
console.log(chalk.gray(' Run "docker-compose up -d" to start the Temporal cluster'));
|
|
110
115
|
console.log(chalk.gray(' Update temporal.yaml files to customize service URLs per environment'));
|
|
111
116
|
console.log();
|
|
@@ -235,6 +240,21 @@ async function generateTemporalConfigClass(projectDir, context) {
|
|
|
235
240
|
await renderAndWrite(templatePath, outputPath, context);
|
|
236
241
|
}
|
|
237
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Generate TemporalWorkerFactoryLifecycle.java class
|
|
245
|
+
*/
|
|
246
|
+
async function generateTemporalLifecycleClass(projectDir, context) {
|
|
247
|
+
const templatePath = path.join(
|
|
248
|
+
__dirname, '..', '..', 'templates', 'shared', 'configurations', 'temporalConfig', 'TemporalWorkerFactoryLifecycle.java.ejs'
|
|
249
|
+
);
|
|
250
|
+
const outputPath = path.join(
|
|
251
|
+
projectDir, 'src', 'main', 'java', context.packagePath,
|
|
252
|
+
'shared', 'infrastructure', 'configurations', 'temporalConfig', 'TemporalWorkerFactoryLifecycle.java'
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
await renderAndWrite(templatePath, outputPath, context);
|
|
256
|
+
}
|
|
257
|
+
|
|
238
258
|
/**
|
|
239
259
|
* Update docker-compose.yaml to add Temporal services
|
|
240
260
|
*/
|
package/src/commands/build.js
CHANGED
|
@@ -7,13 +7,18 @@ const yaml = require('js-yaml');
|
|
|
7
7
|
|
|
8
8
|
const ConfigManager = require('../utils/config-manager');
|
|
9
9
|
const { isEva4jProject } = require('../utils/validator');
|
|
10
|
-
const { toCamelCase, toPackagePath } = require('../utils/naming');
|
|
10
|
+
const { toCamelCase, toPascalCase, toScreamingSnakeCase, toPackagePath } = require('../utils/naming');
|
|
11
11
|
const SharedGenerator = require('../generators/shared-generator');
|
|
12
12
|
const { renderAndWrite } = require('../utils/template-engine');
|
|
13
13
|
const addModuleCommand = require('./add-module');
|
|
14
14
|
const addKafkaClientCommand = require('./add-kafka-client');
|
|
15
|
+
const addRabbitMQClientCommand = require('./add-rabbitmq-client');
|
|
16
|
+
const addTemporalClientCommand = require('./add-temporal-client');
|
|
15
17
|
const generateEntitiesCommand = require('./generate-entities');
|
|
18
|
+
const generateTemporalSystemCommand = require('./generate-temporal-system');
|
|
19
|
+
const generateTemporalActivityCommand = require('./generate-temporal-activity');
|
|
16
20
|
const { generateUnifiedPostmanCollection } = require('../generators/postman-generator');
|
|
21
|
+
const ChecksumManager = require('../utils/checksum-manager');
|
|
17
22
|
|
|
18
23
|
// ── H2 mock config ─────────────────────────────────────────────────────────────
|
|
19
24
|
const H2_DB_YAML = (packageName) => `spring:
|
|
@@ -244,7 +249,16 @@ async function swapToH2(projectDir, packageName, configManager) {
|
|
|
244
249
|
if (await fs.pathExists(kafkaConfigPath)) {
|
|
245
250
|
backups.kafkaConfig = { path: kafkaConfigPath, content: await fs.readFile(kafkaConfigPath, 'utf-8') };
|
|
246
251
|
}
|
|
247
|
-
|
|
252
|
+
|
|
253
|
+
// ── RabbitMQConfig.java — backup so it can be restored later ──────────────
|
|
254
|
+
const rabbitConfigPath = path.join(
|
|
255
|
+
projectDir, 'src', 'main', 'java', packagePath,
|
|
256
|
+
'shared', 'infrastructure', 'configurations', 'rabbitmqConfig', 'RabbitMQConfig.java'
|
|
257
|
+
);
|
|
258
|
+
if (await fs.pathExists(rabbitConfigPath)) {
|
|
259
|
+
backups.rabbitConfig = { path: rabbitConfigPath, content: await fs.readFile(rabbitConfigPath, 'utf-8') };
|
|
260
|
+
}
|
|
261
|
+
// When broker is not installed, omit the key entirely — restoreFromH2() iterates
|
|
248
262
|
// Object.values(backups) and would crash trying to destructure a null entry.
|
|
249
263
|
|
|
250
264
|
// Persist backups BEFORE writing any file so a crash mid-swap is recoverable
|
|
@@ -267,6 +281,11 @@ async function swapToH2(projectDir, packageName, configManager) {
|
|
|
267
281
|
/\n?[ \t]*\/\/ Kafka\n[ \t]*implementation 'org\.springframework\.kafka:spring-kafka'\n[ \t]*testImplementation 'org\.springframework\.kafka:spring-kafka-test'\n\n?[ \t]*/,
|
|
268
282
|
'\n\t'
|
|
269
283
|
);
|
|
284
|
+
// Remove spring-amqp dependencies block when RabbitMQ is installed
|
|
285
|
+
swapped = swapped.replace(
|
|
286
|
+
/\n?[ \t]*\/\/ RabbitMQ\n[ \t]*implementation 'org\.springframework\.boot:spring-boot-starter-amqp'\n[ \t]*testImplementation 'org\.springframework\.amqp:spring-rabbit-test'\n\n?[ \t]*/,
|
|
287
|
+
'\n\t'
|
|
288
|
+
);
|
|
270
289
|
await fs.writeFile(buildGradlePath, swapped, 'utf-8');
|
|
271
290
|
}
|
|
272
291
|
|
|
@@ -278,11 +297,16 @@ async function swapToH2(projectDir, packageName, configManager) {
|
|
|
278
297
|
await fs.remove(kafkaConfigPath);
|
|
279
298
|
}
|
|
280
299
|
|
|
300
|
+
// ── Remove RabbitMQConfig.java (restored from backup on eva build) ────────
|
|
301
|
+
if (backups.rabbitConfig) {
|
|
302
|
+
await fs.remove(rabbitConfigPath);
|
|
303
|
+
}
|
|
304
|
+
|
|
281
305
|
return backups;
|
|
282
306
|
}
|
|
283
307
|
|
|
284
308
|
/**
|
|
285
|
-
* Backup and swap ONLY the broker layer (Kafka → Spring Event bus).
|
|
309
|
+
* Backup and swap ONLY the broker layer (Kafka/RabbitMQ → Spring Event bus).
|
|
286
310
|
* Database config (db.yaml) and SecurityConfig.java are left untouched.
|
|
287
311
|
* Persists backups to .eva4j.json with _mockOnlyBroker = true.
|
|
288
312
|
*/
|
|
@@ -305,15 +329,29 @@ async function swapBrokerOnly(projectDir, packageName, configManager) {
|
|
|
305
329
|
backups.kafkaConfig = { path: kafkaConfigPath, content: await fs.readFile(kafkaConfigPath, 'utf-8') };
|
|
306
330
|
}
|
|
307
331
|
|
|
332
|
+
// ── RabbitMQConfig.java ───────────────────────────────────────────────────
|
|
333
|
+
const rabbitConfigPath = path.join(
|
|
334
|
+
projectDir, 'src', 'main', 'java', packagePath,
|
|
335
|
+
'shared', 'infrastructure', 'configurations', 'rabbitmqConfig', 'RabbitMQConfig.java'
|
|
336
|
+
);
|
|
337
|
+
if (await fs.pathExists(rabbitConfigPath)) {
|
|
338
|
+
backups.rabbitConfig = { path: rabbitConfigPath, content: await fs.readFile(rabbitConfigPath, 'utf-8') };
|
|
339
|
+
}
|
|
340
|
+
|
|
308
341
|
// Persist backups BEFORE writing any file so a crash mid-swap is recoverable
|
|
309
342
|
await configManager.saveMockBackup(backups, { onlyBroker: true });
|
|
310
343
|
|
|
311
344
|
// ── Remove spring-kafka from build.gradle (keep existing DB driver line) ──
|
|
312
345
|
if (backups.buildGradle) {
|
|
313
|
-
|
|
346
|
+
let swapped = backups.buildGradle.content.replace(
|
|
314
347
|
/\n?[ \t]*\/\/ Kafka\n[ \t]*implementation 'org\.springframework\.kafka:spring-kafka'\n[ \t]*testImplementation 'org\.springframework\.kafka:spring-kafka-test'\n\n?[ \t]*/,
|
|
315
348
|
'\n\t'
|
|
316
349
|
);
|
|
350
|
+
// Remove spring-amqp dependencies block when RabbitMQ is installed
|
|
351
|
+
swapped = swapped.replace(
|
|
352
|
+
/\n?[ \t]*\/\/ RabbitMQ\n[ \t]*implementation 'org\.springframework\.boot:spring-boot-starter-amqp'\n[ \t]*testImplementation 'org\.springframework\.amqp:spring-rabbit-test'\n\n?[ \t]*/,
|
|
353
|
+
'\n\t'
|
|
354
|
+
);
|
|
317
355
|
await fs.writeFile(buildGradlePath, swapped, 'utf-8');
|
|
318
356
|
}
|
|
319
357
|
|
|
@@ -322,6 +360,11 @@ async function swapBrokerOnly(projectDir, packageName, configManager) {
|
|
|
322
360
|
await fs.remove(kafkaConfigPath);
|
|
323
361
|
}
|
|
324
362
|
|
|
363
|
+
// ── Remove RabbitMQConfig.java (restored from backup on eva build) ────────
|
|
364
|
+
if (backups.rabbitConfig) {
|
|
365
|
+
await fs.remove(rabbitConfigPath);
|
|
366
|
+
}
|
|
367
|
+
|
|
325
368
|
return backups;
|
|
326
369
|
}
|
|
327
370
|
|
|
@@ -343,6 +386,38 @@ async function restoreFromH2(configManager) {
|
|
|
343
386
|
return Object.keys(backups).length;
|
|
344
387
|
}
|
|
345
388
|
|
|
389
|
+
// ── Temporal module queue helper ─────────────────────────────────────────────
|
|
390
|
+
/**
|
|
391
|
+
* Append module-specific queue configuration to temporal.yaml files (idempotent).
|
|
392
|
+
* Same logic as generate-temporal-flow.js appendModuleQueues().
|
|
393
|
+
*/
|
|
394
|
+
async function appendTemporalModuleQueues(projectDir, moduleName, moduleScreamingSnake) {
|
|
395
|
+
const resourcesDir = path.join(projectDir, 'src', 'main', 'resources', 'parameters');
|
|
396
|
+
|
|
397
|
+
const moduleSection = [
|
|
398
|
+
` ${moduleName}:`,
|
|
399
|
+
` flow-queue: ${moduleScreamingSnake}_WORKFLOW_QUEUE`,
|
|
400
|
+
` heavy-queue: ${moduleScreamingSnake}_HEAVY_TASK_QUEUE`,
|
|
401
|
+
` light-queue: ${moduleScreamingSnake}_LIGHT_TASK_QUEUE`,
|
|
402
|
+
].join('\n');
|
|
403
|
+
|
|
404
|
+
for (const env of ENVS) {
|
|
405
|
+
const yamlPath = path.join(resourcesDir, env, 'temporal.yaml');
|
|
406
|
+
if (!(await fs.pathExists(yamlPath))) continue;
|
|
407
|
+
|
|
408
|
+
let content = await fs.readFile(yamlPath, 'utf-8');
|
|
409
|
+
if (content.includes(`${moduleName}:`)) continue;
|
|
410
|
+
|
|
411
|
+
if (!content.includes('modules:')) {
|
|
412
|
+
content = content.trimEnd() + '\n modules:\n' + moduleSection + '\n';
|
|
413
|
+
} else {
|
|
414
|
+
content = content.trimEnd() + '\n' + moduleSection + '\n';
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
await fs.writeFile(yamlPath, content, 'utf-8');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
346
421
|
// ── Main build command ──────────────────────────────────────────────────────────
|
|
347
422
|
async function buildCommand(options = {}) {
|
|
348
423
|
const projectDir = process.cwd();
|
|
@@ -400,6 +475,39 @@ async function buildCommand(options = {}) {
|
|
|
400
475
|
}
|
|
401
476
|
}
|
|
402
477
|
|
|
478
|
+
// ── Clean up stale mock adapter files ──────────────────────────────────────
|
|
479
|
+
// Mock mode always generates adapters under kafkaMessageBroker/ regardless of
|
|
480
|
+
// the installed broker. When the real broker is RabbitMQ, those files would
|
|
481
|
+
// coexist with the real rabbitmqMessageBroker/ adapters after restore,
|
|
482
|
+
// causing a ConflictingBeanDefinitionException. Remove them proactively.
|
|
483
|
+
if (await configManager.featureExists('rabbitmq')) {
|
|
484
|
+
const { packageName: pkgCleanup } = projectConfig;
|
|
485
|
+
const pkgPathCleanup = toPackagePath(pkgCleanup);
|
|
486
|
+
const modules = projectConfig.modules || [];
|
|
487
|
+
for (const mod of modules) {
|
|
488
|
+
const mockAdapterDir = path.join(
|
|
489
|
+
projectDir, 'src', 'main', 'java', pkgPathCleanup,
|
|
490
|
+
mod, 'infrastructure', 'adapters', 'kafkaMessageBroker'
|
|
491
|
+
);
|
|
492
|
+
if (await fs.pathExists(mockAdapterDir)) {
|
|
493
|
+
// Safety check: only remove if the files are mock implementations
|
|
494
|
+
const files = await fs.readdir(mockAdapterDir);
|
|
495
|
+
const isMockDir = files.length > 0 && await (async () => {
|
|
496
|
+
for (const f of files) {
|
|
497
|
+
const content = await fs.readFile(path.join(mockAdapterDir, f), 'utf-8');
|
|
498
|
+
if (content.includes('ApplicationEventPublisher') && !content.includes('KafkaTemplate')) {
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return false;
|
|
503
|
+
})();
|
|
504
|
+
if (isMockDir) {
|
|
505
|
+
await fs.remove(mockAdapterDir);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
403
511
|
console.log(chalk.green(' ✅ Configuration restored to original.\n'));
|
|
404
512
|
}
|
|
405
513
|
|
|
@@ -423,18 +531,24 @@ async function buildCommand(options = {}) {
|
|
|
423
531
|
|
|
424
532
|
const backups = await swapBrokerOnly(projectDir, pkgName, configManager);
|
|
425
533
|
const hasKafkaBackup = !!backups.kafkaConfig;
|
|
534
|
+
const hasRabbitBackup = !!backups.rabbitConfig;
|
|
535
|
+
const hasBrokerBackup = hasKafkaBackup || hasRabbitBackup;
|
|
426
536
|
console.log(chalk.green(` ✅ ${Object.keys(backups).length} file(s) backed up and replaced`));
|
|
427
537
|
if (hasKafkaBackup) {
|
|
428
538
|
console.log(chalk.green(' ✅ KafkaConfig.java removed (Spring Events will be used instead)'));
|
|
429
539
|
console.log(chalk.green(' ✅ spring-kafka dependencies removed from build.gradle'));
|
|
430
540
|
}
|
|
541
|
+
if (hasRabbitBackup) {
|
|
542
|
+
console.log(chalk.green(' ✅ RabbitMQConfig.java removed (Spring Events will be used instead)'));
|
|
543
|
+
console.log(chalk.green(' ✅ spring-amqp dependencies removed from build.gradle'));
|
|
544
|
+
}
|
|
431
545
|
console.log(chalk.gray(' Backup saved to .eva4j.json — will be restored on next eva build\n'));
|
|
432
546
|
|
|
433
|
-
// ── Regenerate broker layer if system.yaml exists and
|
|
547
|
+
// ── Regenerate broker layer if system.yaml exists and broker was installed
|
|
434
548
|
const systemDirBo = path.join(projectDir, 'system');
|
|
435
549
|
const systemYamlPathBo = path.join(systemDirBo, 'system.yaml');
|
|
436
550
|
|
|
437
|
-
if (
|
|
551
|
+
if (hasBrokerBackup && (await fs.pathExists(systemYamlPathBo))) {
|
|
438
552
|
let systemConfigBo;
|
|
439
553
|
try {
|
|
440
554
|
const content = await fs.readFile(systemYamlPathBo, 'utf-8');
|
|
@@ -475,7 +589,7 @@ async function buildCommand(options = {}) {
|
|
|
475
589
|
await generateEntitiesCommand(mod.name, { force: false, brokerMode: 'mock' });
|
|
476
590
|
}
|
|
477
591
|
}
|
|
478
|
-
} else if (
|
|
592
|
+
} else if (hasBrokerBackup) {
|
|
479
593
|
console.log(chalk.yellow(' ℹ️ No system/system.yaml found — broker files must be regenerated manually.'));
|
|
480
594
|
console.log(chalk.yellow(' Run: eva g entities <module> (with --force if needed)'));
|
|
481
595
|
}
|
|
@@ -495,18 +609,24 @@ async function buildCommand(options = {}) {
|
|
|
495
609
|
|
|
496
610
|
const backups = await swapToH2(projectDir, pkgName, configManager);
|
|
497
611
|
const hasKafkaBackup = !!backups.kafkaConfig;
|
|
612
|
+
const hasRabbitBackup = !!backups.rabbitConfig;
|
|
613
|
+
const hasBrokerBackup = hasKafkaBackup || hasRabbitBackup;
|
|
498
614
|
console.log(chalk.green(` ✅ ${Object.keys(backups).length} file(s) backed up and replaced`));
|
|
499
615
|
if (hasKafkaBackup) {
|
|
500
616
|
console.log(chalk.green(' ✅ KafkaConfig.java removed (Spring Events will be used instead)'));
|
|
501
617
|
console.log(chalk.green(' ✅ spring-kafka dependencies removed from build.gradle'));
|
|
502
618
|
}
|
|
619
|
+
if (hasRabbitBackup) {
|
|
620
|
+
console.log(chalk.green(' ✅ RabbitMQConfig.java removed (Spring Events will be used instead)'));
|
|
621
|
+
console.log(chalk.green(' ✅ spring-amqp dependencies removed from build.gradle'));
|
|
622
|
+
}
|
|
503
623
|
console.log(chalk.gray(' Backup saved to .eva4j.json — will be restored on next eva build\n'));
|
|
504
624
|
|
|
505
|
-
// ── Regenerate broker layer if system.yaml exists and
|
|
625
|
+
// ── Regenerate broker layer if system.yaml exists and broker was installed ──
|
|
506
626
|
const systemDir = path.join(projectDir, 'system');
|
|
507
627
|
const systemYamlPath = path.join(systemDir, 'system.yaml');
|
|
508
628
|
|
|
509
|
-
if (
|
|
629
|
+
if (hasBrokerBackup && (await fs.pathExists(systemYamlPath))) {
|
|
510
630
|
let systemConfig;
|
|
511
631
|
try {
|
|
512
632
|
const content = await fs.readFile(systemYamlPath, 'utf-8');
|
|
@@ -549,7 +669,7 @@ async function buildCommand(options = {}) {
|
|
|
549
669
|
await generateEntitiesCommand(mod.name, { force: false, brokerMode: 'mock' });
|
|
550
670
|
}
|
|
551
671
|
}
|
|
552
|
-
} else if (
|
|
672
|
+
} else if (hasBrokerBackup) {
|
|
553
673
|
console.log(chalk.yellow(' ℹ️ No system/system.yaml found — broker files must be regenerated manually.'));
|
|
554
674
|
console.log(chalk.yellow(' Run: eva g entities <module> (with --force if needed)'));
|
|
555
675
|
}
|
|
@@ -583,6 +703,8 @@ async function buildCommand(options = {}) {
|
|
|
583
703
|
}
|
|
584
704
|
|
|
585
705
|
const { modules = [], messaging } = systemConfig;
|
|
706
|
+
const orchestration = systemConfig.orchestration;
|
|
707
|
+
const temporalEnabled = orchestration && orchestration.enabled === true && orchestration.engine === 'temporal';
|
|
586
708
|
|
|
587
709
|
if (!modules.length) {
|
|
588
710
|
console.log(chalk.yellow('⚠️ No modules defined in system/system.yaml'));
|
|
@@ -592,6 +714,9 @@ async function buildCommand(options = {}) {
|
|
|
592
714
|
console.log(chalk.blue('\n🏗️ eva build\n'));
|
|
593
715
|
console.log(chalk.gray(` Project : ${projectConfig.projectName || projectConfig.artifactId}`));
|
|
594
716
|
console.log(chalk.gray(` Modules : ${modules.map(m => m.name).join(', ')}`));
|
|
717
|
+
if (temporalEnabled) {
|
|
718
|
+
console.log(chalk.gray(` Orchestr: Temporal (${orchestration.temporal && orchestration.temporal.namespace || 'default'})`));
|
|
719
|
+
}
|
|
595
720
|
console.log();
|
|
596
721
|
|
|
597
722
|
// ── STEP 1: Create modules ───────────────────────────────────────────────
|
|
@@ -633,12 +758,34 @@ async function buildCommand(options = {}) {
|
|
|
633
758
|
console.log(chalk.cyan(' ➕ Installing kafka-client'));
|
|
634
759
|
await addKafkaClientCommand();
|
|
635
760
|
}
|
|
761
|
+
} else if (broker === 'rabbitmq') {
|
|
762
|
+
if (await configManager.featureExists('rabbitmq')) {
|
|
763
|
+
console.log(chalk.gray(' ⏭ rabbitmq-client — already installed, skipping'));
|
|
764
|
+
} else {
|
|
765
|
+
console.log(chalk.cyan(' ➕ Installing rabbitmq-client'));
|
|
766
|
+
await addRabbitMQClientCommand();
|
|
767
|
+
}
|
|
636
768
|
} else {
|
|
637
|
-
console.log(chalk.yellow(` ⚠️ Broker '${broker}' is not supported by eva build (only kafka
|
|
769
|
+
console.log(chalk.yellow(` ⚠️ Broker '${broker}' is not supported by eva build (only kafka and rabbitmq are supported)`));
|
|
638
770
|
}
|
|
639
771
|
|
|
640
772
|
console.log();
|
|
641
773
|
|
|
774
|
+
// ── STEP 2b: Install Temporal client (if orchestration configured) ──────
|
|
775
|
+
if (temporalEnabled) {
|
|
776
|
+
console.log(chalk.blue('━━━ Step 2b: Installing Temporal client ━━━━━━━━━━━━━━━━━━━━━━'));
|
|
777
|
+
|
|
778
|
+
if (await configManager.featureExists('temporal')) {
|
|
779
|
+
console.log(chalk.gray(' ⏭ temporal-client — already installed, skipping'));
|
|
780
|
+
} else {
|
|
781
|
+
console.log(chalk.cyan(' ➕ Installing temporal-client'));
|
|
782
|
+
await addTemporalClientCommand();
|
|
783
|
+
await configManager.loadProjectConfig();
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
console.log();
|
|
787
|
+
}
|
|
788
|
+
|
|
642
789
|
// ── STEP 3: Copy domain.yaml files ──────────────────────────────────────
|
|
643
790
|
console.log(chalk.blue('━━━ Step 3: Copying domain.yaml files ━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
644
791
|
|
|
@@ -684,6 +831,115 @@ async function buildCommand(options = {}) {
|
|
|
684
831
|
|
|
685
832
|
console.log();
|
|
686
833
|
|
|
834
|
+
// ── STEP 4b+4c: Generate Temporal infrastructure & workflows ─────────────
|
|
835
|
+
if (temporalEnabled && (await configManager.featureExists('temporal'))) {
|
|
836
|
+
const rawWorkflows = Array.isArray(systemConfig.workflows) ? systemConfig.workflows : [];
|
|
837
|
+
|
|
838
|
+
// Collect all modules that participate in Temporal (trigger or target)
|
|
839
|
+
const temporalModulesSet = new Set();
|
|
840
|
+
for (const wf of rawWorkflows) {
|
|
841
|
+
if (wf.trigger && wf.trigger.module) temporalModulesSet.add(toCamelCase(wf.trigger.module));
|
|
842
|
+
for (const step of wf.steps || []) {
|
|
843
|
+
if (step.target) temporalModulesSet.add(toCamelCase(step.target));
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
// Also include modules with activities: in domain.yaml
|
|
847
|
+
for (const mod of modules) {
|
|
848
|
+
const modCamel = toCamelCase(mod.name);
|
|
849
|
+
const domYaml = path.join(projectDir, 'src', 'main', 'java', packagePath, modCamel, 'domain.yaml');
|
|
850
|
+
if (await fs.pathExists(domYaml)) {
|
|
851
|
+
const data = yaml.load(await fs.readFile(domYaml, 'utf-8'));
|
|
852
|
+
if (data && Array.isArray(data.activities) && data.activities.length > 0) {
|
|
853
|
+
temporalModulesSet.add(modCamel);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (temporalModulesSet.size > 0) {
|
|
859
|
+
// ── Step 4b: Module-level Temporal infrastructure ────────────────────
|
|
860
|
+
console.log(chalk.blue('━━━ Step 4b: Generating Temporal module infrastructure ━━━━━━━'));
|
|
861
|
+
|
|
862
|
+
const temporalFlowTemplates = path.join(__dirname, '..', '..', 'templates', 'temporal-flow');
|
|
863
|
+
|
|
864
|
+
for (const modCamel of temporalModulesSet) {
|
|
865
|
+
const modPascal = toPascalCase(modCamel);
|
|
866
|
+
const modScreamingSnake = toScreamingSnakeCase(modCamel);
|
|
867
|
+
const moduleBasePath = path.join(projectDir, 'src', 'main', 'java', packagePath, modCamel);
|
|
868
|
+
if (!(await fs.pathExists(moduleBasePath))) continue;
|
|
869
|
+
|
|
870
|
+
const checksumMgr = new ChecksumManager(moduleBasePath);
|
|
871
|
+
await checksumMgr.load();
|
|
872
|
+
const wOpts = { force: generateOptions.force, checksumManager: checksumMgr };
|
|
873
|
+
|
|
874
|
+
const ctx = {
|
|
875
|
+
packageName,
|
|
876
|
+
moduleName: modCamel,
|
|
877
|
+
modulePascalCase: modPascal,
|
|
878
|
+
moduleCamelCase: modCamel,
|
|
879
|
+
moduleScreamingSnake: modScreamingSnake,
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
// Marker interfaces (idempotent)
|
|
883
|
+
const interfacesDir = path.join(moduleBasePath, 'domain', 'interfaces');
|
|
884
|
+
await renderAndWrite(
|
|
885
|
+
path.join(temporalFlowTemplates, 'ModuleHeavyActivity.java.ejs'),
|
|
886
|
+
path.join(interfacesDir, `${modPascal}HeavyActivity.java`),
|
|
887
|
+
ctx, wOpts
|
|
888
|
+
);
|
|
889
|
+
await renderAndWrite(
|
|
890
|
+
path.join(temporalFlowTemplates, 'ModuleLightActivity.java.ejs'),
|
|
891
|
+
path.join(interfacesDir, `${modPascal}LightActivity.java`),
|
|
892
|
+
ctx, wOpts
|
|
893
|
+
);
|
|
894
|
+
|
|
895
|
+
// Worker config (idempotent)
|
|
896
|
+
const configDir = path.join(moduleBasePath, 'infrastructure', 'configurations');
|
|
897
|
+
await renderAndWrite(
|
|
898
|
+
path.join(temporalFlowTemplates, 'ModuleTemporalWorkerConfig.java.ejs'),
|
|
899
|
+
path.join(configDir, `${modPascal}TemporalWorkerConfig.java`),
|
|
900
|
+
ctx, wOpts
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
// temporal.yaml queues
|
|
904
|
+
await appendTemporalModuleQueues(projectDir, modCamel, modScreamingSnake);
|
|
905
|
+
|
|
906
|
+
await checksumMgr.save();
|
|
907
|
+
console.log(chalk.green(` ✅ ${modCamel} — Temporal worker infrastructure`));
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Generate activities per module (YAML-driven, non-interactive)
|
|
911
|
+
for (const mod of modules) {
|
|
912
|
+
const modCamel = toCamelCase(mod.name);
|
|
913
|
+
if (!temporalModulesSet.has(modCamel)) continue;
|
|
914
|
+
|
|
915
|
+
const domYaml = path.join(projectDir, 'src', 'main', 'java', packagePath, modCamel, 'domain.yaml');
|
|
916
|
+
if (!(await fs.pathExists(domYaml))) continue;
|
|
917
|
+
const data = yaml.load(await fs.readFile(domYaml, 'utf-8'));
|
|
918
|
+
if (!data || !Array.isArray(data.activities) || data.activities.length === 0) continue;
|
|
919
|
+
|
|
920
|
+
console.log(chalk.cyan(` ⚙️ Generating activities for: ${mod.name}`));
|
|
921
|
+
try {
|
|
922
|
+
await generateTemporalActivityCommand(modCamel, null, { force: generateOptions.force, generateAll: true });
|
|
923
|
+
} catch (err) {
|
|
924
|
+
console.log(chalk.yellow(` ⚠️ Activity generation failed for ${mod.name}: ${err.message}`));
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
console.log();
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// ── Step 4c: Cross-module workflows from system.yaml ──────────────────
|
|
932
|
+
if (rawWorkflows.length > 0) {
|
|
933
|
+
console.log(chalk.blue('━━━ Step 4c: Generating Temporal workflows ━━━━━━━━━━━━━━━━━━'));
|
|
934
|
+
try {
|
|
935
|
+
await generateTemporalSystemCommand({ force: generateOptions.force });
|
|
936
|
+
} catch (err) {
|
|
937
|
+
console.log(chalk.yellow(` ⚠️ Temporal workflow generation failed: ${err.message}`));
|
|
938
|
+
}
|
|
939
|
+
console.log();
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
687
943
|
// ── STEP 5: Generate unified Postman collection ─────────────────────────
|
|
688
944
|
console.log(chalk.blue('━━━ Step 5: Generating unified Postman collection ━━━━━━━━━━━━'));
|
|
689
945
|
|