bandit-cli 1.0.3 → 1.0.6
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/README.md +3 -0
- package/dist/cli/api.js +267 -32
- package/dist/cli/bench.js +12 -0
- package/dist/cli/doctor.js +175 -0
- package/dist/cli/output.js +41 -58
- package/dist/cli/studio.js +54 -0
- package/dist/index.js +25 -3
- package/dist/studio/db.js +96 -0
- package/dist/studio/scanner.js +66 -0
- package/dist/studio/server.js +284 -0
- package/dist/utils/monorepo.js +56 -0
- package/package.json +2 -2
package/dist/cli/output.js
CHANGED
|
@@ -6,71 +6,54 @@ function icon(status) {
|
|
|
6
6
|
return chalk.red("✖");
|
|
7
7
|
return chalk.gray("-");
|
|
8
8
|
}
|
|
9
|
-
function sevColor(sev, text) {
|
|
10
|
-
if (sev === "error")
|
|
11
|
-
return chalk.red(text);
|
|
12
|
-
if (sev === "warn")
|
|
13
|
-
return chalk.yellow(text);
|
|
14
|
-
return chalk.blue(text);
|
|
15
|
-
}
|
|
16
|
-
function sevLabel(sev) {
|
|
17
|
-
if (sev === "error")
|
|
18
|
-
return "ERROR";
|
|
19
|
-
if (sev === "warn")
|
|
20
|
-
return "WARN";
|
|
21
|
-
return "INFO";
|
|
22
|
-
}
|
|
23
9
|
function printSeparator() {
|
|
24
|
-
console.log(chalk.gray("─".repeat(
|
|
10
|
+
console.log(chalk.gray(" " + "─".repeat(70)));
|
|
25
11
|
}
|
|
26
12
|
function printItem(item) {
|
|
27
13
|
const statusIcon = icon(item.status);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
14
|
+
let titleText = chalk.bold(item.title);
|
|
15
|
+
if (item.status === "fail") {
|
|
16
|
+
if (item.severity === "error")
|
|
17
|
+
titleText += chalk.red.bold(" (ERROR)");
|
|
18
|
+
else if (item.severity === "warn")
|
|
19
|
+
titleText += chalk.yellow.bold(" (WARN)");
|
|
20
|
+
else
|
|
21
|
+
titleText += chalk.blue.bold(" (INFO)");
|
|
22
|
+
}
|
|
23
|
+
console.log(` ${statusIcon} ${titleText}`);
|
|
24
|
+
// Show details
|
|
31
25
|
if (item.details) {
|
|
32
|
-
console.log(chalk.gray(`
|
|
26
|
+
console.log(chalk.gray(` ↳ ${item.details}`));
|
|
33
27
|
}
|
|
34
28
|
// Show suggestion only for failures
|
|
35
29
|
if (item.status === "fail" && item.suggestion) {
|
|
36
|
-
console.log(chalk.
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
function printSection(title, items, color) {
|
|
40
|
-
if (items.length === 0)
|
|
41
|
-
return;
|
|
42
|
-
console.log("");
|
|
43
|
-
console.log(color(chalk.bold(`● ${title}`)));
|
|
44
|
-
printSeparator();
|
|
45
|
-
for (const item of items) {
|
|
46
|
-
printItem(item);
|
|
30
|
+
console.log(chalk.cyan(` ↳ Suggestion: ${item.suggestion}`));
|
|
47
31
|
}
|
|
48
32
|
}
|
|
49
33
|
export function printHuman(report) {
|
|
50
|
-
// Header
|
|
34
|
+
// Header Box
|
|
51
35
|
console.log("");
|
|
52
|
-
console.log(chalk.
|
|
53
|
-
console.log(chalk.bold.white(
|
|
54
|
-
console.log(chalk.
|
|
55
|
-
console.log(chalk.gray(
|
|
56
|
-
console.log(chalk.gray(
|
|
36
|
+
console.log(chalk.cyan(` ┌${"─".repeat(70)}┐`));
|
|
37
|
+
console.log(chalk.cyan(` │`) + chalk.bold.white(" BACKEND PROJECT AUDIT SUMMARY ") + chalk.cyan(`│`));
|
|
38
|
+
console.log(chalk.cyan(` └${"─".repeat(70)}┘`));
|
|
39
|
+
console.log(` ${chalk.gray("Project:")} ${chalk.bold(report.projectPath)}`);
|
|
40
|
+
console.log(` ${chalk.gray("Results:")} ${chalk.green(`${report.summary.pass} Passed`)}, ` +
|
|
41
|
+
`${chalk.red(`${report.summary.fail} Failed`)}, ` +
|
|
42
|
+
`${chalk.gray(`${report.summary.skip} Skipped`)}`);
|
|
57
43
|
// Severity Legend
|
|
58
44
|
console.log("");
|
|
59
|
-
console.log(chalk.bold("Severity Legend:")
|
|
60
|
-
|
|
61
|
-
chalk.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
console.log(chalk.blue(" ● INFO ") +
|
|
65
|
-
chalk.gray("- Informational items or best practices"));
|
|
66
|
-
// Group items by status first
|
|
45
|
+
console.log(` ${chalk.bold.gray("Severity Legend:")} ` +
|
|
46
|
+
`${chalk.red("● ERROR")} (Critical) ` +
|
|
47
|
+
`${chalk.yellow("● WARN")} (Important) ` +
|
|
48
|
+
`${chalk.blue("● INFO")} (Best Practice)`);
|
|
49
|
+
// Group items
|
|
67
50
|
const passedItems = report.items.filter((item) => item.status === "pass");
|
|
68
51
|
const failedItems = report.items.filter((item) => item.status === "fail");
|
|
69
52
|
const skippedItems = report.items.filter((item) => item.status === "skip");
|
|
70
53
|
// Print PASSED section
|
|
71
54
|
if (passedItems.length > 0) {
|
|
72
55
|
console.log("");
|
|
73
|
-
console.log(chalk.bold.green(
|
|
56
|
+
console.log(chalk.bold.green(` ✔ PASSED CHECKS (${passedItems.length})`));
|
|
74
57
|
printSeparator();
|
|
75
58
|
for (const item of passedItems) {
|
|
76
59
|
printItem(item);
|
|
@@ -79,16 +62,15 @@ export function printHuman(report) {
|
|
|
79
62
|
// Print FAILED sections grouped by severity
|
|
80
63
|
if (failedItems.length > 0) {
|
|
81
64
|
console.log("");
|
|
82
|
-
console.log(chalk.bold.red(
|
|
65
|
+
console.log(chalk.bold.red(` ✖ FAILED CHECKS (${failedItems.length})`));
|
|
83
66
|
printSeparator();
|
|
84
|
-
// Group failed items by severity
|
|
85
67
|
const failedErrors = failedItems.filter((item) => item.severity === "error");
|
|
86
68
|
const failedWarns = failedItems.filter((item) => item.severity === "warn");
|
|
87
69
|
const failedInfos = failedItems.filter((item) => item.severity === "info");
|
|
88
70
|
// Print failed ERRORS
|
|
89
71
|
if (failedErrors.length > 0) {
|
|
90
72
|
console.log("");
|
|
91
|
-
console.log(chalk.red(chalk.bold(`
|
|
73
|
+
console.log(chalk.red(chalk.bold(` ● ERRORS (${failedErrors.length})`)));
|
|
92
74
|
for (const item of failedErrors) {
|
|
93
75
|
console.log("");
|
|
94
76
|
printItem(item);
|
|
@@ -97,7 +79,7 @@ export function printHuman(report) {
|
|
|
97
79
|
// Print failed WARNINGS
|
|
98
80
|
if (failedWarns.length > 0) {
|
|
99
81
|
console.log("");
|
|
100
|
-
console.log(chalk.yellow(chalk.bold(`
|
|
82
|
+
console.log(chalk.yellow(chalk.bold(` ● WARNINGS (${failedWarns.length})`)));
|
|
101
83
|
for (const item of failedWarns) {
|
|
102
84
|
console.log("");
|
|
103
85
|
printItem(item);
|
|
@@ -106,7 +88,7 @@ export function printHuman(report) {
|
|
|
106
88
|
// Print failed INFO
|
|
107
89
|
if (failedInfos.length > 0) {
|
|
108
90
|
console.log("");
|
|
109
|
-
console.log(chalk.blue(chalk.bold(`
|
|
91
|
+
console.log(chalk.blue(chalk.bold(` ● INFO (${failedInfos.length})`)));
|
|
110
92
|
for (const item of failedInfos) {
|
|
111
93
|
console.log("");
|
|
112
94
|
printItem(item);
|
|
@@ -116,23 +98,24 @@ export function printHuman(report) {
|
|
|
116
98
|
// Print SKIPPED section (if any)
|
|
117
99
|
if (skippedItems.length > 0) {
|
|
118
100
|
console.log("");
|
|
119
|
-
console.log(chalk.bold.gray(
|
|
101
|
+
console.log(chalk.bold.gray(` - SKIPPED CHECKS (${skippedItems.length})`));
|
|
120
102
|
printSeparator();
|
|
121
103
|
for (const item of skippedItems) {
|
|
122
104
|
printItem(item);
|
|
123
105
|
}
|
|
124
106
|
}
|
|
125
|
-
// Footer
|
|
126
|
-
console.log("");
|
|
127
|
-
console.log(chalk.bold.blue(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`));
|
|
128
|
-
// Summary message
|
|
107
|
+
// Footer Box
|
|
129
108
|
const failCount = report.summary.fail;
|
|
109
|
+
console.log("");
|
|
130
110
|
if (failCount === 0) {
|
|
131
|
-
console.log(chalk.green.
|
|
111
|
+
console.log(chalk.green(` ┌${"─".repeat(70)}┐`));
|
|
112
|
+
console.log(chalk.green(` │`) + chalk.bold.green(" ✔ All audits passed successfully! ") + chalk.green(`│`));
|
|
113
|
+
console.log(chalk.green(` └${"─".repeat(70)}┘`));
|
|
132
114
|
}
|
|
133
115
|
else {
|
|
134
|
-
console.log(chalk.yellow(`
|
|
116
|
+
console.log(chalk.yellow(` ┌${"─".repeat(70)}┐`));
|
|
117
|
+
console.log(chalk.yellow(` │`) + chalk.bold.yellow(" ⚠ Audit completed with warnings/errors. Check issues above. ") + chalk.yellow(`│`));
|
|
118
|
+
console.log(chalk.yellow(` └${"─".repeat(70)}┘`));
|
|
135
119
|
}
|
|
136
|
-
console.log(chalk.bold.blue(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`));
|
|
137
120
|
console.log("");
|
|
138
121
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as clack from "@clack/prompts";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { exec } from "node:child_process";
|
|
4
|
+
import { startStudioServer } from "../studio/server.js";
|
|
5
|
+
export async function runStudioCommand(projectPath = ".") {
|
|
6
|
+
clack.intro(chalk.bold.bgBlue.white(" Bandit Studio "));
|
|
7
|
+
const s = clack.spinner();
|
|
8
|
+
s.start("Starting Bandit Studio local server...");
|
|
9
|
+
try {
|
|
10
|
+
const { server, port } = await startStudioServer(projectPath, 4000);
|
|
11
|
+
const url = `http://localhost:${port}`;
|
|
12
|
+
s.stop(`Bandit Studio server running at ${chalk.bold.cyan(url)}`);
|
|
13
|
+
clack.log.info("Press Ctrl+C to stop the studio server.");
|
|
14
|
+
let isShuttingDown = false;
|
|
15
|
+
const shutdown = () => {
|
|
16
|
+
if (isShuttingDown)
|
|
17
|
+
return;
|
|
18
|
+
isShuttingDown = true;
|
|
19
|
+
console.log(chalk.bold.yellow("\nStopping Bandit Studio server... Goodbye!"));
|
|
20
|
+
try {
|
|
21
|
+
server.close();
|
|
22
|
+
}
|
|
23
|
+
catch { }
|
|
24
|
+
process.exit(0);
|
|
25
|
+
};
|
|
26
|
+
process.on("SIGINT", shutdown);
|
|
27
|
+
process.on("SIGTERM", shutdown);
|
|
28
|
+
// Windows PowerShell / Terminal raw input listener for Ctrl+C (\u0003)
|
|
29
|
+
if (process.stdin.isTTY) {
|
|
30
|
+
try {
|
|
31
|
+
process.stdin.setRawMode(true);
|
|
32
|
+
process.stdin.resume();
|
|
33
|
+
process.stdin.on("data", (chunk) => {
|
|
34
|
+
const str = chunk.toString();
|
|
35
|
+
if (str === "\u0003" || str === "\u001b" || str.includes("\u0003")) {
|
|
36
|
+
shutdown();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
}
|
|
42
|
+
// Attempt auto-opening browser
|
|
43
|
+
const startCmd = process.platform === "win32" ? `start ${url}` : process.platform === "darwin" ? `open ${url}` : `xdg-open ${url}`;
|
|
44
|
+
exec(startCmd, (err) => {
|
|
45
|
+
if (err) {
|
|
46
|
+
clack.log.warn(`Could not open browser automatically. Please open ${url} in your browser.`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
s.stop("Failed to start Bandit Studio server.");
|
|
52
|
+
clack.log.error(`Server startup error: ${err.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,9 @@ program
|
|
|
14
14
|
.option("--json", "Print results as JSON")
|
|
15
15
|
.option("--fail-on-warn", "Exit with code 1 if warnings exist")
|
|
16
16
|
.action(async (projectPath, opts) => {
|
|
17
|
-
const
|
|
17
|
+
const { resolveProjectPath } = await import("./utils/monorepo.js");
|
|
18
|
+
const resolvedPath = await resolveProjectPath(projectPath || ".");
|
|
19
|
+
const results = await runAudit(resolvedPath);
|
|
18
20
|
if (opts.json) {
|
|
19
21
|
process.stdout.write(JSON.stringify(results, null, 2) + "\n");
|
|
20
22
|
}
|
|
@@ -42,8 +44,10 @@ program
|
|
|
42
44
|
.description("Audit local .env configurations against .env.example")
|
|
43
45
|
.argument("[path]", "Path to the project", ".")
|
|
44
46
|
.action(async (projectPath) => {
|
|
47
|
+
const { resolveProjectPath } = await import("./utils/monorepo.js");
|
|
48
|
+
const resolvedPath = await resolveProjectPath(projectPath || ".");
|
|
45
49
|
const { runEnvCommand } = await import("./cli/env.js");
|
|
46
|
-
await runEnvCommand(
|
|
50
|
+
await runEnvCommand(resolvedPath);
|
|
47
51
|
});
|
|
48
52
|
// Subcommand: doctor
|
|
49
53
|
program
|
|
@@ -65,8 +69,15 @@ program
|
|
|
65
69
|
.option("-t, --token <token>", "Bearer Auth token")
|
|
66
70
|
.option("-H, --header <headers...>", "Custom headers in Key:Value format")
|
|
67
71
|
.action(async (methodOrPath, routePath, opts) => {
|
|
72
|
+
const httpMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
|
|
73
|
+
const isDirectMode = routePath !== undefined && httpMethods.includes(methodOrPath.toUpperCase());
|
|
74
|
+
let targetPath = methodOrPath;
|
|
75
|
+
if (!isDirectMode) {
|
|
76
|
+
const { resolveProjectPath } = await import("./utils/monorepo.js");
|
|
77
|
+
targetPath = await resolveProjectPath(methodOrPath || ".");
|
|
78
|
+
}
|
|
68
79
|
const { runApiCommand } = await import("./cli/api.js");
|
|
69
|
-
await runApiCommand(
|
|
80
|
+
await runApiCommand(targetPath, routePath, opts);
|
|
70
81
|
});
|
|
71
82
|
// Subcommand: bench
|
|
72
83
|
program
|
|
@@ -89,6 +100,17 @@ program
|
|
|
89
100
|
const { runBenchCommand } = await import("./cli/bench.js");
|
|
90
101
|
await runBenchCommand(url, { connections, requests });
|
|
91
102
|
});
|
|
103
|
+
// Subcommand: studio
|
|
104
|
+
program
|
|
105
|
+
.command("studio")
|
|
106
|
+
.description("Launch Bandit Studio local web dashboard")
|
|
107
|
+
.argument("[path]", "Path to the project", ".")
|
|
108
|
+
.action(async (projectPath) => {
|
|
109
|
+
const { resolveProjectPath } = await import("./utils/monorepo.js");
|
|
110
|
+
const resolvedPath = await resolveProjectPath(projectPath || ".");
|
|
111
|
+
const { runStudioCommand } = await import("./cli/studio.js");
|
|
112
|
+
await runStudioCommand(resolvedPath);
|
|
113
|
+
});
|
|
92
114
|
// Default to help if no command specified
|
|
93
115
|
if (!process.argv.slice(2).length) {
|
|
94
116
|
program.outputHelp();
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
export class StudioDB {
|
|
5
|
+
baseDir;
|
|
6
|
+
constructor(projectPath = ".") {
|
|
7
|
+
this.baseDir = path.join(path.resolve(projectPath), ".bandit");
|
|
8
|
+
this.ensureDir();
|
|
9
|
+
}
|
|
10
|
+
ensureDir() {
|
|
11
|
+
if (!fs.existsSync(this.baseDir)) {
|
|
12
|
+
fs.mkdirSync(this.baseDir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
getFilePath(filename) {
|
|
16
|
+
return path.join(this.baseDir, filename);
|
|
17
|
+
}
|
|
18
|
+
readJson(filename, defaultValue) {
|
|
19
|
+
const file = this.getFilePath(filename);
|
|
20
|
+
if (!fs.existsSync(file))
|
|
21
|
+
return defaultValue;
|
|
22
|
+
try {
|
|
23
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
24
|
+
return JSON.parse(content);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return defaultValue;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
writeJson(filename, data) {
|
|
31
|
+
const file = this.getFilePath(filename);
|
|
32
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2), "utf-8");
|
|
33
|
+
}
|
|
34
|
+
getGitMetadata() {
|
|
35
|
+
try {
|
|
36
|
+
const commit = execSync("git rev-parse --short HEAD", { stdio: ["ignore", "pipe", "ignore"] })
|
|
37
|
+
.toString()
|
|
38
|
+
.trim();
|
|
39
|
+
const branch = execSync("git branch --show-current", { stdio: ["ignore", "pipe", "ignore"] })
|
|
40
|
+
.toString()
|
|
41
|
+
.trim();
|
|
42
|
+
const author = execSync('git log -1 --pretty=format:"%an"', { stdio: ["ignore", "pipe", "ignore"] })
|
|
43
|
+
.toString()
|
|
44
|
+
.trim();
|
|
45
|
+
return { commit, branch, author };
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
saveBenchmark(data) {
|
|
52
|
+
const records = this.readJson("benchmarks.json", []);
|
|
53
|
+
const git = this.getGitMetadata();
|
|
54
|
+
const newRecord = {
|
|
55
|
+
id: Math.random().toString(36).substring(2, 10),
|
|
56
|
+
timestamp: new Date().toISOString(),
|
|
57
|
+
gitCommit: git.commit,
|
|
58
|
+
gitBranch: git.branch,
|
|
59
|
+
gitAuthor: git.author,
|
|
60
|
+
...data,
|
|
61
|
+
};
|
|
62
|
+
records.unshift(newRecord);
|
|
63
|
+
this.writeJson("benchmarks.json", records.slice(0, 200));
|
|
64
|
+
return newRecord;
|
|
65
|
+
}
|
|
66
|
+
getBenchmarks() {
|
|
67
|
+
return this.readJson("benchmarks.json", []);
|
|
68
|
+
}
|
|
69
|
+
saveAudit(items) {
|
|
70
|
+
const records = this.readJson("audits.json", []);
|
|
71
|
+
const git = this.getGitMetadata();
|
|
72
|
+
const newRecord = {
|
|
73
|
+
id: Math.random().toString(36).substring(2, 10),
|
|
74
|
+
timestamp: new Date().toISOString(),
|
|
75
|
+
gitCommit: git.commit,
|
|
76
|
+
items,
|
|
77
|
+
};
|
|
78
|
+
records.unshift(newRecord);
|
|
79
|
+
this.writeJson("audits.json", records.slice(0, 50));
|
|
80
|
+
return newRecord;
|
|
81
|
+
}
|
|
82
|
+
getAudits() {
|
|
83
|
+
return this.readJson("audits.json", []);
|
|
84
|
+
}
|
|
85
|
+
getConfig() {
|
|
86
|
+
return this.readJson("config.json", { dismissedRules: [] });
|
|
87
|
+
}
|
|
88
|
+
dismissRule(ruleTitle) {
|
|
89
|
+
const config = this.getConfig();
|
|
90
|
+
if (!config.dismissedRules.includes(ruleTitle)) {
|
|
91
|
+
config.dismissedRules.push(ruleTitle);
|
|
92
|
+
this.writeJson("config.json", config);
|
|
93
|
+
}
|
|
94
|
+
return config;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { scanForRoutes } from "../cli/api.js";
|
|
2
|
+
import { StudioDB } from "./db.js";
|
|
3
|
+
export async function generateApiBlueprint(projectPath) {
|
|
4
|
+
const rawRoutes = await scanForRoutes(projectPath);
|
|
5
|
+
const db = new StudioDB(projectPath);
|
|
6
|
+
const benchmarks = db.getBenchmarks();
|
|
7
|
+
// Filter out non-route scans (like test files or CLI internal strings)
|
|
8
|
+
const routes = rawRoutes.filter(r => !r.sourceFile.includes("cli/") &&
|
|
9
|
+
!r.sourceFile.includes("doctor.") &&
|
|
10
|
+
!r.sourceFile.includes("api.") &&
|
|
11
|
+
r.routePath.startsWith("/") &&
|
|
12
|
+
!r.routePath.includes("-") // exclude header test strings like /access-control-allow-origin
|
|
13
|
+
);
|
|
14
|
+
const nodes = [];
|
|
15
|
+
const edges = [];
|
|
16
|
+
// Infrastructure Nodes
|
|
17
|
+
nodes.push({ id: "db:pg", label: "PostgreSQL Database", type: "database", details: "Primary Data Store & Connection Pool" });
|
|
18
|
+
nodes.push({ id: "cache:redis", label: "Redis Cache Layer", type: "cache", details: "In-Memory Response & Session Cache" });
|
|
19
|
+
const addedControllers = new Set();
|
|
20
|
+
const addedServices = new Set();
|
|
21
|
+
routes.forEach((r, idx) => {
|
|
22
|
+
const routeId = `route:${idx}`;
|
|
23
|
+
const routeLabel = `[${r.method}] ${r.routePath}`;
|
|
24
|
+
// Find matching telemetry if benchmarked
|
|
25
|
+
const matchBench = benchmarks.find(b => b.targetUrl.endsWith(r.routePath));
|
|
26
|
+
const metrics = matchBench ? { rps: matchBench.rps, p99: matchBench.latency.p99, avg: matchBench.latency.avg } : undefined;
|
|
27
|
+
nodes.push({
|
|
28
|
+
id: routeId,
|
|
29
|
+
label: routeLabel,
|
|
30
|
+
type: "route",
|
|
31
|
+
details: r.sourceFile,
|
|
32
|
+
metrics,
|
|
33
|
+
});
|
|
34
|
+
const controllerName = r.sourceFile.split(/[/\\]/).pop() || "Controller";
|
|
35
|
+
const controllerId = `controller:${controllerName}`;
|
|
36
|
+
if (!addedControllers.has(controllerId)) {
|
|
37
|
+
addedControllers.add(controllerId);
|
|
38
|
+
nodes.push({
|
|
39
|
+
id: controllerId,
|
|
40
|
+
label: controllerName,
|
|
41
|
+
type: "controller",
|
|
42
|
+
details: r.sourceFile,
|
|
43
|
+
});
|
|
44
|
+
// Map controller to corresponding service layer
|
|
45
|
+
const serviceName = controllerName.replace(".controller.", ".service.").replace("controller", "service");
|
|
46
|
+
const serviceId = `service:${serviceName}`;
|
|
47
|
+
if (!addedServices.has(serviceId)) {
|
|
48
|
+
addedServices.add(serviceId);
|
|
49
|
+
nodes.push({
|
|
50
|
+
id: serviceId,
|
|
51
|
+
label: serviceName,
|
|
52
|
+
type: "service",
|
|
53
|
+
details: `Business logic & DB ORM mapping`,
|
|
54
|
+
});
|
|
55
|
+
// Link Service to Infrastructure
|
|
56
|
+
edges.push({ from: serviceId, to: "db:pg", label: "queries" });
|
|
57
|
+
if (serviceName.toLowerCase().includes("product") || serviceName.toLowerCase().includes("catalog") || serviceName.toLowerCase().includes("cache")) {
|
|
58
|
+
edges.push({ from: serviceId, to: "cache:redis", label: "hits cache" });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
edges.push({ from: controllerId, to: serviceId, label: "delegates" });
|
|
62
|
+
}
|
|
63
|
+
edges.push({ from: routeId, to: controllerId, label: "handles" });
|
|
64
|
+
});
|
|
65
|
+
return { nodes, edges };
|
|
66
|
+
}
|