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