@hyqf98/easy_db_mcp_server 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,6 @@
1
1
  import mysql from 'mysql2/promise';
2
+ import fs from 'fs';
3
+ import { expandTilde, formatFileSize, ensureDirectoryExists, getDefaultFilePath } from '../utils/file.js';
2
4
  export class MySQLAdapter {
3
5
  pool;
4
6
  config;
@@ -59,33 +61,51 @@ export class MySQLAdapter {
59
61
  async executeQuery(sql, database) {
60
62
  if (!this.pool)
61
63
  throw new Error('Not connected');
62
- // If database is specified and different from config, we need to handle it
63
- // For now, we'll allow queries to use fully qualified table names
64
- // Verify it's a SELECT query
65
64
  const trimmed = sql.trim().toUpperCase();
66
65
  if (!trimmed.startsWith('SELECT')) {
67
66
  throw new Error('Only SELECT queries allowed in executeQuery');
68
67
  }
69
- const [rows] = await this.pool.query(sql);
70
- return {
71
- rows: rows,
72
- rowCount: Array.isArray(rows) ? rows.length : 0,
73
- };
68
+ // Get a connection from the pool to handle database switching
69
+ const connection = await this.pool.getConnection();
70
+ try {
71
+ // Switch to the specified database if provided
72
+ if (database && database !== this.config.database) {
73
+ await connection.query(`USE ${mysql.escapeId(database)}`);
74
+ }
75
+ const [rows] = await connection.query(sql);
76
+ return {
77
+ rows: rows,
78
+ rowCount: Array.isArray(rows) ? rows.length : 0,
79
+ };
80
+ }
81
+ finally {
82
+ connection.release();
83
+ }
74
84
  }
75
85
  async executeSQL(sql, database) {
76
86
  if (!this.pool)
77
87
  throw new Error('Not connected');
78
- // Allow SQL execution with database-specific queries
79
- const [result] = await this.pool.query(sql);
80
- // @ts-ignore - MySQL result structure
81
- if (result.affectedRows !== undefined) {
82
- // @ts-ignore
83
- return { affectedRows: result.affectedRows };
88
+ // Get a connection from the pool to handle database switching
89
+ const connection = await this.pool.getConnection();
90
+ try {
91
+ // Switch to the specified database if provided
92
+ if (database && database !== this.config.database) {
93
+ await connection.query(`USE ${mysql.escapeId(database)}`);
94
+ }
95
+ const [result] = await connection.query(sql);
96
+ // @ts-ignore - MySQL result structure
97
+ if (result.affectedRows !== undefined) {
98
+ // @ts-ignore
99
+ return { affectedRows: result.affectedRows };
100
+ }
101
+ return {
102
+ rows: result,
103
+ rowCount: Array.isArray(result) ? result.length : 0,
104
+ };
105
+ }
106
+ finally {
107
+ connection.release();
84
108
  }
85
- return {
86
- rows: result,
87
- rowCount: Array.isArray(result) ? result.length : 0,
88
- };
89
109
  }
90
110
  async close() {
91
111
  if (this.pool) {
@@ -93,4 +113,302 @@ export class MySQLAdapter {
93
113
  this.pool = undefined;
94
114
  }
95
115
  }
116
+ async executeTransaction(queries, database) {
117
+ if (!this.pool)
118
+ throw new Error('Not connected');
119
+ const connection = await this.pool.getConnection();
120
+ try {
121
+ // Switch to specified database if needed
122
+ if (database && database !== this.config.database) {
123
+ await connection.query(`USE ${mysql.escapeId(database)}`);
124
+ }
125
+ await connection.beginTransaction();
126
+ const results = [];
127
+ let totalAffectedRows = 0;
128
+ for (const query of queries) {
129
+ const [result] = await connection.query(query);
130
+ // @ts-ignore - MySQL result structure
131
+ if (result.affectedRows !== undefined) {
132
+ // @ts-ignore
133
+ totalAffectedRows += result.affectedRows;
134
+ }
135
+ results.push(result);
136
+ }
137
+ await connection.commit();
138
+ return {
139
+ success: true,
140
+ affectedRows: totalAffectedRows,
141
+ results,
142
+ };
143
+ }
144
+ catch (error) {
145
+ await connection.rollback();
146
+ throw error;
147
+ }
148
+ finally {
149
+ connection.release();
150
+ }
151
+ }
152
+ async batchInsert(table, data, database) {
153
+ if (!this.pool)
154
+ throw new Error('Not connected');
155
+ if (data.length === 0) {
156
+ return { insertedRows: 0, duplicateRows: 0 };
157
+ }
158
+ const connection = await this.pool.getConnection();
159
+ try {
160
+ if (database && database !== this.config.database) {
161
+ await connection.query(`USE ${mysql.escapeId(database)}`);
162
+ }
163
+ // Build bulk insert query
164
+ const columns = Object.keys(data[0]);
165
+ const placeholders = columns.map(() => '?').join(', ');
166
+ const columnList = columns.map((c) => mysql.escapeId(c)).join(', ');
167
+ const query = `INSERT INTO ${mysql.escapeId(table)} (${columnList}) VALUES (${placeholders})`;
168
+ let insertedRows = 0;
169
+ let duplicateRows = 0;
170
+ for (const row of data) {
171
+ try {
172
+ const values = columns.map((col) => row[col]);
173
+ await connection.query(query, values);
174
+ insertedRows++;
175
+ }
176
+ catch (error) {
177
+ // Check for duplicate key error
178
+ if (error.code === 'ER_DUP_ENTRY') {
179
+ duplicateRows++;
180
+ }
181
+ else {
182
+ throw error;
183
+ }
184
+ }
185
+ }
186
+ return { insertedRows, duplicateRows };
187
+ }
188
+ finally {
189
+ connection.release();
190
+ }
191
+ }
192
+ async batchUpdate(table, updates, database) {
193
+ if (!this.pool)
194
+ throw new Error('Not connected');
195
+ const connection = await this.pool.getConnection();
196
+ try {
197
+ if (database && database !== this.config.database) {
198
+ await connection.query(`USE ${mysql.escapeId(database)}`);
199
+ }
200
+ // Build SET clause
201
+ const setClause = Object.entries(updates.set)
202
+ .map(([key, value]) => `${mysql.escapeId(key)} = ${this.escapeValue(value)}`)
203
+ .join(', ');
204
+ const query = `UPDATE ${mysql.escapeId(table)} SET ${setClause} WHERE ${updates.where}`;
205
+ const [result] = await connection.query(query);
206
+ // @ts-ignore
207
+ return { affectedRows: result.affectedRows || 0 };
208
+ }
209
+ finally {
210
+ connection.release();
211
+ }
212
+ }
213
+ escapeValue(value) {
214
+ if (value === null)
215
+ return 'NULL';
216
+ if (typeof value === 'number')
217
+ return String(value);
218
+ if (typeof value === 'boolean')
219
+ return value ? '1' : '0';
220
+ return mysql.escape(String(value));
221
+ }
222
+ async exportData(table, format, filePath, options, database) {
223
+ if (!this.pool)
224
+ throw new Error('Not connected');
225
+ const connection = await this.pool.getConnection();
226
+ try {
227
+ if (database && database !== this.config.database) {
228
+ await connection.query(`USE ${mysql.escapeId(database)}`);
229
+ }
230
+ // Build query
231
+ let query = `SELECT * FROM ${mysql.escapeId(table)}`;
232
+ if (options?.where) {
233
+ query += ` WHERE ${options.where}`;
234
+ }
235
+ if (options?.limit) {
236
+ query += ` LIMIT ${options.limit}`;
237
+ }
238
+ const [rows] = await connection.query(query);
239
+ const data = rows;
240
+ // Determine file path
241
+ const targetPath = filePath
242
+ ? expandTilde(filePath)
243
+ : getDefaultFilePath(table, format);
244
+ ensureDirectoryExists(targetPath);
245
+ // Export data
246
+ let content;
247
+ if (format === 'json') {
248
+ content = JSON.stringify(data, null, 2);
249
+ }
250
+ else {
251
+ // CSV format
252
+ if (data.length === 0) {
253
+ content = '';
254
+ }
255
+ else {
256
+ const headers = Object.keys(data[0]);
257
+ const csvRows = [
258
+ headers.join(','),
259
+ ...data.map((row) => headers.map((h) => {
260
+ const val = row[h];
261
+ if (val === null)
262
+ return '';
263
+ if (typeof val === 'string')
264
+ return '"' + val.replace(/"/g, '""') + '"';
265
+ return String(val);
266
+ }).join(',')),
267
+ ];
268
+ content = csvRows.join('\n');
269
+ }
270
+ }
271
+ fs.writeFileSync(targetPath, content, 'utf-8');
272
+ const stats = fs.statSync(targetPath);
273
+ return {
274
+ success: true,
275
+ filePath: targetPath,
276
+ rowCount: data.length,
277
+ fileSize: formatFileSize(stats.size),
278
+ };
279
+ }
280
+ finally {
281
+ connection.release();
282
+ }
283
+ }
284
+ async createTable(table, columns, database) {
285
+ if (!this.pool)
286
+ throw new Error('Not connected');
287
+ const connection = await this.pool.getConnection();
288
+ try {
289
+ if (database && database !== this.config.database) {
290
+ await connection.query(`USE ${mysql.escapeId(database)}`);
291
+ }
292
+ const columnDefs = columns.map((col) => {
293
+ let def = `${mysql.escapeId(col.name)} ${col.type}`;
294
+ if (col.nullable === false)
295
+ def += ' NOT NULL';
296
+ if (col.primaryKey)
297
+ def += ' PRIMARY KEY';
298
+ if (col.defaultValue !== undefined) {
299
+ def += ` DEFAULT ${this.escapeValue(col.defaultValue)}`;
300
+ }
301
+ return def;
302
+ });
303
+ const query = `CREATE TABLE ${mysql.escapeId(table)} (${columnDefs.join(', ')})`;
304
+ await connection.query(query);
305
+ return { success: true, tableName: table };
306
+ }
307
+ finally {
308
+ connection.release();
309
+ }
310
+ }
311
+ async dropTable(table, ifExists = false, database) {
312
+ if (!this.pool)
313
+ throw new Error('Not connected');
314
+ const connection = await this.pool.getConnection();
315
+ try {
316
+ if (database && database !== this.config.database) {
317
+ await connection.query(`USE ${mysql.escapeId(database)}`);
318
+ }
319
+ const query = ifExists
320
+ ? `DROP TABLE IF EXISTS ${mysql.escapeId(table)}`
321
+ : `DROP TABLE ${mysql.escapeId(table)}`;
322
+ await connection.query(query);
323
+ return { success: true, tableName: table };
324
+ }
325
+ finally {
326
+ connection.release();
327
+ }
328
+ }
329
+ async getTableStats(table, database) {
330
+ if (!this.pool)
331
+ throw new Error('Not connected');
332
+ const connection = await this.pool.getConnection();
333
+ try {
334
+ const dbName = database || this.config.database;
335
+ if (!dbName)
336
+ throw new Error('Database name required');
337
+ if (database && database !== this.config.database) {
338
+ await connection.query(`USE ${mysql.escapeId(database)}`);
339
+ }
340
+ // Get row count
341
+ const [countResult] = await connection.query(`SELECT COUNT(*) as count FROM ${mysql.escapeId(table)}`);
342
+ const rowCount = countResult[0].count;
343
+ // Get column count
344
+ const [colResult] = await connection.query(`SELECT COUNT(*) as count FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?`, [dbName, table]);
345
+ const columns = colResult[0].count;
346
+ // Get indexes
347
+ const [indexResult] = await connection.query(`SELECT INDEX_NAME FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND INDEX_NAME != 'PRIMARY' GROUP BY INDEX_NAME`, [dbName, table]);
348
+ const indexes = indexResult.map((row) => row.INDEX_NAME);
349
+ // Get table size
350
+ const [sizeResult] = await connection.query(`SELECT ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) as size_mb FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?`, [dbName, table]);
351
+ const sizeMb = sizeResult[0]?.size_mb || 0;
352
+ const size = sizeMb > 0 ? `${sizeMb} MB` : '< 0.01 MB';
353
+ return {
354
+ tableName: table,
355
+ rowCount,
356
+ columns,
357
+ indexes,
358
+ size,
359
+ };
360
+ }
361
+ finally {
362
+ connection.release();
363
+ }
364
+ }
365
+ async previewData(table, page = 1, pageSize = 50, orderBy, database) {
366
+ if (!this.pool)
367
+ throw new Error('Not connected');
368
+ const connection = await this.pool.getConnection();
369
+ try {
370
+ if (database && database !== this.config.database) {
371
+ await connection.query(`USE ${mysql.escapeId(database)}`);
372
+ }
373
+ // Get total row count
374
+ const [countResult] = await connection.query(`SELECT COUNT(*) as count FROM ${mysql.escapeId(table)}`);
375
+ const totalRows = countResult[0].count;
376
+ const totalPages = Math.ceil(totalRows / pageSize);
377
+ const offset = (page - 1) * pageSize;
378
+ // Build query
379
+ let query = `SELECT * FROM ${mysql.escapeId(table)}`;
380
+ if (orderBy) {
381
+ query += ` ORDER BY ${mysql.escapeId(orderBy)}`;
382
+ }
383
+ query += ` LIMIT ${pageSize} OFFSET ${offset}`;
384
+ const [rows] = await connection.query(query);
385
+ return {
386
+ rows: rows,
387
+ currentPage: page,
388
+ totalPages,
389
+ totalRows,
390
+ };
391
+ }
392
+ finally {
393
+ connection.release();
394
+ }
395
+ }
396
+ async sampleData(table, count = 10, database) {
397
+ if (!this.pool)
398
+ throw new Error('Not connected');
399
+ const connection = await this.pool.getConnection();
400
+ try {
401
+ if (database && database !== this.config.database) {
402
+ await connection.query(`USE ${mysql.escapeId(database)}`);
403
+ }
404
+ const [rows] = await connection.query(`SELECT * FROM ${mysql.escapeId(table)} ORDER BY RAND() LIMIT ${count}`);
405
+ return {
406
+ rows: rows,
407
+ sampleCount: rows.length,
408
+ };
409
+ }
410
+ finally {
411
+ connection.release();
412
+ }
413
+ }
96
414
  }
@@ -1,4 +1,4 @@
1
- import type { DatabaseAdapter, TableInfo, TableColumn, QueryResult } from './base.js';
1
+ import type { DatabaseAdapter, TableInfo, TableColumn, QueryResult, TransactionResult, BatchInsertResult, BatchUpdateResult, BatchUpdateOptions, ExportResult, ExportOptions, CreateTableResult, DropTableResult, TableColumnDef, TableStatsResult, PreviewDataResult, SampleDataResult } from './base.js';
2
2
  import type { DatabaseConfig } from '../config.js';
3
3
  export declare class PostgreSQLAdapter implements DatabaseAdapter {
4
4
  private pool?;
@@ -12,4 +12,14 @@ export declare class PostgreSQLAdapter implements DatabaseAdapter {
12
12
  affectedRows: number;
13
13
  }>;
14
14
  close(): Promise<void>;
15
+ executeTransaction(queries: string[], database?: string): Promise<TransactionResult>;
16
+ batchInsert(table: string, data: Record<string, unknown>[], database?: string): Promise<BatchInsertResult>;
17
+ batchUpdate(table: string, updates: BatchUpdateOptions, database?: string): Promise<BatchUpdateResult>;
18
+ private escapeValue;
19
+ exportData(table: string, format: 'json' | 'csv', filePath?: string, options?: ExportOptions, database?: string): Promise<ExportResult>;
20
+ createTable(table: string, columns: TableColumnDef[], database?: string): Promise<CreateTableResult>;
21
+ dropTable(table: string, ifExists?: boolean, database?: string): Promise<DropTableResult>;
22
+ getTableStats(table: string, database?: string): Promise<TableStatsResult>;
23
+ previewData(table: string, page?: number, pageSize?: number, orderBy?: string, database?: string): Promise<PreviewDataResult>;
24
+ sampleData(table: string, count?: number, database?: string): Promise<SampleDataResult>;
15
25
  }