@berthojoris/mcp-mysql-server 1.6.3 → 1.7.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.
- package/DOCUMENTATIONS.md +259 -25
- package/README.md +25 -3
- package/dist/config/featureConfig.js +24 -0
- package/dist/index.d.ts +122 -0
- package/dist/index.js +75 -0
- package/dist/mcp-server.js +348 -0
- package/dist/tools/backupRestoreTools.d.ts +91 -0
- package/dist/tools/backupRestoreTools.js +584 -0
- package/dist/tools/dataExportTools.d.ts +91 -2
- package/dist/tools/dataExportTools.js +726 -66
- package/package.json +2 -2
|
@@ -5,27 +5,79 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.DataExportTools = void 0;
|
|
7
7
|
const connection_1 = __importDefault(require("../db/connection"));
|
|
8
|
+
const config_1 = require("../config/config");
|
|
8
9
|
class DataExportTools {
|
|
9
10
|
constructor(security) {
|
|
10
11
|
this.db = connection_1.default.getInstance();
|
|
11
12
|
this.security = security;
|
|
12
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Validate database access
|
|
16
|
+
*/
|
|
17
|
+
validateDatabaseAccess(requestedDatabase) {
|
|
18
|
+
const connectedDatabase = config_1.dbConfig.database;
|
|
19
|
+
if (!connectedDatabase) {
|
|
20
|
+
return {
|
|
21
|
+
valid: false,
|
|
22
|
+
database: "",
|
|
23
|
+
error: "No database configured. Please specify a database in your connection settings.",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (requestedDatabase && requestedDatabase !== connectedDatabase) {
|
|
27
|
+
return {
|
|
28
|
+
valid: false,
|
|
29
|
+
database: "",
|
|
30
|
+
error: `Access denied: You are connected to '${connectedDatabase}' but requested '${requestedDatabase}'. Cross-database access is not permitted.`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
valid: true,
|
|
35
|
+
database: connectedDatabase,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Escape string value for SQL INSERT statements
|
|
40
|
+
*/
|
|
41
|
+
escapeValue(value) {
|
|
42
|
+
if (value === null)
|
|
43
|
+
return "NULL";
|
|
44
|
+
if (typeof value === "number")
|
|
45
|
+
return String(value);
|
|
46
|
+
if (typeof value === "boolean")
|
|
47
|
+
return value ? "1" : "0";
|
|
48
|
+
if (value instanceof Date) {
|
|
49
|
+
return `'${value.toISOString().slice(0, 19).replace("T", " ")}'`;
|
|
50
|
+
}
|
|
51
|
+
if (Buffer.isBuffer(value)) {
|
|
52
|
+
return `X'${value.toString("hex")}'`;
|
|
53
|
+
}
|
|
54
|
+
// Escape string
|
|
55
|
+
const escaped = String(value)
|
|
56
|
+
.replace(/\\/g, "\\\\")
|
|
57
|
+
.replace(/'/g, "\\'")
|
|
58
|
+
.replace(/"/g, '\\"')
|
|
59
|
+
.replace(/\n/g, "\\n")
|
|
60
|
+
.replace(/\r/g, "\\r")
|
|
61
|
+
.replace(/\t/g, "\\t")
|
|
62
|
+
.replace(/\0/g, "\\0");
|
|
63
|
+
return `'${escaped}'`;
|
|
64
|
+
}
|
|
13
65
|
/**
|
|
14
66
|
* Export table data to CSV format
|
|
15
67
|
*/
|
|
16
68
|
async exportTableToCSV(params) {
|
|
17
69
|
try {
|
|
18
|
-
const { table_name, filters = [], pagination, sorting, include_headers = true } = params;
|
|
70
|
+
const { table_name, filters = [], pagination, sorting, include_headers = true, } = params;
|
|
19
71
|
// Validate table name
|
|
20
72
|
const tableValidation = this.security.validateIdentifier(table_name);
|
|
21
73
|
if (!tableValidation.valid) {
|
|
22
74
|
return {
|
|
23
|
-
status:
|
|
24
|
-
error: tableValidation.error
|
|
75
|
+
status: "error",
|
|
76
|
+
error: tableValidation.error,
|
|
25
77
|
};
|
|
26
78
|
}
|
|
27
79
|
// Build WHERE clause
|
|
28
|
-
let whereClause =
|
|
80
|
+
let whereClause = "";
|
|
29
81
|
const whereParams = [];
|
|
30
82
|
if (filters && filters.length > 0) {
|
|
31
83
|
const whereConditions = [];
|
|
@@ -34,80 +86,80 @@ class DataExportTools {
|
|
|
34
86
|
const fieldValidation = this.security.validateIdentifier(filter.field);
|
|
35
87
|
if (!fieldValidation.valid) {
|
|
36
88
|
return {
|
|
37
|
-
status:
|
|
38
|
-
error: `Invalid field name: ${filter.field}
|
|
89
|
+
status: "error",
|
|
90
|
+
error: `Invalid field name: ${filter.field}`,
|
|
39
91
|
};
|
|
40
92
|
}
|
|
41
93
|
const fieldName = this.security.escapeIdentifier(filter.field);
|
|
42
94
|
switch (filter.operator) {
|
|
43
|
-
case
|
|
95
|
+
case "eq":
|
|
44
96
|
whereConditions.push(`${fieldName} = ?`);
|
|
45
97
|
whereParams.push(filter.value);
|
|
46
98
|
break;
|
|
47
|
-
case
|
|
99
|
+
case "neq":
|
|
48
100
|
whereConditions.push(`${fieldName} != ?`);
|
|
49
101
|
whereParams.push(filter.value);
|
|
50
102
|
break;
|
|
51
|
-
case
|
|
103
|
+
case "gt":
|
|
52
104
|
whereConditions.push(`${fieldName} > ?`);
|
|
53
105
|
whereParams.push(filter.value);
|
|
54
106
|
break;
|
|
55
|
-
case
|
|
107
|
+
case "gte":
|
|
56
108
|
whereConditions.push(`${fieldName} >= ?`);
|
|
57
109
|
whereParams.push(filter.value);
|
|
58
110
|
break;
|
|
59
|
-
case
|
|
111
|
+
case "lt":
|
|
60
112
|
whereConditions.push(`${fieldName} < ?`);
|
|
61
113
|
whereParams.push(filter.value);
|
|
62
114
|
break;
|
|
63
|
-
case
|
|
115
|
+
case "lte":
|
|
64
116
|
whereConditions.push(`${fieldName} <= ?`);
|
|
65
117
|
whereParams.push(filter.value);
|
|
66
118
|
break;
|
|
67
|
-
case
|
|
119
|
+
case "like":
|
|
68
120
|
whereConditions.push(`${fieldName} LIKE ?`);
|
|
69
121
|
whereParams.push(filter.value);
|
|
70
122
|
break;
|
|
71
|
-
case
|
|
123
|
+
case "in":
|
|
72
124
|
if (Array.isArray(filter.value)) {
|
|
73
|
-
const placeholders = filter.value.map(() =>
|
|
125
|
+
const placeholders = filter.value.map(() => "?").join(", ");
|
|
74
126
|
whereConditions.push(`${fieldName} IN (${placeholders})`);
|
|
75
127
|
whereParams.push(...filter.value);
|
|
76
128
|
}
|
|
77
129
|
else {
|
|
78
130
|
return {
|
|
79
|
-
status:
|
|
80
|
-
error:
|
|
131
|
+
status: "error",
|
|
132
|
+
error: "IN operator requires an array of values",
|
|
81
133
|
};
|
|
82
134
|
}
|
|
83
135
|
break;
|
|
84
136
|
default:
|
|
85
137
|
return {
|
|
86
|
-
status:
|
|
87
|
-
error: `Unsupported operator: ${filter.operator}
|
|
138
|
+
status: "error",
|
|
139
|
+
error: `Unsupported operator: ${filter.operator}`,
|
|
88
140
|
};
|
|
89
141
|
}
|
|
90
142
|
}
|
|
91
143
|
if (whereConditions.length > 0) {
|
|
92
|
-
whereClause =
|
|
144
|
+
whereClause = "WHERE " + whereConditions.join(" AND ");
|
|
93
145
|
}
|
|
94
146
|
}
|
|
95
147
|
// Build ORDER BY clause
|
|
96
|
-
let orderByClause =
|
|
148
|
+
let orderByClause = "";
|
|
97
149
|
if (sorting) {
|
|
98
150
|
const fieldValidation = this.security.validateIdentifier(sorting.field);
|
|
99
151
|
if (!fieldValidation.valid) {
|
|
100
152
|
return {
|
|
101
|
-
status:
|
|
102
|
-
error: `Invalid sort field name: ${sorting.field}
|
|
153
|
+
status: "error",
|
|
154
|
+
error: `Invalid sort field name: ${sorting.field}`,
|
|
103
155
|
};
|
|
104
156
|
}
|
|
105
157
|
const fieldName = this.security.escapeIdentifier(sorting.field);
|
|
106
|
-
const direction = sorting.direction.toUpperCase() ===
|
|
158
|
+
const direction = sorting.direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
|
|
107
159
|
orderByClause = `ORDER BY ${fieldName} ${direction}`;
|
|
108
160
|
}
|
|
109
161
|
// Build LIMIT clause
|
|
110
|
-
let limitClause =
|
|
162
|
+
let limitClause = "";
|
|
111
163
|
if (pagination) {
|
|
112
164
|
const offset = (pagination.page - 1) * pagination.limit;
|
|
113
165
|
limitClause = `LIMIT ${offset}, ${pagination.limit}`;
|
|
@@ -120,48 +172,52 @@ class DataExportTools {
|
|
|
120
172
|
// If no results, return empty CSV
|
|
121
173
|
if (results.length === 0) {
|
|
122
174
|
return {
|
|
123
|
-
status:
|
|
175
|
+
status: "success",
|
|
124
176
|
data: {
|
|
125
|
-
csv: include_headers ?
|
|
126
|
-
row_count: 0
|
|
127
|
-
}
|
|
177
|
+
csv: include_headers ? "" : "",
|
|
178
|
+
row_count: 0,
|
|
179
|
+
},
|
|
128
180
|
};
|
|
129
181
|
}
|
|
130
182
|
// Generate CSV
|
|
131
|
-
let csv =
|
|
183
|
+
let csv = "";
|
|
132
184
|
// Add headers if requested
|
|
133
185
|
if (include_headers) {
|
|
134
|
-
const headers = Object.keys(results[0]).join(
|
|
135
|
-
csv += headers +
|
|
186
|
+
const headers = Object.keys(results[0]).join(",");
|
|
187
|
+
csv += headers + "\n";
|
|
136
188
|
}
|
|
137
189
|
// Add data rows
|
|
138
190
|
for (const row of results) {
|
|
139
|
-
const values = Object.values(row)
|
|
191
|
+
const values = Object.values(row)
|
|
192
|
+
.map((value) => {
|
|
140
193
|
if (value === null)
|
|
141
|
-
return
|
|
142
|
-
if (typeof value ===
|
|
194
|
+
return "";
|
|
195
|
+
if (typeof value === "string") {
|
|
143
196
|
// Escape quotes and wrap in quotes if contains comma or newline
|
|
144
|
-
if (value.includes(
|
|
197
|
+
if (value.includes(",") ||
|
|
198
|
+
value.includes("\n") ||
|
|
199
|
+
value.includes('"')) {
|
|
145
200
|
return `"${value.replace(/"/g, '""')}"`;
|
|
146
201
|
}
|
|
147
202
|
return value;
|
|
148
203
|
}
|
|
149
204
|
return String(value);
|
|
150
|
-
})
|
|
151
|
-
|
|
205
|
+
})
|
|
206
|
+
.join(",");
|
|
207
|
+
csv += values + "\n";
|
|
152
208
|
}
|
|
153
209
|
return {
|
|
154
|
-
status:
|
|
210
|
+
status: "success",
|
|
155
211
|
data: {
|
|
156
212
|
csv: csv,
|
|
157
|
-
row_count: results.length
|
|
158
|
-
}
|
|
213
|
+
row_count: results.length,
|
|
214
|
+
},
|
|
159
215
|
};
|
|
160
216
|
}
|
|
161
217
|
catch (error) {
|
|
162
218
|
return {
|
|
163
|
-
status:
|
|
164
|
-
error: error.message
|
|
219
|
+
status: "error",
|
|
220
|
+
error: error.message,
|
|
165
221
|
};
|
|
166
222
|
}
|
|
167
223
|
}
|
|
@@ -170,20 +226,20 @@ class DataExportTools {
|
|
|
170
226
|
*/
|
|
171
227
|
async exportQueryToCSV(params) {
|
|
172
228
|
try {
|
|
173
|
-
const { query, params: queryParams = [], include_headers = true } = params;
|
|
229
|
+
const { query, params: queryParams = [], include_headers = true, } = params;
|
|
174
230
|
// Validate query is a SELECT statement
|
|
175
231
|
if (!this.security.isReadOnlyQuery(query)) {
|
|
176
232
|
return {
|
|
177
|
-
status:
|
|
178
|
-
error:
|
|
233
|
+
status: "error",
|
|
234
|
+
error: "Only SELECT queries can be exported to CSV",
|
|
179
235
|
};
|
|
180
236
|
}
|
|
181
237
|
// Validate parameters
|
|
182
238
|
const paramValidation = this.security.validateParameters(queryParams);
|
|
183
239
|
if (!paramValidation.valid) {
|
|
184
240
|
return {
|
|
185
|
-
status:
|
|
186
|
-
error: paramValidation.error
|
|
241
|
+
status: "error",
|
|
242
|
+
error: paramValidation.error,
|
|
187
243
|
};
|
|
188
244
|
}
|
|
189
245
|
// Execute query
|
|
@@ -191,51 +247,655 @@ class DataExportTools {
|
|
|
191
247
|
// If no results, return empty CSV
|
|
192
248
|
if (results.length === 0) {
|
|
193
249
|
return {
|
|
194
|
-
status:
|
|
250
|
+
status: "success",
|
|
195
251
|
data: {
|
|
196
|
-
csv: include_headers ?
|
|
197
|
-
row_count: 0
|
|
252
|
+
csv: include_headers ? "" : "",
|
|
253
|
+
row_count: 0,
|
|
198
254
|
},
|
|
199
|
-
queryLog: this.db.getFormattedQueryLogs(1)
|
|
255
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
200
256
|
};
|
|
201
257
|
}
|
|
202
258
|
// Generate CSV
|
|
203
|
-
let csv =
|
|
259
|
+
let csv = "";
|
|
204
260
|
// Add headers if requested
|
|
205
261
|
if (include_headers) {
|
|
206
|
-
const headers = Object.keys(results[0]).join(
|
|
207
|
-
csv += headers +
|
|
262
|
+
const headers = Object.keys(results[0]).join(",");
|
|
263
|
+
csv += headers + "\n";
|
|
208
264
|
}
|
|
209
265
|
// Add data rows
|
|
210
266
|
for (const row of results) {
|
|
211
|
-
const values = Object.values(row)
|
|
267
|
+
const values = Object.values(row)
|
|
268
|
+
.map((value) => {
|
|
212
269
|
if (value === null)
|
|
213
|
-
return
|
|
214
|
-
if (typeof value ===
|
|
270
|
+
return "";
|
|
271
|
+
if (typeof value === "string") {
|
|
215
272
|
// Escape quotes and wrap in quotes if contains comma or newline
|
|
216
|
-
if (value.includes(
|
|
273
|
+
if (value.includes(",") ||
|
|
274
|
+
value.includes("\n") ||
|
|
275
|
+
value.includes('"')) {
|
|
217
276
|
return `"${value.replace(/"/g, '""')}"`;
|
|
218
277
|
}
|
|
219
278
|
return value;
|
|
220
279
|
}
|
|
221
280
|
return String(value);
|
|
222
|
-
})
|
|
223
|
-
|
|
281
|
+
})
|
|
282
|
+
.join(",");
|
|
283
|
+
csv += values + "\n";
|
|
224
284
|
}
|
|
225
285
|
return {
|
|
226
|
-
status:
|
|
286
|
+
status: "success",
|
|
227
287
|
data: {
|
|
228
288
|
csv: csv,
|
|
229
|
-
row_count: results.length
|
|
289
|
+
row_count: results.length,
|
|
290
|
+
},
|
|
291
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
return {
|
|
296
|
+
status: "error",
|
|
297
|
+
error: error.message,
|
|
298
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Export table data to JSON format
|
|
304
|
+
*/
|
|
305
|
+
async exportTableToJSON(params) {
|
|
306
|
+
try {
|
|
307
|
+
const { table_name, filters = [], pagination, sorting, pretty = true, database, } = params;
|
|
308
|
+
// Validate database access
|
|
309
|
+
const dbValidation = this.validateDatabaseAccess(database);
|
|
310
|
+
if (!dbValidation.valid) {
|
|
311
|
+
return { status: "error", error: dbValidation.error };
|
|
312
|
+
}
|
|
313
|
+
// Validate table name
|
|
314
|
+
const tableValidation = this.security.validateIdentifier(table_name);
|
|
315
|
+
if (!tableValidation.valid) {
|
|
316
|
+
return { status: "error", error: tableValidation.error };
|
|
317
|
+
}
|
|
318
|
+
// Build WHERE clause
|
|
319
|
+
let whereClause = "";
|
|
320
|
+
const whereParams = [];
|
|
321
|
+
if (filters && filters.length > 0) {
|
|
322
|
+
const whereConditions = [];
|
|
323
|
+
for (const filter of filters) {
|
|
324
|
+
const fieldValidation = this.security.validateIdentifier(filter.field);
|
|
325
|
+
if (!fieldValidation.valid) {
|
|
326
|
+
return {
|
|
327
|
+
status: "error",
|
|
328
|
+
error: `Invalid field name: ${filter.field}`,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
const fieldName = this.security.escapeIdentifier(filter.field);
|
|
332
|
+
switch (filter.operator) {
|
|
333
|
+
case "eq":
|
|
334
|
+
whereConditions.push(`${fieldName} = ?`);
|
|
335
|
+
whereParams.push(filter.value);
|
|
336
|
+
break;
|
|
337
|
+
case "neq":
|
|
338
|
+
whereConditions.push(`${fieldName} != ?`);
|
|
339
|
+
whereParams.push(filter.value);
|
|
340
|
+
break;
|
|
341
|
+
case "gt":
|
|
342
|
+
whereConditions.push(`${fieldName} > ?`);
|
|
343
|
+
whereParams.push(filter.value);
|
|
344
|
+
break;
|
|
345
|
+
case "gte":
|
|
346
|
+
whereConditions.push(`${fieldName} >= ?`);
|
|
347
|
+
whereParams.push(filter.value);
|
|
348
|
+
break;
|
|
349
|
+
case "lt":
|
|
350
|
+
whereConditions.push(`${fieldName} < ?`);
|
|
351
|
+
whereParams.push(filter.value);
|
|
352
|
+
break;
|
|
353
|
+
case "lte":
|
|
354
|
+
whereConditions.push(`${fieldName} <= ?`);
|
|
355
|
+
whereParams.push(filter.value);
|
|
356
|
+
break;
|
|
357
|
+
case "like":
|
|
358
|
+
whereConditions.push(`${fieldName} LIKE ?`);
|
|
359
|
+
whereParams.push(filter.value);
|
|
360
|
+
break;
|
|
361
|
+
case "in":
|
|
362
|
+
if (Array.isArray(filter.value)) {
|
|
363
|
+
const placeholders = filter.value.map(() => "?").join(", ");
|
|
364
|
+
whereConditions.push(`${fieldName} IN (${placeholders})`);
|
|
365
|
+
whereParams.push(...filter.value);
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
return {
|
|
369
|
+
status: "error",
|
|
370
|
+
error: "IN operator requires an array of values",
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
break;
|
|
374
|
+
default:
|
|
375
|
+
return {
|
|
376
|
+
status: "error",
|
|
377
|
+
error: `Unsupported operator: ${filter.operator}`,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (whereConditions.length > 0) {
|
|
382
|
+
whereClause = "WHERE " + whereConditions.join(" AND ");
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// Build ORDER BY clause
|
|
386
|
+
let orderByClause = "";
|
|
387
|
+
if (sorting) {
|
|
388
|
+
const fieldValidation = this.security.validateIdentifier(sorting.field);
|
|
389
|
+
if (!fieldValidation.valid) {
|
|
390
|
+
return {
|
|
391
|
+
status: "error",
|
|
392
|
+
error: `Invalid sort field name: ${sorting.field}`,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const fieldName = this.security.escapeIdentifier(sorting.field);
|
|
396
|
+
const direction = sorting.direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
|
|
397
|
+
orderByClause = `ORDER BY ${fieldName} ${direction}`;
|
|
398
|
+
}
|
|
399
|
+
// Build LIMIT clause
|
|
400
|
+
let limitClause = "";
|
|
401
|
+
if (pagination) {
|
|
402
|
+
const offset = (pagination.page - 1) * pagination.limit;
|
|
403
|
+
limitClause = `LIMIT ${offset}, ${pagination.limit}`;
|
|
404
|
+
}
|
|
405
|
+
// Construct and execute query
|
|
406
|
+
const escapedTableName = this.security.escapeIdentifier(table_name);
|
|
407
|
+
const query = `SELECT * FROM ${escapedTableName} ${whereClause} ${orderByClause} ${limitClause}`;
|
|
408
|
+
const results = await this.db.query(query, whereParams);
|
|
409
|
+
// Generate JSON
|
|
410
|
+
const json = pretty
|
|
411
|
+
? JSON.stringify(results, null, 2)
|
|
412
|
+
: JSON.stringify(results);
|
|
413
|
+
return {
|
|
414
|
+
status: "success",
|
|
415
|
+
data: {
|
|
416
|
+
json: json,
|
|
417
|
+
row_count: results.length,
|
|
418
|
+
table_name: table_name,
|
|
419
|
+
},
|
|
420
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
return {
|
|
425
|
+
status: "error",
|
|
426
|
+
error: error.message,
|
|
427
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Export query results to JSON format
|
|
433
|
+
*/
|
|
434
|
+
async exportQueryToJSON(params) {
|
|
435
|
+
try {
|
|
436
|
+
const { query, params: queryParams = [], pretty = true } = params;
|
|
437
|
+
// Validate query is a SELECT statement
|
|
438
|
+
if (!this.security.isReadOnlyQuery(query)) {
|
|
439
|
+
return {
|
|
440
|
+
status: "error",
|
|
441
|
+
error: "Only SELECT queries can be exported to JSON",
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
// Validate parameters
|
|
445
|
+
const paramValidation = this.security.validateParameters(queryParams);
|
|
446
|
+
if (!paramValidation.valid) {
|
|
447
|
+
return { status: "error", error: paramValidation.error };
|
|
448
|
+
}
|
|
449
|
+
// Execute query
|
|
450
|
+
const results = await this.db.query(query, queryParams);
|
|
451
|
+
// Generate JSON
|
|
452
|
+
const json = pretty
|
|
453
|
+
? JSON.stringify(results, null, 2)
|
|
454
|
+
: JSON.stringify(results);
|
|
455
|
+
return {
|
|
456
|
+
status: "success",
|
|
457
|
+
data: {
|
|
458
|
+
json: json,
|
|
459
|
+
row_count: results.length,
|
|
460
|
+
},
|
|
461
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
return {
|
|
466
|
+
status: "error",
|
|
467
|
+
error: error.message,
|
|
468
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Export table data to SQL INSERT statements
|
|
474
|
+
*/
|
|
475
|
+
async exportTableToSql(params) {
|
|
476
|
+
try {
|
|
477
|
+
const { table_name, filters = [], include_create_table = false, batch_size = 100, database, } = params;
|
|
478
|
+
// Validate database access
|
|
479
|
+
const dbValidation = this.validateDatabaseAccess(database);
|
|
480
|
+
if (!dbValidation.valid) {
|
|
481
|
+
return { status: "error", error: dbValidation.error };
|
|
482
|
+
}
|
|
483
|
+
// Validate table name
|
|
484
|
+
const tableValidation = this.security.validateIdentifier(table_name);
|
|
485
|
+
if (!tableValidation.valid) {
|
|
486
|
+
return { status: "error", error: tableValidation.error };
|
|
487
|
+
}
|
|
488
|
+
const escapedTableName = this.security.escapeIdentifier(table_name);
|
|
489
|
+
let sql = "";
|
|
490
|
+
let queryCount = 0;
|
|
491
|
+
// Add CREATE TABLE if requested
|
|
492
|
+
if (include_create_table) {
|
|
493
|
+
const createQuery = `SHOW CREATE TABLE ${escapedTableName}`;
|
|
494
|
+
const createResults = await this.db.query(createQuery);
|
|
495
|
+
queryCount++;
|
|
496
|
+
if (createResults.length > 0) {
|
|
497
|
+
sql += `-- Table structure for ${table_name}\n`;
|
|
498
|
+
sql += `${createResults[0]["Create Table"]};\n\n`;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
// Build WHERE clause
|
|
502
|
+
let whereClause = "";
|
|
503
|
+
const whereParams = [];
|
|
504
|
+
if (filters && filters.length > 0) {
|
|
505
|
+
const whereConditions = [];
|
|
506
|
+
for (const filter of filters) {
|
|
507
|
+
const fieldValidation = this.security.validateIdentifier(filter.field);
|
|
508
|
+
if (!fieldValidation.valid) {
|
|
509
|
+
return {
|
|
510
|
+
status: "error",
|
|
511
|
+
error: `Invalid field name: ${filter.field}`,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
const fieldName = this.security.escapeIdentifier(filter.field);
|
|
515
|
+
switch (filter.operator) {
|
|
516
|
+
case "eq":
|
|
517
|
+
whereConditions.push(`${fieldName} = ?`);
|
|
518
|
+
whereParams.push(filter.value);
|
|
519
|
+
break;
|
|
520
|
+
case "neq":
|
|
521
|
+
whereConditions.push(`${fieldName} != ?`);
|
|
522
|
+
whereParams.push(filter.value);
|
|
523
|
+
break;
|
|
524
|
+
case "gt":
|
|
525
|
+
whereConditions.push(`${fieldName} > ?`);
|
|
526
|
+
whereParams.push(filter.value);
|
|
527
|
+
break;
|
|
528
|
+
case "gte":
|
|
529
|
+
whereConditions.push(`${fieldName} >= ?`);
|
|
530
|
+
whereParams.push(filter.value);
|
|
531
|
+
break;
|
|
532
|
+
case "lt":
|
|
533
|
+
whereConditions.push(`${fieldName} < ?`);
|
|
534
|
+
whereParams.push(filter.value);
|
|
535
|
+
break;
|
|
536
|
+
case "lte":
|
|
537
|
+
whereConditions.push(`${fieldName} <= ?`);
|
|
538
|
+
whereParams.push(filter.value);
|
|
539
|
+
break;
|
|
540
|
+
case "like":
|
|
541
|
+
whereConditions.push(`${fieldName} LIKE ?`);
|
|
542
|
+
whereParams.push(filter.value);
|
|
543
|
+
break;
|
|
544
|
+
case "in":
|
|
545
|
+
if (Array.isArray(filter.value)) {
|
|
546
|
+
const placeholders = filter.value.map(() => "?").join(", ");
|
|
547
|
+
whereConditions.push(`${fieldName} IN (${placeholders})`);
|
|
548
|
+
whereParams.push(...filter.value);
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
return {
|
|
552
|
+
status: "error",
|
|
553
|
+
error: "IN operator requires an array of values",
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
break;
|
|
557
|
+
default:
|
|
558
|
+
return {
|
|
559
|
+
status: "error",
|
|
560
|
+
error: `Unsupported operator: ${filter.operator}`,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (whereConditions.length > 0) {
|
|
565
|
+
whereClause = "WHERE " + whereConditions.join(" AND ");
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// Get data
|
|
569
|
+
const dataQuery = `SELECT * FROM ${escapedTableName} ${whereClause}`;
|
|
570
|
+
const results = await this.db.query(dataQuery, whereParams);
|
|
571
|
+
queryCount++;
|
|
572
|
+
if (results.length > 0) {
|
|
573
|
+
const columns = Object.keys(results[0]);
|
|
574
|
+
const escapedColumns = columns
|
|
575
|
+
.map((c) => this.security.escapeIdentifier(c))
|
|
576
|
+
.join(", ");
|
|
577
|
+
sql += `-- Data for table ${table_name} (${results.length} rows)\n`;
|
|
578
|
+
// Generate INSERT statements in batches
|
|
579
|
+
for (let i = 0; i < results.length; i += batch_size) {
|
|
580
|
+
const batch = results.slice(i, i + batch_size);
|
|
581
|
+
const values = batch
|
|
582
|
+
.map((row) => {
|
|
583
|
+
const rowValues = columns.map((col) => this.escapeValue(row[col]));
|
|
584
|
+
return `(${rowValues.join(", ")})`;
|
|
585
|
+
})
|
|
586
|
+
.join(",\n");
|
|
587
|
+
sql += `INSERT INTO ${escapedTableName} (${escapedColumns}) VALUES\n${values};\n\n`;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
status: "success",
|
|
592
|
+
data: {
|
|
593
|
+
sql: sql,
|
|
594
|
+
row_count: results.length,
|
|
595
|
+
table_name: table_name,
|
|
596
|
+
},
|
|
597
|
+
queryLog: this.db.getFormattedQueryLogs(queryCount),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
return {
|
|
602
|
+
status: "error",
|
|
603
|
+
error: error.message,
|
|
604
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Import data from CSV string
|
|
610
|
+
*/
|
|
611
|
+
async importFromCSV(params) {
|
|
612
|
+
try {
|
|
613
|
+
const { table_name, csv_data, has_headers = true, column_mapping, skip_errors = false, batch_size = 100, database, } = params;
|
|
614
|
+
// Validate database access
|
|
615
|
+
const dbValidation = this.validateDatabaseAccess(database);
|
|
616
|
+
if (!dbValidation.valid) {
|
|
617
|
+
return { status: "error", error: dbValidation.error };
|
|
618
|
+
}
|
|
619
|
+
// Validate table name
|
|
620
|
+
const tableValidation = this.security.validateIdentifier(table_name);
|
|
621
|
+
if (!tableValidation.valid) {
|
|
622
|
+
return { status: "error", error: tableValidation.error };
|
|
623
|
+
}
|
|
624
|
+
// Parse CSV
|
|
625
|
+
const rows = this.parseCSV(csv_data);
|
|
626
|
+
if (rows.length === 0) {
|
|
627
|
+
return { status: "error", error: "CSV data is empty" };
|
|
628
|
+
}
|
|
629
|
+
let headers;
|
|
630
|
+
let dataRows;
|
|
631
|
+
if (has_headers) {
|
|
632
|
+
headers = rows[0];
|
|
633
|
+
dataRows = rows.slice(1);
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
// If no headers, we need column_mapping or use column indexes
|
|
637
|
+
if (!column_mapping) {
|
|
638
|
+
return {
|
|
639
|
+
status: "error",
|
|
640
|
+
error: "Column mapping is required when CSV has no headers",
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
headers = Object.keys(column_mapping);
|
|
644
|
+
dataRows = rows;
|
|
645
|
+
}
|
|
646
|
+
// Apply column mapping if provided
|
|
647
|
+
const finalHeaders = column_mapping
|
|
648
|
+
? headers.map((h) => column_mapping[h] || h)
|
|
649
|
+
: headers;
|
|
650
|
+
// Validate all column names
|
|
651
|
+
for (const col of finalHeaders) {
|
|
652
|
+
const colValidation = this.security.validateIdentifier(col);
|
|
653
|
+
if (!colValidation.valid) {
|
|
654
|
+
return { status: "error", error: `Invalid column name: ${col}` };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const escapedTableName = this.security.escapeIdentifier(table_name);
|
|
658
|
+
const escapedColumns = finalHeaders
|
|
659
|
+
.map((c) => this.security.escapeIdentifier(c))
|
|
660
|
+
.join(", ");
|
|
661
|
+
let successCount = 0;
|
|
662
|
+
let errorCount = 0;
|
|
663
|
+
const errors = [];
|
|
664
|
+
let queryCount = 0;
|
|
665
|
+
// Insert in batches
|
|
666
|
+
for (let i = 0; i < dataRows.length; i += batch_size) {
|
|
667
|
+
const batch = dataRows.slice(i, i + batch_size);
|
|
668
|
+
try {
|
|
669
|
+
const values = batch
|
|
670
|
+
.map((row) => {
|
|
671
|
+
const rowValues = row.map((val) => {
|
|
672
|
+
if (val === "" || val === null || val === undefined)
|
|
673
|
+
return "NULL";
|
|
674
|
+
return this.escapeValue(val);
|
|
675
|
+
});
|
|
676
|
+
return `(${rowValues.join(", ")})`;
|
|
677
|
+
})
|
|
678
|
+
.join(", ");
|
|
679
|
+
const query = `INSERT INTO ${escapedTableName} (${escapedColumns}) VALUES ${values}`;
|
|
680
|
+
await this.db.query(query);
|
|
681
|
+
queryCount++;
|
|
682
|
+
successCount += batch.length;
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
if (skip_errors) {
|
|
686
|
+
// Try inserting rows one by one
|
|
687
|
+
for (let j = 0; j < batch.length; j++) {
|
|
688
|
+
try {
|
|
689
|
+
const rowValues = batch[j].map((val) => {
|
|
690
|
+
if (val === "" || val === null || val === undefined)
|
|
691
|
+
return "NULL";
|
|
692
|
+
return this.escapeValue(val);
|
|
693
|
+
});
|
|
694
|
+
const query = `INSERT INTO ${escapedTableName} (${escapedColumns}) VALUES (${rowValues.join(", ")})`;
|
|
695
|
+
await this.db.query(query);
|
|
696
|
+
queryCount++;
|
|
697
|
+
successCount++;
|
|
698
|
+
}
|
|
699
|
+
catch (rowError) {
|
|
700
|
+
errorCount++;
|
|
701
|
+
errors.push({
|
|
702
|
+
row: i + j + (has_headers ? 2 : 1),
|
|
703
|
+
error: rowError.message,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
return {
|
|
710
|
+
status: "error",
|
|
711
|
+
error: `Import failed at row ${i + 1}: ${error.message}`,
|
|
712
|
+
data: { rows_imported: successCount },
|
|
713
|
+
queryLog: this.db.getFormattedQueryLogs(queryCount),
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return {
|
|
719
|
+
status: errorCount > 0 ? "partial" : "success",
|
|
720
|
+
data: {
|
|
721
|
+
message: errorCount > 0
|
|
722
|
+
? `Import completed with ${errorCount} errors`
|
|
723
|
+
: "Import completed successfully",
|
|
724
|
+
rows_imported: successCount,
|
|
725
|
+
rows_failed: errorCount,
|
|
726
|
+
errors: errors.length > 0 ? errors.slice(0, 10) : undefined,
|
|
727
|
+
},
|
|
728
|
+
queryLog: this.db.getFormattedQueryLogs(queryCount),
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
return {
|
|
733
|
+
status: "error",
|
|
734
|
+
error: error.message,
|
|
735
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Parse CSV string into array of arrays
|
|
741
|
+
*/
|
|
742
|
+
parseCSV(csv) {
|
|
743
|
+
const rows = [];
|
|
744
|
+
const lines = csv.split(/\r?\n/);
|
|
745
|
+
for (const line of lines) {
|
|
746
|
+
if (!line.trim())
|
|
747
|
+
continue;
|
|
748
|
+
const row = [];
|
|
749
|
+
let current = "";
|
|
750
|
+
let inQuotes = false;
|
|
751
|
+
for (let i = 0; i < line.length; i++) {
|
|
752
|
+
const char = line[i];
|
|
753
|
+
const nextChar = line[i + 1];
|
|
754
|
+
if (inQuotes) {
|
|
755
|
+
if (char === '"' && nextChar === '"') {
|
|
756
|
+
current += '"';
|
|
757
|
+
i++; // Skip next quote
|
|
758
|
+
}
|
|
759
|
+
else if (char === '"') {
|
|
760
|
+
inQuotes = false;
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
current += char;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
if (char === '"') {
|
|
768
|
+
inQuotes = true;
|
|
769
|
+
}
|
|
770
|
+
else if (char === ",") {
|
|
771
|
+
row.push(current);
|
|
772
|
+
current = "";
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
current += char;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
row.push(current);
|
|
780
|
+
rows.push(row);
|
|
781
|
+
}
|
|
782
|
+
return rows;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Import data from JSON string
|
|
786
|
+
*/
|
|
787
|
+
async importFromJSON(params) {
|
|
788
|
+
try {
|
|
789
|
+
const { table_name, json_data, column_mapping, skip_errors = false, batch_size = 100, database, } = params;
|
|
790
|
+
// Validate database access
|
|
791
|
+
const dbValidation = this.validateDatabaseAccess(database);
|
|
792
|
+
if (!dbValidation.valid) {
|
|
793
|
+
return { status: "error", error: dbValidation.error };
|
|
794
|
+
}
|
|
795
|
+
// Validate table name
|
|
796
|
+
const tableValidation = this.security.validateIdentifier(table_name);
|
|
797
|
+
if (!tableValidation.valid) {
|
|
798
|
+
return { status: "error", error: tableValidation.error };
|
|
799
|
+
}
|
|
800
|
+
// Parse JSON
|
|
801
|
+
let data;
|
|
802
|
+
try {
|
|
803
|
+
data = JSON.parse(json_data);
|
|
804
|
+
}
|
|
805
|
+
catch (e) {
|
|
806
|
+
return { status: "error", error: "Invalid JSON data" };
|
|
807
|
+
}
|
|
808
|
+
if (!Array.isArray(data)) {
|
|
809
|
+
return {
|
|
810
|
+
status: "error",
|
|
811
|
+
error: "JSON data must be an array of objects",
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
if (data.length === 0) {
|
|
815
|
+
return { status: "error", error: "JSON data is empty" };
|
|
816
|
+
}
|
|
817
|
+
// Get columns from first object
|
|
818
|
+
let columns = Object.keys(data[0]);
|
|
819
|
+
// Apply column mapping if provided
|
|
820
|
+
if (column_mapping) {
|
|
821
|
+
columns = columns.map((c) => column_mapping[c] || c);
|
|
822
|
+
}
|
|
823
|
+
// Validate all column names
|
|
824
|
+
for (const col of columns) {
|
|
825
|
+
const colValidation = this.security.validateIdentifier(col);
|
|
826
|
+
if (!colValidation.valid) {
|
|
827
|
+
return { status: "error", error: `Invalid column name: ${col}` };
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
const escapedTableName = this.security.escapeIdentifier(table_name);
|
|
831
|
+
const originalColumns = Object.keys(data[0]);
|
|
832
|
+
const escapedColumns = columns
|
|
833
|
+
.map((c) => this.security.escapeIdentifier(c))
|
|
834
|
+
.join(", ");
|
|
835
|
+
let successCount = 0;
|
|
836
|
+
let errorCount = 0;
|
|
837
|
+
const errors = [];
|
|
838
|
+
let queryCount = 0;
|
|
839
|
+
// Insert in batches
|
|
840
|
+
for (let i = 0; i < data.length; i += batch_size) {
|
|
841
|
+
const batch = data.slice(i, i + batch_size);
|
|
842
|
+
try {
|
|
843
|
+
const values = batch
|
|
844
|
+
.map((row) => {
|
|
845
|
+
const rowValues = originalColumns.map((col) => this.escapeValue(row[col]));
|
|
846
|
+
return `(${rowValues.join(", ")})`;
|
|
847
|
+
})
|
|
848
|
+
.join(", ");
|
|
849
|
+
const query = `INSERT INTO ${escapedTableName} (${escapedColumns}) VALUES ${values}`;
|
|
850
|
+
await this.db.query(query);
|
|
851
|
+
queryCount++;
|
|
852
|
+
successCount += batch.length;
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
if (skip_errors) {
|
|
856
|
+
// Try inserting rows one by one
|
|
857
|
+
for (let j = 0; j < batch.length; j++) {
|
|
858
|
+
try {
|
|
859
|
+
const rowValues = originalColumns.map((col) => this.escapeValue(batch[j][col]));
|
|
860
|
+
const query = `INSERT INTO ${escapedTableName} (${escapedColumns}) VALUES (${rowValues.join(", ")})`;
|
|
861
|
+
await this.db.query(query);
|
|
862
|
+
queryCount++;
|
|
863
|
+
successCount++;
|
|
864
|
+
}
|
|
865
|
+
catch (rowError) {
|
|
866
|
+
errorCount++;
|
|
867
|
+
errors.push({ row: i + j + 1, error: rowError.message });
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
return {
|
|
873
|
+
status: "error",
|
|
874
|
+
error: `Import failed at row ${i + 1}: ${error.message}`,
|
|
875
|
+
data: { rows_imported: successCount },
|
|
876
|
+
queryLog: this.db.getFormattedQueryLogs(queryCount),
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return {
|
|
882
|
+
status: errorCount > 0 ? "partial" : "success",
|
|
883
|
+
data: {
|
|
884
|
+
message: errorCount > 0
|
|
885
|
+
? `Import completed with ${errorCount} errors`
|
|
886
|
+
: "Import completed successfully",
|
|
887
|
+
rows_imported: successCount,
|
|
888
|
+
rows_failed: errorCount,
|
|
889
|
+
errors: errors.length > 0 ? errors.slice(0, 10) : undefined,
|
|
230
890
|
},
|
|
231
|
-
queryLog: this.db.getFormattedQueryLogs(
|
|
891
|
+
queryLog: this.db.getFormattedQueryLogs(queryCount),
|
|
232
892
|
};
|
|
233
893
|
}
|
|
234
894
|
catch (error) {
|
|
235
895
|
return {
|
|
236
|
-
status:
|
|
896
|
+
status: "error",
|
|
237
897
|
error: error.message,
|
|
238
|
-
queryLog: this.db.getFormattedQueryLogs(1)
|
|
898
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
239
899
|
};
|
|
240
900
|
}
|
|
241
901
|
}
|