simplemdg-dev-cli 1.5.1 → 2.4.4

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 (140) hide show
  1. package/README.md +65 -243
  2. package/USER_GUIDE.md +55 -249
  3. package/dist/commands/cds.command.js +69 -60
  4. package/dist/commands/cds.command.js.map +1 -1
  5. package/dist/commands/cf-db.command.d.ts +2 -0
  6. package/dist/commands/cf-db.command.js +606 -0
  7. package/dist/commands/cf-db.command.js.map +1 -0
  8. package/dist/commands/cf.command.js +1625 -198
  9. package/dist/commands/cf.command.js.map +1 -1
  10. package/dist/commands/gitlab.command.d.ts +2 -0
  11. package/dist/commands/gitlab.command.js +351 -0
  12. package/dist/commands/gitlab.command.js.map +1 -0
  13. package/dist/commands/npmrc.command.js +50 -44
  14. package/dist/commands/npmrc.command.js.map +1 -1
  15. package/dist/core/cache.d.ts +1 -1
  16. package/dist/core/cache.js +58 -31
  17. package/dist/core/cache.js.map +1 -1
  18. package/dist/core/cds.js +32 -22
  19. package/dist/core/cds.js.map +1 -1
  20. package/dist/core/cf-env-parser.d.ts +1 -1
  21. package/dist/core/cf-env-parser.js +4 -1
  22. package/dist/core/cf-env-parser.js.map +1 -1
  23. package/dist/core/cf.d.ts +1 -1
  24. package/dist/core/cf.js +46 -31
  25. package/dist/core/cf.js.map +1 -1
  26. package/dist/core/db/db-btp.d.ts +48 -0
  27. package/dist/core/db/db-btp.js +162 -0
  28. package/dist/core/db/db-btp.js.map +1 -0
  29. package/dist/core/db/db-cache.d.ts +35 -0
  30. package/dist/core/db/db-cache.js +164 -0
  31. package/dist/core/db/db-cache.js.map +1 -0
  32. package/dist/core/db/db-connection.d.ts +22 -0
  33. package/dist/core/db/db-connection.js +73 -0
  34. package/dist/core/db/db-connection.js.map +1 -0
  35. package/dist/core/db/db-crypto.d.ts +3 -0
  36. package/dist/core/db/db-crypto.js +54 -0
  37. package/dist/core/db/db-crypto.js.map +1 -0
  38. package/dist/core/db/db-hana-adapter.d.ts +32 -0
  39. package/dist/core/db/db-hana-adapter.js +243 -0
  40. package/dist/core/db/db-hana-adapter.js.map +1 -0
  41. package/dist/core/db/db-metadata.d.ts +25 -0
  42. package/dist/core/db/db-metadata.js +150 -0
  43. package/dist/core/db/db-metadata.js.map +1 -0
  44. package/dist/core/db/db-postgres-adapter.d.ts +30 -0
  45. package/dist/core/db/db-postgres-adapter.js +245 -0
  46. package/dist/core/db/db-postgres-adapter.js.map +1 -0
  47. package/dist/core/db/db-query-files.d.ts +20 -0
  48. package/dist/core/db/db-query-files.js +106 -0
  49. package/dist/core/db/db-query-files.js.map +1 -0
  50. package/dist/core/db/db-query-history.d.ts +5 -0
  51. package/dist/core/db/db-query-history.js +49 -0
  52. package/dist/core/db/db-query-history.js.map +1 -0
  53. package/dist/core/db/db-row.d.ts +22 -0
  54. package/dist/core/db/db-row.js +70 -0
  55. package/dist/core/db/db-row.js.map +1 -0
  56. package/dist/core/db/db-studio-html.d.ts +4 -0
  57. package/dist/core/db/db-studio-html.js +437 -0
  58. package/dist/core/db/db-studio-html.js.map +1 -0
  59. package/dist/core/db/db-studio-server.d.ts +11 -0
  60. package/dist/core/db/db-studio-server.js +465 -0
  61. package/dist/core/db/db-studio-server.js.map +1 -0
  62. package/dist/core/db/db-types.d.ts +174 -0
  63. package/dist/core/db/db-types.js +3 -0
  64. package/dist/core/db/db-types.js.map +1 -0
  65. package/dist/core/db/db-vcap-parser.d.ts +7 -0
  66. package/dist/core/db/db-vcap-parser.js +137 -0
  67. package/dist/core/db/db-vcap-parser.js.map +1 -0
  68. package/dist/core/doctor.d.ts +1 -1
  69. package/dist/core/doctor.js +14 -8
  70. package/dist/core/doctor.js.map +1 -1
  71. package/dist/core/guide.js +31 -26
  72. package/dist/core/guide.js.map +1 -1
  73. package/dist/core/install.d.ts +1 -1
  74. package/dist/core/install.js +17 -11
  75. package/dist/core/install.js.map +1 -1
  76. package/dist/core/navigator.d.ts +17 -0
  77. package/dist/core/navigator.js +140 -0
  78. package/dist/core/navigator.js.map +1 -0
  79. package/dist/core/npmrc.js +29 -16
  80. package/dist/core/npmrc.js.map +1 -1
  81. package/dist/core/process.js +11 -6
  82. package/dist/core/process.js.map +1 -1
  83. package/dist/core/prompts.js +16 -8
  84. package/dist/core/prompts.js.map +1 -1
  85. package/dist/core/repository.d.ts +1 -1
  86. package/dist/core/repository.js +16 -9
  87. package/dist/core/repository.js.map +1 -1
  88. package/dist/core/scanner.d.ts +1 -1
  89. package/dist/core/scanner.js +13 -7
  90. package/dist/core/scanner.js.map +1 -1
  91. package/dist/core/tooling.d.ts +28 -0
  92. package/dist/core/tooling.js +168 -0
  93. package/dist/core/tooling.js.map +1 -0
  94. package/dist/core/types.js +2 -1
  95. package/dist/core/version-conflict.d.ts +2 -2
  96. package/dist/core/version-conflict.js +11 -6
  97. package/dist/core/version-conflict.js.map +1 -1
  98. package/dist/index.js +65 -48
  99. package/dist/index.js.map +1 -1
  100. package/dist/types-local.js +2 -1
  101. package/package.json +12 -6
  102. package/src/commands/cds.command.ts +529 -0
  103. package/src/commands/cf-db.command.ts +636 -0
  104. package/src/commands/cf.command.ts +3345 -0
  105. package/src/commands/gitlab.command.ts +373 -0
  106. package/src/commands/npmrc.command.ts +581 -0
  107. package/src/core/cache.ts +332 -0
  108. package/src/core/cds.ts +278 -0
  109. package/src/core/cf-env-parser.ts +131 -0
  110. package/src/core/cf.ts +271 -0
  111. package/src/core/db/db-btp.ts +207 -0
  112. package/src/core/db/db-cache.ts +215 -0
  113. package/src/core/db/db-connection.ts +79 -0
  114. package/src/core/db/db-crypto.ts +53 -0
  115. package/src/core/db/db-hana-adapter.ts +294 -0
  116. package/src/core/db/db-metadata.ts +174 -0
  117. package/src/core/db/db-postgres-adapter.ts +275 -0
  118. package/src/core/db/db-query-files.ts +130 -0
  119. package/src/core/db/db-query-history.ts +53 -0
  120. package/src/core/db/db-row.ts +93 -0
  121. package/src/core/db/db-studio-html.ts +439 -0
  122. package/src/core/db/db-studio-server.ts +559 -0
  123. package/src/core/db/db-types.ts +195 -0
  124. package/src/core/db/db-vcap-parser.ts +182 -0
  125. package/src/core/doctor.ts +70 -0
  126. package/src/core/guide.ts +261 -0
  127. package/src/core/install.ts +91 -0
  128. package/src/core/navigator.ts +164 -0
  129. package/src/core/npmrc.ts +171 -0
  130. package/src/core/process.ts +75 -0
  131. package/src/core/prompts.ts +225 -0
  132. package/src/core/repository.ts +36 -0
  133. package/src/core/scanner.ts +41 -0
  134. package/src/core/tooling.ts +207 -0
  135. package/src/core/types.ts +152 -0
  136. package/src/core/version-conflict.ts +46 -0
  137. package/src/index.ts +460 -0
  138. package/src/types/external.d.ts +3 -0
  139. package/src/types-local.ts +11 -0
  140. package/tsconfig.json +17 -0
@@ -0,0 +1,559 @@
1
+ import http from "node:http";
2
+ import net from "node:net";
3
+ import { execa } from "execa";
4
+ import chalk from "chalk";
5
+ import { renderStudioHtml } from "./db-studio-html";
6
+ import { StudioConnectionPool } from "./db-connection";
7
+ import {
8
+ duplicateConnection,
9
+ getResolvedConnection,
10
+ listPublicConnections,
11
+ removeConnection,
12
+ renameConnection,
13
+ upsertConnectionFromDraft,
14
+ } from "./db-cache";
15
+ import type { TConnectionDraft } from "./db-cache";
16
+ import { testConnectionProfile } from "./db-connection";
17
+ import {
18
+ analyzeSqlSafety,
19
+ appendSafeLimit,
20
+ generateCountSql,
21
+ generateCreateTableDdl,
22
+ generateSelectSql,
23
+ looksLikeProduction,
24
+ } from "./db-metadata";
25
+ import type { TResolvedDatabaseConnection } from "./db-types";
26
+ import {
27
+ deleteSavedQuery,
28
+ listSavedQueries,
29
+ renameSavedQuery,
30
+ saveQuery,
31
+ } from "./db-query-files";
32
+ import { deleteRow, insertRow, updateRow } from "./db-row";
33
+ import { appendQueryHistory, listQueryHistory } from "./db-query-history";
34
+ import {
35
+ detectAppDatabaseServices,
36
+ ensureCloudFoundrySession,
37
+ getCloudFoundryTargetSummary,
38
+ importConnectionFromApp,
39
+ listCloudFoundryAppsWithCache,
40
+ } from "./db-btp";
41
+ import type { TDatabaseObjectKind, TDatabaseType } from "./db-types";
42
+
43
+ export type TStudioServerOptions = {
44
+ port?: number;
45
+ readOnly?: boolean;
46
+ queryTimeoutMs?: number;
47
+ };
48
+
49
+ export type TStudioServerHandle = {
50
+ url: string;
51
+ port: number;
52
+ close: () => Promise<void>;
53
+ };
54
+
55
+ type TJsonBody = Record<string, unknown>;
56
+
57
+ function isPortAvailable(port: number): Promise<boolean> {
58
+ return new Promise((resolve) => {
59
+ const tester = net.createServer();
60
+ tester.once("error", () => resolve(false));
61
+ tester.once("listening", () => tester.close(() => resolve(true)));
62
+ tester.listen(port, "127.0.0.1");
63
+ });
64
+ }
65
+
66
+ async function findAvailablePort(preferredPort: number): Promise<number> {
67
+ for (let candidate = preferredPort; candidate < preferredPort + 50; candidate += 1) {
68
+ if (await isPortAvailable(candidate)) {
69
+ return candidate;
70
+ }
71
+ }
72
+
73
+ throw new Error(`No available port found between ${preferredPort} and ${preferredPort + 49}`);
74
+ }
75
+
76
+ async function readJsonBody(req: http.IncomingMessage): Promise<TJsonBody> {
77
+ const chunks: Buffer[] = [];
78
+
79
+ for await (const chunk of req) {
80
+ chunks.push(Buffer.from(chunk));
81
+ }
82
+
83
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
84
+
85
+ if (!raw) {
86
+ return {};
87
+ }
88
+
89
+ return JSON.parse(raw) as TJsonBody;
90
+ }
91
+
92
+ function sendJson(res: http.ServerResponse, value: unknown, status = 200): void {
93
+ const payload = JSON.stringify(value);
94
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
95
+ res.end(payload);
96
+ }
97
+
98
+ function sendText(res: http.ServerResponse, value: string, contentType: string, fileName?: string): void {
99
+ const headers: Record<string, string> = { "content-type": contentType };
100
+ if (fileName) {
101
+ headers["content-disposition"] = `attachment; filename="${fileName}"`;
102
+ }
103
+ res.writeHead(200, headers);
104
+ res.end(value);
105
+ }
106
+
107
+ function toCsv(fields: string[], rows: Array<Record<string, unknown>>): string {
108
+ const escapeCell = (value: unknown): string => {
109
+ const text = value === null || value === undefined ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
110
+ return `"${text.replace(/"/g, '""')}"`;
111
+ };
112
+
113
+ const header = fields.map(escapeCell).join(",");
114
+ const lines = rows.map((row) => fields.map((field) => escapeCell(row[field])).join(","));
115
+ return [header, ...lines].join("\n");
116
+ }
117
+
118
+ function getString(body: TJsonBody, key: string): string {
119
+ const value = body[key];
120
+ return typeof value === "string" ? value : "";
121
+ }
122
+
123
+ function getNumber(body: TJsonBody, key: string, fallback: number): number {
124
+ const value = Number(body[key]);
125
+ return Number.isFinite(value) ? value : fallback;
126
+ }
127
+
128
+ function draftFromBody(body: TJsonBody): TConnectionDraft {
129
+ const type = getString(body, "type") === "hana" ? "hana" : "postgresql";
130
+ return {
131
+ name: getString(body, "name") || `${type} connection`,
132
+ type,
133
+ host: getString(body, "host"),
134
+ port: getNumber(body, "port", type === "hana" ? 443 : 5432),
135
+ database: getString(body, "database") || undefined,
136
+ schema: getString(body, "schema") || undefined,
137
+ username: getString(body, "username"),
138
+ password: getString(body, "password"),
139
+ ssl: body.ssl === undefined ? true : Boolean(body.ssl),
140
+ sslValidateCertificate: Boolean(body.sslValidateCertificate),
141
+ tags: Array.isArray(body.tags) ? (body.tags as string[]) : undefined,
142
+ };
143
+ }
144
+
145
+ function getObject(body: TJsonBody, key: string): Record<string, unknown> {
146
+ const value = body[key];
147
+ return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
148
+ }
149
+
150
+ function resolvedFromDraft(draft: TConnectionDraft): TResolvedDatabaseConnection {
151
+ const now = new Date().toISOString();
152
+ return {
153
+ id: "draft",
154
+ name: draft.name,
155
+ type: draft.type,
156
+ host: draft.host,
157
+ port: draft.port,
158
+ database: draft.database,
159
+ schema: draft.schema,
160
+ username: draft.username,
161
+ password: draft.password,
162
+ ssl: draft.ssl,
163
+ sslValidateCertificate: draft.sslValidateCertificate,
164
+ createdAt: now,
165
+ updatedAt: now,
166
+ };
167
+ }
168
+
169
+ async function openBrowser(url: string): Promise<void> {
170
+ const command = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
171
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
172
+ await execa(command, args, { reject: false, detached: true, stdio: "ignore" }).catch(() => undefined);
173
+ }
174
+
175
+ export async function startStudioServer(options: TStudioServerOptions = {}): Promise<TStudioServerHandle> {
176
+ const preferredPort = options.port && options.port > 0 ? options.port : 45888;
177
+ const port = await findAvailablePort(preferredPort);
178
+ const pool = new StudioConnectionPool({ queryTimeoutMs: options.queryTimeoutMs });
179
+ const serverReadOnlyDefault = options.readOnly ?? false;
180
+
181
+ const router = async (req: http.IncomingMessage, res: http.ServerResponse): Promise<void> => {
182
+ const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
183
+ const pathname = url.pathname;
184
+ const method = req.method ?? "GET";
185
+
186
+ if (pathname === "/" && method === "GET") {
187
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
188
+ res.end(renderStudioHtml({ readOnlyDefault: serverReadOnlyDefault }));
189
+ return;
190
+ }
191
+
192
+ // --- Connections ---------------------------------------------------------
193
+ if (pathname === "/api/connections" && method === "GET") {
194
+ sendJson(res, { connections: await listPublicConnections() });
195
+ return;
196
+ }
197
+
198
+ if (pathname === "/api/connections/test" && method === "POST") {
199
+ const body = await readJsonBody(req);
200
+ const resolved = await getResolvedConnection(getString(body, "connectionId"));
201
+ const result = await testConnectionProfile(resolved, { queryTimeoutMs: options.queryTimeoutMs });
202
+ sendJson(res, result);
203
+ return;
204
+ }
205
+
206
+ if (pathname === "/api/connections/test-draft" && method === "POST") {
207
+ const body = await readJsonBody(req);
208
+ const result = await testConnectionProfile(resolvedFromDraft(draftFromBody(body)), { queryTimeoutMs: options.queryTimeoutMs });
209
+ sendJson(res, result);
210
+ return;
211
+ }
212
+
213
+ if (pathname === "/api/connections/create" && method === "POST") {
214
+ const body = await readJsonBody(req);
215
+ const profile = await upsertConnectionFromDraft(draftFromBody(body));
216
+ const { encryptedPassword: _omit, ...publicProfile } = profile;
217
+ void _omit;
218
+ sendJson(res, { connection: publicProfile });
219
+ return;
220
+ }
221
+
222
+ if (pathname === "/api/connections/rename" && method === "POST") {
223
+ const body = await readJsonBody(req);
224
+ const profile = await renameConnection(getString(body, "id"), getString(body, "name"));
225
+ sendJson(res, { id: profile.id, name: profile.name });
226
+ return;
227
+ }
228
+
229
+ if (pathname === "/api/connections/duplicate" && method === "POST") {
230
+ const body = await readJsonBody(req);
231
+ const profile = await duplicateConnection(getString(body, "id"));
232
+ sendJson(res, { id: profile.id, name: profile.name });
233
+ return;
234
+ }
235
+
236
+ if (pathname === "/api/connections/remove" && method === "POST") {
237
+ const body = await readJsonBody(req);
238
+ await pool.closeConnection(getString(body, "id"));
239
+ const removed = await removeConnection(getString(body, "id"));
240
+ sendJson(res, { removed });
241
+ return;
242
+ }
243
+
244
+ if (pathname === "/api/connections/import-from-app" && method === "POST") {
245
+ const body = await readJsonBody(req);
246
+ const { profile } = await importConnectionFromApp({
247
+ app: getString(body, "app"),
248
+ serviceName: getString(body, "serviceName") || undefined,
249
+ type: (getString(body, "type") || undefined) as TDatabaseType | undefined,
250
+ });
251
+ const { encryptedPassword: _omitPassword, ...publicProfile } = profile;
252
+ void _omitPassword;
253
+ sendJson(res, { connection: publicProfile });
254
+ return;
255
+ }
256
+
257
+ // --- BTP -----------------------------------------------------------------
258
+ if (pathname === "/api/btp/current-target" && method === "GET") {
259
+ const session = await ensureCloudFoundrySession();
260
+ const target = await getCloudFoundryTargetSummary();
261
+ sendJson(res, {
262
+ loggedIn: session.loggedIn,
263
+ message: session.message,
264
+ target,
265
+ productionWarning: looksLikeProduction(target.org, target.space),
266
+ });
267
+ return;
268
+ }
269
+
270
+ if (pathname === "/api/btp/apps" && method === "GET") {
271
+ const session = await ensureCloudFoundrySession();
272
+ if (!session.loggedIn) {
273
+ sendJson(res, { loggedIn: false, message: session.message, apps: [] });
274
+ return;
275
+ }
276
+ const apps = await listCloudFoundryAppsWithCache({ refresh: url.searchParams.get("refresh") === "true" });
277
+ sendJson(res, { loggedIn: true, apps });
278
+ return;
279
+ }
280
+
281
+ if (pathname === "/api/btp/env" && method === "POST") {
282
+ const body = await readJsonBody(req);
283
+ const candidates = await detectAppDatabaseServices(getString(body, "app"));
284
+ // Never expose passwords to the browser.
285
+ const safeCandidates = candidates.map(({ password: _password, ...rest }) => {
286
+ void _password;
287
+ return rest;
288
+ });
289
+ sendJson(res, { services: safeCandidates });
290
+ return;
291
+ }
292
+
293
+ // --- Catalog -------------------------------------------------------------
294
+ if (pathname === "/api/catalog/schemas" && method === "GET") {
295
+ const adapter = await pool.getAdapter(url.searchParams.get("connectionId") ?? "");
296
+ sendJson(res, { schemas: await adapter.listSchemas() });
297
+ return;
298
+ }
299
+
300
+ if (pathname === "/api/catalog/objects" && method === "GET") {
301
+ const adapter = await pool.getAdapter(url.searchParams.get("connectionId") ?? "");
302
+ const kindsParam = url.searchParams.get("kinds");
303
+ const kinds = kindsParam ? (kindsParam.split(",").filter(Boolean) as TDatabaseObjectKind[]) : undefined;
304
+ const objects = await adapter.listObjects({
305
+ schema: url.searchParams.get("schema") ?? undefined,
306
+ search: url.searchParams.get("search") ?? undefined,
307
+ kinds,
308
+ });
309
+ sendJson(res, { objects });
310
+ return;
311
+ }
312
+
313
+ if (pathname === "/api/catalog/columns" && method === "GET") {
314
+ const adapter = await pool.getAdapter(url.searchParams.get("connectionId") ?? "");
315
+ const schema = url.searchParams.get("schema") ?? "";
316
+ const table = url.searchParams.get("table") ?? "";
317
+ const [columns, indexes] = await Promise.all([
318
+ adapter.listColumns(schema, table),
319
+ adapter.listIndexes(schema, table).catch(() => []),
320
+ ]);
321
+ sendJson(res, { columns, indexes });
322
+ return;
323
+ }
324
+
325
+ if (pathname === "/api/catalog/ddl" && method === "GET") {
326
+ const adapter = await pool.getAdapter(url.searchParams.get("connectionId") ?? "");
327
+ const schema = url.searchParams.get("schema") ?? "";
328
+ const table = url.searchParams.get("table") ?? "";
329
+ const columns = await adapter.listColumns(schema, table);
330
+ sendJson(res, { ddl: generateCreateTableDdl(adapter.type, schema, table, columns) });
331
+ return;
332
+ }
333
+
334
+ if (pathname === "/api/catalog/indexes" && method === "GET") {
335
+ const adapter = await pool.getAdapter(url.searchParams.get("connectionId") ?? "");
336
+ sendJson(res, { indexes: await adapter.listIndexes(url.searchParams.get("schema") ?? "", url.searchParams.get("table") ?? "") });
337
+ return;
338
+ }
339
+
340
+ // --- Table data ----------------------------------------------------------
341
+ if (pathname === "/api/table/data" && method === "POST") {
342
+ const body = await readJsonBody(req);
343
+ const adapter = await pool.getAdapter(getString(body, "connectionId"));
344
+ const result = await adapter.getTableData({
345
+ schema: getString(body, "schema"),
346
+ table: getString(body, "table"),
347
+ limit: getNumber(body, "limit", 100),
348
+ offset: getNumber(body, "offset", 0),
349
+ where: getString(body, "where") || undefined,
350
+ orderBy: getString(body, "orderBy") || undefined,
351
+ orderDirection: getString(body, "orderDirection") === "desc" ? "desc" : "asc",
352
+ });
353
+ sendJson(res, { result });
354
+ return;
355
+ }
356
+
357
+ if (pathname === "/api/table/count" && method === "POST") {
358
+ const body = await readJsonBody(req);
359
+ const adapter = await pool.getAdapter(getString(body, "connectionId"));
360
+ const count = await adapter.countRows(getString(body, "schema"), getString(body, "table"));
361
+ sendJson(res, { count });
362
+ return;
363
+ }
364
+
365
+ if ((pathname === "/api/table/row/update" || pathname === "/api/table/row/insert" || pathname === "/api/table/row/delete") && method === "POST") {
366
+ const body = await readJsonBody(req);
367
+ const readOnly = body.readOnly === undefined ? serverReadOnlyDefault : Boolean(body.readOnly);
368
+
369
+ if (readOnly) {
370
+ sendJson(res, { ok: false, blocked: true, error: "Read-only mode is on. Turn it off to modify data." });
371
+ return;
372
+ }
373
+
374
+ const adapter = await pool.getAdapter(getString(body, "connectionId"));
375
+ const schema = getString(body, "schema");
376
+ const table = getString(body, "table");
377
+
378
+ try {
379
+ let result;
380
+ if (pathname.endsWith("/update")) {
381
+ result = await updateRow(adapter, { schema, table, changes: getObject(body, "changes"), keys: getObject(body, "keys") });
382
+ } else if (pathname.endsWith("/insert")) {
383
+ result = await insertRow(adapter, { schema, table, values: getObject(body, "values") });
384
+ } else {
385
+ result = await deleteRow(adapter, { schema, table, keys: getObject(body, "keys") });
386
+ }
387
+ sendJson(res, { ok: true, result });
388
+ } catch (error) {
389
+ sendJson(res, { ok: false, error: error instanceof Error ? error.message : String(error) });
390
+ }
391
+ return;
392
+ }
393
+
394
+ if (pathname === "/api/table/sql" && method === "POST") {
395
+ const body = await readJsonBody(req);
396
+ const adapter = await pool.getAdapter(getString(body, "connectionId"));
397
+ const schema = getString(body, "schema");
398
+ const table = getString(body, "table");
399
+ sendJson(res, {
400
+ select: generateSelectSql(adapter.type, schema, table, getNumber(body, "limit", 100)),
401
+ count: generateCountSql(adapter.type, schema, table),
402
+ });
403
+ return;
404
+ }
405
+
406
+ // --- Query run -----------------------------------------------------------
407
+ if (pathname === "/api/query/run" && method === "POST") {
408
+ const body = await readJsonBody(req);
409
+ const connectionId = getString(body, "connectionId");
410
+ const sql = getString(body, "sql");
411
+ const limit = getNumber(body, "limit", 0);
412
+ const readOnly = body.readOnly === undefined ? serverReadOnlyDefault : Boolean(body.readOnly);
413
+ const confirmDangerous = Boolean(body.confirmDangerous);
414
+
415
+ if (!connectionId) {
416
+ sendJson(res, { ok: false, error: "Select a connection first." });
417
+ return;
418
+ }
419
+
420
+ const safety = analyzeSqlSafety(sql, { readOnly });
421
+
422
+ if (safety.blockedByReadOnly) {
423
+ sendJson(res, { ok: false, blocked: true, safety, error: `Read-only mode blocks: ${safety.matchedKeywords.join(", ")}` });
424
+ return;
425
+ }
426
+
427
+ if (safety.isDestructive && !confirmDangerous) {
428
+ sendJson(res, { ok: false, needsConfirmation: true, safety });
429
+ return;
430
+ }
431
+
432
+ const connection = await getResolvedConnection(connectionId).catch(() => undefined);
433
+ const adapter = await pool.getAdapter(connectionId);
434
+ const effectiveSql = appendSafeLimit(adapter.type, sql, limit);
435
+
436
+ try {
437
+ const result = await adapter.runQuery(effectiveSql, { maxRows: limit > 0 ? limit : undefined });
438
+ await appendQueryHistory({
439
+ connectionId,
440
+ connectionName: connection?.name,
441
+ connectionType: adapter.type,
442
+ sql,
443
+ durationMs: result.durationMs,
444
+ success: true,
445
+ rowCount: result.rowCount,
446
+ });
447
+ sendJson(res, { ok: true, result, safety, effectiveSql });
448
+ } catch (error) {
449
+ const message = error instanceof Error ? error.message : String(error);
450
+ await appendQueryHistory({
451
+ connectionId,
452
+ connectionName: connection?.name,
453
+ connectionType: adapter.type,
454
+ sql,
455
+ durationMs: 0,
456
+ success: false,
457
+ error: message,
458
+ });
459
+ sendJson(res, { ok: false, error: message });
460
+ }
461
+ return;
462
+ }
463
+
464
+ // --- Saved queries -------------------------------------------------------
465
+ if (pathname === "/api/queries" && method === "GET") {
466
+ sendJson(res, { queries: await listSavedQueries() });
467
+ return;
468
+ }
469
+
470
+ if (pathname === "/api/queries" && method === "POST") {
471
+ const body = await readJsonBody(req);
472
+ const query = await saveQuery({
473
+ name: getString(body, "name"),
474
+ sql: getString(body, "sql"),
475
+ connectionId: getString(body, "connectionId") || undefined,
476
+ connectionType: (getString(body, "connectionType") || undefined) as TDatabaseType | undefined,
477
+ tags: Array.isArray(body.tags) ? (body.tags as string[]) : undefined,
478
+ });
479
+ sendJson(res, { query });
480
+ return;
481
+ }
482
+
483
+ if (pathname.startsWith("/api/queries/") && method === "PUT") {
484
+ const id = decodeURIComponent(pathname.slice("/api/queries/".length));
485
+ const body = await readJsonBody(req);
486
+ const name = getString(body, "name");
487
+ const sql = getString(body, "sql");
488
+ const query = sql
489
+ ? await saveQuery({ id, name, sql, connectionId: getString(body, "connectionId") || undefined })
490
+ : await renameSavedQuery(id, name);
491
+ sendJson(res, { query });
492
+ return;
493
+ }
494
+
495
+ if (pathname.startsWith("/api/queries/") && method === "DELETE") {
496
+ const id = decodeURIComponent(pathname.slice("/api/queries/".length));
497
+ sendJson(res, { deleted: await deleteSavedQuery(id) });
498
+ return;
499
+ }
500
+
501
+ // --- History -------------------------------------------------------------
502
+ if (pathname === "/api/history" && method === "GET") {
503
+ sendJson(res, { history: await listQueryHistory(100) });
504
+ return;
505
+ }
506
+
507
+ // --- Export --------------------------------------------------------------
508
+ if (pathname === "/api/export/csv" && method === "POST") {
509
+ const body = await readJsonBody(req);
510
+ const fields = Array.isArray(body.fields) ? (body.fields as string[]) : [];
511
+ const rows = Array.isArray(body.rows) ? (body.rows as Array<Record<string, unknown>>) : [];
512
+ sendText(res, toCsv(fields, rows), "text/csv; charset=utf-8", "result.csv");
513
+ return;
514
+ }
515
+
516
+ if (pathname === "/api/export/json" && method === "POST") {
517
+ const body = await readJsonBody(req);
518
+ const rows = Array.isArray(body.rows) ? body.rows : [];
519
+ sendText(res, JSON.stringify(rows, null, 2), "application/json; charset=utf-8", "result.json");
520
+ return;
521
+ }
522
+
523
+ res.writeHead(404, { "content-type": "application/json; charset=utf-8" });
524
+ res.end(JSON.stringify({ error: "Not found" }));
525
+ };
526
+
527
+ const server = http.createServer((req, res) => {
528
+ router(req, res).catch((error: unknown) => {
529
+ const message = error instanceof Error ? error.message : String(error);
530
+ if (!res.headersSent) {
531
+ sendJson(res, { error: message }, 500);
532
+ } else {
533
+ res.end();
534
+ }
535
+ });
536
+ });
537
+
538
+ await new Promise<void>((resolve) => server.listen(port, "127.0.0.1", resolve));
539
+
540
+ const url = `http://127.0.0.1:${port}`;
541
+ console.log(chalk.green(`SimpleMDG CF DB Studio: ${url}`));
542
+ if (serverReadOnlyDefault) {
543
+ console.log(chalk.yellow("Read-only mode is ON. Write/DDL statements are blocked."));
544
+ }
545
+ console.log(chalk.gray("Server is bound to 127.0.0.1 only. Press Ctrl+C to stop."));
546
+
547
+ if (!process.env.SMDG_STUDIO_NO_OPEN) {
548
+ await openBrowser(url);
549
+ }
550
+
551
+ return {
552
+ url,
553
+ port,
554
+ close: async () => {
555
+ await pool.closeAll();
556
+ await new Promise<void>((resolve) => server.close(() => resolve()));
557
+ },
558
+ };
559
+ }