@berthojoris/mcp-mysql-server 1.40.6 → 1.42.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/CHANGELOG.md +32 -0
- package/DOCUMENTATIONS.md +88 -18
- package/README.md +15 -5
- package/bin/mcp-mysql.js +5 -5
- package/dist/config/featureConfig.d.ts +3 -2
- package/dist/config/featureConfig.js +35 -4
- package/dist/index.d.ts +53 -1
- package/dist/index.js +56 -4
- package/dist/mcp-server.js +334 -3
- package/dist/security/securityLayer.d.ts +1 -1
- package/dist/security/securityLayer.js +2 -2
- package/dist/tools/dataExportTools.d.ts +11 -0
- package/dist/tools/dataExportTools.js +68 -27
- package/dist/tools/ddlTools.d.ts +2 -0
- package/dist/tools/ddlTools.js +125 -20
- package/dist/tools/relationalSeederTools.d.ts +207 -0
- package/dist/tools/relationalSeederTools.js +1432 -0
- package/dist/tools/toolArgumentValidation.js +117 -0
- package/dist/tools/utilityTools.d.ts +16 -1
- package/dist/tools/utilityTools.js +81 -18
- package/manifest.json +2901 -744
- package/package.json +1 -1
package/dist/mcp-server.js
CHANGED
|
@@ -17,6 +17,8 @@ const toolArgumentValidation_js_1 = require("./tools/toolArgumentValidation.js")
|
|
|
17
17
|
// Layer 2 (Categories): MCP_CATEGORIES (optional, for fine-grained control)
|
|
18
18
|
const permissions = process.env.MCP_PERMISSIONS || process.env.MCP_CONFIG || "";
|
|
19
19
|
const categories = process.env.MCP_CATEGORIES || "";
|
|
20
|
+
const SERVER_NAME = "mysql-mcp-server";
|
|
21
|
+
const SERVER_VERSION = "1.42.0";
|
|
20
22
|
// Declare the MySQL MCP instance (will be initialized in main())
|
|
21
23
|
let mysqlMCP;
|
|
22
24
|
// Define all available tools with their schemas
|
|
@@ -404,6 +406,293 @@ const TOOLS = [
|
|
|
404
406
|
required: ["table_name", "condition_sets"],
|
|
405
407
|
},
|
|
406
408
|
},
|
|
409
|
+
{
|
|
410
|
+
name: "plan_seed_data",
|
|
411
|
+
description: "🌱 RELATIONAL SEEDER: Analyzes target tables, foreign keys, constraints, and row counts to build a safe parent-first seed plan. Use this before generating preview or inserting dummy relational data.",
|
|
412
|
+
inputSchema: {
|
|
413
|
+
type: "object",
|
|
414
|
+
properties: {
|
|
415
|
+
database: {
|
|
416
|
+
type: "string",
|
|
417
|
+
description: "Optional: specific connected database name",
|
|
418
|
+
},
|
|
419
|
+
target_tables: {
|
|
420
|
+
type: "array",
|
|
421
|
+
description: "Target tables to seed",
|
|
422
|
+
minItems: 1,
|
|
423
|
+
items: { type: "string" },
|
|
424
|
+
},
|
|
425
|
+
rows_per_table: {
|
|
426
|
+
oneOf: [
|
|
427
|
+
{ type: "number", minimum: 0 },
|
|
428
|
+
{ type: "object", additionalProperties: { type: "number", minimum: 0 } },
|
|
429
|
+
],
|
|
430
|
+
description: "Rows to create for target tables, either a single number or table-to-count map (default: 10)",
|
|
431
|
+
},
|
|
432
|
+
include_dependencies: {
|
|
433
|
+
type: "boolean",
|
|
434
|
+
description: "Include parent tables required by foreign keys (default: true)",
|
|
435
|
+
},
|
|
436
|
+
include_children: {
|
|
437
|
+
type: "boolean",
|
|
438
|
+
description: "Include child tables that reference included tables for nested relational seeding (default: false)",
|
|
439
|
+
},
|
|
440
|
+
child_rows_per_parent: {
|
|
441
|
+
type: "number",
|
|
442
|
+
description: "Rows to create in child tables for each parent row (default: 2)",
|
|
443
|
+
minimum: 1,
|
|
444
|
+
maximum: 20,
|
|
445
|
+
},
|
|
446
|
+
respect_existing_data: {
|
|
447
|
+
type: "boolean",
|
|
448
|
+
description: "Reuse existing parent records when possible instead of creating new parent rows (default: true)",
|
|
449
|
+
},
|
|
450
|
+
strategy: {
|
|
451
|
+
type: "string",
|
|
452
|
+
enum: ["append"],
|
|
453
|
+
description: "Seed strategy (currently append)",
|
|
454
|
+
},
|
|
455
|
+
random_seed: {
|
|
456
|
+
type: "number",
|
|
457
|
+
description: "Deterministic seed for repeatable preview and execution (default: 42)",
|
|
458
|
+
},
|
|
459
|
+
max_rows_per_table: {
|
|
460
|
+
type: "number",
|
|
461
|
+
description: "Safety cap for generated rows per table (default: 1000)",
|
|
462
|
+
minimum: 1,
|
|
463
|
+
maximum: 10000,
|
|
464
|
+
},
|
|
465
|
+
max_related_tables: {
|
|
466
|
+
type: "number",
|
|
467
|
+
description: "Safety cap for dependency/child expansion (default: 25)",
|
|
468
|
+
minimum: 1,
|
|
469
|
+
maximum: 100,
|
|
470
|
+
},
|
|
471
|
+
seed_rules: {
|
|
472
|
+
type: "object",
|
|
473
|
+
description: "Optional generator overrides keyed by table.column or column name",
|
|
474
|
+
additionalProperties: true,
|
|
475
|
+
},
|
|
476
|
+
require_confirmation: {
|
|
477
|
+
type: "boolean",
|
|
478
|
+
description: "Require confirm_token for non-dry-run execution (default: true)",
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
required: ["target_tables"],
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
name: "generate_seed_preview",
|
|
486
|
+
description: "🌱 RELATIONAL SEEDER: Generates deterministic dummy row previews from a seed plan without writing to the database. Shows symbolic foreign-key placeholders for review before execution.",
|
|
487
|
+
inputSchema: {
|
|
488
|
+
type: "object",
|
|
489
|
+
properties: {
|
|
490
|
+
plan_id: {
|
|
491
|
+
type: "string",
|
|
492
|
+
description: "Seed plan ID returned by plan_seed_data",
|
|
493
|
+
},
|
|
494
|
+
locale: {
|
|
495
|
+
type: "string",
|
|
496
|
+
description: "Optional locale hint for generated values (default: en_US)",
|
|
497
|
+
},
|
|
498
|
+
realistic: {
|
|
499
|
+
type: "boolean",
|
|
500
|
+
description: "Whether to prefer realistic generated values (default: true)",
|
|
501
|
+
},
|
|
502
|
+
max_preview_rows_per_table: {
|
|
503
|
+
type: "number",
|
|
504
|
+
description: "Maximum preview rows per table (default: 3)",
|
|
505
|
+
minimum: 1,
|
|
506
|
+
maximum: 25,
|
|
507
|
+
},
|
|
508
|
+
email_domain: {
|
|
509
|
+
type: "string",
|
|
510
|
+
description: "Safe email domain for generated emails (default: example.test)",
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
required: ["plan_id"],
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
name: "execute_seed_plan",
|
|
518
|
+
description: "🌱 RELATIONAL SEEDER: Executes a confirmed seed plan with dry-run enabled by default, production-name guard, transaction rollback on errors, and foreign-key ID resolution.",
|
|
519
|
+
inputSchema: {
|
|
520
|
+
type: "object",
|
|
521
|
+
properties: {
|
|
522
|
+
plan_id: {
|
|
523
|
+
type: "string",
|
|
524
|
+
description: "Seed plan ID returned by plan_seed_data",
|
|
525
|
+
},
|
|
526
|
+
dry_run: {
|
|
527
|
+
type: "boolean",
|
|
528
|
+
description: "If true, returns execution preview without inserting rows (default: true)",
|
|
529
|
+
},
|
|
530
|
+
use_transaction: {
|
|
531
|
+
type: "boolean",
|
|
532
|
+
description: "Use a transaction and rollback on error (default: true)",
|
|
533
|
+
},
|
|
534
|
+
batch_size: {
|
|
535
|
+
type: "number",
|
|
536
|
+
description: "Reserved batch-size hint for large plans (default: 1 for ID-safe inserts)",
|
|
537
|
+
minimum: 1,
|
|
538
|
+
maximum: 10000,
|
|
539
|
+
},
|
|
540
|
+
on_error: {
|
|
541
|
+
type: "string",
|
|
542
|
+
enum: ["rollback", "stop"],
|
|
543
|
+
description: "Error behavior (default: rollback)",
|
|
544
|
+
},
|
|
545
|
+
confirm_token: {
|
|
546
|
+
type: "string",
|
|
547
|
+
description: "Confirmation token returned by plan_seed_data; required when dry_run is false",
|
|
548
|
+
},
|
|
549
|
+
allow_production: {
|
|
550
|
+
type: "boolean",
|
|
551
|
+
description: "Allow writes to production-like database names (default: false)",
|
|
552
|
+
},
|
|
553
|
+
email_domain: {
|
|
554
|
+
type: "string",
|
|
555
|
+
description: "Safe email domain for generated emails (default: example.test)",
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
required: ["plan_id"],
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
name: "validate_seed_integrity",
|
|
563
|
+
description: "🌱 RELATIONAL SEEDER: Validates seed results with FK orphan checks, required-column checks, unique-collision checks, and inserted row-count checks.",
|
|
564
|
+
inputSchema: {
|
|
565
|
+
type: "object",
|
|
566
|
+
properties: {
|
|
567
|
+
plan_id: {
|
|
568
|
+
type: "string",
|
|
569
|
+
description: "Seed plan ID returned by plan_seed_data",
|
|
570
|
+
},
|
|
571
|
+
tables: {
|
|
572
|
+
type: "array",
|
|
573
|
+
description: "Optional table subset to validate",
|
|
574
|
+
items: { type: "string" },
|
|
575
|
+
},
|
|
576
|
+
check_foreign_keys: {
|
|
577
|
+
type: "boolean",
|
|
578
|
+
description: "Check foreign key integrity (default: true)",
|
|
579
|
+
},
|
|
580
|
+
check_orphans: {
|
|
581
|
+
type: "boolean",
|
|
582
|
+
description: "Alias for check_foreign_keys",
|
|
583
|
+
},
|
|
584
|
+
check_required_columns: {
|
|
585
|
+
type: "boolean",
|
|
586
|
+
description: "Check NOT NULL required columns (default: true)",
|
|
587
|
+
},
|
|
588
|
+
check_unique_collisions: {
|
|
589
|
+
type: "boolean",
|
|
590
|
+
description: "Check unique index collisions (default: true)",
|
|
591
|
+
},
|
|
592
|
+
check_row_counts: {
|
|
593
|
+
type: "boolean",
|
|
594
|
+
description: "Check inserted row counts when execution metadata is available (default: true)",
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
required: ["plan_id"],
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
name: "infer_seed_rules",
|
|
602
|
+
description: "🌱 ADVANCED SEEDER: Infers safe seed generator rules from schema metadata, sample row patterns, unique constraints, and optional ecommerce/POS/CRM domain presets without returning raw PII samples.",
|
|
603
|
+
inputSchema: {
|
|
604
|
+
type: "object",
|
|
605
|
+
properties: {
|
|
606
|
+
database: {
|
|
607
|
+
type: "string",
|
|
608
|
+
description: "Optional: specific connected database name",
|
|
609
|
+
},
|
|
610
|
+
tables: {
|
|
611
|
+
type: "array",
|
|
612
|
+
description: "Optional table subset to analyze; defaults to schema tables up to max_tables",
|
|
613
|
+
items: { type: "string" },
|
|
614
|
+
},
|
|
615
|
+
domain: {
|
|
616
|
+
type: "string",
|
|
617
|
+
enum: ["auto", "generic", "ecommerce", "pos", "crm"],
|
|
618
|
+
description: "Optional domain preset for better business-like dummy data (default: auto)",
|
|
619
|
+
},
|
|
620
|
+
sample_size: {
|
|
621
|
+
type: "number",
|
|
622
|
+
description: "Number of sample rows per table used to infer ranges and enum-like choices (default: 25, max: 100)",
|
|
623
|
+
minimum: 0,
|
|
624
|
+
maximum: 100,
|
|
625
|
+
},
|
|
626
|
+
max_tables: {
|
|
627
|
+
type: "number",
|
|
628
|
+
description: "Maximum number of tables to analyze when tables is omitted (default: 50)",
|
|
629
|
+
minimum: 1,
|
|
630
|
+
maximum: 200,
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
name: "seed_from_template",
|
|
637
|
+
description: "🌱 TEMPLATE SEEDER: Creates a plan-first FK-aware seed workflow from ecommerce, POS, or CRM templates. It detects matching tables, applies domain rules, and returns a seed plan for preview/execution.",
|
|
638
|
+
inputSchema: {
|
|
639
|
+
type: "object",
|
|
640
|
+
properties: {
|
|
641
|
+
database: {
|
|
642
|
+
type: "string",
|
|
643
|
+
description: "Optional: specific connected database name",
|
|
644
|
+
},
|
|
645
|
+
template: {
|
|
646
|
+
type: "string",
|
|
647
|
+
enum: ["ecommerce", "pos", "crm"],
|
|
648
|
+
description: "Business seed template to apply",
|
|
649
|
+
},
|
|
650
|
+
scale: {
|
|
651
|
+
type: "string",
|
|
652
|
+
enum: ["small", "medium", "large"],
|
|
653
|
+
description: "Template size preset (default: small)",
|
|
654
|
+
},
|
|
655
|
+
include: {
|
|
656
|
+
type: "array",
|
|
657
|
+
description: "Optional concrete table list to seed instead of auto-detected template matches",
|
|
658
|
+
items: { type: "string" },
|
|
659
|
+
},
|
|
660
|
+
exclude: {
|
|
661
|
+
type: "array",
|
|
662
|
+
description: "Optional table names to ignore during template matching",
|
|
663
|
+
items: { type: "string" },
|
|
664
|
+
},
|
|
665
|
+
rows_per_table: {
|
|
666
|
+
oneOf: [
|
|
667
|
+
{ type: "number", minimum: 0 },
|
|
668
|
+
{ type: "object", additionalProperties: { type: "number", minimum: 0 } },
|
|
669
|
+
],
|
|
670
|
+
description: "Optional row-count override for the generated plan",
|
|
671
|
+
},
|
|
672
|
+
include_dependencies: {
|
|
673
|
+
type: "boolean",
|
|
674
|
+
description: "Include parent tables required by foreign keys (default: true)",
|
|
675
|
+
},
|
|
676
|
+
include_children: {
|
|
677
|
+
type: "boolean",
|
|
678
|
+
description: "Include child tables that reference included template tables (default: false)",
|
|
679
|
+
},
|
|
680
|
+
respect_existing_data: {
|
|
681
|
+
type: "boolean",
|
|
682
|
+
description: "Reuse existing parent records when possible instead of creating new parent rows (default: true)",
|
|
683
|
+
},
|
|
684
|
+
random_seed: {
|
|
685
|
+
type: "number",
|
|
686
|
+
description: "Deterministic seed for repeatable preview and execution",
|
|
687
|
+
},
|
|
688
|
+
require_confirmation: {
|
|
689
|
+
type: "boolean",
|
|
690
|
+
description: "Require confirm_token for non-dry-run execution (default: true)",
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
required: ["template"],
|
|
694
|
+
},
|
|
695
|
+
},
|
|
407
696
|
{
|
|
408
697
|
name: "run_select_query",
|
|
409
698
|
description: "⚡ PRIMARY TOOL FOR SELECT QUERIES. Executes read-only SELECT statements with parameterization, optimizer hints, query caching, and dry-run mode. Supports complex queries with JOINs, subqueries, and aggregations. ⚠️ ONLY for SELECT - use execute_write_query for INSERT/UPDATE/DELETE, use execute_ddl for CREATE/ALTER/DROP.",
|
|
@@ -1862,8 +2151,8 @@ const TOOLS = [
|
|
|
1862
2151
|
];
|
|
1863
2152
|
// Create the MCP server
|
|
1864
2153
|
const server = new index_js_1.Server({
|
|
1865
|
-
name:
|
|
1866
|
-
version:
|
|
2154
|
+
name: SERVER_NAME,
|
|
2155
|
+
version: SERVER_VERSION,
|
|
1867
2156
|
}, {
|
|
1868
2157
|
capabilities: {
|
|
1869
2158
|
tools: {},
|
|
@@ -1884,6 +2173,20 @@ const TOOL_METHOD_OVERRIDES = {
|
|
|
1884
2173
|
export_query_to_csv: "exportQueryToCSV",
|
|
1885
2174
|
};
|
|
1886
2175
|
const toCamelCase = (value) => value.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
2176
|
+
const getRuntimeToolCatalog = () => {
|
|
2177
|
+
const enabledTools = (0, toolRegistry_js_1.getEnabledTools)(mysqlMCP, TOOLS);
|
|
2178
|
+
return {
|
|
2179
|
+
tools: TOOLS.map((tool) => ({
|
|
2180
|
+
name: tool.name,
|
|
2181
|
+
description: tool.description,
|
|
2182
|
+
inputSchema: tool.inputSchema,
|
|
2183
|
+
})),
|
|
2184
|
+
enabledToolNames: enabledTools.map((tool) => tool.name),
|
|
2185
|
+
accessProfile: mysqlMCP.getAccessProfile(),
|
|
2186
|
+
serverName: SERVER_NAME,
|
|
2187
|
+
serverVersion: SERVER_VERSION,
|
|
2188
|
+
};
|
|
2189
|
+
};
|
|
1887
2190
|
const getCursorRequestFilePath = () => {
|
|
1888
2191
|
const configuredPath = process.env.MYSQL_MCP_CURSOR_REQUEST_FILE ||
|
|
1889
2192
|
process.env.MCP_MYSQL_REQUEST_FILE ||
|
|
@@ -1919,6 +2222,13 @@ const executeToolByName = async (toolName, args = {}) => {
|
|
|
1919
2222
|
if (toolName === "cursor_execute_request") {
|
|
1920
2223
|
throw new Error("cursor_execute_request cannot dispatch to itself");
|
|
1921
2224
|
}
|
|
2225
|
+
const validation = (0, toolArgumentValidation_js_1.validateToolArguments)(toolName, args);
|
|
2226
|
+
if (!validation.valid) {
|
|
2227
|
+
throw new Error(`Validation Error: ${validation.errors?.join(", ") || "Invalid arguments"}`);
|
|
2228
|
+
}
|
|
2229
|
+
if (toolName === "list_all_tools") {
|
|
2230
|
+
return await mysqlMCP.listAllTools(getRuntimeToolCatalog());
|
|
2231
|
+
}
|
|
1922
2232
|
const methodName = TOOL_METHOD_OVERRIDES[toolName] || toCamelCase(toolName);
|
|
1923
2233
|
const method = mysqlMCP[methodName];
|
|
1924
2234
|
if (typeof method !== "function") {
|
|
@@ -2025,6 +2335,24 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
2025
2335
|
case "bulk_delete":
|
|
2026
2336
|
result = await mysqlMCP.bulkDelete((args || {}));
|
|
2027
2337
|
break;
|
|
2338
|
+
case "plan_seed_data":
|
|
2339
|
+
result = await mysqlMCP.planSeedData(args || {});
|
|
2340
|
+
break;
|
|
2341
|
+
case "generate_seed_preview":
|
|
2342
|
+
result = await mysqlMCP.generateSeedPreview(args || {});
|
|
2343
|
+
break;
|
|
2344
|
+
case "execute_seed_plan":
|
|
2345
|
+
result = await mysqlMCP.executeSeedPlan(args || {});
|
|
2346
|
+
break;
|
|
2347
|
+
case "validate_seed_integrity":
|
|
2348
|
+
result = await mysqlMCP.validateSeedIntegrity(args || {});
|
|
2349
|
+
break;
|
|
2350
|
+
case "infer_seed_rules":
|
|
2351
|
+
result = await mysqlMCP.inferSeedRules(args || {});
|
|
2352
|
+
break;
|
|
2353
|
+
case "seed_from_template":
|
|
2354
|
+
result = await mysqlMCP.seedFromTemplate(args || {});
|
|
2355
|
+
break;
|
|
2028
2356
|
// Query Tools
|
|
2029
2357
|
case "run_select_query":
|
|
2030
2358
|
result = await mysqlMCP.runSelectQuery((args || {}));
|
|
@@ -2055,7 +2383,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
2055
2383
|
result = await mysqlMCP.testConnection();
|
|
2056
2384
|
break;
|
|
2057
2385
|
case "list_all_tools":
|
|
2058
|
-
result = await mysqlMCP.listAllTools();
|
|
2386
|
+
result = await mysqlMCP.listAllTools(getRuntimeToolCatalog());
|
|
2059
2387
|
break;
|
|
2060
2388
|
case "read_changelog":
|
|
2061
2389
|
result = await mysqlMCP.readChangelog((args || {}));
|
|
@@ -2102,6 +2430,9 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
2102
2430
|
case "export_table_to_csv":
|
|
2103
2431
|
result = await mysqlMCP.exportTableToCSV((args || {}));
|
|
2104
2432
|
break;
|
|
2433
|
+
case "export_query_to_csv":
|
|
2434
|
+
result = await mysqlMCP.exportQueryToCSV((args || {}));
|
|
2435
|
+
break;
|
|
2105
2436
|
// Query Optimization Tools
|
|
2106
2437
|
case "analyze_query":
|
|
2107
2438
|
result = mysqlMCP.analyzeQuery((args || {}));
|
|
@@ -71,7 +71,7 @@ export declare class SecurityLayer {
|
|
|
71
71
|
/**
|
|
72
72
|
* Check if a query is a read-only SELECT query or information query (SHOW, DESCRIBE, etc.)
|
|
73
73
|
*/
|
|
74
|
-
isReadOnlyQuery(query: string): boolean;
|
|
74
|
+
isReadOnlyQuery(query: string, bypassDangerousCheck?: boolean): boolean;
|
|
75
75
|
/**
|
|
76
76
|
* Check if a query contains dangerous operations
|
|
77
77
|
*/
|
|
@@ -404,13 +404,13 @@ class SecurityLayer {
|
|
|
404
404
|
/**
|
|
405
405
|
* Check if a query is a read-only SELECT query or information query (SHOW, DESCRIBE, etc.)
|
|
406
406
|
*/
|
|
407
|
-
isReadOnlyQuery(query) {
|
|
407
|
+
isReadOnlyQuery(query, bypassDangerousCheck = false) {
|
|
408
408
|
// Check if it's an information query first (SHOW, DESCRIBE, EXPLAIN, etc.)
|
|
409
409
|
if (this.isInformationQuery(query)) {
|
|
410
410
|
return true;
|
|
411
411
|
}
|
|
412
412
|
// Check if it's a SELECT query
|
|
413
|
-
const validation = this.validateQuery(query);
|
|
413
|
+
const validation = this.validateQuery(query, bypassDangerousCheck);
|
|
414
414
|
return validation.valid && validation.queryType === "SELECT";
|
|
415
415
|
}
|
|
416
416
|
/**
|
|
@@ -12,6 +12,8 @@ export declare class DataExportTools {
|
|
|
12
12
|
* Escape string value for SQL INSERT statements
|
|
13
13
|
*/
|
|
14
14
|
private escapeValue;
|
|
15
|
+
private escapeCsvValue;
|
|
16
|
+
private rowsToCSV;
|
|
15
17
|
/**
|
|
16
18
|
* Export table data to CSV format
|
|
17
19
|
*/
|
|
@@ -26,4 +28,13 @@ export declare class DataExportTools {
|
|
|
26
28
|
data?: any;
|
|
27
29
|
error?: string;
|
|
28
30
|
}>;
|
|
31
|
+
exportQueryToCSV(queryParams: {
|
|
32
|
+
query: string;
|
|
33
|
+
params?: any[];
|
|
34
|
+
include_headers?: boolean;
|
|
35
|
+
}): Promise<{
|
|
36
|
+
status: string;
|
|
37
|
+
data?: any;
|
|
38
|
+
error?: string;
|
|
39
|
+
}>;
|
|
29
40
|
}
|
|
@@ -62,6 +62,33 @@ class DataExportTools {
|
|
|
62
62
|
.replace(/\0/g, "\\0");
|
|
63
63
|
return `'${escaped}'`;
|
|
64
64
|
}
|
|
65
|
+
escapeCsvValue(value) {
|
|
66
|
+
if (value === null || value === undefined)
|
|
67
|
+
return "";
|
|
68
|
+
const normalizedValue = value instanceof Date
|
|
69
|
+
? value.toISOString()
|
|
70
|
+
: Buffer.isBuffer(value)
|
|
71
|
+
? value.toString("base64")
|
|
72
|
+
: String(value);
|
|
73
|
+
if (/[",\r\n]/.test(normalizedValue)) {
|
|
74
|
+
return `"${normalizedValue.replace(/"/g, '""')}"`;
|
|
75
|
+
}
|
|
76
|
+
return normalizedValue;
|
|
77
|
+
}
|
|
78
|
+
rowsToCSV(rows, includeHeaders) {
|
|
79
|
+
if (rows.length === 0) {
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
const columns = Object.keys(rows[0]);
|
|
83
|
+
const csvRows = [];
|
|
84
|
+
if (includeHeaders) {
|
|
85
|
+
csvRows.push(columns.map((column) => this.escapeCsvValue(column)).join(","));
|
|
86
|
+
}
|
|
87
|
+
for (const row of rows) {
|
|
88
|
+
csvRows.push(columns.map((column) => this.escapeCsvValue(row[column])).join(","));
|
|
89
|
+
}
|
|
90
|
+
return `${csvRows.join("\n")}\n`;
|
|
91
|
+
}
|
|
65
92
|
/**
|
|
66
93
|
* Export table data to CSV format
|
|
67
94
|
*/
|
|
@@ -179,33 +206,7 @@ class DataExportTools {
|
|
|
179
206
|
},
|
|
180
207
|
};
|
|
181
208
|
}
|
|
182
|
-
|
|
183
|
-
let csv = "";
|
|
184
|
-
// Add headers if requested
|
|
185
|
-
if (include_headers) {
|
|
186
|
-
const headers = Object.keys(results[0]).join(",");
|
|
187
|
-
csv += headers + "\n";
|
|
188
|
-
}
|
|
189
|
-
// Add data rows
|
|
190
|
-
for (const row of results) {
|
|
191
|
-
const values = Object.values(row)
|
|
192
|
-
.map((value) => {
|
|
193
|
-
if (value === null)
|
|
194
|
-
return "";
|
|
195
|
-
if (typeof value === "string") {
|
|
196
|
-
// Escape quotes and wrap in quotes if contains comma or newline
|
|
197
|
-
if (value.includes(",") ||
|
|
198
|
-
value.includes("\n") ||
|
|
199
|
-
value.includes('"')) {
|
|
200
|
-
return `"${value.replace(/"/g, '""')}"`;
|
|
201
|
-
}
|
|
202
|
-
return value;
|
|
203
|
-
}
|
|
204
|
-
return String(value);
|
|
205
|
-
})
|
|
206
|
-
.join(",");
|
|
207
|
-
csv += values + "\n";
|
|
208
|
-
}
|
|
209
|
+
const csv = this.rowsToCSV(results, include_headers);
|
|
209
210
|
return {
|
|
210
211
|
status: "success",
|
|
211
212
|
data: {
|
|
@@ -221,5 +222,45 @@ class DataExportTools {
|
|
|
221
222
|
};
|
|
222
223
|
}
|
|
223
224
|
}
|
|
225
|
+
async exportQueryToCSV(queryParams) {
|
|
226
|
+
try {
|
|
227
|
+
const { query, params = [], include_headers = true, } = queryParams;
|
|
228
|
+
const queryValidation = this.security.validateQuery(query, this.security.hasExecutePermission());
|
|
229
|
+
if (!queryValidation.valid) {
|
|
230
|
+
return {
|
|
231
|
+
status: "error",
|
|
232
|
+
error: `Query validation failed: ${queryValidation.error}`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (queryValidation.queryType !== "SELECT") {
|
|
236
|
+
return {
|
|
237
|
+
status: "error",
|
|
238
|
+
error: "export_query_to_csv only accepts SELECT queries.",
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const paramValidation = this.security.validateParameters(params);
|
|
242
|
+
if (!paramValidation.valid) {
|
|
243
|
+
return {
|
|
244
|
+
status: "error",
|
|
245
|
+
error: `Parameter validation failed: ${paramValidation.error}`,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const results = await this.db.query(query, paramValidation.sanitizedParams, false);
|
|
249
|
+
const maskedResults = this.security.masking.processResults(results);
|
|
250
|
+
return {
|
|
251
|
+
status: "success",
|
|
252
|
+
data: {
|
|
253
|
+
csv: this.rowsToCSV(maskedResults, include_headers),
|
|
254
|
+
row_count: maskedResults.length,
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
return {
|
|
260
|
+
status: "error",
|
|
261
|
+
error: error.message,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
224
265
|
}
|
|
225
266
|
exports.DataExportTools = DataExportTools;
|