@berthojoris/mcp-mysql-server 1.2.5 → 1.4.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/LICENSE +21 -21
- package/README.md +290 -81
- package/dist/auth/authService.d.ts +29 -0
- package/dist/auth/authService.js +114 -0
- package/dist/config/featureConfig.d.ts +5 -0
- package/dist/config/featureConfig.js +59 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +18 -1
- package/dist/mcp-server.js +95 -1
- package/dist/tools/dataExportTools.d.ts +33 -0
- package/dist/tools/dataExportTools.js +240 -0
- package/dist/tools/databaseTools.js +14 -14
- package/dist/tools/utilityTools.js +25 -25
- package/dist/validation/schemas.d.ts +87 -0
- package/dist/validation/schemas.js +62 -2
- package/manifest.json +247 -247
- package/package.json +2 -2
|
@@ -36,6 +36,10 @@ exports.toolCategoryMap = {
|
|
|
36
36
|
'readRecords': ToolCategory.READ,
|
|
37
37
|
'updateRecord': ToolCategory.UPDATE,
|
|
38
38
|
'deleteRecord': ToolCategory.DELETE,
|
|
39
|
+
// Bulk operations
|
|
40
|
+
'bulkInsert': ToolCategory.CREATE,
|
|
41
|
+
'bulkUpdate': ToolCategory.UPDATE,
|
|
42
|
+
'bulkDelete': ToolCategory.DELETE,
|
|
39
43
|
// Query tools
|
|
40
44
|
'runQuery': ToolCategory.READ,
|
|
41
45
|
'executeSql': ToolCategory.EXECUTE,
|
|
@@ -48,6 +52,8 @@ exports.toolCategoryMap = {
|
|
|
48
52
|
'describeConnection': ToolCategory.UTILITY,
|
|
49
53
|
'testConnection': ToolCategory.UTILITY,
|
|
50
54
|
'getTableRelationships': ToolCategory.UTILITY,
|
|
55
|
+
'exportTableToCSV': ToolCategory.UTILITY,
|
|
56
|
+
'exportQueryToCSV': ToolCategory.UTILITY,
|
|
51
57
|
// Transaction tools
|
|
52
58
|
'beginTransaction': ToolCategory.TRANSACTION,
|
|
53
59
|
'commitTransaction': ToolCategory.TRANSACTION,
|
|
@@ -67,6 +73,7 @@ exports.toolCategoryMap = {
|
|
|
67
73
|
*/
|
|
68
74
|
class FeatureConfig {
|
|
69
75
|
constructor(configStr) {
|
|
76
|
+
this.originalConfigString = configStr || process.env.MCP_CONFIG || '';
|
|
70
77
|
this.enabledCategories = this.parseConfig(configStr);
|
|
71
78
|
}
|
|
72
79
|
/**
|
|
@@ -88,6 +95,7 @@ class FeatureConfig {
|
|
|
88
95
|
* Update configuration at runtime
|
|
89
96
|
*/
|
|
90
97
|
setConfig(configStr) {
|
|
98
|
+
this.originalConfigString = configStr;
|
|
91
99
|
this.enabledCategories = this.parseConfig(configStr);
|
|
92
100
|
}
|
|
93
101
|
/**
|
|
@@ -102,6 +110,57 @@ class FeatureConfig {
|
|
|
102
110
|
}
|
|
103
111
|
return this.enabledCategories.has(category);
|
|
104
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Get detailed permission error message for a specific tool
|
|
115
|
+
*/
|
|
116
|
+
getPermissionError(toolName) {
|
|
117
|
+
const category = exports.toolCategoryMap[toolName];
|
|
118
|
+
if (!category) {
|
|
119
|
+
return `Unknown tool '${toolName}'. This tool is not recognized by the MCP server.`;
|
|
120
|
+
}
|
|
121
|
+
const isAllPermissions = !this.originalConfigString.trim();
|
|
122
|
+
const currentPermissions = isAllPermissions ? 'all' : this.originalConfigString;
|
|
123
|
+
const actionDescriptions = {
|
|
124
|
+
[ToolCategory.LIST]: 'list databases and tables',
|
|
125
|
+
[ToolCategory.READ]: 'read data from tables',
|
|
126
|
+
[ToolCategory.CREATE]: 'create new records',
|
|
127
|
+
[ToolCategory.UPDATE]: 'update existing records',
|
|
128
|
+
[ToolCategory.DELETE]: 'delete records',
|
|
129
|
+
[ToolCategory.EXECUTE]: 'execute custom SQL queries',
|
|
130
|
+
[ToolCategory.DDL]: 'create, alter, or drop tables (schema changes)',
|
|
131
|
+
[ToolCategory.UTILITY]: 'use utility functions',
|
|
132
|
+
[ToolCategory.TRANSACTION]: 'manage database transactions',
|
|
133
|
+
[ToolCategory.PROCEDURE]: 'manage stored procedures'
|
|
134
|
+
};
|
|
135
|
+
const toolDescriptions = {
|
|
136
|
+
'createTable': 'create new tables',
|
|
137
|
+
'alterTable': 'modify table structure',
|
|
138
|
+
'dropTable': 'delete tables',
|
|
139
|
+
'executeDdl': 'execute DDL statements',
|
|
140
|
+
'createRecord': 'insert new records',
|
|
141
|
+
'updateRecord': 'update existing records',
|
|
142
|
+
'deleteRecord': 'delete records',
|
|
143
|
+
'bulkInsert': 'insert multiple records in batches',
|
|
144
|
+
'bulkUpdate': 'update multiple records in batches',
|
|
145
|
+
'bulkDelete': 'delete multiple records in batches',
|
|
146
|
+
'executeSql': 'execute custom SQL statements',
|
|
147
|
+
'runQuery': 'run SELECT queries',
|
|
148
|
+
'beginTransaction': 'start database transactions',
|
|
149
|
+
'commitTransaction': 'commit database transactions',
|
|
150
|
+
'rollbackTransaction': 'rollback database transactions',
|
|
151
|
+
'executeInTransaction': 'execute queries within transactions',
|
|
152
|
+
'createStoredProcedure': 'create stored procedures',
|
|
153
|
+
'dropStoredProcedure': 'delete stored procedures',
|
|
154
|
+
'executeStoredProcedure': 'execute stored procedures',
|
|
155
|
+
'exportTableToCSV': 'export table data to CSV',
|
|
156
|
+
'exportQueryToCSV': 'export query results to CSV'
|
|
157
|
+
};
|
|
158
|
+
const toolDescription = toolDescriptions[toolName] || actionDescriptions[category];
|
|
159
|
+
const requiredPermission = category;
|
|
160
|
+
return `Permission denied: Cannot ${toolDescription}. ` +
|
|
161
|
+
`This action requires '${requiredPermission}' permission, but your current MCP configuration only allows: ${currentPermissions}. ` +
|
|
162
|
+
`To enable this feature, update your MCP server configuration to include '${requiredPermission}' in the permissions list.`;
|
|
163
|
+
}
|
|
105
164
|
/**
|
|
106
165
|
* Check if a category is enabled
|
|
107
166
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare class MySQLMCP {
|
|
|
10
10
|
private ddlTools;
|
|
11
11
|
private transactionTools;
|
|
12
12
|
private storedProcedureTools;
|
|
13
|
+
private dataExportTools;
|
|
13
14
|
private security;
|
|
14
15
|
private featureConfig;
|
|
15
16
|
constructor(permissionsConfig?: string);
|
|
@@ -224,6 +225,32 @@ export declare class MySQLMCP {
|
|
|
224
225
|
data?: any;
|
|
225
226
|
error?: string;
|
|
226
227
|
}>;
|
|
228
|
+
exportTableToCSV(params: {
|
|
229
|
+
table_name: string;
|
|
230
|
+
filters?: any[];
|
|
231
|
+
pagination?: {
|
|
232
|
+
page: number;
|
|
233
|
+
limit: number;
|
|
234
|
+
};
|
|
235
|
+
sorting?: {
|
|
236
|
+
field: string;
|
|
237
|
+
direction: 'asc' | 'desc';
|
|
238
|
+
};
|
|
239
|
+
include_headers?: boolean;
|
|
240
|
+
}): Promise<{
|
|
241
|
+
status: string;
|
|
242
|
+
data?: any;
|
|
243
|
+
error?: string;
|
|
244
|
+
}>;
|
|
245
|
+
exportQueryToCSV(params: {
|
|
246
|
+
query: string;
|
|
247
|
+
params?: any[];
|
|
248
|
+
include_headers?: boolean;
|
|
249
|
+
}): Promise<{
|
|
250
|
+
status: string;
|
|
251
|
+
data?: any;
|
|
252
|
+
error?: string;
|
|
253
|
+
}>;
|
|
227
254
|
getFeatureStatus(): {
|
|
228
255
|
status: string;
|
|
229
256
|
data: {
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const utilityTools_1 = require("./tools/utilityTools");
|
|
|
11
11
|
const ddlTools_1 = require("./tools/ddlTools");
|
|
12
12
|
const transactionTools_1 = require("./tools/transactionTools");
|
|
13
13
|
const storedProcedureTools_1 = require("./tools/storedProcedureTools");
|
|
14
|
+
const dataExportTools_1 = require("./tools/dataExportTools");
|
|
14
15
|
const securityLayer_1 = __importDefault(require("./security/securityLayer"));
|
|
15
16
|
const connection_1 = __importDefault(require("./db/connection"));
|
|
16
17
|
const featureConfig_1 = require("./config/featureConfig");
|
|
@@ -29,13 +30,14 @@ class MySQLMCP {
|
|
|
29
30
|
this.ddlTools = new ddlTools_1.DdlTools();
|
|
30
31
|
this.transactionTools = new transactionTools_1.TransactionTools();
|
|
31
32
|
this.storedProcedureTools = new storedProcedureTools_1.StoredProcedureTools(this.security);
|
|
33
|
+
this.dataExportTools = new dataExportTools_1.DataExportTools(this.security);
|
|
32
34
|
}
|
|
33
35
|
// Helper method to check if tool is enabled
|
|
34
36
|
checkToolEnabled(toolName) {
|
|
35
37
|
if (!this.featureConfig.isToolEnabled(toolName)) {
|
|
36
38
|
return {
|
|
37
39
|
enabled: false,
|
|
38
|
-
error:
|
|
40
|
+
error: this.featureConfig.getPermissionError(toolName)
|
|
39
41
|
};
|
|
40
42
|
}
|
|
41
43
|
return { enabled: true };
|
|
@@ -253,6 +255,21 @@ class MySQLMCP {
|
|
|
253
255
|
}
|
|
254
256
|
return await this.storedProcedureTools.showCreateProcedure(params);
|
|
255
257
|
}
|
|
258
|
+
// Data Export Tools
|
|
259
|
+
async exportTableToCSV(params) {
|
|
260
|
+
const check = this.checkToolEnabled('exportTableToCSV');
|
|
261
|
+
if (!check.enabled) {
|
|
262
|
+
return { status: 'error', error: check.error };
|
|
263
|
+
}
|
|
264
|
+
return await this.dataExportTools.exportTableToCSV(params);
|
|
265
|
+
}
|
|
266
|
+
async exportQueryToCSV(params) {
|
|
267
|
+
const check = this.checkToolEnabled('exportQueryToCSV');
|
|
268
|
+
if (!check.enabled) {
|
|
269
|
+
return { status: 'error', error: check.error };
|
|
270
|
+
}
|
|
271
|
+
return await this.dataExportTools.exportQueryToCSV(params);
|
|
272
|
+
}
|
|
256
273
|
// Get feature configuration status
|
|
257
274
|
getFeatureStatus() {
|
|
258
275
|
return {
|
package/dist/mcp-server.js
CHANGED
|
@@ -692,11 +692,85 @@ const TOOLS = [
|
|
|
692
692
|
required: ['procedure_name'],
|
|
693
693
|
},
|
|
694
694
|
},
|
|
695
|
+
// Data Export Tools
|
|
696
|
+
{
|
|
697
|
+
name: 'export_table_to_csv',
|
|
698
|
+
description: 'Export table data to CSV format with optional filtering, pagination, and sorting.',
|
|
699
|
+
inputSchema: {
|
|
700
|
+
type: 'object',
|
|
701
|
+
properties: {
|
|
702
|
+
table_name: {
|
|
703
|
+
type: 'string',
|
|
704
|
+
description: 'Name of the table to export',
|
|
705
|
+
},
|
|
706
|
+
filters: {
|
|
707
|
+
type: 'array',
|
|
708
|
+
description: 'Array of filter conditions',
|
|
709
|
+
items: {
|
|
710
|
+
type: 'object',
|
|
711
|
+
properties: {
|
|
712
|
+
field: { type: 'string' },
|
|
713
|
+
operator: {
|
|
714
|
+
type: 'string',
|
|
715
|
+
enum: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'like', 'in']
|
|
716
|
+
},
|
|
717
|
+
value: {
|
|
718
|
+
description: 'Value to compare against (can be string, number, boolean, or array for "in" operator)'
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
required: ['field', 'operator', 'value'],
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
pagination: {
|
|
725
|
+
type: 'object',
|
|
726
|
+
properties: {
|
|
727
|
+
page: { type: 'number', description: 'Page number (starting from 1)' },
|
|
728
|
+
limit: { type: 'number', description: 'Number of records per page' },
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
sorting: {
|
|
732
|
+
type: 'object',
|
|
733
|
+
properties: {
|
|
734
|
+
field: { type: 'string', description: 'Field name to sort by' },
|
|
735
|
+
direction: { type: 'string', enum: ['asc', 'desc'] },
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
include_headers: {
|
|
739
|
+
type: 'boolean',
|
|
740
|
+
description: 'Whether to include column headers in the CSV output',
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
required: ['table_name'],
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
name: 'export_query_to_csv',
|
|
748
|
+
description: 'Export the results of a SELECT query to CSV format.',
|
|
749
|
+
inputSchema: {
|
|
750
|
+
type: 'object',
|
|
751
|
+
properties: {
|
|
752
|
+
query: {
|
|
753
|
+
type: 'string',
|
|
754
|
+
description: 'SQL SELECT query to execute and export',
|
|
755
|
+
},
|
|
756
|
+
params: {
|
|
757
|
+
type: 'array',
|
|
758
|
+
description: 'Optional array of parameters for parameterized queries',
|
|
759
|
+
items: {},
|
|
760
|
+
},
|
|
761
|
+
include_headers: {
|
|
762
|
+
type: 'boolean',
|
|
763
|
+
description: 'Whether to include column headers in the CSV output',
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
required: ['query'],
|
|
767
|
+
},
|
|
768
|
+
},
|
|
695
769
|
];
|
|
696
770
|
// Create the MCP server
|
|
697
771
|
const server = new index_js_1.Server({
|
|
698
772
|
name: 'mysql-mcp-server',
|
|
699
|
-
version: '1.
|
|
773
|
+
version: '1.3.0',
|
|
700
774
|
}, {
|
|
701
775
|
capabilities: {
|
|
702
776
|
tools: {},
|
|
@@ -806,6 +880,13 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
806
880
|
case 'show_create_procedure':
|
|
807
881
|
result = await mysqlMCP.showCreateProcedure(args);
|
|
808
882
|
break;
|
|
883
|
+
// Data Export Tools
|
|
884
|
+
case 'export_table_to_csv':
|
|
885
|
+
result = await mysqlMCP.exportTableToCSV(args);
|
|
886
|
+
break;
|
|
887
|
+
case 'export_query_to_csv':
|
|
888
|
+
result = await mysqlMCP.exportQueryToCSV(args);
|
|
889
|
+
break;
|
|
809
890
|
default:
|
|
810
891
|
throw new Error(`Unknown tool: ${name}`);
|
|
811
892
|
}
|
|
@@ -857,6 +938,19 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
857
938
|
};
|
|
858
939
|
}
|
|
859
940
|
catch (error) {
|
|
941
|
+
// Check if this is a permission error
|
|
942
|
+
if (error.message && error.message.includes('Permission denied')) {
|
|
943
|
+
return {
|
|
944
|
+
content: [
|
|
945
|
+
{
|
|
946
|
+
type: 'text',
|
|
947
|
+
text: `❌ ${error.message}`,
|
|
948
|
+
},
|
|
949
|
+
],
|
|
950
|
+
isError: true,
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
// Handle other errors with generic message
|
|
860
954
|
return {
|
|
861
955
|
content: [
|
|
862
956
|
{
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { FilterCondition, Pagination, Sorting } from '../validation/schemas';
|
|
2
|
+
import SecurityLayer from '../security/securityLayer';
|
|
3
|
+
export declare class DataExportTools {
|
|
4
|
+
private db;
|
|
5
|
+
private security;
|
|
6
|
+
constructor(security: SecurityLayer);
|
|
7
|
+
/**
|
|
8
|
+
* Export table data to CSV format
|
|
9
|
+
*/
|
|
10
|
+
exportTableToCSV(params: {
|
|
11
|
+
table_name: string;
|
|
12
|
+
filters?: FilterCondition[];
|
|
13
|
+
pagination?: Pagination;
|
|
14
|
+
sorting?: Sorting;
|
|
15
|
+
include_headers?: boolean;
|
|
16
|
+
}): Promise<{
|
|
17
|
+
status: string;
|
|
18
|
+
data?: any;
|
|
19
|
+
error?: string;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Export query results to CSV format
|
|
23
|
+
*/
|
|
24
|
+
exportQueryToCSV(params: {
|
|
25
|
+
query: string;
|
|
26
|
+
params?: any[];
|
|
27
|
+
include_headers?: boolean;
|
|
28
|
+
}): Promise<{
|
|
29
|
+
status: string;
|
|
30
|
+
data?: any;
|
|
31
|
+
error?: string;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
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.DataExportTools = void 0;
|
|
7
|
+
const connection_1 = __importDefault(require("../db/connection"));
|
|
8
|
+
class DataExportTools {
|
|
9
|
+
constructor(security) {
|
|
10
|
+
this.db = connection_1.default.getInstance();
|
|
11
|
+
this.security = security;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Export table data to CSV format
|
|
15
|
+
*/
|
|
16
|
+
async exportTableToCSV(params) {
|
|
17
|
+
try {
|
|
18
|
+
const { table_name, filters = [], pagination, sorting, include_headers = true } = params;
|
|
19
|
+
// Validate table name
|
|
20
|
+
const tableValidation = this.security.validateIdentifier(table_name);
|
|
21
|
+
if (!tableValidation.valid) {
|
|
22
|
+
return {
|
|
23
|
+
status: 'error',
|
|
24
|
+
error: tableValidation.error
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// Build WHERE clause
|
|
28
|
+
let whereClause = '';
|
|
29
|
+
const whereParams = [];
|
|
30
|
+
if (filters && filters.length > 0) {
|
|
31
|
+
const whereConditions = [];
|
|
32
|
+
for (const filter of filters) {
|
|
33
|
+
// Validate field name
|
|
34
|
+
const fieldValidation = this.security.validateIdentifier(filter.field);
|
|
35
|
+
if (!fieldValidation.valid) {
|
|
36
|
+
return {
|
|
37
|
+
status: 'error',
|
|
38
|
+
error: `Invalid field name: ${filter.field}`
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const fieldName = this.security.escapeIdentifier(filter.field);
|
|
42
|
+
switch (filter.operator) {
|
|
43
|
+
case 'eq':
|
|
44
|
+
whereConditions.push(`${fieldName} = ?`);
|
|
45
|
+
whereParams.push(filter.value);
|
|
46
|
+
break;
|
|
47
|
+
case 'neq':
|
|
48
|
+
whereConditions.push(`${fieldName} != ?`);
|
|
49
|
+
whereParams.push(filter.value);
|
|
50
|
+
break;
|
|
51
|
+
case 'gt':
|
|
52
|
+
whereConditions.push(`${fieldName} > ?`);
|
|
53
|
+
whereParams.push(filter.value);
|
|
54
|
+
break;
|
|
55
|
+
case 'gte':
|
|
56
|
+
whereConditions.push(`${fieldName} >= ?`);
|
|
57
|
+
whereParams.push(filter.value);
|
|
58
|
+
break;
|
|
59
|
+
case 'lt':
|
|
60
|
+
whereConditions.push(`${fieldName} < ?`);
|
|
61
|
+
whereParams.push(filter.value);
|
|
62
|
+
break;
|
|
63
|
+
case 'lte':
|
|
64
|
+
whereConditions.push(`${fieldName} <= ?`);
|
|
65
|
+
whereParams.push(filter.value);
|
|
66
|
+
break;
|
|
67
|
+
case 'like':
|
|
68
|
+
whereConditions.push(`${fieldName} LIKE ?`);
|
|
69
|
+
whereParams.push(filter.value);
|
|
70
|
+
break;
|
|
71
|
+
case 'in':
|
|
72
|
+
if (Array.isArray(filter.value)) {
|
|
73
|
+
const placeholders = filter.value.map(() => '?').join(', ');
|
|
74
|
+
whereConditions.push(`${fieldName} IN (${placeholders})`);
|
|
75
|
+
whereParams.push(...filter.value);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
return {
|
|
79
|
+
status: 'error',
|
|
80
|
+
error: 'IN operator requires an array of values'
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
default:
|
|
85
|
+
return {
|
|
86
|
+
status: 'error',
|
|
87
|
+
error: `Unsupported operator: ${filter.operator}`
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (whereConditions.length > 0) {
|
|
92
|
+
whereClause = 'WHERE ' + whereConditions.join(' AND ');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Build ORDER BY clause
|
|
96
|
+
let orderByClause = '';
|
|
97
|
+
if (sorting) {
|
|
98
|
+
const fieldValidation = this.security.validateIdentifier(sorting.field);
|
|
99
|
+
if (!fieldValidation.valid) {
|
|
100
|
+
return {
|
|
101
|
+
status: 'error',
|
|
102
|
+
error: `Invalid sort field name: ${sorting.field}`
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const fieldName = this.security.escapeIdentifier(sorting.field);
|
|
106
|
+
const direction = sorting.direction.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
|
107
|
+
orderByClause = `ORDER BY ${fieldName} ${direction}`;
|
|
108
|
+
}
|
|
109
|
+
// Build LIMIT clause
|
|
110
|
+
let limitClause = '';
|
|
111
|
+
if (pagination) {
|
|
112
|
+
const offset = (pagination.page - 1) * pagination.limit;
|
|
113
|
+
limitClause = `LIMIT ${offset}, ${pagination.limit}`;
|
|
114
|
+
}
|
|
115
|
+
// Construct the query
|
|
116
|
+
const escapedTableName = this.security.escapeIdentifier(table_name);
|
|
117
|
+
const query = `SELECT * FROM ${escapedTableName} ${whereClause} ${orderByClause} ${limitClause}`;
|
|
118
|
+
// Execute query
|
|
119
|
+
const results = await this.db.query(query, whereParams);
|
|
120
|
+
// If no results, return empty CSV
|
|
121
|
+
if (results.length === 0) {
|
|
122
|
+
return {
|
|
123
|
+
status: 'success',
|
|
124
|
+
data: {
|
|
125
|
+
csv: include_headers ? '' : '',
|
|
126
|
+
row_count: 0
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// Generate CSV
|
|
131
|
+
let csv = '';
|
|
132
|
+
// Add headers if requested
|
|
133
|
+
if (include_headers) {
|
|
134
|
+
const headers = Object.keys(results[0]).join(',');
|
|
135
|
+
csv += headers + '\n';
|
|
136
|
+
}
|
|
137
|
+
// Add data rows
|
|
138
|
+
for (const row of results) {
|
|
139
|
+
const values = Object.values(row).map(value => {
|
|
140
|
+
if (value === null)
|
|
141
|
+
return '';
|
|
142
|
+
if (typeof value === 'string') {
|
|
143
|
+
// Escape quotes and wrap in quotes if contains comma or newline
|
|
144
|
+
if (value.includes(',') || value.includes('\n') || value.includes('"')) {
|
|
145
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
146
|
+
}
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
return String(value);
|
|
150
|
+
}).join(',');
|
|
151
|
+
csv += values + '\n';
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
status: 'success',
|
|
155
|
+
data: {
|
|
156
|
+
csv: csv,
|
|
157
|
+
row_count: results.length
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
return {
|
|
163
|
+
status: 'error',
|
|
164
|
+
error: error.message
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Export query results to CSV format
|
|
170
|
+
*/
|
|
171
|
+
async exportQueryToCSV(params) {
|
|
172
|
+
try {
|
|
173
|
+
const { query, params: queryParams = [], include_headers = true } = params;
|
|
174
|
+
// Validate query is a SELECT statement
|
|
175
|
+
if (!this.security.isReadOnlyQuery(query)) {
|
|
176
|
+
return {
|
|
177
|
+
status: 'error',
|
|
178
|
+
error: 'Only SELECT queries can be exported to CSV'
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// Validate parameters
|
|
182
|
+
const paramValidation = this.security.validateParameters(queryParams);
|
|
183
|
+
if (!paramValidation.valid) {
|
|
184
|
+
return {
|
|
185
|
+
status: 'error',
|
|
186
|
+
error: paramValidation.error
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// Execute query
|
|
190
|
+
const results = await this.db.query(query, queryParams);
|
|
191
|
+
// If no results, return empty CSV
|
|
192
|
+
if (results.length === 0) {
|
|
193
|
+
return {
|
|
194
|
+
status: 'success',
|
|
195
|
+
data: {
|
|
196
|
+
csv: include_headers ? '' : '',
|
|
197
|
+
row_count: 0
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
// Generate CSV
|
|
202
|
+
let csv = '';
|
|
203
|
+
// Add headers if requested
|
|
204
|
+
if (include_headers) {
|
|
205
|
+
const headers = Object.keys(results[0]).join(',');
|
|
206
|
+
csv += headers + '\n';
|
|
207
|
+
}
|
|
208
|
+
// Add data rows
|
|
209
|
+
for (const row of results) {
|
|
210
|
+
const values = Object.values(row).map(value => {
|
|
211
|
+
if (value === null)
|
|
212
|
+
return '';
|
|
213
|
+
if (typeof value === 'string') {
|
|
214
|
+
// Escape quotes and wrap in quotes if contains comma or newline
|
|
215
|
+
if (value.includes(',') || value.includes('\n') || value.includes('"')) {
|
|
216
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
217
|
+
}
|
|
218
|
+
return value;
|
|
219
|
+
}
|
|
220
|
+
return String(value);
|
|
221
|
+
}).join(',');
|
|
222
|
+
csv += values + '\n';
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
status: 'success',
|
|
226
|
+
data: {
|
|
227
|
+
csv: csv,
|
|
228
|
+
row_count: results.length
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
return {
|
|
234
|
+
status: 'error',
|
|
235
|
+
error: error.message
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
exports.DataExportTools = DataExportTools;
|
|
@@ -108,20 +108,20 @@ class DatabaseTools {
|
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
110
|
try {
|
|
111
|
-
const query = `
|
|
112
|
-
SELECT
|
|
113
|
-
COLUMN_NAME as column_name,
|
|
114
|
-
DATA_TYPE as data_type,
|
|
115
|
-
IS_NULLABLE as is_nullable,
|
|
116
|
-
COLUMN_KEY as column_key,
|
|
117
|
-
COLUMN_DEFAULT as column_default,
|
|
118
|
-
EXTRA as extra
|
|
119
|
-
FROM
|
|
120
|
-
INFORMATION_SCHEMA.COLUMNS
|
|
121
|
-
WHERE
|
|
122
|
-
TABLE_NAME = ?
|
|
123
|
-
ORDER BY
|
|
124
|
-
ORDINAL_POSITION
|
|
111
|
+
const query = `
|
|
112
|
+
SELECT
|
|
113
|
+
COLUMN_NAME as column_name,
|
|
114
|
+
DATA_TYPE as data_type,
|
|
115
|
+
IS_NULLABLE as is_nullable,
|
|
116
|
+
COLUMN_KEY as column_key,
|
|
117
|
+
COLUMN_DEFAULT as column_default,
|
|
118
|
+
EXTRA as extra
|
|
119
|
+
FROM
|
|
120
|
+
INFORMATION_SCHEMA.COLUMNS
|
|
121
|
+
WHERE
|
|
122
|
+
TABLE_NAME = ?
|
|
123
|
+
ORDER BY
|
|
124
|
+
ORDINAL_POSITION
|
|
125
125
|
`;
|
|
126
126
|
const results = await this.db.query(query, [params.table_name]);
|
|
127
127
|
return {
|
|
@@ -71,33 +71,33 @@ class UtilityTools {
|
|
|
71
71
|
try {
|
|
72
72
|
const { table_name } = params;
|
|
73
73
|
// Query to get foreign keys where this table is the parent
|
|
74
|
-
const parentQuery = `
|
|
75
|
-
SELECT
|
|
76
|
-
TABLE_NAME as child_table,
|
|
77
|
-
COLUMN_NAME as child_column,
|
|
78
|
-
REFERENCED_TABLE_NAME as parent_table,
|
|
79
|
-
REFERENCED_COLUMN_NAME as parent_column,
|
|
80
|
-
CONSTRAINT_NAME as constraint_name
|
|
81
|
-
FROM
|
|
82
|
-
INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
|
83
|
-
WHERE
|
|
84
|
-
REFERENCED_TABLE_NAME = ?
|
|
85
|
-
AND REFERENCED_TABLE_SCHEMA = DATABASE()
|
|
74
|
+
const parentQuery = `
|
|
75
|
+
SELECT
|
|
76
|
+
TABLE_NAME as child_table,
|
|
77
|
+
COLUMN_NAME as child_column,
|
|
78
|
+
REFERENCED_TABLE_NAME as parent_table,
|
|
79
|
+
REFERENCED_COLUMN_NAME as parent_column,
|
|
80
|
+
CONSTRAINT_NAME as constraint_name
|
|
81
|
+
FROM
|
|
82
|
+
INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
|
83
|
+
WHERE
|
|
84
|
+
REFERENCED_TABLE_NAME = ?
|
|
85
|
+
AND REFERENCED_TABLE_SCHEMA = DATABASE()
|
|
86
86
|
`;
|
|
87
87
|
// Query to get foreign keys where this table is the child
|
|
88
|
-
const childQuery = `
|
|
89
|
-
SELECT
|
|
90
|
-
TABLE_NAME as child_table,
|
|
91
|
-
COLUMN_NAME as child_column,
|
|
92
|
-
REFERENCED_TABLE_NAME as parent_table,
|
|
93
|
-
REFERENCED_COLUMN_NAME as parent_column,
|
|
94
|
-
CONSTRAINT_NAME as constraint_name
|
|
95
|
-
FROM
|
|
96
|
-
INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
|
97
|
-
WHERE
|
|
98
|
-
TABLE_NAME = ?
|
|
99
|
-
AND REFERENCED_TABLE_NAME IS NOT NULL
|
|
100
|
-
AND TABLE_SCHEMA = DATABASE()
|
|
88
|
+
const childQuery = `
|
|
89
|
+
SELECT
|
|
90
|
+
TABLE_NAME as child_table,
|
|
91
|
+
COLUMN_NAME as child_column,
|
|
92
|
+
REFERENCED_TABLE_NAME as parent_table,
|
|
93
|
+
REFERENCED_COLUMN_NAME as parent_column,
|
|
94
|
+
CONSTRAINT_NAME as constraint_name
|
|
95
|
+
FROM
|
|
96
|
+
INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
|
97
|
+
WHERE
|
|
98
|
+
TABLE_NAME = ?
|
|
99
|
+
AND REFERENCED_TABLE_NAME IS NOT NULL
|
|
100
|
+
AND TABLE_SCHEMA = DATABASE()
|
|
101
101
|
`;
|
|
102
102
|
// Execute both queries
|
|
103
103
|
const parentRelationships = await this.db.query(parentQuery, [table_name]);
|