eva4j 1.0.13 → 1.0.14

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 (44) hide show
  1. package/AGENTS.md +51 -9
  2. package/DOMAIN_YAML_GUIDE.md +150 -0
  3. package/bin/eva4j.js +31 -1
  4. package/design-system.md +797 -0
  5. package/docs/commands/EVALUATE_SYSTEM.md +542 -0
  6. package/docs/commands/GENERATE_ENTITIES.md +196 -0
  7. package/docs/commands/INDEX.md +10 -1
  8. package/examples/domain-endpoints-relations.yaml +353 -0
  9. package/examples/domain-endpoints-versioned.yaml +144 -0
  10. package/examples/domain-endpoints.yaml +135 -0
  11. package/examples/system.yaml +289 -0
  12. package/package.json +1 -1
  13. package/src/commands/create.js +6 -3
  14. package/src/commands/evaluate-system.js +384 -0
  15. package/src/commands/generate-entities.js +677 -14
  16. package/src/commands/generate-kafka-event.js +59 -5
  17. package/src/commands/generate-system.js +243 -0
  18. package/src/generators/base-generator.js +9 -1
  19. package/src/utils/naming.js +3 -2
  20. package/src/utils/system-validator.js +314 -0
  21. package/src/utils/yaml-to-entity.js +31 -2
  22. package/templates/aggregate/AggregateRepository.java.ejs +5 -0
  23. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +9 -0
  24. package/templates/aggregate/DomainEventHandler.java.ejs +24 -20
  25. package/templates/aggregate/JpaRepository.java.ejs +5 -0
  26. package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1103 -0
  27. package/templates/base/root/skill-build-domain-yaml.ejs +292 -0
  28. package/templates/base/root/skill-build-system-yaml.ejs +252 -0
  29. package/templates/base/root/system.yaml.ejs +97 -0
  30. package/templates/crud/EndpointsController.java.ejs +178 -0
  31. package/templates/crud/FindByQuery.java.ejs +17 -0
  32. package/templates/crud/FindByQueryHandler.java.ejs +57 -0
  33. package/templates/crud/ScaffoldCommand.java.ejs +12 -0
  34. package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
  35. package/templates/crud/ScaffoldQuery.java.ejs +12 -0
  36. package/templates/crud/ScaffoldQueryHandler.java.ejs +40 -0
  37. package/templates/crud/SubEntityAddCommand.java.ejs +17 -0
  38. package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
  39. package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
  40. package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
  41. package/templates/crud/TransitionCommand.java.ejs +9 -0
  42. package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
  43. package/templates/evaluate/report.html.ejs +971 -0
  44. package/templates/kafka-event/Event.java.ejs +7 -0
@@ -0,0 +1,384 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const path = require('path');
5
+ const fs = require('fs-extra');
6
+ const yaml = require('js-yaml');
7
+ const http = require('http');
8
+ const ejs = require('ejs');
9
+ const ora = require('ora');
10
+
11
+ const { validateSystem } = require('../utils/system-validator');
12
+
13
+ // ── Module icon heuristic ────────────────────────────────────────────────────
14
+
15
+ const ICON_RULES = [
16
+ [/payment|billing|invoice|charge/i, '💳'],
17
+ [/notification|alert|email|sms|message|notify/i, '🔔'],
18
+ [/customer|user|account|profile|member|client/i, '👤'],
19
+ [/movie|film|cinema|content|catalog|media/i, '🎬'],
20
+ [/theater|venue|seat|hall|screen/i, '🏛️'],
21
+ [/reservation|booking|ticket|order/i, '🎟️'],
22
+ [/product|item|inventory|catalog|stock/i, '🛍️'],
23
+ [/shipping|delivery|logistics|warehouse/i, '📦'],
24
+ [/auth|security|identity|session/i, '🔐'],
25
+ [/report|analytics|metric|stat/i, '📊'],
26
+ [/search|index|discover/i, '🔍'],
27
+ [/screening|schedule|program|event/i, '📽️'],
28
+ ];
29
+
30
+ const COLOR_PALETTE = [
31
+ '#4a9eff', // blue
32
+ '#9b6dff', // purple
33
+ '#f5c842', // gold
34
+ '#2dcc8f', // green
35
+ '#ff8c42', // orange
36
+ '#e63950', // red/accent
37
+ '#40c4d0', // teal
38
+ '#ff6bac', // pink
39
+ '#a8e063', // lime
40
+ '#ffa07a', // salmon
41
+ ];
42
+
43
+ function assignIcon(name) {
44
+ for (const [pattern, icon] of ICON_RULES) {
45
+ if (pattern.test(name)) return icon;
46
+ }
47
+ return '📁';
48
+ }
49
+
50
+ // ── Flow auto-generation from async events ───────────────────────────────────
51
+
52
+ // Maps trailing verb in an event name to the action verb used in useCases
53
+ const EVENT_VERB_MAP = {
54
+ created: 'Create',
55
+ confirmed: 'Confirm',
56
+ approved: 'Approve',
57
+ rejected: 'Reject',
58
+ cancelled: 'Cancel',
59
+ canceled: 'Cancel',
60
+ locked: 'Lock',
61
+ unlocked: 'Unlock',
62
+ expired: 'Expire',
63
+ scheduled: 'Schedule',
64
+ processed: 'Process',
65
+ published: 'Publish',
66
+ updated: 'Update',
67
+ deleted: 'Delete',
68
+ completed: 'Complete',
69
+ failed: 'Fail',
70
+ started: 'Start',
71
+ initiated: 'Initiate',
72
+ activated: 'Activate',
73
+ deactivated: 'Deactivate',
74
+ registered: 'Register',
75
+ requested: 'Request',
76
+ };
77
+
78
+ function extractEventVerb(eventName) {
79
+ // e.g. "ReservationCreatedEvent" → "created" → "Create"
80
+ const withoutSuffix = eventName.replace(/Event$/, '');
81
+ const parts = withoutSuffix.split(/(?=[A-Z])/); // split on uppercase
82
+ const lastWord = parts[parts.length - 1].toLowerCase();
83
+ return EVENT_VERB_MAP[lastWord] || null;
84
+ }
85
+
86
+ function extractEventSubject(eventName) {
87
+ // e.g. "ReservationCreatedEvent" → "Reservation"
88
+ const withoutSuffix = eventName.replace(/Event$/, '');
89
+ const verb = extractEventVerb(eventName);
90
+ if (!verb) return withoutSuffix;
91
+ const verbKey = Object.keys(EVENT_VERB_MAP).find(
92
+ (k) => EVENT_VERB_MAP[k] === verb
93
+ );
94
+ if (!verbKey) return withoutSuffix;
95
+ // Remove the trailing verb word from the event name
96
+ const verbCamel = verbKey.charAt(0).toUpperCase() + verbKey.slice(1);
97
+ return withoutSuffix.replace(new RegExp(verbCamel + '$', 'i'), '');
98
+ }
99
+
100
+ function findTriggerEndpoint(verb, producerName, modulesConfig) {
101
+ if (!verb) return null;
102
+ const mod = modulesConfig.find((m) => m.name === producerName);
103
+ if (!mod) return null;
104
+ return (mod.exposes || []).find((ep) => {
105
+ const uc = ep.useCase || '';
106
+ return uc.toLowerCase().startsWith(verb.toLowerCase()) || uc.includes(verb);
107
+ }) || null;
108
+ }
109
+
110
+ function buildEventFlows(systemConfig, modulesMap) {
111
+ const asyncEvents = (systemConfig.integrations || {}).async || [];
112
+ const syncIntegrations = (systemConfig.integrations || {}).sync || [];
113
+ const modulesConfig = systemConfig.modules || [];
114
+
115
+ const flows = [];
116
+
117
+ for (const ev of asyncEvents) {
118
+ const verb = extractEventVerb(ev.event);
119
+ const subject = extractEventSubject(ev.event);
120
+ const triggerEndpoint = findTriggerEndpoint(verb, ev.producer, modulesConfig);
121
+
122
+ const producerMod = modulesMap[ev.producer] || { color: '#888888', label: ev.producer, icon: '📁' };
123
+ const consumers = (ev.consumers || []).map((c) => (typeof c === 'string' ? c : c.module));
124
+
125
+ // Find sync calls made by this producer module that might be part of this action
126
+ const producerSyncCalls = syncIntegrations.filter((s) => s.caller === ev.producer);
127
+
128
+ const steps = [];
129
+
130
+ // Step 1: HTTP trigger (from client to producer)
131
+ if (triggerEndpoint) {
132
+ const syncCallsForStep = producerSyncCalls.map((s) => ({
133
+ to: s.calls,
134
+ label: (s.using || [])[0] || `GET /${s.calls}`,
135
+ port: s.port,
136
+ }));
137
+ steps.push({
138
+ id: 1,
139
+ type: 'http',
140
+ from: 'client',
141
+ to: ev.producer,
142
+ label: `${triggerEndpoint.method} ${triggerEndpoint.path}`,
143
+ desc: triggerEndpoint.description || `${verb} ${subject}`,
144
+ syncCalls: syncCallsForStep.length > 0 ? syncCallsForStep : undefined,
145
+ });
146
+ } else {
147
+ steps.push({
148
+ id: 1,
149
+ type: 'http',
150
+ from: 'client',
151
+ to: ev.producer,
152
+ label: `${verb || 'trigger'} /${subject.toLowerCase()}`,
153
+ desc: `Acción que desencadena el evento`,
154
+ });
155
+ }
156
+
157
+ // Step 2: Kafka event
158
+ steps.push({
159
+ id: 2,
160
+ type: 'event',
161
+ from: ev.producer,
162
+ event: ev.event,
163
+ topic: ev.topic,
164
+ to: consumers,
165
+ desc: `${ev.event} publicado en Kafka (topic: ${ev.topic})`,
166
+ });
167
+
168
+ // Step 3+: Consumer actions
169
+ for (let i = 0; i < consumers.length; i++) {
170
+ const consumer = consumers[i];
171
+ const consumerMod = modulesConfig.find((m) => m.name === consumer);
172
+ // Find a likely endpoint that the consumer would trigger on receiving this event
173
+ let actionLabel = `Procesa ${ev.event}`;
174
+ if (consumerMod) {
175
+ const verbLower = (verb || '').toLowerCase();
176
+ const match = (consumerMod.exposes || []).find((ep) => {
177
+ const uc = (ep.useCase || '').toLowerCase();
178
+ const method = (ep.method || '').toUpperCase();
179
+ return (uc.includes(verbLower) || uc.includes(subject.toLowerCase())) &&
180
+ (method === 'PUT' || method === 'PATCH' || method === 'POST');
181
+ });
182
+ if (match) {
183
+ actionLabel = `${match.useCase} (${match.method} ${match.path})`;
184
+ }
185
+ }
186
+ steps.push({
187
+ id: i + 3,
188
+ type: 'action',
189
+ from: consumer,
190
+ to: consumer,
191
+ label: actionLabel,
192
+ desc: `${consumer} reacciona al evento ${ev.event}`,
193
+ });
194
+ }
195
+
196
+ flows.push({
197
+ id: ev.event,
198
+ label: ev.event.replace(/Event$/, '').replace(/([A-Z])/g, ' $1').trim(),
199
+ icon: producerMod.icon || '📨',
200
+ description: `${ev.producer} → [${consumers.join(', ')}] vía topic ${ev.topic}`,
201
+ color: producerMod.color,
202
+ steps,
203
+ });
204
+ }
205
+
206
+ return flows;
207
+ }
208
+
209
+ // ── Data extraction ──────────────────────────────────────────────────────────
210
+
211
+ function extractReportData(systemConfig, validation) {
212
+ const modulesConfig = systemConfig.modules || [];
213
+ const asyncEvents = (systemConfig.integrations || {}).async || [];
214
+ const syncIntegrations = (systemConfig.integrations || {}).sync || [];
215
+
216
+ // Build modules map with color + icon
217
+ const modulesMap = {};
218
+ for (let i = 0; i < modulesConfig.length; i++) {
219
+ const mod = modulesConfig[i];
220
+ modulesMap[mod.name] = {
221
+ id: mod.name,
222
+ label: toPascalCase(mod.name),
223
+ icon: assignIcon(mod.name),
224
+ color: COLOR_PALETTE[i % COLOR_PALETTE.length],
225
+ desc: mod.description || mod.name,
226
+ };
227
+ }
228
+
229
+ // Normalize events (consumers can be strings or objects with .module)
230
+ const events = asyncEvents.map((ev) => ({
231
+ event: ev.event,
232
+ producer: ev.producer,
233
+ topic: ev.topic,
234
+ consumers: (ev.consumers || []).map((c) => (typeof c === 'string' ? c : c.module)),
235
+ }));
236
+
237
+ // Normalize sync integrations
238
+ const syncList = syncIntegrations.map((s) => ({
239
+ caller: s.caller,
240
+ calls: s.calls,
241
+ port: s.port || `${toPascalCase(s.calls)}Service`,
242
+ endpoints: s.using || [],
243
+ }));
244
+
245
+ // Build endpoints per module
246
+ const endpoints = {};
247
+ for (const mod of modulesConfig) {
248
+ endpoints[mod.name] = (mod.exposes || []).map((ep) => `${ep.method} ${ep.path}`);
249
+ }
250
+
251
+ // Auto-generate flows
252
+ const flows = buildEventFlows(systemConfig, modulesMap);
253
+
254
+ return {
255
+ systemName: (systemConfig.system || {}).name || 'eva4j system',
256
+ modules: Object.values(modulesMap),
257
+ events,
258
+ syncIntegrations: syncList,
259
+ endpoints,
260
+ flows,
261
+ validation,
262
+ generatedAt: new Date().toISOString(),
263
+ };
264
+ }
265
+
266
+ // ── Command ──────────────────────────────────────────────────────────────────
267
+
268
+ async function evaluateSystemCommand(type, options = {}) {
269
+ if (type !== 'system') {
270
+ console.error(chalk.red(`❌ Unknown evaluation type: '${type}'`));
271
+ console.log(chalk.gray("Usage: eva evaluate system"));
272
+ console.log(chalk.gray("Only 'system' is supported at this time."));
273
+ process.exit(1);
274
+ }
275
+
276
+ const port = parseInt(options.port || '3000', 10);
277
+ const outputPath = path.resolve(process.cwd(), options.output || './system-report.html');
278
+
279
+ // ── 1. Read system.yaml ─────────────────────────────────────────────────
280
+ const systemYamlPath = path.join(process.cwd(), 'system.yaml');
281
+ if (!(await fs.pathExists(systemYamlPath))) {
282
+ console.error(chalk.red('❌ system.yaml not found in current directory'));
283
+ console.error(chalk.gray('Run this command from the root of an eva4j project'));
284
+ process.exit(1);
285
+ }
286
+
287
+ let systemConfig;
288
+ try {
289
+ const content = await fs.readFile(systemYamlPath, 'utf-8');
290
+ systemConfig = yaml.load(content);
291
+ } catch (err) {
292
+ console.error(chalk.red('❌ Failed to parse system.yaml:'), err.message);
293
+ process.exit(1);
294
+ }
295
+
296
+ const spinner = ora('Analyzing system.yaml...').start();
297
+
298
+ // ── 2. Run validation ───────────────────────────────────────────────────
299
+ const validation = validateSystem(systemConfig);
300
+
301
+ // ── 3. Extract report data ──────────────────────────────────────────────
302
+ const reportData = extractReportData(systemConfig, validation);
303
+
304
+ // ── 4. Render HTML ──────────────────────────────────────────────────────
305
+ const templatePath = path.join(__dirname, '../../templates/evaluate/report.html.ejs');
306
+ let htmlContent;
307
+ try {
308
+ const templateContent = await fs.readFile(templatePath, 'utf-8');
309
+ htmlContent = ejs.render(templateContent, { data: reportData });
310
+ } catch (err) {
311
+ spinner.fail('Failed to render HTML template');
312
+ console.error(chalk.red(err.message));
313
+ process.exit(1);
314
+ }
315
+
316
+ // ── 5. Write HTML file ──────────────────────────────────────────────────
317
+ await fs.ensureDir(path.dirname(outputPath));
318
+ await fs.writeFile(outputPath, htmlContent, 'utf-8');
319
+
320
+ spinner.succeed(chalk.green('Analysis complete!'));
321
+
322
+ // ── 6. Print validation summary ─────────────────────────────────────────
323
+ console.log();
324
+ console.log(chalk.bold('📊 Validation Summary'));
325
+ console.log(chalk.gray('─'.repeat(40)));
326
+ console.log(
327
+ ` ${chalk.red('🔴 Errors:')} ${chalk.red.bold(validation.errors.length)}`
328
+ );
329
+ console.log(
330
+ ` ${chalk.yellow('🟡 Warnings:')} ${chalk.yellow.bold(validation.warnings.length)}`
331
+ );
332
+ console.log(
333
+ ` ${chalk.green('🟢 Passed:')} ${chalk.green.bold(validation.ok.length)}`
334
+ );
335
+ console.log(
336
+ ` ${chalk.blue('📈 Score:')} ${chalk.blue.bold(validation.score + '%')}`
337
+ );
338
+ console.log();
339
+
340
+ if (validation.errors.length > 0) {
341
+ console.log(chalk.red('Critical issues found:'));
342
+ validation.errors.forEach((e) => console.log(chalk.red(` • ${e}`)));
343
+ console.log();
344
+ }
345
+
346
+ if (validation.warnings.length > 0) {
347
+ console.log(chalk.yellow('Warnings:'));
348
+ validation.warnings.forEach((w) => console.log(chalk.yellow(` • ${w}`)));
349
+ console.log();
350
+ }
351
+
352
+ // ── 7. Start HTTP server ─────────────────────────────────────────────────
353
+ const server = http.createServer((req, res) => {
354
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
355
+ res.end(htmlContent);
356
+ });
357
+
358
+ server.listen(port, () => {
359
+ console.log(chalk.gray(`Report written to: ${outputPath}`));
360
+ console.log();
361
+ console.log(chalk.bold.green(`🌐 Server running at: http://localhost:${port}`));
362
+ console.log(chalk.gray('Open the URL in your browser to view the report'));
363
+ console.log(chalk.gray('Press Ctrl+C to stop\n'));
364
+ });
365
+
366
+ server.on('error', (err) => {
367
+ if (err.code === 'EADDRINUSE') {
368
+ console.error(chalk.red(`❌ Port ${port} is already in use. Try --port <other-port>`));
369
+ } else {
370
+ console.error(chalk.red('❌ Server error:'), err.message);
371
+ }
372
+ process.exit(1);
373
+ });
374
+ }
375
+
376
+ // ── Helpers ──────────────────────────────────────────────────────────────────
377
+
378
+ function toPascalCase(str) {
379
+ return str
380
+ .replace(/[-_ ]+(.)/g, (_, c) => c.toUpperCase())
381
+ .replace(/^(.)/, (c) => c.toUpperCase());
382
+ }
383
+
384
+ module.exports = evaluateSystemCommand;