@berthojoris/mcp-mysql-server 1.6.3 → 1.8.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.
@@ -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: 'error',
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: 'error',
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 'eq':
95
+ case "eq":
44
96
  whereConditions.push(`${fieldName} = ?`);
45
97
  whereParams.push(filter.value);
46
98
  break;
47
- case 'neq':
99
+ case "neq":
48
100
  whereConditions.push(`${fieldName} != ?`);
49
101
  whereParams.push(filter.value);
50
102
  break;
51
- case 'gt':
103
+ case "gt":
52
104
  whereConditions.push(`${fieldName} > ?`);
53
105
  whereParams.push(filter.value);
54
106
  break;
55
- case 'gte':
107
+ case "gte":
56
108
  whereConditions.push(`${fieldName} >= ?`);
57
109
  whereParams.push(filter.value);
58
110
  break;
59
- case 'lt':
111
+ case "lt":
60
112
  whereConditions.push(`${fieldName} < ?`);
61
113
  whereParams.push(filter.value);
62
114
  break;
63
- case 'lte':
115
+ case "lte":
64
116
  whereConditions.push(`${fieldName} <= ?`);
65
117
  whereParams.push(filter.value);
66
118
  break;
67
- case 'like':
119
+ case "like":
68
120
  whereConditions.push(`${fieldName} LIKE ?`);
69
121
  whereParams.push(filter.value);
70
122
  break;
71
- case 'in':
123
+ case "in":
72
124
  if (Array.isArray(filter.value)) {
73
- const placeholders = filter.value.map(() => '?').join(', ');
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: 'error',
80
- error: 'IN operator requires an array of values'
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: 'error',
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 = 'WHERE ' + whereConditions.join(' AND ');
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: 'error',
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() === 'DESC' ? 'DESC' : 'ASC';
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: 'success',
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 + '\n';
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).map(value => {
191
+ const values = Object.values(row)
192
+ .map((value) => {
140
193
  if (value === null)
141
- return '';
142
- if (typeof value === 'string') {
194
+ return "";
195
+ if (typeof value === "string") {
143
196
  // Escape quotes and wrap in quotes if contains comma or newline
144
- if (value.includes(',') || value.includes('\n') || 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
- }).join(',');
151
- csv += values + '\n';
205
+ })
206
+ .join(",");
207
+ csv += values + "\n";
152
208
  }
153
209
  return {
154
- status: 'success',
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: 'error',
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: 'error',
178
- error: 'Only SELECT queries can be exported to CSV'
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: 'error',
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: 'success',
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 + '\n';
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).map(value => {
267
+ const values = Object.values(row)
268
+ .map((value) => {
212
269
  if (value === null)
213
- return '';
214
- if (typeof value === 'string') {
270
+ return "";
271
+ if (typeof value === "string") {
215
272
  // Escape quotes and wrap in quotes if contains comma or newline
216
- if (value.includes(',') || value.includes('\n') || 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
- }).join(',');
223
- csv += values + '\n';
281
+ })
282
+ .join(",");
283
+ csv += values + "\n";
224
284
  }
225
285
  return {
226
- status: 'success',
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(1)
891
+ queryLog: this.db.getFormattedQueryLogs(queryCount),
232
892
  };
233
893
  }
234
894
  catch (error) {
235
895
  return {
236
- status: 'error',
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
  }