@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 pg from 'pg';
2
+ import fs from 'fs';
3
+ import { expandTilde, formatFileSize, ensureDirectoryExists, getDefaultFilePath } from '../utils/file.js';
2
4
  const { Pool } = pg;
3
5
  export class PostgreSQLAdapter {
4
6
  pool;
@@ -64,11 +66,34 @@ export class PostgreSQLAdapter {
64
66
  async executeQuery(sql, database) {
65
67
  if (!this.pool)
66
68
  throw new Error('Not connected');
67
- // Allow queries with fully qualified table names (schema.table)
68
69
  const trimmed = sql.trim().toUpperCase();
69
70
  if (!trimmed.startsWith('SELECT')) {
70
71
  throw new Error('Only SELECT queries allowed in executeQuery');
71
72
  }
73
+ // If database is specified and different from default, use it
74
+ const dbName = database || this.config.database;
75
+ if (dbName && dbName !== this.config.database) {
76
+ // For PostgreSQL, we need to connect to the specific database
77
+ // Create a temporary pool for the different database
78
+ const tempPool = new Pool({
79
+ host: this.config.host,
80
+ port: this.config.port || 5432,
81
+ user: this.config.user,
82
+ password: this.config.password,
83
+ database: dbName,
84
+ max: 1,
85
+ });
86
+ try {
87
+ const result = await tempPool.query(sql);
88
+ return {
89
+ rows: result.rows,
90
+ rowCount: result.rowCount || 0,
91
+ };
92
+ }
93
+ finally {
94
+ await tempPool.end();
95
+ }
96
+ }
72
97
  const result = await this.pool.query(sql);
73
98
  return {
74
99
  rows: result.rows,
@@ -78,7 +103,33 @@ export class PostgreSQLAdapter {
78
103
  async executeSQL(sql, database) {
79
104
  if (!this.pool)
80
105
  throw new Error('Not connected');
81
- // Allow SQL execution with database-specific queries
106
+ // If database is specified and different from default, use it
107
+ const dbName = database || this.config.database;
108
+ if (dbName && dbName !== this.config.database) {
109
+ // For PostgreSQL, we need to connect to the specific database
110
+ // Create a temporary pool for the different database
111
+ const tempPool = new Pool({
112
+ host: this.config.host,
113
+ port: this.config.port || 5432,
114
+ user: this.config.user,
115
+ password: this.config.password,
116
+ database: dbName,
117
+ max: 1,
118
+ });
119
+ try {
120
+ const result = await tempPool.query(sql);
121
+ if (result.rowCount !== null && result.command !== 'SELECT') {
122
+ return { affectedRows: result.rowCount };
123
+ }
124
+ return {
125
+ rows: result.rows,
126
+ rowCount: result.rowCount || 0,
127
+ };
128
+ }
129
+ finally {
130
+ await tempPool.end();
131
+ }
132
+ }
82
133
  const result = await this.pool.query(sql);
83
134
  if (result.rowCount !== null && result.command !== 'SELECT') {
84
135
  return { affectedRows: result.rowCount };
@@ -94,4 +145,411 @@ export class PostgreSQLAdapter {
94
145
  this.pool = undefined;
95
146
  }
96
147
  }
148
+ async executeTransaction(queries, database) {
149
+ if (!this.pool)
150
+ throw new Error('Not connected');
151
+ let client = null;
152
+ // If different database, use temporary connection
153
+ if (database && database !== this.config.database) {
154
+ const tempPool = new pg.Pool({
155
+ host: this.config.host,
156
+ port: this.config.port || 5432,
157
+ user: this.config.user,
158
+ password: this.config.password,
159
+ database,
160
+ max: 1,
161
+ });
162
+ try {
163
+ client = await tempPool.connect();
164
+ await client.query('BEGIN');
165
+ const results = [];
166
+ let totalAffectedRows = 0;
167
+ for (const query of queries) {
168
+ const result = await client.query(query);
169
+ if (result.rowCount !== null) {
170
+ totalAffectedRows += result.rowCount;
171
+ }
172
+ results.push(result);
173
+ }
174
+ await client.query('COMMIT');
175
+ return {
176
+ success: true,
177
+ affectedRows: totalAffectedRows,
178
+ results,
179
+ };
180
+ }
181
+ catch (error) {
182
+ if (client)
183
+ await client.query('ROLLBACK');
184
+ throw error;
185
+ }
186
+ finally {
187
+ if (client)
188
+ client.release();
189
+ await tempPool.end();
190
+ }
191
+ }
192
+ // Use default pool
193
+ client = await this.pool.connect();
194
+ try {
195
+ await client.query('BEGIN');
196
+ const results = [];
197
+ let totalAffectedRows = 0;
198
+ for (const query of queries) {
199
+ const result = await client.query(query);
200
+ if (result.rowCount !== null) {
201
+ totalAffectedRows += result.rowCount;
202
+ }
203
+ results.push(result);
204
+ }
205
+ await client.query('COMMIT');
206
+ return {
207
+ success: true,
208
+ affectedRows: totalAffectedRows,
209
+ results,
210
+ };
211
+ }
212
+ catch (error) {
213
+ await client.query('ROLLBACK');
214
+ throw error;
215
+ }
216
+ finally {
217
+ client.release();
218
+ }
219
+ }
220
+ async batchInsert(table, data, database) {
221
+ if (!this.pool)
222
+ throw new Error('Not connected');
223
+ if (data.length === 0) {
224
+ return { insertedRows: 0, duplicateRows: 0 };
225
+ }
226
+ let pool = this.pool;
227
+ let dbName = database || this.config.database;
228
+ // Use temporary pool for different database
229
+ if (database && database !== this.config.database) {
230
+ pool = new pg.Pool({
231
+ host: this.config.host,
232
+ port: this.config.port || 5432,
233
+ user: this.config.user,
234
+ password: this.config.password,
235
+ database,
236
+ max: 1,
237
+ });
238
+ }
239
+ const client = await pool.connect();
240
+ try {
241
+ await client.query('BEGIN');
242
+ const columns = Object.keys(data[0]);
243
+ const columnList = columns.map((c) => pg.escapeIdentifier(c)).join(', ');
244
+ let insertedRows = 0;
245
+ let duplicateRows = 0;
246
+ for (const row of data) {
247
+ const values = columns.map((col) => row[col]);
248
+ const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');
249
+ try {
250
+ const query = `INSERT INTO ${pg.escapeIdentifier(table)} (${columnList}) VALUES (${placeholders})`;
251
+ await client.query(query, values);
252
+ insertedRows++;
253
+ }
254
+ catch (error) {
255
+ if (error.code === '23505') { // unique_violation
256
+ duplicateRows++;
257
+ }
258
+ else {
259
+ await client.query('ROLLBACK');
260
+ throw error;
261
+ }
262
+ }
263
+ }
264
+ await client.query('COMMIT');
265
+ return { insertedRows, duplicateRows };
266
+ }
267
+ finally {
268
+ client.release();
269
+ if (pool !== this.pool)
270
+ await pool.end();
271
+ }
272
+ }
273
+ async batchUpdate(table, updates, database) {
274
+ if (!this.pool)
275
+ throw new Error('Not connected');
276
+ let pool = this.pool;
277
+ if (database && database !== this.config.database) {
278
+ pool = new pg.Pool({
279
+ host: this.config.host,
280
+ port: this.config.port || 5432,
281
+ user: this.config.user,
282
+ password: this.config.password,
283
+ database,
284
+ max: 1,
285
+ });
286
+ }
287
+ const client = await pool.connect();
288
+ try {
289
+ const setClause = Object.entries(updates.set)
290
+ .map(([key, value]) => `${pg.escapeIdentifier(key)} = ${this.escapeValue(value)}`)
291
+ .join(', ');
292
+ const query = `UPDATE ${pg.escapeIdentifier(table)} SET ${setClause} WHERE ${updates.where}`;
293
+ const result = await client.query(query);
294
+ return { affectedRows: result.rowCount || 0 };
295
+ }
296
+ finally {
297
+ client.release();
298
+ if (pool !== this.pool)
299
+ await pool.end();
300
+ }
301
+ }
302
+ escapeValue(value) {
303
+ if (value === null)
304
+ return 'NULL';
305
+ if (typeof value === 'number')
306
+ return String(value);
307
+ if (typeof value === 'boolean')
308
+ return value ? 'TRUE' : 'FALSE';
309
+ return `'${String(value).replace(/'/g, "''")}'`;
310
+ }
311
+ async exportData(table, format, filePath, options, database) {
312
+ if (!this.pool)
313
+ throw new Error('Not connected');
314
+ let pool = this.pool;
315
+ if (database && database !== this.config.database) {
316
+ pool = new pg.Pool({
317
+ host: this.config.host,
318
+ port: this.config.port || 5432,
319
+ user: this.config.user,
320
+ password: this.config.password,
321
+ database,
322
+ max: 1,
323
+ });
324
+ }
325
+ const client = await pool.connect();
326
+ try {
327
+ let query = `SELECT * FROM ${pg.escapeIdentifier(table)}`;
328
+ if (options?.where) {
329
+ query += ` WHERE ${options.where}`;
330
+ }
331
+ if (options?.limit) {
332
+ query += ` LIMIT ${options.limit}`;
333
+ }
334
+ const result = await client.query(query);
335
+ const data = result.rows;
336
+ const targetPath = filePath
337
+ ? expandTilde(filePath)
338
+ : getDefaultFilePath(table, format);
339
+ ensureDirectoryExists(targetPath);
340
+ let content;
341
+ if (format === 'json') {
342
+ content = JSON.stringify(data, null, 2);
343
+ }
344
+ else {
345
+ if (data.length === 0) {
346
+ content = '';
347
+ }
348
+ else {
349
+ const headers = Object.keys(data[0]);
350
+ const csvRows = [
351
+ headers.join(','),
352
+ ...data.map((row) => headers.map((h) => {
353
+ const val = row[h];
354
+ if (val === null)
355
+ return '';
356
+ if (typeof val === 'string')
357
+ return '"' + val.replace(/"/g, '""') + '"';
358
+ return String(val);
359
+ }).join(',')),
360
+ ];
361
+ content = csvRows.join('\n');
362
+ }
363
+ }
364
+ fs.writeFileSync(targetPath, content, 'utf-8');
365
+ const stats = fs.statSync(targetPath);
366
+ return {
367
+ success: true,
368
+ filePath: targetPath,
369
+ rowCount: data.length,
370
+ fileSize: formatFileSize(stats.size),
371
+ };
372
+ }
373
+ finally {
374
+ client.release();
375
+ if (pool !== this.pool)
376
+ await pool.end();
377
+ }
378
+ }
379
+ async createTable(table, columns, database) {
380
+ if (!this.pool)
381
+ throw new Error('Not connected');
382
+ let pool = this.pool;
383
+ if (database && database !== this.config.database) {
384
+ pool = new pg.Pool({
385
+ host: this.config.host,
386
+ port: this.config.port || 5432,
387
+ user: this.config.user,
388
+ password: this.config.password,
389
+ database,
390
+ max: 1,
391
+ });
392
+ }
393
+ const client = await pool.connect();
394
+ try {
395
+ const columnDefs = columns.map((col) => {
396
+ let def = `${pg.escapeIdentifier(col.name)} ${col.type}`;
397
+ if (col.nullable === false)
398
+ def += ' NOT NULL';
399
+ if (col.primaryKey)
400
+ def += ' PRIMARY KEY';
401
+ if (col.defaultValue !== undefined) {
402
+ def += ` DEFAULT ${this.escapeValue(col.defaultValue)}`;
403
+ }
404
+ return def;
405
+ });
406
+ const query = `CREATE TABLE ${pg.escapeIdentifier(table)} (${columnDefs.join(', ')})`;
407
+ await client.query(query);
408
+ return { success: true, tableName: table };
409
+ }
410
+ finally {
411
+ client.release();
412
+ if (pool !== this.pool)
413
+ await pool.end();
414
+ }
415
+ }
416
+ async dropTable(table, ifExists = false, database) {
417
+ if (!this.pool)
418
+ throw new Error('Not connected');
419
+ let pool = this.pool;
420
+ if (database && database !== this.config.database) {
421
+ pool = new pg.Pool({
422
+ host: this.config.host,
423
+ port: this.config.port || 5432,
424
+ user: this.config.user,
425
+ password: this.config.password,
426
+ database,
427
+ max: 1,
428
+ });
429
+ }
430
+ const client = await pool.connect();
431
+ try {
432
+ const query = ifExists
433
+ ? `DROP TABLE IF EXISTS ${pg.escapeIdentifier(table)}`
434
+ : `DROP TABLE ${pg.escapeIdentifier(table)}`;
435
+ await client.query(query);
436
+ return { success: true, tableName: table };
437
+ }
438
+ finally {
439
+ client.release();
440
+ if (pool !== this.pool)
441
+ await pool.end();
442
+ }
443
+ }
444
+ async getTableStats(table, database) {
445
+ if (!this.pool)
446
+ throw new Error('Not connected');
447
+ let pool = this.pool;
448
+ if (database && database !== this.config.database) {
449
+ pool = new pg.Pool({
450
+ host: this.config.host,
451
+ port: this.config.port || 5432,
452
+ user: this.config.user,
453
+ password: this.config.password,
454
+ database,
455
+ max: 1,
456
+ });
457
+ }
458
+ const client = await pool.connect();
459
+ try {
460
+ // Get row count
461
+ const countResult = await client.query(`SELECT COUNT(*) as count FROM ${pg.escapeIdentifier(table)}`);
462
+ const rowCount = parseInt(countResult.rows[0].count, 10);
463
+ // Get column count
464
+ const colResult = await client.query(`SELECT COUNT(*) as count FROM information_schema.columns WHERE table_name = $1`, [table]);
465
+ const columns = parseInt(colResult.rows[0].count, 10);
466
+ // Get indexes
467
+ const indexResult = await client.query(`SELECT indexname FROM pg_indexes WHERE tablename = $1 AND schemaname = 'public'`, [table]);
468
+ const indexes = indexResult.rows.map((row) => row.indexname);
469
+ // Get table size
470
+ const sizeResult = await client.query(`SELECT pg_size_pretty(pg_total_relation_size($1::regclass)) as size`, [table]);
471
+ const size = sizeResult.rows[0].size;
472
+ return {
473
+ tableName: table,
474
+ rowCount,
475
+ columns,
476
+ indexes,
477
+ size,
478
+ };
479
+ }
480
+ finally {
481
+ client.release();
482
+ if (pool !== this.pool)
483
+ await pool.end();
484
+ }
485
+ }
486
+ async previewData(table, page = 1, pageSize = 50, orderBy, database) {
487
+ if (!this.pool)
488
+ throw new Error('Not connected');
489
+ let pool = this.pool;
490
+ if (database && database !== this.config.database) {
491
+ pool = new pg.Pool({
492
+ host: this.config.host,
493
+ port: this.config.port || 5432,
494
+ user: this.config.user,
495
+ password: this.config.password,
496
+ database,
497
+ max: 1,
498
+ });
499
+ }
500
+ const client = await pool.connect();
501
+ try {
502
+ // Get total row count
503
+ const countResult = await client.query(`SELECT COUNT(*) as count FROM ${pg.escapeIdentifier(table)}`);
504
+ const totalRows = parseInt(countResult.rows[0].count, 10);
505
+ const totalPages = Math.ceil(totalRows / pageSize);
506
+ const offset = (page - 1) * pageSize;
507
+ // Build query
508
+ let query = `SELECT * FROM ${pg.escapeIdentifier(table)}`;
509
+ if (orderBy) {
510
+ query += ` ORDER BY ${pg.escapeIdentifier(orderBy)}`;
511
+ }
512
+ query += ` LIMIT ${pageSize} OFFSET ${offset}`;
513
+ const result = await client.query(query);
514
+ return {
515
+ rows: result.rows,
516
+ currentPage: page,
517
+ totalPages,
518
+ totalRows,
519
+ };
520
+ }
521
+ finally {
522
+ client.release();
523
+ if (pool !== this.pool)
524
+ await pool.end();
525
+ }
526
+ }
527
+ async sampleData(table, count = 10, database) {
528
+ if (!this.pool)
529
+ throw new Error('Not connected');
530
+ let pool = this.pool;
531
+ if (database && database !== this.config.database) {
532
+ pool = new pg.Pool({
533
+ host: this.config.host,
534
+ port: this.config.port || 5432,
535
+ user: this.config.user,
536
+ password: this.config.password,
537
+ database,
538
+ max: 1,
539
+ });
540
+ }
541
+ const client = await pool.connect();
542
+ try {
543
+ const result = await client.query(`SELECT * FROM ${pg.escapeIdentifier(table)} ORDER BY RANDOM() LIMIT ${count}`);
544
+ return {
545
+ rows: result.rows,
546
+ sampleCount: result.rows.length,
547
+ };
548
+ }
549
+ finally {
550
+ client.release();
551
+ if (pool !== this.pool)
552
+ await pool.end();
553
+ }
554
+ }
97
555
  }
@@ -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 SQLiteAdapter implements DatabaseAdapter {
4
4
  private db?;
@@ -12,4 +12,14 @@ export declare class SQLiteAdapter implements DatabaseAdapter {
12
12
  affectedRows: number;
13
13
  }>;
14
14
  close(): Promise<void>;
15
+ executeTransaction(queries: string[]): Promise<TransactionResult>;
16
+ batchInsert(table: string, data: Record<string, unknown>[]): Promise<BatchInsertResult>;
17
+ batchUpdate(table: string, updates: BatchUpdateOptions): Promise<BatchUpdateResult>;
18
+ private escapeValue;
19
+ exportData(table: string, format: 'json' | 'csv', filePath?: string, options?: ExportOptions): Promise<ExportResult>;
20
+ createTable(table: string, columns: TableColumnDef[]): Promise<CreateTableResult>;
21
+ dropTable(table: string, ifExists?: boolean): Promise<DropTableResult>;
22
+ getTableStats(table: string): Promise<TableStatsResult>;
23
+ previewData(table: string, page?: number, pageSize?: number, orderBy?: string): Promise<PreviewDataResult>;
24
+ sampleData(table: string, count?: number): Promise<SampleDataResult>;
15
25
  }