@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.
- package/README.md +202 -8
- package/dist/database/base.d.ts +79 -0
- package/dist/database/mysql.d.ts +11 -1
- package/dist/database/mysql.js +336 -18
- package/dist/database/postgresql.d.ts +11 -1
- package/dist/database/postgresql.js +460 -2
- package/dist/database/sqlite.d.ts +11 -1
- package/dist/database/sqlite.js +223 -0
- package/dist/index.js +241 -172
- package/dist/utils/file.d.ts +4 -0
- package/dist/utils/file.js +37 -0
- package/package.json +1 -1
- package/src/database/base.ts +100 -0
- package/src/database/mysql.ts +428 -19
- package/src/database/postgresql.ts +554 -2
- package/src/database/sqlite.ts +282 -0
- package/src/index.ts +310 -195
- package/src/utils/file.ts +46 -0
package/src/database/sqlite.ts
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
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
|
export class SQLiteAdapter implements DatabaseAdapter {
|
|
11
25
|
private db?: Database.Database;
|
|
@@ -90,4 +104,272 @@ export class SQLiteAdapter implements DatabaseAdapter {
|
|
|
90
104
|
this.db = undefined;
|
|
91
105
|
}
|
|
92
106
|
}
|
|
107
|
+
|
|
108
|
+
async executeTransaction(queries: string[]): Promise<TransactionResult> {
|
|
109
|
+
if (!this.db) throw new Error('Not connected');
|
|
110
|
+
|
|
111
|
+
const results: unknown[] = [];
|
|
112
|
+
let totalAffectedRows = 0;
|
|
113
|
+
|
|
114
|
+
this.db.exec('BEGIN TRANSACTION');
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
for (const query of queries) {
|
|
118
|
+
const trimmed = query.trim().toUpperCase();
|
|
119
|
+
|
|
120
|
+
if (trimmed.startsWith('SELECT')) {
|
|
121
|
+
const rows = this.db.prepare(query).all() as Record<string, unknown>[];
|
|
122
|
+
results.push({ rows, rowCount: rows.length });
|
|
123
|
+
} else {
|
|
124
|
+
const result = this.db.prepare(query).run();
|
|
125
|
+
totalAffectedRows += result.changes;
|
|
126
|
+
results.push({ affectedRows: result.changes });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.db.exec('COMMIT');
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
success: true,
|
|
134
|
+
affectedRows: totalAffectedRows,
|
|
135
|
+
results,
|
|
136
|
+
};
|
|
137
|
+
} catch (error) {
|
|
138
|
+
this.db.exec('ROLLBACK');
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async batchInsert(
|
|
144
|
+
table: string,
|
|
145
|
+
data: Record<string, unknown>[]
|
|
146
|
+
): Promise<BatchInsertResult> {
|
|
147
|
+
if (!this.db) throw new Error('Not connected');
|
|
148
|
+
|
|
149
|
+
if (data.length === 0) {
|
|
150
|
+
return { insertedRows: 0, duplicateRows: 0 };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const columns = Object.keys(data[0]);
|
|
154
|
+
const columnList = columns.join(', ');
|
|
155
|
+
const placeholders = columns.map(() => '?').join(', ');
|
|
156
|
+
|
|
157
|
+
const query = `INSERT INTO ${table} (${columnList}) VALUES (${placeholders})`;
|
|
158
|
+
const stmt = this.db.prepare(query);
|
|
159
|
+
|
|
160
|
+
let insertedRows = 0;
|
|
161
|
+
let duplicateRows = 0;
|
|
162
|
+
|
|
163
|
+
this.db.exec('BEGIN TRANSACTION');
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
for (const row of data) {
|
|
167
|
+
try {
|
|
168
|
+
const values = columns.map((col) => row[col]);
|
|
169
|
+
stmt.run(values);
|
|
170
|
+
insertedRows++;
|
|
171
|
+
} catch (error: any) {
|
|
172
|
+
if (error.code === 'SQLITE_CONSTRAINT') {
|
|
173
|
+
duplicateRows++;
|
|
174
|
+
} else {
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.db.exec('COMMIT');
|
|
181
|
+
|
|
182
|
+
return { insertedRows, duplicateRows };
|
|
183
|
+
} catch (error) {
|
|
184
|
+
this.db.exec('ROLLBACK');
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async batchUpdate(
|
|
190
|
+
table: string,
|
|
191
|
+
updates: BatchUpdateOptions
|
|
192
|
+
): Promise<BatchUpdateResult> {
|
|
193
|
+
if (!this.db) throw new Error('Not connected');
|
|
194
|
+
|
|
195
|
+
const setClause = Object.entries(updates.set)
|
|
196
|
+
.map(([key, value]) => `${key} = ${this.escapeValue(value)}`)
|
|
197
|
+
.join(', ');
|
|
198
|
+
|
|
199
|
+
const query = `UPDATE ${table} SET ${setClause} WHERE ${updates.where}`;
|
|
200
|
+
const result = this.db.prepare(query).run();
|
|
201
|
+
|
|
202
|
+
return { affectedRows: result.changes };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private escapeValue(value: unknown): string {
|
|
206
|
+
if (value === null) return 'NULL';
|
|
207
|
+
if (typeof value === 'number') return String(value);
|
|
208
|
+
if (typeof value === 'boolean') return value ? '1' : '0';
|
|
209
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async exportData(
|
|
213
|
+
table: string,
|
|
214
|
+
format: 'json' | 'csv',
|
|
215
|
+
filePath?: string,
|
|
216
|
+
options?: ExportOptions
|
|
217
|
+
): Promise<ExportResult> {
|
|
218
|
+
if (!this.db) throw new Error('Not connected');
|
|
219
|
+
|
|
220
|
+
let query = `SELECT * FROM ${table}`;
|
|
221
|
+
if (options?.where) {
|
|
222
|
+
query += ` WHERE ${options.where}`;
|
|
223
|
+
}
|
|
224
|
+
if (options?.limit) {
|
|
225
|
+
query += ` LIMIT ${options.limit}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const data = this.db.prepare(query).all() as Record<string, unknown>[];
|
|
229
|
+
|
|
230
|
+
const targetPath = filePath
|
|
231
|
+
? expandTilde(filePath)
|
|
232
|
+
: getDefaultFilePath(table, format);
|
|
233
|
+
|
|
234
|
+
ensureDirectoryExists(targetPath);
|
|
235
|
+
|
|
236
|
+
let content: string;
|
|
237
|
+
if (format === 'json') {
|
|
238
|
+
content = JSON.stringify(data, null, 2);
|
|
239
|
+
} else {
|
|
240
|
+
if (data.length === 0) {
|
|
241
|
+
content = '';
|
|
242
|
+
} else {
|
|
243
|
+
const headers = Object.keys(data[0]);
|
|
244
|
+
const csvRows = [
|
|
245
|
+
headers.join(','),
|
|
246
|
+
...data.map((row) =>
|
|
247
|
+
headers.map((h) => {
|
|
248
|
+
const val = row[h];
|
|
249
|
+
if (val === null) return '';
|
|
250
|
+
if (typeof val === 'string') return '"' + val.replace(/"/g, '""') + '"';
|
|
251
|
+
return String(val);
|
|
252
|
+
}).join(',')
|
|
253
|
+
),
|
|
254
|
+
];
|
|
255
|
+
content = csvRows.join('\n');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
fs.writeFileSync(targetPath, content, 'utf-8');
|
|
260
|
+
const stats = fs.statSync(targetPath);
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
success: true,
|
|
264
|
+
filePath: targetPath,
|
|
265
|
+
rowCount: data.length,
|
|
266
|
+
fileSize: formatFileSize(stats.size),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async createTable(table: string, columns: TableColumnDef[]): Promise<CreateTableResult> {
|
|
271
|
+
if (!this.db) throw new Error('Not connected');
|
|
272
|
+
|
|
273
|
+
const columnDefs = columns.map((col) => {
|
|
274
|
+
let def = `${col.name} ${col.type}`;
|
|
275
|
+
if (col.nullable === false) def += ' NOT NULL';
|
|
276
|
+
if (col.primaryKey) def += ' PRIMARY KEY';
|
|
277
|
+
if (col.defaultValue !== undefined) {
|
|
278
|
+
def += ` DEFAULT ${this.escapeValue(col.defaultValue)}`;
|
|
279
|
+
}
|
|
280
|
+
return def;
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const query = `CREATE TABLE ${table} (${columnDefs.join(', ')})`;
|
|
284
|
+
this.db.exec(query);
|
|
285
|
+
|
|
286
|
+
return { success: true, tableName: table };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async dropTable(table: string, ifExists = false): Promise<DropTableResult> {
|
|
290
|
+
if (!this.db) throw new Error('Not connected');
|
|
291
|
+
|
|
292
|
+
const query = ifExists
|
|
293
|
+
? `DROP TABLE IF EXISTS ${table}`
|
|
294
|
+
: `DROP TABLE ${table}`;
|
|
295
|
+
|
|
296
|
+
this.db.exec(query);
|
|
297
|
+
|
|
298
|
+
return { success: true, tableName: table };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async getTableStats(table: string): Promise<TableStatsResult> {
|
|
302
|
+
if (!this.db) throw new Error('Not connected');
|
|
303
|
+
|
|
304
|
+
// Get row count
|
|
305
|
+
const countResult = this.db.prepare(`SELECT COUNT(*) as count FROM ${table}`).get() as any;
|
|
306
|
+
const rowCount = countResult.count;
|
|
307
|
+
|
|
308
|
+
// Get column count
|
|
309
|
+
const colResult = this.db.prepare(`PRAGMA table_info(${table})`).all();
|
|
310
|
+
const columns = colResult.length;
|
|
311
|
+
|
|
312
|
+
// Get indexes
|
|
313
|
+
const indexResult = this.db.prepare(`PRAGMA index_list(${table})`).all() as any[];
|
|
314
|
+
const indexes = indexResult.map((row) => row.name);
|
|
315
|
+
|
|
316
|
+
// Get table size (SQLite doesn't have a direct way, estimate from page count)
|
|
317
|
+
const pragmaResult = this.db.prepare(`PRAGMA page_count`).get() as any;
|
|
318
|
+
const pageSizeResult = this.db.prepare(`PRAGMA page_size`).get() as any;
|
|
319
|
+
const sizeBytes = pragmaResult.page_count * pageSizeResult.page_size;
|
|
320
|
+
const size = formatFileSize(sizeBytes);
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
tableName: table,
|
|
324
|
+
rowCount,
|
|
325
|
+
columns,
|
|
326
|
+
indexes,
|
|
327
|
+
size,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async previewData(
|
|
332
|
+
table: string,
|
|
333
|
+
page = 1,
|
|
334
|
+
pageSize = 50,
|
|
335
|
+
orderBy?: string
|
|
336
|
+
): Promise<PreviewDataResult> {
|
|
337
|
+
if (!this.db) throw new Error('Not connected');
|
|
338
|
+
|
|
339
|
+
// Get total row count
|
|
340
|
+
const countResult = this.db.prepare(`SELECT COUNT(*) as count FROM ${table}`).get() as any;
|
|
341
|
+
const totalRows = countResult.count;
|
|
342
|
+
|
|
343
|
+
const totalPages = Math.ceil(totalRows / pageSize);
|
|
344
|
+
const offset = (page - 1) * pageSize;
|
|
345
|
+
|
|
346
|
+
// Build query
|
|
347
|
+
let query = `SELECT * FROM ${table}`;
|
|
348
|
+
if (orderBy) {
|
|
349
|
+
query += ` ORDER BY ${orderBy}`;
|
|
350
|
+
}
|
|
351
|
+
query += ` LIMIT ${pageSize} OFFSET ${offset}`;
|
|
352
|
+
|
|
353
|
+
const rows = this.db.prepare(query).all() as Record<string, unknown>[];
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
rows,
|
|
357
|
+
currentPage: page,
|
|
358
|
+
totalPages,
|
|
359
|
+
totalRows,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async sampleData(table: string, count = 10): Promise<SampleDataResult> {
|
|
364
|
+
if (!this.db) throw new Error('Not connected');
|
|
365
|
+
|
|
366
|
+
const rows = this.db
|
|
367
|
+
.prepare(`SELECT * FROM ${table} ORDER BY RANDOM() LIMIT ${count}`)
|
|
368
|
+
.all() as Record<string, unknown>[];
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
rows,
|
|
372
|
+
sampleCount: rows.length,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
93
375
|
}
|