@jessejoris/mcp-mysql-via-api 1.0.0

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.
@@ -0,0 +1,1502 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createApiServer = createApiServer;
7
+ const express_1 = __importDefault(require("express"));
8
+ const cors_1 = __importDefault(require("cors"));
9
+ const dbPool_js_1 = require("./dbPool.js");
10
+ const permissionManager_js_1 = require("../permissions/permissionManager.js");
11
+ function createApiServer(options = {}) {
12
+ const app = (0, express_1.default)();
13
+ const poolManager = options.poolManager || dbPool_js_1.DatabasePoolManager.getInstance();
14
+ const expectedApiKey = options.apiKey !== undefined ? options.apiKey : process.env.MYSQL_API_KEY || "";
15
+ const permissionsConfig = options.permissions !== undefined
16
+ ? options.permissions
17
+ : process.env.API_PERMISSIONS || process.env.MCP_PERMISSIONS || "all";
18
+ const apiPermissionManager = new permissionManager_js_1.PermissionManager(permissionsConfig);
19
+ app.use((0, cors_1.default)());
20
+ app.use(express_1.default.json({ limit: "50mb" }));
21
+ // Helper: sanitize identifier (table or column name)
22
+ const sanitizeIdentifier = (id) => {
23
+ if (!id || typeof id !== "string") {
24
+ throw new Error("Invalid identifier: identifier must be a non-empty string");
25
+ }
26
+ const clean = id.replace(/`/g, "").trim();
27
+ if (!/^[a-zA-Z0-9_$]+$/.test(clean)) {
28
+ throw new Error(`Invalid identifier format: ${id}`);
29
+ }
30
+ return `\`${clean}\``;
31
+ };
32
+ // Helper: qualified table identifier (optional database)
33
+ const getFullTableIdentifier = (table, db) => {
34
+ const safeTable = sanitizeIdentifier(table);
35
+ if (db && typeof db === "string" && db.trim().length > 0) {
36
+ return `${sanitizeIdentifier(db)}.${safeTable}`;
37
+ }
38
+ return safeTable;
39
+ };
40
+ // Helper: dangerous SQL check
41
+ const checkDangerousSql = (sql) => {
42
+ const upper = sql.toUpperCase();
43
+ for (const keyword of permissionManager_js_1.DANGEROUS_SQL_KEYWORDS) {
44
+ const regex = new RegExp(`\\b${keyword.replace(/\s+/g, "\\s+")}\\b`, "i");
45
+ if (regex.test(upper)) {
46
+ throw new Error(`Forbidden SQL: Statement contains blocked dangerous keyword '${keyword}'`);
47
+ }
48
+ }
49
+ };
50
+ // Helper: operator whitelist and normalization mapping
51
+ const OPERATOR_MAP = {
52
+ "=": "=",
53
+ "==": "=",
54
+ "eq": "=",
55
+ "!=": "!=",
56
+ "<>": "!=",
57
+ "neq": "!=",
58
+ ">": ">",
59
+ "gt": ">",
60
+ ">=": ">=",
61
+ "gte": ">=",
62
+ "<": "<",
63
+ "lt": "<",
64
+ "<=": "<=",
65
+ "lte": "<=",
66
+ "like": "LIKE",
67
+ "not like": "NOT LIKE",
68
+ "in": "IN",
69
+ "not in": "NOT IN",
70
+ "is null": "IS NULL",
71
+ "is not null": "IS NOT NULL",
72
+ "between": "BETWEEN",
73
+ };
74
+ // Helper: build safe parameterized WHERE clauses
75
+ const buildWhereClauses = (filters) => {
76
+ const whereClauses = [];
77
+ const values = [];
78
+ if (!Array.isArray(filters)) {
79
+ return { whereSql: "", values: [] };
80
+ }
81
+ for (const f of filters) {
82
+ if (!f || !f.field || f.operator === undefined)
83
+ continue;
84
+ const safeField = sanitizeIdentifier(f.field);
85
+ const rawOp = String(f.operator).trim().toLowerCase();
86
+ const canonicalOp = OPERATOR_MAP[rawOp];
87
+ if (!canonicalOp) {
88
+ throw new Error(`Unsupported or invalid filter operator: '${f.operator}'`);
89
+ }
90
+ if (canonicalOp === "IS NULL" || canonicalOp === "IS NOT NULL") {
91
+ whereClauses.push(`${safeField} ${canonicalOp}`);
92
+ }
93
+ else if (canonicalOp === "IN" || canonicalOp === "NOT IN") {
94
+ if (Array.isArray(f.value) && f.value.length > 0) {
95
+ const placeholders = f.value.map(() => "?").join(", ");
96
+ whereClauses.push(`${safeField} ${canonicalOp} (${placeholders})`);
97
+ values.push(...f.value);
98
+ }
99
+ else {
100
+ whereClauses.push(canonicalOp === "IN" ? "1=0" : "1=1");
101
+ }
102
+ }
103
+ else if (canonicalOp === "BETWEEN") {
104
+ whereClauses.push(`${safeField} BETWEEN ? AND ?`);
105
+ values.push(f.value, f.secondValue);
106
+ }
107
+ else {
108
+ whereClauses.push(`${safeField} ${canonicalOp} ?`);
109
+ values.push(f.value);
110
+ }
111
+ }
112
+ const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
113
+ return { whereSql, values };
114
+ };
115
+ // Helper: discover table primary key column
116
+ const getTablePrimaryKey = async (table, db) => {
117
+ const safeTable = getFullTableIdentifier(table, db);
118
+ const pool = poolManager.getPool();
119
+ try {
120
+ const [cols] = await pool.query(`DESCRIBE ${safeTable}`);
121
+ const pkCol = cols.find((c) => c.Key === "PRI");
122
+ return pkCol ? pkCol.Field : "id";
123
+ }
124
+ catch {
125
+ return "id";
126
+ }
127
+ };
128
+ // Auth Middleware
129
+ const authMiddleware = (req, res, next) => {
130
+ if (!expectedApiKey) {
131
+ return next();
132
+ }
133
+ const authHeader = req.headers["authorization"];
134
+ const xApiKey = req.headers["x-api-key"];
135
+ let token = "";
136
+ if (authHeader && authHeader.startsWith("Bearer ")) {
137
+ token = authHeader.slice(7).trim();
138
+ }
139
+ else if (xApiKey) {
140
+ token = xApiKey.trim();
141
+ }
142
+ if (!token || token !== expectedApiKey) {
143
+ return res.status(401).json({
144
+ success: false,
145
+ error: {
146
+ code: "UNAUTHORIZED",
147
+ message: "Invalid or missing API authentication credentials",
148
+ timestamp: new Date().toISOString(),
149
+ },
150
+ });
151
+ }
152
+ next();
153
+ };
154
+ // Server-level Permission Middleware
155
+ const permissionMiddleware = (req, res, next) => {
156
+ if (!permissionsConfig ||
157
+ permissionsConfig === "all" ||
158
+ permissionsConfig === "admin" ||
159
+ permissionsConfig === "*") {
160
+ return next();
161
+ }
162
+ const path = req.path.toLowerCase();
163
+ const method = req.method.toUpperCase();
164
+ let requiredPerm = null;
165
+ if (method === "GET") {
166
+ if (path === "/info" || path === "/health") {
167
+ requiredPerm = null;
168
+ }
169
+ else if (path.startsWith("/databases") ||
170
+ path.startsWith("/tables/find") ||
171
+ path === "/tables" ||
172
+ path.includes("/schema") ||
173
+ path.startsWith("/schema")) {
174
+ requiredPerm = "list";
175
+ }
176
+ else if (path.includes("/records") ||
177
+ path.includes("/count") ||
178
+ path.includes("/columns") ||
179
+ path.startsWith("/tables/search-data")) {
180
+ requiredPerm = "read";
181
+ }
182
+ else if (path.includes("/export")) {
183
+ requiredPerm = "utility";
184
+ }
185
+ }
186
+ else if (method === "POST") {
187
+ if (path.includes("/records/bulk-delete")) {
188
+ requiredPerm = "delete";
189
+ }
190
+ else if (path.includes("/records")) {
191
+ requiredPerm = "create";
192
+ }
193
+ else if (path === "/query/select") {
194
+ requiredPerm = "read";
195
+ }
196
+ else if (path === "/query/write") {
197
+ requiredPerm = "execute";
198
+ }
199
+ else if (path === "/query/export") {
200
+ requiredPerm = "utility";
201
+ }
202
+ else if (path === "/ddl" || path === "/tables") {
203
+ requiredPerm = "ddl";
204
+ }
205
+ }
206
+ else if (method === "PUT") {
207
+ if (path.includes("/records")) {
208
+ requiredPerm = "update";
209
+ }
210
+ else if (path.startsWith("/tables/")) {
211
+ requiredPerm = "ddl";
212
+ }
213
+ }
214
+ else if (method === "DELETE") {
215
+ if (path.includes("/records")) {
216
+ requiredPerm = "delete";
217
+ }
218
+ else if (path.startsWith("/tables/")) {
219
+ requiredPerm = "ddl";
220
+ }
221
+ }
222
+ if (requiredPerm && !apiPermissionManager.isPermissionAllowed(requiredPerm)) {
223
+ return res.status(403).json({
224
+ success: false,
225
+ error: {
226
+ code: "FORBIDDEN",
227
+ message: `API endpoint '${method} ${req.originalUrl}' is blocked by server permission policy '${permissionsConfig}'. Required permission: '${requiredPerm}'.`,
228
+ details: {
229
+ requiredPermission: requiredPerm,
230
+ activePermissions: apiPermissionManager.getActivePermissions(),
231
+ policy: apiPermissionManager.getProfileSummary().preset,
232
+ },
233
+ timestamp: new Date().toISOString(),
234
+ },
235
+ });
236
+ }
237
+ next();
238
+ };
239
+ // Health endpoint (Public, measures DB ping latency)
240
+ app.get("/health", async (_req, res) => {
241
+ const startPing = Date.now();
242
+ try {
243
+ const pool = poolManager.getPool();
244
+ await pool.query("SELECT 1 as ping");
245
+ const latencyMs = Date.now() - startPing;
246
+ return res.json({
247
+ success: true,
248
+ data: {
249
+ status: "healthy",
250
+ timestamp: new Date().toISOString(),
251
+ database: "connected",
252
+ latencyMs,
253
+ },
254
+ meta: { timestamp: new Date().toISOString(), latencyMs },
255
+ });
256
+ }
257
+ catch (err) {
258
+ return res.status(503).json({
259
+ success: false,
260
+ error: {
261
+ code: "DATABASE_UNAVAILABLE",
262
+ message: err.message,
263
+ timestamp: new Date().toISOString(),
264
+ },
265
+ });
266
+ }
267
+ });
268
+ // Apply Auth & Permission Middleware to all /api routes
269
+ app.use("/api", authMiddleware);
270
+ app.use("/api", permissionMiddleware);
271
+ // Server metadata
272
+ app.get("/api/info", (_req, res) => {
273
+ return res.json({
274
+ success: true,
275
+ data: {
276
+ name: "mysql-mcp-api-server",
277
+ version: "1.1.0",
278
+ authRequired: Boolean(expectedApiKey),
279
+ permissionPolicy: apiPermissionManager.getProfileSummary(),
280
+ timestamp: new Date().toISOString(),
281
+ },
282
+ meta: { timestamp: new Date().toISOString() },
283
+ });
284
+ });
285
+ // 1. List Databases
286
+ app.get("/api/databases", async (_req, res) => {
287
+ try {
288
+ const pool = poolManager.getPool();
289
+ const [rows] = await pool.query("SHOW DATABASES");
290
+ const databases = rows.map((r) => r.Database || Object.values(r)[0]);
291
+ return res.json({
292
+ success: true,
293
+ data: databases,
294
+ meta: { timestamp: new Date().toISOString(), count: databases.length },
295
+ });
296
+ }
297
+ catch (err) {
298
+ return res.status(500).json({
299
+ success: false,
300
+ error: { code: "QUERY_FAILED", message: err.message, timestamp: new Date().toISOString() },
301
+ });
302
+ }
303
+ });
304
+ // 2. List Tables
305
+ app.get("/api/tables", async (req, res) => {
306
+ try {
307
+ const pool = poolManager.getPool();
308
+ const db = req.query.database;
309
+ const sql = db ? `SHOW TABLES FROM ${sanitizeIdentifier(db)}` : "SHOW TABLES";
310
+ const [rows] = await pool.query(sql);
311
+ const tables = rows.map((r) => Object.values(r)[0]);
312
+ return res.json({
313
+ success: true,
314
+ data: tables,
315
+ meta: { timestamp: new Date().toISOString(), count: tables.length },
316
+ });
317
+ }
318
+ catch (err) {
319
+ return res.status(500).json({
320
+ success: false,
321
+ error: { code: "QUERY_FAILED", message: err.message, timestamp: new Date().toISOString() },
322
+ });
323
+ }
324
+ });
325
+ // 3. Find Tables by Keyword (ranked concept lookup)
326
+ app.get("/api/tables/find", async (req, res) => {
327
+ try {
328
+ const pool = poolManager.getPool();
329
+ const keyword = (req.query.keyword || "").toLowerCase();
330
+ const limit = parseInt(req.query.limit || "20", 10);
331
+ const db = req.query.database;
332
+ const sql = db ? `SHOW TABLES FROM ${sanitizeIdentifier(db)}` : "SHOW TABLES";
333
+ const [tableRows] = await pool.query(sql);
334
+ const tables = tableRows.map((r) => String(Object.values(r)[0]));
335
+ const matched = tables.filter((t) => t.toLowerCase().includes(keyword)).slice(0, limit);
336
+ return res.json({
337
+ success: true,
338
+ data: matched,
339
+ meta: { timestamp: new Date().toISOString(), count: matched.length },
340
+ });
341
+ }
342
+ catch (err) {
343
+ return res.status(500).json({
344
+ success: false,
345
+ error: { code: "FIND_TABLES_FAILED", message: err.message, timestamp: new Date().toISOString() },
346
+ });
347
+ }
348
+ });
349
+ // 4. Search Data Across Tables (Guarded text search)
350
+ app.get("/api/tables/search-data", async (req, res) => {
351
+ try {
352
+ const pool = poolManager.getPool();
353
+ const keyword = req.query.keyword;
354
+ if (!keyword) {
355
+ return res.status(400).json({
356
+ success: false,
357
+ error: { code: "BAD_REQUEST", message: "keyword query parameter is required", timestamp: new Date().toISOString() },
358
+ });
359
+ }
360
+ const db = req.query.database;
361
+ const maxTables = Math.min(100, Math.max(1, parseInt(req.query.max_tables || "20", 10)));
362
+ const limitPerTable = Math.min(50, Math.max(1, parseInt(req.query.limit_per_table || "5", 10)));
363
+ let targetTables = [];
364
+ if (req.query.tables) {
365
+ targetTables = typeof req.query.tables === "string"
366
+ ? req.query.tables.split(",").map((s) => s.trim())
367
+ : req.query.tables;
368
+ }
369
+ else {
370
+ const sql = db ? `SHOW TABLES FROM ${sanitizeIdentifier(db)}` : "SHOW TABLES";
371
+ const [tableRows] = await pool.query(sql);
372
+ targetTables = tableRows.map((r) => String(Object.values(r)[0]));
373
+ }
374
+ targetTables = targetTables.slice(0, maxTables);
375
+ const results = [];
376
+ for (const table of targetTables) {
377
+ const safeTable = getFullTableIdentifier(table, db);
378
+ const [cols] = await pool.query(`DESCRIBE ${safeTable}`);
379
+ const textCols = cols
380
+ .filter((c) => /char|text|varchar|enum|set/i.test(c.Type))
381
+ .map((c) => c.Field);
382
+ if (textCols.length === 0)
383
+ continue;
384
+ const orClauses = textCols.map((c) => `${sanitizeIdentifier(c)} LIKE ?`).join(" OR ");
385
+ const searchPattern = `%${keyword}%`;
386
+ const params = textCols.map(() => searchPattern);
387
+ const [rows] = await pool.query(`SELECT * FROM ${safeTable} WHERE ${orClauses} LIMIT ${limitPerTable}`, params);
388
+ if (rows.length > 0) {
389
+ results.push({
390
+ table,
391
+ matchedColumns: textCols,
392
+ rows,
393
+ });
394
+ }
395
+ }
396
+ return res.json({
397
+ success: true,
398
+ data: results,
399
+ meta: {
400
+ timestamp: new Date().toISOString(),
401
+ tablesSearched: targetTables.length,
402
+ matchedTables: results.length,
403
+ },
404
+ });
405
+ }
406
+ catch (err) {
407
+ return res.status(500).json({
408
+ success: false,
409
+ error: { code: "SEARCH_DATA_FAILED", message: err.message, timestamp: new Date().toISOString() },
410
+ });
411
+ }
412
+ });
413
+ // 5. Read Table Schema
414
+ app.get("/api/tables/:table/schema", async (req, res) => {
415
+ try {
416
+ const pool = poolManager.getPool();
417
+ const table = req.params.table;
418
+ const db = req.query.database;
419
+ const safeTable = getFullTableIdentifier(table, db);
420
+ const [columns] = await pool.query(`DESCRIBE ${safeTable}`);
421
+ const [indexes] = await pool.query(`SHOW INDEX FROM ${safeTable}`);
422
+ return res.json({
423
+ success: true,
424
+ data: {
425
+ table,
426
+ database: db || undefined,
427
+ columns: columns.map((c) => ({
428
+ field: c.Field,
429
+ type: c.Type,
430
+ null: c.Null,
431
+ key: c.Key,
432
+ default: c.Default,
433
+ extra: c.Extra,
434
+ })),
435
+ indexes: indexes.map((idx) => ({
436
+ name: idx.Key_name,
437
+ column: idx.Column_name,
438
+ unique: idx.Non_unique === 0,
439
+ })),
440
+ },
441
+ meta: { timestamp: new Date().toISOString() },
442
+ });
443
+ }
444
+ catch (err) {
445
+ return res.status(500).json({
446
+ success: false,
447
+ error: { code: "SCHEMA_FETCH_FAILED", message: err.message, timestamp: new Date().toISOString() },
448
+ });
449
+ }
450
+ });
451
+ // 6. Read Records with Default Pagination + Bypass
452
+ app.get("/api/tables/:table/records", async (req, res) => {
453
+ try {
454
+ const pool = poolManager.getPool();
455
+ const table = req.params.table;
456
+ const db = req.query.database;
457
+ const safeTable = getFullTableIdentifier(table, db);
458
+ let filters = [];
459
+ const rawFilters = req.query.filters || req.query.conditions;
460
+ if (rawFilters) {
461
+ try {
462
+ filters = typeof rawFilters === "string" ? JSON.parse(rawFilters) : rawFilters;
463
+ }
464
+ catch {
465
+ // ignore malformed
466
+ }
467
+ }
468
+ const columnsStr = req.query.columns;
469
+ const safeCols = columnsStr
470
+ ? columnsStr.split(",").map((c) => sanitizeIdentifier(c.trim())).join(", ")
471
+ : "*";
472
+ const sortField = req.query.sort_field;
473
+ const sortDir = req.query.sort_direction?.toUpperCase() === "DESC" ? "DESC" : "ASC";
474
+ const bypass = req.query.bypass === "true" ||
475
+ req.query.bypass === true ||
476
+ req.query.all === "true" ||
477
+ req.query.limit === "0";
478
+ const page = Math.max(1, parseInt(req.query.page || "1", 10));
479
+ const limit = Math.max(1, parseInt(req.query.limit || "50", 10));
480
+ const { whereSql, values } = buildWhereClauses(filters);
481
+ // Count total matching
482
+ const [countRows] = await pool.query(`SELECT COUNT(*) as total FROM ${safeTable} ${whereSql}`, values);
483
+ const total = Number(countRows[0]?.total || 0);
484
+ // Sorting
485
+ let orderSql = "";
486
+ if (sortField) {
487
+ orderSql = `ORDER BY ${sanitizeIdentifier(sortField)} ${sortDir}`;
488
+ }
489
+ // Pagination / Bypass
490
+ let limitSql = "";
491
+ if (!bypass) {
492
+ const offset = (page - 1) * limit;
493
+ limitSql = `LIMIT ${limit} OFFSET ${offset}`;
494
+ }
495
+ else if (limit) {
496
+ limitSql = `LIMIT ${limit}`;
497
+ }
498
+ const querySql = `SELECT ${safeCols} FROM ${safeTable} ${whereSql} ${orderSql} ${limitSql}`.trim();
499
+ const [records] = await pool.query(querySql, values);
500
+ const totalPages = bypass ? 1 : Math.ceil(total / limit) || 1;
501
+ const hasMore = bypass ? false : page * limit < total;
502
+ return res.json({
503
+ success: true,
504
+ data: records,
505
+ meta: {
506
+ pagination: {
507
+ page: bypass ? 1 : page,
508
+ limit: bypass ? records.length : limit,
509
+ total,
510
+ totalPages,
511
+ hasMore,
512
+ bypassed: bypass,
513
+ },
514
+ timestamp: new Date().toISOString(),
515
+ },
516
+ });
517
+ }
518
+ catch (err) {
519
+ return res.status(500).json({
520
+ success: false,
521
+ error: { code: "READ_RECORDS_FAILED", message: err.message, timestamp: new Date().toISOString() },
522
+ });
523
+ }
524
+ });
525
+ // 7. Get Single Record by ID / Primary Key
526
+ app.get("/api/tables/:table/records/:id", async (req, res) => {
527
+ try {
528
+ const pool = poolManager.getPool();
529
+ const table = req.params.table;
530
+ const id = req.params.id;
531
+ const db = req.query.database;
532
+ const keyCol = req.query.key_column || (await getTablePrimaryKey(table, db));
533
+ const safeTable = getFullTableIdentifier(table, db);
534
+ const safeKey = sanitizeIdentifier(keyCol);
535
+ const [rows] = await pool.query(`SELECT * FROM ${safeTable} WHERE ${safeKey} = ? LIMIT 1`, [id]);
536
+ if (rows.length === 0) {
537
+ return res.status(404).json({
538
+ success: false,
539
+ error: {
540
+ code: "RECORD_NOT_FOUND",
541
+ message: `Record with ${keyCol}='${id}' not found in table '${table}'`,
542
+ timestamp: new Date().toISOString(),
543
+ },
544
+ });
545
+ }
546
+ return res.json({
547
+ success: true,
548
+ data: rows[0],
549
+ meta: { timestamp: new Date().toISOString() },
550
+ });
551
+ }
552
+ catch (err) {
553
+ return res.status(500).json({
554
+ success: false,
555
+ error: { code: "GET_RECORD_FAILED", message: err.message, timestamp: new Date().toISOString() },
556
+ });
557
+ }
558
+ });
559
+ // 8. Count Records
560
+ app.get("/api/tables/:table/count", async (req, res) => {
561
+ try {
562
+ const pool = poolManager.getPool();
563
+ const table = req.params.table;
564
+ const db = req.query.database;
565
+ const safeTable = getFullTableIdentifier(table, db);
566
+ let filters = [];
567
+ const rawFilters = req.query.filters || req.query.conditions;
568
+ if (rawFilters) {
569
+ try {
570
+ filters = typeof rawFilters === "string" ? JSON.parse(rawFilters) : rawFilters;
571
+ }
572
+ catch {
573
+ // ignore
574
+ }
575
+ }
576
+ const { whereSql, values } = buildWhereClauses(filters);
577
+ const [rows] = await pool.query(`SELECT COUNT(*) as total FROM ${safeTable} ${whereSql}`, values);
578
+ return res.json({
579
+ success: true,
580
+ data: { total: Number(rows[0]?.total || 0) },
581
+ meta: { timestamp: new Date().toISOString() },
582
+ });
583
+ }
584
+ catch (err) {
585
+ return res.status(500).json({
586
+ success: false,
587
+ error: { code: "COUNT_FAILED", message: err.message, timestamp: new Date().toISOString() },
588
+ });
589
+ }
590
+ });
591
+ // 9. Column Statistics (Data Analysis)
592
+ app.get("/api/tables/:table/columns/:column/stats", async (req, res) => {
593
+ try {
594
+ const pool = poolManager.getPool();
595
+ const table = req.params.table;
596
+ const column = req.params.column;
597
+ const db = req.query.database;
598
+ const safeTable = getFullTableIdentifier(table, db);
599
+ const safeCol = sanitizeIdentifier(column);
600
+ // Aggregate statistics
601
+ const [statsRows] = await pool.query(`
602
+ SELECT
603
+ COUNT(*) as totalRows,
604
+ COUNT(${safeCol}) as nonNullCount,
605
+ COUNT(DISTINCT ${safeCol}) as distinctCount,
606
+ MIN(${safeCol}) as minValue,
607
+ MAX(${safeCol}) as maxValue
608
+ FROM ${safeTable}
609
+ `);
610
+ const stats = statsRows[0] || {};
611
+ const totalRows = Number(stats.totalRows || 0);
612
+ const nonNullCount = Number(stats.nonNullCount || 0);
613
+ const nullCount = totalRows - nonNullCount;
614
+ const nullPercentage = totalRows > 0 ? Number(((nullCount / totalRows) * 100).toFixed(2)) : 0;
615
+ // Top frequent values
616
+ const [freqRows] = await pool.query(`
617
+ SELECT ${safeCol} as value, COUNT(*) as frequency
618
+ FROM ${safeTable}
619
+ WHERE ${safeCol} IS NOT NULL
620
+ GROUP BY ${safeCol}
621
+ ORDER BY frequency DESC
622
+ LIMIT 5
623
+ `);
624
+ return res.json({
625
+ success: true,
626
+ data: {
627
+ table,
628
+ column,
629
+ totalRows,
630
+ nonNullCount,
631
+ nullCount,
632
+ nullPercentage,
633
+ distinctCount: Number(stats.distinctCount || 0),
634
+ minValue: stats.minValue,
635
+ maxValue: stats.maxValue,
636
+ topValues: freqRows,
637
+ },
638
+ meta: { timestamp: new Date().toISOString() },
639
+ });
640
+ }
641
+ catch (err) {
642
+ return res.status(500).json({
643
+ success: false,
644
+ error: { code: "STATS_FAILED", message: err.message, timestamp: new Date().toISOString() },
645
+ });
646
+ }
647
+ });
648
+ // 10. Create Record
649
+ app.post("/api/tables/:table/records", async (req, res) => {
650
+ try {
651
+ const pool = poolManager.getPool();
652
+ const table = req.params.table;
653
+ const db = req.body.database || req.query.database;
654
+ const safeTable = getFullTableIdentifier(table, db);
655
+ const data = req.body.data;
656
+ if (!data || typeof data !== "object" || Object.keys(data).length === 0) {
657
+ return res.status(400).json({
658
+ success: false,
659
+ error: { code: "BAD_REQUEST", message: "Data payload cannot be empty", timestamp: new Date().toISOString() },
660
+ });
661
+ }
662
+ const keys = Object.keys(data);
663
+ const safeCols = keys.map((k) => sanitizeIdentifier(k)).join(", ");
664
+ const placeholders = keys.map(() => "?").join(", ");
665
+ const values = keys.map((k) => data[k]);
666
+ const sql = `INSERT INTO ${safeTable} (${safeCols}) VALUES (${placeholders})`;
667
+ const [result] = await pool.query(sql, values);
668
+ return res.json({
669
+ success: true,
670
+ data: {
671
+ insertedId: result.insertId,
672
+ affectedRows: result.affectedRows,
673
+ },
674
+ meta: { timestamp: new Date().toISOString() },
675
+ });
676
+ }
677
+ catch (err) {
678
+ return res.status(500).json({
679
+ success: false,
680
+ error: { code: "INSERT_FAILED", message: err.message, timestamp: new Date().toISOString() },
681
+ });
682
+ }
683
+ });
684
+ // 11. Bulk Insert
685
+ app.post("/api/tables/:table/records/bulk", async (req, res) => {
686
+ try {
687
+ const pool = poolManager.getPool();
688
+ const table = req.params.table;
689
+ const db = req.body.database || req.query.database;
690
+ const safeTable = getFullTableIdentifier(table, db);
691
+ const records = req.body.records;
692
+ if (!Array.isArray(records) || records.length === 0) {
693
+ return res.status(400).json({
694
+ success: false,
695
+ error: { code: "BAD_REQUEST", message: "Records array cannot be empty", timestamp: new Date().toISOString() },
696
+ });
697
+ }
698
+ const keys = Object.keys(records[0]);
699
+ const safeCols = keys.map((k) => sanitizeIdentifier(k)).join(", ");
700
+ const rowPlaceholder = `(${keys.map(() => "?").join(", ")})`;
701
+ const allPlaceholders = records.map(() => rowPlaceholder).join(", ");
702
+ const values = [];
703
+ for (const rec of records) {
704
+ for (const k of keys) {
705
+ values.push(rec[k] !== undefined ? rec[k] : null);
706
+ }
707
+ }
708
+ const sql = `INSERT INTO ${safeTable} (${safeCols}) VALUES ${allPlaceholders}`;
709
+ const [result] = await pool.query(sql, values);
710
+ return res.json({
711
+ success: true,
712
+ data: {
713
+ affectedRows: result.affectedRows,
714
+ count: records.length,
715
+ },
716
+ meta: { timestamp: new Date().toISOString() },
717
+ });
718
+ }
719
+ catch (err) {
720
+ return res.status(500).json({
721
+ success: false,
722
+ error: { code: "BULK_INSERT_FAILED", message: err.message, timestamp: new Date().toISOString() },
723
+ });
724
+ }
725
+ });
726
+ // 12. Update Record by ID / Primary Key
727
+ app.put("/api/tables/:table/records/:id", async (req, res) => {
728
+ try {
729
+ const pool = poolManager.getPool();
730
+ const table = req.params.table;
731
+ const id = req.params.id;
732
+ const db = req.body.database || req.query.database;
733
+ const keyCol = req.body.key_column || (await getTablePrimaryKey(table, db));
734
+ const safeTable = getFullTableIdentifier(table, db);
735
+ const safeKey = sanitizeIdentifier(keyCol);
736
+ const data = req.body.data || req.body;
737
+ delete data.database;
738
+ delete data.key_column;
739
+ if (!data || Object.keys(data).length === 0) {
740
+ return res.status(400).json({
741
+ success: false,
742
+ error: { code: "BAD_REQUEST", message: "Update data cannot be empty", timestamp: new Date().toISOString() },
743
+ });
744
+ }
745
+ const setClauses = [];
746
+ const values = [];
747
+ for (const [k, v] of Object.entries(data)) {
748
+ setClauses.push(`${sanitizeIdentifier(k)} = ?`);
749
+ values.push(v);
750
+ }
751
+ values.push(id);
752
+ const sql = `UPDATE ${safeTable} SET ${setClauses.join(", ")} WHERE ${safeKey} = ?`;
753
+ const [result] = await pool.query(sql, values);
754
+ return res.json({
755
+ success: true,
756
+ data: {
757
+ affectedRows: result.affectedRows,
758
+ changedRows: result.changedRows || result.affectedRows,
759
+ },
760
+ meta: { timestamp: new Date().toISOString() },
761
+ });
762
+ }
763
+ catch (err) {
764
+ return res.status(500).json({
765
+ success: false,
766
+ error: { code: "UPDATE_BY_ID_FAILED", message: err.message, timestamp: new Date().toISOString() },
767
+ });
768
+ }
769
+ });
770
+ // 13. Update Records by Filter Conditions
771
+ app.put("/api/tables/:table/records", async (req, res) => {
772
+ try {
773
+ const pool = poolManager.getPool();
774
+ const table = req.params.table;
775
+ const db = req.body.database || req.query.database;
776
+ const safeTable = getFullTableIdentifier(table, db);
777
+ const { data } = req.body;
778
+ const filters = req.body.filters || req.body.conditions;
779
+ if (!data || Object.keys(data).length === 0) {
780
+ return res.status(400).json({
781
+ success: false,
782
+ error: { code: "BAD_REQUEST", message: "Update data cannot be empty", timestamp: new Date().toISOString() },
783
+ });
784
+ }
785
+ if (!Array.isArray(filters) || filters.length === 0) {
786
+ return res.status(400).json({
787
+ success: false,
788
+ error: { code: "SAFETY_VIOLATION", message: "Filters are required for update to prevent accidental table-wide overwrite", timestamp: new Date().toISOString() },
789
+ });
790
+ }
791
+ const setClauses = [];
792
+ const values = [];
793
+ for (const [k, v] of Object.entries(data)) {
794
+ setClauses.push(`${sanitizeIdentifier(k)} = ?`);
795
+ values.push(v);
796
+ }
797
+ const { whereSql, values: filterValues } = buildWhereClauses(filters);
798
+ values.push(...filterValues);
799
+ const sql = `UPDATE ${safeTable} SET ${setClauses.join(", ")} ${whereSql}`;
800
+ const [result] = await pool.query(sql, values);
801
+ return res.json({
802
+ success: true,
803
+ data: {
804
+ affectedRows: result.affectedRows,
805
+ changedRows: result.changedRows || result.affectedRows,
806
+ },
807
+ meta: { timestamp: new Date().toISOString() },
808
+ });
809
+ }
810
+ catch (err) {
811
+ return res.status(500).json({
812
+ success: false,
813
+ error: { code: "UPDATE_FAILED", message: err.message, timestamp: new Date().toISOString() },
814
+ });
815
+ }
816
+ });
817
+ // 14. Bulk Update
818
+ app.put("/api/tables/:table/records/bulk", async (req, res) => {
819
+ try {
820
+ const pool = poolManager.getPool();
821
+ const table = req.params.table;
822
+ const db = req.body.database || req.query.database;
823
+ const safeTable = getFullTableIdentifier(table, db);
824
+ const { records, key_column } = req.body;
825
+ if (!Array.isArray(records) || records.length === 0 || !key_column) {
826
+ return res.status(400).json({
827
+ success: false,
828
+ error: { code: "BAD_REQUEST", message: "Records and key_column are required", timestamp: new Date().toISOString() },
829
+ });
830
+ }
831
+ const safeKey = sanitizeIdentifier(key_column);
832
+ let totalAffected = 0;
833
+ for (const rec of records) {
834
+ const keyValue = rec[key_column];
835
+ if (keyValue === undefined)
836
+ continue;
837
+ const updateData = { ...rec };
838
+ delete updateData[key_column];
839
+ const setClauses = [];
840
+ const values = [];
841
+ for (const [k, v] of Object.entries(updateData)) {
842
+ setClauses.push(`${sanitizeIdentifier(k)} = ?`);
843
+ values.push(v);
844
+ }
845
+ if (setClauses.length > 0) {
846
+ values.push(keyValue);
847
+ const sql = `UPDATE ${safeTable} SET ${setClauses.join(", ")} WHERE ${safeKey} = ?`;
848
+ const [result] = await pool.query(sql, values);
849
+ totalAffected += result.affectedRows;
850
+ }
851
+ }
852
+ return res.json({
853
+ success: true,
854
+ data: { affectedRows: totalAffected, count: records.length },
855
+ meta: { timestamp: new Date().toISOString() },
856
+ });
857
+ }
858
+ catch (err) {
859
+ return res.status(500).json({
860
+ success: false,
861
+ error: { code: "BULK_UPDATE_FAILED", message: err.message, timestamp: new Date().toISOString() },
862
+ });
863
+ }
864
+ });
865
+ // 15. Delete Single Record by ID / Primary Key
866
+ app.delete("/api/tables/:table/records/:id", async (req, res) => {
867
+ try {
868
+ const pool = poolManager.getPool();
869
+ const table = req.params.table;
870
+ const id = req.params.id;
871
+ const db = req.body.database || req.query.database;
872
+ const keyCol = req.body.key_column || (await getTablePrimaryKey(table, db));
873
+ const safeTable = getFullTableIdentifier(table, db);
874
+ const safeKey = sanitizeIdentifier(keyCol);
875
+ const [result] = await pool.query(`DELETE FROM ${safeTable} WHERE ${safeKey} = ?`, [id]);
876
+ return res.json({
877
+ success: true,
878
+ data: { affectedRows: result.affectedRows },
879
+ meta: { timestamp: new Date().toISOString() },
880
+ });
881
+ }
882
+ catch (err) {
883
+ return res.status(500).json({
884
+ success: false,
885
+ error: { code: "DELETE_BY_ID_FAILED", message: err.message, timestamp: new Date().toISOString() },
886
+ });
887
+ }
888
+ });
889
+ // 16. Delete Records by Filter Conditions
890
+ app.delete("/api/tables/:table/records", async (req, res) => {
891
+ try {
892
+ const pool = poolManager.getPool();
893
+ const table = req.params.table;
894
+ const db = req.body.database || req.query.database;
895
+ const safeTable = getFullTableIdentifier(table, db);
896
+ const filters = req.body.filters || req.body.conditions;
897
+ if (!Array.isArray(filters) || filters.length === 0) {
898
+ return res.status(400).json({
899
+ success: false,
900
+ error: { code: "SAFETY_VIOLATION", message: "Filters are required for delete to prevent accidental table truncation", timestamp: new Date().toISOString() },
901
+ });
902
+ }
903
+ const { whereSql, values } = buildWhereClauses(filters);
904
+ const sql = `DELETE FROM ${safeTable} ${whereSql}`;
905
+ const [result] = await pool.query(sql, values);
906
+ return res.json({
907
+ success: true,
908
+ data: { affectedRows: result.affectedRows },
909
+ meta: { timestamp: new Date().toISOString() },
910
+ });
911
+ }
912
+ catch (err) {
913
+ return res.status(500).json({
914
+ success: false,
915
+ error: { code: "DELETE_FAILED", message: err.message, timestamp: new Date().toISOString() },
916
+ });
917
+ }
918
+ });
919
+ // 17. Bulk Delete
920
+ app.post("/api/tables/:table/records/bulk-delete", async (req, res) => {
921
+ try {
922
+ const pool = poolManager.getPool();
923
+ const table = req.params.table;
924
+ const db = req.body.database || req.query.database;
925
+ const safeTable = getFullTableIdentifier(table, db);
926
+ const { key_column, keys } = req.body;
927
+ if (!key_column || !Array.isArray(keys) || keys.length === 0) {
928
+ return res.status(400).json({
929
+ success: false,
930
+ error: { code: "BAD_REQUEST", message: "key_column and keys are required", timestamp: new Date().toISOString() },
931
+ });
932
+ }
933
+ const placeholders = keys.map(() => "?").join(", ");
934
+ const sql = `DELETE FROM ${safeTable} WHERE ${sanitizeIdentifier(key_column)} IN (${placeholders})`;
935
+ const [result] = await pool.query(sql, keys);
936
+ return res.json({
937
+ success: true,
938
+ data: { affectedRows: result.affectedRows, count: keys.length },
939
+ meta: { timestamp: new Date().toISOString() },
940
+ });
941
+ }
942
+ catch (err) {
943
+ return res.status(500).json({
944
+ success: false,
945
+ error: { code: "BULK_DELETE_FAILED", message: err.message, timestamp: new Date().toISOString() },
946
+ });
947
+ }
948
+ });
949
+ // 18. Run Select Query (with pagination or bypass)
950
+ app.post("/api/query/select", async (req, res) => {
951
+ try {
952
+ const pool = poolManager.getPool();
953
+ const { query, params = [], page = 1, limit = 50, bypass = false, database } = req.body;
954
+ if (!query || typeof query !== "string") {
955
+ return res.status(400).json({
956
+ success: false,
957
+ error: { code: "BAD_REQUEST", message: "Query string is required", timestamp: new Date().toISOString() },
958
+ });
959
+ }
960
+ let trimmed = query.trim();
961
+ while (trimmed.endsWith(";")) {
962
+ trimmed = trimmed.slice(0, -1).trim();
963
+ }
964
+ checkDangerousSql(trimmed);
965
+ if (!/^(SELECT|SHOW|DESCRIBE|DESC|EXPLAIN)/i.test(trimmed)) {
966
+ return res.status(400).json({
967
+ success: false,
968
+ error: { code: "READ_ONLY_VIOLATION", message: "Only read-only queries (SELECT, SHOW, DESCRIBE, EXPLAIN) are allowed on this endpoint", timestamp: new Date().toISOString() },
969
+ });
970
+ }
971
+ let finalSql = trimmed;
972
+ if (!bypass && limit && !/LIMIT\s+\d+/i.test(trimmed)) {
973
+ const offset = (page - 1) * limit;
974
+ finalSql = `${trimmed} LIMIT ${limit} OFFSET ${offset}`;
975
+ }
976
+ if (database) {
977
+ await pool.query(`USE ${sanitizeIdentifier(database)}`);
978
+ }
979
+ const [rows] = await pool.query(finalSql, params);
980
+ const hasMore = !bypass && Array.isArray(rows) && rows.length === limit;
981
+ return res.json({
982
+ success: true,
983
+ data: rows,
984
+ meta: {
985
+ pagination: {
986
+ page: bypass ? 1 : page,
987
+ limit: bypass ? (rows ? rows.length : 0) : limit,
988
+ total: rows ? rows.length : 0,
989
+ totalPages: bypass ? 1 : (hasMore ? page + 1 : page),
990
+ hasMore,
991
+ bypassed: bypass,
992
+ },
993
+ timestamp: new Date().toISOString(),
994
+ },
995
+ });
996
+ }
997
+ catch (err) {
998
+ return res.status(500).json({
999
+ success: false,
1000
+ error: { code: "SELECT_QUERY_FAILED", message: err.message, timestamp: new Date().toISOString() },
1001
+ });
1002
+ }
1003
+ });
1004
+ // 19. Execute Write Query
1005
+ app.post("/api/query/write", async (req, res) => {
1006
+ try {
1007
+ const pool = poolManager.getPool();
1008
+ const { query, params = [], database } = req.body;
1009
+ if (!query || typeof query !== "string") {
1010
+ return res.status(400).json({
1011
+ success: false,
1012
+ error: { code: "BAD_REQUEST", message: "Query string is required", timestamp: new Date().toISOString() },
1013
+ });
1014
+ }
1015
+ checkDangerousSql(query);
1016
+ if (database) {
1017
+ await pool.query(`USE ${sanitizeIdentifier(database)}`);
1018
+ }
1019
+ const [result] = await pool.query(query, params);
1020
+ return res.json({
1021
+ success: true,
1022
+ data: {
1023
+ affectedRows: result.affectedRows || 0,
1024
+ insertId: result.insertId || 0,
1025
+ warningCount: result.warningStatus || 0,
1026
+ },
1027
+ meta: { timestamp: new Date().toISOString() },
1028
+ });
1029
+ }
1030
+ catch (err) {
1031
+ return res.status(500).json({
1032
+ success: false,
1033
+ error: { code: "WRITE_QUERY_FAILED", message: err.message, timestamp: new Date().toISOString() },
1034
+ });
1035
+ }
1036
+ });
1037
+ // 20. Create Table
1038
+ app.post("/api/tables", async (req, res) => {
1039
+ try {
1040
+ const pool = poolManager.getPool();
1041
+ const { table_name, columns, primary_key, database } = req.body;
1042
+ if (!table_name || !Array.isArray(columns) || columns.length === 0) {
1043
+ return res.status(400).json({
1044
+ success: false,
1045
+ error: { code: "BAD_REQUEST", message: "table_name and columns are required", timestamp: new Date().toISOString() },
1046
+ });
1047
+ }
1048
+ const colDefs = [];
1049
+ for (const col of columns) {
1050
+ let def = `${sanitizeIdentifier(col.name)} ${col.type}`;
1051
+ if (col.nullable === false)
1052
+ def += " NOT NULL";
1053
+ if (col.autoIncrement)
1054
+ def += " AUTO_INCREMENT";
1055
+ if (col.defaultValue !== undefined)
1056
+ def += ` DEFAULT ${typeof col.defaultValue === "string" ? `'${col.defaultValue}'` : col.defaultValue}`;
1057
+ if (col.primaryKey)
1058
+ def += " PRIMARY KEY";
1059
+ colDefs.push(def);
1060
+ }
1061
+ if (primary_key) {
1062
+ const pkCols = Array.isArray(primary_key)
1063
+ ? primary_key.map((k) => sanitizeIdentifier(k)).join(", ")
1064
+ : sanitizeIdentifier(primary_key);
1065
+ colDefs.push(`PRIMARY KEY (${pkCols})`);
1066
+ }
1067
+ const safeTable = getFullTableIdentifier(table_name, database);
1068
+ const sql = `CREATE TABLE ${safeTable} (${colDefs.join(", ")})`;
1069
+ await pool.query(sql);
1070
+ return res.json({
1071
+ success: true,
1072
+ data: { table: table_name, created: true },
1073
+ meta: { timestamp: new Date().toISOString() },
1074
+ });
1075
+ }
1076
+ catch (err) {
1077
+ return res.status(500).json({
1078
+ success: false,
1079
+ error: { code: "CREATE_TABLE_FAILED", message: err.message, timestamp: new Date().toISOString() },
1080
+ });
1081
+ }
1082
+ });
1083
+ // 21. Alter Table
1084
+ app.put("/api/tables/:table", async (req, res) => {
1085
+ try {
1086
+ const pool = poolManager.getPool();
1087
+ const table = req.params.table;
1088
+ const { action, column_definition, database } = req.body;
1089
+ if (!action || !column_definition) {
1090
+ return res.status(400).json({
1091
+ success: false,
1092
+ error: { code: "BAD_REQUEST", message: "action and column_definition are required", timestamp: new Date().toISOString() },
1093
+ });
1094
+ }
1095
+ const safeTable = getFullTableIdentifier(table, database);
1096
+ let sql = "";
1097
+ if (action === "add_column") {
1098
+ sql = `ALTER TABLE ${safeTable} ADD COLUMN ${sanitizeIdentifier(column_definition.name)} ${column_definition.type}`;
1099
+ }
1100
+ else if (action === "drop_column") {
1101
+ sql = `ALTER TABLE ${safeTable} DROP COLUMN ${sanitizeIdentifier(column_definition.name)}`;
1102
+ }
1103
+ else if (action === "modify_column") {
1104
+ sql = `ALTER TABLE ${safeTable} MODIFY COLUMN ${sanitizeIdentifier(column_definition.name)} ${column_definition.type}`;
1105
+ }
1106
+ else {
1107
+ return res.status(400).json({
1108
+ success: false,
1109
+ error: { code: "BAD_REQUEST", message: `Unsupported action '${action}'`, timestamp: new Date().toISOString() },
1110
+ });
1111
+ }
1112
+ await pool.query(sql);
1113
+ return res.json({
1114
+ success: true,
1115
+ data: { table, action, altered: true },
1116
+ meta: { timestamp: new Date().toISOString() },
1117
+ });
1118
+ }
1119
+ catch (err) {
1120
+ return res.status(500).json({
1121
+ success: false,
1122
+ error: { code: "ALTER_TABLE_FAILED", message: err.message, timestamp: new Date().toISOString() },
1123
+ });
1124
+ }
1125
+ });
1126
+ // 22. Drop Table
1127
+ app.delete("/api/tables/:table", async (req, res) => {
1128
+ try {
1129
+ const pool = poolManager.getPool();
1130
+ const table = req.params.table;
1131
+ const db = req.body.database || req.query.database;
1132
+ const ifExists = req.body.if_exists !== false;
1133
+ const safeTable = getFullTableIdentifier(table, db);
1134
+ const sql = `DROP TABLE ${ifExists ? "IF EXISTS " : ""}${safeTable}`;
1135
+ await pool.query(sql);
1136
+ return res.json({
1137
+ success: true,
1138
+ data: { table, dropped: true },
1139
+ meta: { timestamp: new Date().toISOString() },
1140
+ });
1141
+ }
1142
+ catch (err) {
1143
+ return res.status(500).json({
1144
+ success: false,
1145
+ error: { code: "DROP_TABLE_FAILED", message: err.message, timestamp: new Date().toISOString() },
1146
+ });
1147
+ }
1148
+ });
1149
+ // 23. Execute DDL
1150
+ app.post("/api/ddl", async (req, res) => {
1151
+ try {
1152
+ const pool = poolManager.getPool();
1153
+ const { query, database } = req.body;
1154
+ if (!query || typeof query !== "string") {
1155
+ return res.status(400).json({
1156
+ success: false,
1157
+ error: { code: "BAD_REQUEST", message: "Query string is required", timestamp: new Date().toISOString() },
1158
+ });
1159
+ }
1160
+ checkDangerousSql(query);
1161
+ if (database) {
1162
+ await pool.query(`USE ${sanitizeIdentifier(database)}`);
1163
+ }
1164
+ const [result] = await pool.query(query);
1165
+ return res.json({
1166
+ success: true,
1167
+ data: { executed: true, result },
1168
+ meta: { timestamp: new Date().toISOString() },
1169
+ });
1170
+ }
1171
+ catch (err) {
1172
+ return res.status(500).json({
1173
+ success: false,
1174
+ error: { code: "DDL_EXECUTION_FAILED", message: err.message, timestamp: new Date().toISOString() },
1175
+ });
1176
+ }
1177
+ });
1178
+ // 24. Schema Summary
1179
+ app.get("/api/schema/summary", async (req, res) => {
1180
+ try {
1181
+ const pool = poolManager.getPool();
1182
+ const db = req.query.database;
1183
+ const maxTables = Math.min(200, Math.max(1, parseInt(req.query.max_tables || "50", 10)));
1184
+ const sql = db ? `SHOW TABLES FROM ${sanitizeIdentifier(db)}` : "SHOW TABLES";
1185
+ const [tableRows] = await pool.query(sql);
1186
+ const tables = tableRows.map((r) => String(Object.values(r)[0]));
1187
+ const summaries = [];
1188
+ for (const t of tables.slice(0, maxTables)) {
1189
+ const safeT = getFullTableIdentifier(t, db);
1190
+ const [cols] = await pool.query(`DESCRIBE ${safeT}`);
1191
+ const [cnt] = await pool.query(`SELECT COUNT(*) as total FROM ${safeT}`);
1192
+ summaries.push({
1193
+ name: t,
1194
+ rowCount: cnt[0]?.total || 0,
1195
+ columnCount: cols.length,
1196
+ columns: cols.map((c) => ({ name: c.Field, type: c.Type })),
1197
+ });
1198
+ }
1199
+ return res.json({
1200
+ success: true,
1201
+ data: {
1202
+ totalTables: tables.length,
1203
+ tables: summaries,
1204
+ },
1205
+ meta: { timestamp: new Date().toISOString() },
1206
+ });
1207
+ }
1208
+ catch (err) {
1209
+ return res.status(500).json({
1210
+ success: false,
1211
+ error: { code: "SUMMARY_FAILED", message: err.message, timestamp: new Date().toISOString() },
1212
+ });
1213
+ }
1214
+ });
1215
+ // 25. RAG Schema Context (Ultra-compact schema for LLM context windows)
1216
+ app.get("/api/schema/rag-context", async (req, res) => {
1217
+ try {
1218
+ const pool = poolManager.getPool();
1219
+ const db = req.query.database;
1220
+ const maxTables = Math.min(200, Math.max(1, parseInt(req.query.max_tables || "50", 10)));
1221
+ const maxColumns = Math.min(100, Math.max(1, parseInt(req.query.max_columns || "15", 10)));
1222
+ const keywordFilter = (req.query.keyword_filter || "").toLowerCase();
1223
+ const sql = db ? `SHOW TABLES FROM ${sanitizeIdentifier(db)}` : "SHOW TABLES";
1224
+ const [tableRows] = await pool.query(sql);
1225
+ let tables = tableRows.map((r) => String(Object.values(r)[0]));
1226
+ if (keywordFilter) {
1227
+ tables = tables.filter((t) => t.toLowerCase().includes(keywordFilter));
1228
+ }
1229
+ tables = tables.slice(0, maxTables);
1230
+ const compactTables = [];
1231
+ let compactText = `# Database Schema Context (Total Tables: ${tables.length})\n\n`;
1232
+ for (const t of tables) {
1233
+ const safeT = getFullTableIdentifier(t, db);
1234
+ const [cols] = await pool.query(`DESCRIBE ${safeT}`);
1235
+ const [cnt] = await pool.query(`SELECT COUNT(*) as total FROM ${safeT}`);
1236
+ const rowCount = cnt[0]?.total || 0;
1237
+ const tableCols = cols.slice(0, maxColumns).map((c) => {
1238
+ let flags = "";
1239
+ if (c.Key === "PRI")
1240
+ flags += " [PK]";
1241
+ if (c.Null === "NO")
1242
+ flags += " NOT NULL";
1243
+ return `${c.Field} (${c.Type}${flags})`;
1244
+ });
1245
+ compactTables.push({
1246
+ table: t,
1247
+ estimatedRows: rowCount,
1248
+ columns: tableCols,
1249
+ });
1250
+ compactText += `## Table: ${t} (~${rowCount} rows)\n`;
1251
+ compactText += `Columns: ${tableCols.join(", ")}\n\n`;
1252
+ }
1253
+ return res.json({
1254
+ success: true,
1255
+ data: {
1256
+ tableCount: tables.length,
1257
+ tables: compactTables,
1258
+ compactContextString: compactText.trim(),
1259
+ },
1260
+ meta: { timestamp: new Date().toISOString() },
1261
+ });
1262
+ }
1263
+ catch (err) {
1264
+ return res.status(500).json({
1265
+ success: false,
1266
+ error: { code: "RAG_CONTEXT_FAILED", message: err.message, timestamp: new Date().toISOString() },
1267
+ });
1268
+ }
1269
+ });
1270
+ // 26. Schema ERD (Mermaid)
1271
+ app.get("/api/schema/erd", async (req, res) => {
1272
+ try {
1273
+ const pool = poolManager.getPool();
1274
+ const db = req.query.database;
1275
+ const sql = db ? `SHOW TABLES FROM ${sanitizeIdentifier(db)}` : "SHOW TABLES";
1276
+ const [tableRows] = await pool.query(sql);
1277
+ const tables = tableRows.map((r) => String(Object.values(r)[0]));
1278
+ let mermaid = "erDiagram\n";
1279
+ for (const t of tables) {
1280
+ mermaid += ` ${t} {\n`;
1281
+ const safeT = getFullTableIdentifier(t, db);
1282
+ const [cols] = await pool.query(`DESCRIBE ${safeT}`);
1283
+ for (const c of cols) {
1284
+ mermaid += ` ${c.Type.split("(")[0]} ${c.Field}\n`;
1285
+ }
1286
+ mermaid += ` }\n`;
1287
+ }
1288
+ return res.json({
1289
+ success: true,
1290
+ data: {
1291
+ mermaidDiagram: mermaid,
1292
+ tableCount: tables.length,
1293
+ },
1294
+ meta: { timestamp: new Date().toISOString() },
1295
+ });
1296
+ }
1297
+ catch (err) {
1298
+ return res.status(500).json({
1299
+ success: false,
1300
+ error: { code: "ERD_FAILED", message: err.message, timestamp: new Date().toISOString() },
1301
+ });
1302
+ }
1303
+ });
1304
+ // 27. Schema Relationships
1305
+ app.get("/api/schema/relationships", async (_req, res) => {
1306
+ try {
1307
+ const pool = poolManager.getPool();
1308
+ const [rows] = await pool.query(`
1309
+ SELECT
1310
+ TABLE_NAME as tableName,
1311
+ COLUMN_NAME as columnName,
1312
+ REFERENCED_TABLE_NAME as referencedTable,
1313
+ REFERENCED_COLUMN_NAME as referencedColumn
1314
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
1315
+ WHERE REFERENCED_TABLE_SCHEMA = DATABASE()
1316
+ `);
1317
+ return res.json({
1318
+ success: true,
1319
+ data: rows,
1320
+ meta: { timestamp: new Date().toISOString(), count: rows.length },
1321
+ });
1322
+ }
1323
+ catch (err) {
1324
+ return res.status(500).json({
1325
+ success: false,
1326
+ error: { code: "RELATIONSHIPS_FAILED", message: err.message, timestamp: new Date().toISOString() },
1327
+ });
1328
+ }
1329
+ });
1330
+ // 28. Schema Search
1331
+ app.get("/api/schema/search", async (req, res) => {
1332
+ try {
1333
+ const pool = poolManager.getPool();
1334
+ const query = (req.query.query || "").toLowerCase();
1335
+ const db = req.query.database;
1336
+ const sql = db ? `SHOW TABLES FROM ${sanitizeIdentifier(db)}` : "SHOW TABLES";
1337
+ const [tableRows] = await pool.query(sql);
1338
+ const tables = tableRows.map((r) => String(Object.values(r)[0]));
1339
+ const matches = [];
1340
+ for (const t of tables) {
1341
+ const matchesTable = t.toLowerCase().includes(query);
1342
+ const safeT = getFullTableIdentifier(t, db);
1343
+ const [cols] = await pool.query(`DESCRIBE ${safeT}`);
1344
+ const matchingCols = cols.filter((c) => c.Field.toLowerCase().includes(query));
1345
+ if (matchesTable || matchingCols.length > 0) {
1346
+ matches.push({
1347
+ table: t,
1348
+ matchesTableName: matchesTable,
1349
+ matchingColumns: matchingCols.map((c) => c.Field),
1350
+ });
1351
+ }
1352
+ }
1353
+ return res.json({
1354
+ success: true,
1355
+ data: matches,
1356
+ meta: { timestamp: new Date().toISOString(), count: matches.length },
1357
+ });
1358
+ }
1359
+ catch (err) {
1360
+ return res.status(500).json({
1361
+ success: false,
1362
+ error: { code: "SCHEMA_SEARCH_FAILED", message: err.message, timestamp: new Date().toISOString() },
1363
+ });
1364
+ }
1365
+ });
1366
+ // 29. Export Table to CSV
1367
+ app.get("/api/tables/:table/export", async (req, res) => {
1368
+ try {
1369
+ const pool = poolManager.getPool();
1370
+ const table = req.params.table;
1371
+ const db = req.query.database;
1372
+ const limit = parseInt(req.query.limit || "1000", 10);
1373
+ const safeTable = getFullTableIdentifier(table, db);
1374
+ const [rows] = await pool.query(`SELECT * FROM ${safeTable} LIMIT ${limit}`);
1375
+ if (!rows || rows.length === 0) {
1376
+ return res.json({
1377
+ success: true,
1378
+ data: { csv: "", rowCount: 0 },
1379
+ meta: { timestamp: new Date().toISOString() },
1380
+ });
1381
+ }
1382
+ const headers = Object.keys(rows[0]);
1383
+ const csvLines = [headers.join(",")];
1384
+ for (const row of rows) {
1385
+ const line = headers
1386
+ .map((h) => {
1387
+ const val = row[h];
1388
+ if (val === null || val === undefined)
1389
+ return "";
1390
+ const str = String(val).replace(/"/g, '""');
1391
+ return str.includes(",") || str.includes('"') || str.includes("\n") ? `"${str}"` : str;
1392
+ })
1393
+ .join(",");
1394
+ csvLines.push(line);
1395
+ }
1396
+ return res.json({
1397
+ success: true,
1398
+ data: {
1399
+ csv: csvLines.join("\n"),
1400
+ rowCount: rows.length,
1401
+ },
1402
+ meta: { timestamp: new Date().toISOString() },
1403
+ });
1404
+ }
1405
+ catch (err) {
1406
+ return res.status(500).json({
1407
+ success: false,
1408
+ error: { code: "CSV_EXPORT_FAILED", message: err.message, timestamp: new Date().toISOString() },
1409
+ });
1410
+ }
1411
+ });
1412
+ // 30. Export Query to CSV (Strictly guarded to read-only queries)
1413
+ app.post("/api/query/export", async (req, res) => {
1414
+ try {
1415
+ const pool = poolManager.getPool();
1416
+ const { query, limit = 1000, database } = req.body;
1417
+ if (!query || typeof query !== "string") {
1418
+ return res.status(400).json({
1419
+ success: false,
1420
+ error: { code: "BAD_REQUEST", message: "Query is required", timestamp: new Date().toISOString() },
1421
+ });
1422
+ }
1423
+ let trimmed = query.trim();
1424
+ while (trimmed.endsWith(";")) {
1425
+ trimmed = trimmed.slice(0, -1).trim();
1426
+ }
1427
+ checkDangerousSql(trimmed);
1428
+ if (!/^(SELECT|SHOW|DESCRIBE|DESC|EXPLAIN)/i.test(trimmed)) {
1429
+ return res.status(400).json({
1430
+ success: false,
1431
+ error: {
1432
+ code: "READ_ONLY_VIOLATION",
1433
+ message: "export_query_to_csv only supports read-only queries (SELECT, SHOW, DESCRIBE, EXPLAIN)",
1434
+ timestamp: new Date().toISOString(),
1435
+ },
1436
+ });
1437
+ }
1438
+ if (database) {
1439
+ await pool.query(`USE ${sanitizeIdentifier(database)}`);
1440
+ }
1441
+ const [rows] = await pool.query(`${trimmed} LIMIT ${limit}`);
1442
+ if (!Array.isArray(rows) || rows.length === 0) {
1443
+ return res.json({
1444
+ success: true,
1445
+ data: { csv: "", rowCount: 0 },
1446
+ meta: { timestamp: new Date().toISOString() },
1447
+ });
1448
+ }
1449
+ const headers = Object.keys(rows[0]);
1450
+ const csvLines = [headers.join(",")];
1451
+ for (const row of rows) {
1452
+ const line = headers
1453
+ .map((h) => {
1454
+ const val = row[h];
1455
+ if (val === null || val === undefined)
1456
+ return "";
1457
+ const str = String(val).replace(/"/g, '""');
1458
+ return str.includes(",") || str.includes('"') || str.includes("\n") ? `"${str}"` : str;
1459
+ })
1460
+ .join(",");
1461
+ csvLines.push(line);
1462
+ }
1463
+ return res.json({
1464
+ success: true,
1465
+ data: {
1466
+ csv: csvLines.join("\n"),
1467
+ rowCount: rows.length,
1468
+ },
1469
+ meta: { timestamp: new Date().toISOString() },
1470
+ });
1471
+ }
1472
+ catch (err) {
1473
+ return res.status(500).json({
1474
+ success: false,
1475
+ error: { code: "QUERY_EXPORT_FAILED", message: err.message, timestamp: new Date().toISOString() },
1476
+ });
1477
+ }
1478
+ });
1479
+ // 31. 404 Catch-All Handler (Strict JSON guarantee - no HTML error pages)
1480
+ app.use((_req, res) => {
1481
+ return res.status(404).json({
1482
+ success: false,
1483
+ error: {
1484
+ code: "ROUTE_NOT_FOUND",
1485
+ message: "Endpoint not found on MySQL API server",
1486
+ timestamp: new Date().toISOString(),
1487
+ },
1488
+ });
1489
+ });
1490
+ // 32. Global Error Handler (Strict JSON guarantee - no HTML or uncaught crashes)
1491
+ app.use((err, _req, res, _next) => {
1492
+ return res.status(err.status || 500).json({
1493
+ success: false,
1494
+ error: {
1495
+ code: err.code || "INTERNAL_SERVER_ERROR",
1496
+ message: err.message || "An unhandled server error occurred",
1497
+ timestamp: new Date().toISOString(),
1498
+ },
1499
+ });
1500
+ });
1501
+ return app;
1502
+ }