@hyqf98/easy_db_mcp_server 1.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 +166 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +58 -0
- package/dist/database/base.d.ts +44 -0
- package/dist/database/base.js +1 -0
- package/dist/database/factory.d.ts +3 -0
- package/dist/database/factory.js +15 -0
- package/dist/database/mysql.d.ts +15 -0
- package/dist/database/mysql.js +96 -0
- package/dist/database/postgresql.d.ts +15 -0
- package/dist/database/postgresql.js +97 -0
- package/dist/database/sqlite.d.ts +15 -0
- package/dist/database/sqlite.js +69 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +213 -0
- package/package.json +40 -0
- package/src/config.ts +75 -0
- package/src/database/base.ts +50 -0
- package/src/database/factory.ts +18 -0
- package/src/database/mysql.ts +124 -0
- package/src/database/postgresql.ts +126 -0
- package/src/database/sqlite.ts +93 -0
- package/src/index.ts +256 -0
- package/tsconfig.json +17 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { loadConfig } from './config.js';
|
|
6
|
+
import { createAdapter } from './database/factory.js';
|
|
7
|
+
// Create server instance
|
|
8
|
+
const server = new Server({
|
|
9
|
+
name: 'hyqf98@easy_db_mcp_server',
|
|
10
|
+
version: '1.0.0',
|
|
11
|
+
}, {
|
|
12
|
+
capabilities: {
|
|
13
|
+
tools: {},
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
// Load configuration and create adapter
|
|
17
|
+
let adapter;
|
|
18
|
+
try {
|
|
19
|
+
const config = loadConfig();
|
|
20
|
+
adapter = createAdapter(config);
|
|
21
|
+
await adapter.connect();
|
|
22
|
+
console.error(`Connected to ${config.type} database`);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
console.error('Failed to connect to database:', error);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
// List available tools
|
|
29
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
30
|
+
return {
|
|
31
|
+
tools: [
|
|
32
|
+
{
|
|
33
|
+
name: 'list_tables',
|
|
34
|
+
description: 'List all tables in the database. If EASYDB_DATABASE is not set, the database parameter must be provided.',
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
database: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'Database name (optional if EASYDB_DATABASE is set, otherwise required)',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'describe_table',
|
|
47
|
+
description: 'Get the structure of a table including columns, types, and constraints. If EASYDB_DATABASE is not set, the database parameter must be provided.',
|
|
48
|
+
inputSchema: {
|
|
49
|
+
type: 'object',
|
|
50
|
+
properties: {
|
|
51
|
+
table: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
description: 'Table name',
|
|
54
|
+
},
|
|
55
|
+
database: {
|
|
56
|
+
type: 'string',
|
|
57
|
+
description: 'Database name (optional if EASYDB_DATABASE is set, otherwise required)',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
required: ['table'],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'execute_query',
|
|
65
|
+
description: 'Execute a SELECT query (read-only). If EASYDB_DATABASE is not set, the database parameter must be provided.',
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: {
|
|
69
|
+
sql: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
description: 'SQL SELECT query to execute',
|
|
72
|
+
},
|
|
73
|
+
database: {
|
|
74
|
+
type: 'string',
|
|
75
|
+
description: 'Database name (optional if EASYDB_DATABASE is set, otherwise required)',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
required: ['sql'],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'execute_sql',
|
|
83
|
+
description: 'Execute any SQL statement (requires EASYDB_ALLOW_WRITE=true). If EASYDB_DATABASE is not set, the database parameter must be provided.',
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
sql: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
description: 'SQL statement to execute (INSERT, UPDATE, DELETE, DDL, etc.)',
|
|
90
|
+
},
|
|
91
|
+
database: {
|
|
92
|
+
type: 'string',
|
|
93
|
+
description: 'Database name (optional if EASYDB_DATABASE is set, otherwise required)',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
required: ['sql'],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
// Handle tool calls
|
|
103
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
104
|
+
const { name, arguments: args } = request.params;
|
|
105
|
+
const config = loadConfig();
|
|
106
|
+
// Helper function to validate database parameter
|
|
107
|
+
const getDatabase = (paramDb) => {
|
|
108
|
+
if (paramDb) {
|
|
109
|
+
return paramDb;
|
|
110
|
+
}
|
|
111
|
+
if (config.database) {
|
|
112
|
+
return config.database;
|
|
113
|
+
}
|
|
114
|
+
throw new Error('Database name is required. Either set EASYDB_DATABASE environment variable or pass the database parameter in the tool call.');
|
|
115
|
+
};
|
|
116
|
+
try {
|
|
117
|
+
switch (name) {
|
|
118
|
+
case 'list_tables': {
|
|
119
|
+
const database = getDatabase(args?.database);
|
|
120
|
+
const tables = await adapter.listTables(database);
|
|
121
|
+
return {
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
type: 'text',
|
|
125
|
+
text: JSON.stringify(tables, null, 2),
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
case 'describe_table': {
|
|
131
|
+
const database = getDatabase(args?.database);
|
|
132
|
+
const columns = await adapter.describeTable(args?.table, database);
|
|
133
|
+
return {
|
|
134
|
+
content: [
|
|
135
|
+
{
|
|
136
|
+
type: 'text',
|
|
137
|
+
text: JSON.stringify(columns, null, 2),
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
case 'execute_query': {
|
|
143
|
+
const database = getDatabase(args?.database);
|
|
144
|
+
const result = await adapter.executeQuery(args?.sql, database);
|
|
145
|
+
return {
|
|
146
|
+
content: [
|
|
147
|
+
{
|
|
148
|
+
type: 'text',
|
|
149
|
+
text: JSON.stringify(result, null, 2),
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
case 'execute_sql': {
|
|
155
|
+
if (!config.allowWrite) {
|
|
156
|
+
throw new Error('SQL execution requires EASYDB_ALLOW_WRITE=true for safety. Please enable this environment variable if you want to allow write operations.');
|
|
157
|
+
}
|
|
158
|
+
const sql = args?.sql;
|
|
159
|
+
const trimmed = sql.trim().toUpperCase();
|
|
160
|
+
// Check for DDL statements
|
|
161
|
+
const isDDL = trimmed.startsWith('CREATE ') ||
|
|
162
|
+
trimmed.startsWith('DROP ') ||
|
|
163
|
+
trimmed.startsWith('ALTER ') ||
|
|
164
|
+
trimmed.startsWith('TRUNCATE ');
|
|
165
|
+
if (isDDL && !config.allowDDL) {
|
|
166
|
+
throw new Error('DDL statements require EASYDB_ALLOW_DDL=true for safety. Please enable this environment variable if you want to allow DDL operations.');
|
|
167
|
+
}
|
|
168
|
+
const database = getDatabase(args?.database);
|
|
169
|
+
const result = await adapter.executeSQL(sql, database);
|
|
170
|
+
return {
|
|
171
|
+
content: [
|
|
172
|
+
{
|
|
173
|
+
type: 'text',
|
|
174
|
+
text: JSON.stringify(result, null, 2),
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
default:
|
|
180
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
return {
|
|
185
|
+
content: [
|
|
186
|
+
{
|
|
187
|
+
type: 'text',
|
|
188
|
+
text: JSON.stringify({
|
|
189
|
+
success: false,
|
|
190
|
+
error: error instanceof Error ? error.message : String(error),
|
|
191
|
+
}, null, 2),
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
isError: true,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
// Start server
|
|
199
|
+
async function main() {
|
|
200
|
+
const transport = new StdioServerTransport();
|
|
201
|
+
await server.connect(transport);
|
|
202
|
+
console.error('EasyDB MCP Server running');
|
|
203
|
+
}
|
|
204
|
+
main().catch((error) => {
|
|
205
|
+
console.error('Server error:', error);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
});
|
|
208
|
+
// Cleanup on exit
|
|
209
|
+
process.on('SIGINT', async () => {
|
|
210
|
+
console.error('\\nShutting down...');
|
|
211
|
+
await adapter.close();
|
|
212
|
+
process.exit(0);
|
|
213
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hyqf98/easy_db_mcp_server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for database access (MySQL, PostgreSQL, SQLite)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hyqf98@easy_db_mcp_server": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsx src/index.ts",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"test": "echo \"Tests not yet implemented\""
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"database",
|
|
18
|
+
"mysql",
|
|
19
|
+
"postgresql",
|
|
20
|
+
"sqlite"
|
|
21
|
+
],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.0.0"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
29
|
+
"mysql2": "^3.11.0",
|
|
30
|
+
"pg": "^8.13.1",
|
|
31
|
+
"better-sqlite3": "^11.7.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^22.10.2",
|
|
35
|
+
"@types/pg": "^8.11.10",
|
|
36
|
+
"@types/better-sqlite3": "^7.6.12",
|
|
37
|
+
"typescript": "^5.7.2",
|
|
38
|
+
"tsx": "^4.19.2"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export interface DatabaseConfig {
|
|
2
|
+
type: 'mysql' | 'postgresql' | 'sqlite';
|
|
3
|
+
host?: string;
|
|
4
|
+
port?: number;
|
|
5
|
+
user?: string;
|
|
6
|
+
password?: string;
|
|
7
|
+
database?: string;
|
|
8
|
+
allowWrite: boolean;
|
|
9
|
+
allowDDL: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function loadConfig(): DatabaseConfig {
|
|
13
|
+
const type = process.env.EASYDB_TYPE?.trim();
|
|
14
|
+
|
|
15
|
+
if (!type) {
|
|
16
|
+
throw new Error('EASYDB_TYPE is required. Must be one of: mysql, postgresql, sqlite');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const normalizedType = type.toLowerCase();
|
|
20
|
+
if (!['mysql', 'postgresql', 'sqlite'].includes(normalizedType)) {
|
|
21
|
+
throw new Error(`EASYDB_TYPE must be one of: mysql, postgresql, sqlite. Got: ${type}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Parse and validate port
|
|
25
|
+
let port: number | undefined;
|
|
26
|
+
const portEnv = process.env.EASYDB_PORT?.trim();
|
|
27
|
+
if (portEnv) {
|
|
28
|
+
port = parseInt(portEnv, 10);
|
|
29
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
30
|
+
throw new Error(`EASYDB_PORT must be a valid port number (1-65535). Got: ${portEnv}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const config: DatabaseConfig = {
|
|
35
|
+
type: normalizedType as DatabaseConfig['type'],
|
|
36
|
+
host: process.env.EASYDB_HOST?.trim(),
|
|
37
|
+
port,
|
|
38
|
+
user: process.env.EASYDB_USER?.trim(),
|
|
39
|
+
password: process.env.EASYDB_PASSWORD,
|
|
40
|
+
database: process.env.EASYDB_DATABASE?.trim(),
|
|
41
|
+
allowWrite: process.env.EASYDB_ALLOW_WRITE === 'true',
|
|
42
|
+
allowDDL: process.env.EASYDB_ALLOW_DDL === 'true',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Validate required fields based on type
|
|
46
|
+
if (normalizedType !== 'sqlite') {
|
|
47
|
+
if (!config.host || config.host.length === 0) {
|
|
48
|
+
throw new Error('EASYDB_HOST is required for mysql/postgresql');
|
|
49
|
+
}
|
|
50
|
+
if (!config.user || config.user.length === 0) {
|
|
51
|
+
throw new Error('EASYDB_USER is required for mysql/postgresql');
|
|
52
|
+
}
|
|
53
|
+
if (!config.password) {
|
|
54
|
+
throw new Error('EASYDB_PASSWORD is required for mysql/postgresql');
|
|
55
|
+
}
|
|
56
|
+
// database is optional - can be specified in tool calls
|
|
57
|
+
} else {
|
|
58
|
+
if (!config.database || config.database.length === 0) {
|
|
59
|
+
throw new Error('EASYDB_DATABASE (file path) is required for sqlite');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return config;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getDefaultPort(type: DatabaseConfig['type']): number {
|
|
67
|
+
switch (type) {
|
|
68
|
+
case 'mysql':
|
|
69
|
+
return 3306;
|
|
70
|
+
case 'postgresql':
|
|
71
|
+
return 5432;
|
|
72
|
+
case 'sqlite':
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface TableColumn {
|
|
2
|
+
name: string;
|
|
3
|
+
type: string;
|
|
4
|
+
nullable: boolean;
|
|
5
|
+
defaultValue: string | null;
|
|
6
|
+
primaryKey: boolean;
|
|
7
|
+
extra?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TableInfo {
|
|
11
|
+
name: string;
|
|
12
|
+
rowCount?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface QueryResult {
|
|
16
|
+
rows: Record<string, unknown>[];
|
|
17
|
+
rowCount: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DatabaseAdapter {
|
|
21
|
+
/**
|
|
22
|
+
* Establish connection to the database
|
|
23
|
+
*/
|
|
24
|
+
connect(): Promise<void>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* List all tables in the database
|
|
28
|
+
*/
|
|
29
|
+
listTables(database?: string): Promise<TableInfo[]>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get table structure/column information
|
|
33
|
+
*/
|
|
34
|
+
describeTable(table: string, database?: string): Promise<TableColumn[]>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Execute a SELECT query
|
|
38
|
+
*/
|
|
39
|
+
executeQuery(sql: string, database?: string): Promise<QueryResult>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Execute any SQL statement (INSERT, UPDATE, DELETE, DDL)
|
|
43
|
+
*/
|
|
44
|
+
executeSQL(sql: string, database?: string): Promise<QueryResult | { affectedRows: number }>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Close the database connection
|
|
48
|
+
*/
|
|
49
|
+
close(): Promise<void>;
|
|
50
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { MySQLAdapter } from './mysql.js';
|
|
2
|
+
import { PostgreSQLAdapter } from './postgresql.js';
|
|
3
|
+
import { SQLiteAdapter } from './sqlite.js';
|
|
4
|
+
import type { DatabaseAdapter } from './base.js';
|
|
5
|
+
import type { DatabaseConfig } from '../config.js';
|
|
6
|
+
|
|
7
|
+
export function createAdapter(config: DatabaseConfig): DatabaseAdapter {
|
|
8
|
+
switch (config.type) {
|
|
9
|
+
case 'mysql':
|
|
10
|
+
return new MySQLAdapter(config);
|
|
11
|
+
case 'postgresql':
|
|
12
|
+
return new PostgreSQLAdapter(config);
|
|
13
|
+
case 'sqlite':
|
|
14
|
+
return new SQLiteAdapter(config);
|
|
15
|
+
default:
|
|
16
|
+
throw new Error(`Unsupported database type: ${config.type}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import mysql from 'mysql2/promise';
|
|
2
|
+
import type {
|
|
3
|
+
DatabaseAdapter,
|
|
4
|
+
TableInfo,
|
|
5
|
+
TableColumn,
|
|
6
|
+
QueryResult,
|
|
7
|
+
} from './base.js';
|
|
8
|
+
import type { DatabaseConfig } from '../config.js';
|
|
9
|
+
|
|
10
|
+
export class MySQLAdapter implements DatabaseAdapter {
|
|
11
|
+
private pool?: mysql.Pool;
|
|
12
|
+
private config: DatabaseConfig;
|
|
13
|
+
|
|
14
|
+
constructor(config: DatabaseConfig) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async connect(): Promise<void> {
|
|
19
|
+
this.pool = mysql.createPool({
|
|
20
|
+
host: this.config.host,
|
|
21
|
+
port: this.config.port || 3306,
|
|
22
|
+
user: this.config.user,
|
|
23
|
+
password: this.config.password,
|
|
24
|
+
database: this.config.database,
|
|
25
|
+
waitForConnections: true,
|
|
26
|
+
connectionLimit: 10,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Test connection
|
|
30
|
+
const connection = await this.pool.getConnection();
|
|
31
|
+
connection.release();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async listTables(database?: string): Promise<TableInfo[]> {
|
|
35
|
+
if (!this.pool) throw new Error('Not connected');
|
|
36
|
+
|
|
37
|
+
const dbName = database || this.config.database;
|
|
38
|
+
if (!dbName) throw new Error('Database name required');
|
|
39
|
+
|
|
40
|
+
const [rows] = await this.pool.query<mysql.RowDataPacket[]>(
|
|
41
|
+
'SELECT TABLE_NAME as name, TABLE_ROWS as rowCount FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ?',
|
|
42
|
+
[dbName]
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return rows.map((row: any) => ({
|
|
46
|
+
name: row.name,
|
|
47
|
+
rowCount: row.rowCount,
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async describeTable(table: string, database?: string): Promise<TableColumn[]> {
|
|
52
|
+
if (!this.pool) throw new Error('Not connected');
|
|
53
|
+
|
|
54
|
+
const dbName = database || this.config.database;
|
|
55
|
+
if (!dbName) throw new Error('Database name required');
|
|
56
|
+
|
|
57
|
+
const [rows] = await this.pool.query<mysql.RowDataPacket[]>(
|
|
58
|
+
`SELECT
|
|
59
|
+
COLUMN_NAME as name,
|
|
60
|
+
DATA_TYPE as type,
|
|
61
|
+
IS_NULLABLE as nullable,
|
|
62
|
+
COLUMN_DEFAULT as defaultValue,
|
|
63
|
+
COLUMN_KEY as primaryKey,
|
|
64
|
+
EXTRA as extra
|
|
65
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
66
|
+
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
|
67
|
+
ORDER BY ORDINAL_POSITION`,
|
|
68
|
+
[dbName, table]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return rows.map((row: any) => ({
|
|
72
|
+
name: row.name,
|
|
73
|
+
type: row.type,
|
|
74
|
+
nullable: row.nullable === 'YES',
|
|
75
|
+
defaultValue: row.defaultValue,
|
|
76
|
+
primaryKey: row.primaryKey === 'PRI',
|
|
77
|
+
extra: row.extra,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async executeQuery(sql: string, database?: string): Promise<QueryResult> {
|
|
82
|
+
if (!this.pool) throw new Error('Not connected');
|
|
83
|
+
|
|
84
|
+
// If database is specified and different from config, we need to handle it
|
|
85
|
+
// For now, we'll allow queries to use fully qualified table names
|
|
86
|
+
// Verify it's a SELECT query
|
|
87
|
+
const trimmed = sql.trim().toUpperCase();
|
|
88
|
+
if (!trimmed.startsWith('SELECT')) {
|
|
89
|
+
throw new Error('Only SELECT queries allowed in executeQuery');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const [rows] = await this.pool.query(sql);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
rows: rows as Record<string, unknown>[],
|
|
96
|
+
rowCount: Array.isArray(rows) ? rows.length : 0,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async executeSQL(sql: string, database?: string): Promise<QueryResult | { affectedRows: number }> {
|
|
101
|
+
if (!this.pool) throw new Error('Not connected');
|
|
102
|
+
|
|
103
|
+
// Allow SQL execution with database-specific queries
|
|
104
|
+
const [result] = await this.pool.query(sql);
|
|
105
|
+
|
|
106
|
+
// @ts-ignore - MySQL result structure
|
|
107
|
+
if (result.affectedRows !== undefined) {
|
|
108
|
+
// @ts-ignore
|
|
109
|
+
return { affectedRows: result.affectedRows };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
rows: result as Record<string, unknown>[],
|
|
114
|
+
rowCount: Array.isArray(result) ? result.length : 0,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async close(): Promise<void> {
|
|
119
|
+
if (this.pool) {
|
|
120
|
+
await this.pool.end();
|
|
121
|
+
this.pool = undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
import type {
|
|
3
|
+
DatabaseAdapter,
|
|
4
|
+
TableInfo,
|
|
5
|
+
TableColumn,
|
|
6
|
+
QueryResult,
|
|
7
|
+
} from './base.js';
|
|
8
|
+
import type { DatabaseConfig } from '../config.js';
|
|
9
|
+
|
|
10
|
+
const { Pool } = pg;
|
|
11
|
+
|
|
12
|
+
export class PostgreSQLAdapter implements DatabaseAdapter {
|
|
13
|
+
private pool?: pg.Pool;
|
|
14
|
+
private config: DatabaseConfig;
|
|
15
|
+
|
|
16
|
+
constructor(config: DatabaseConfig) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async connect(): Promise<void> {
|
|
21
|
+
this.pool = new Pool({
|
|
22
|
+
host: this.config.host,
|
|
23
|
+
port: this.config.port || 5432,
|
|
24
|
+
user: this.config.user,
|
|
25
|
+
password: this.config.password,
|
|
26
|
+
database: this.config.database,
|
|
27
|
+
max: 10,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Test connection
|
|
31
|
+
const client = await this.pool.connect();
|
|
32
|
+
client.release();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async listTables(database?: string): Promise<TableInfo[]> {
|
|
36
|
+
if (!this.pool) throw new Error('Not connected');
|
|
37
|
+
|
|
38
|
+
const dbName = database || this.config.database;
|
|
39
|
+
if (!dbName) throw new Error('Database name required');
|
|
40
|
+
|
|
41
|
+
const result = await this.pool.query(
|
|
42
|
+
`SELECT tablename as name, n_live_tup as "rowCount"
|
|
43
|
+
FROM pg_stat_user_tables
|
|
44
|
+
WHERE schemaname = 'public'`
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return result.rows.map((row: any) => ({
|
|
48
|
+
name: row.name,
|
|
49
|
+
rowCount: row.rowCount,
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async describeTable(table: string, database?: string): Promise<TableColumn[]> {
|
|
54
|
+
if (!this.pool) throw new Error('Not connected');
|
|
55
|
+
|
|
56
|
+
const dbName = database || this.config.database;
|
|
57
|
+
|
|
58
|
+
const result = await this.pool.query(
|
|
59
|
+
`SELECT
|
|
60
|
+
column_name as name,
|
|
61
|
+
data_type as type,
|
|
62
|
+
is_nullable as nullable,
|
|
63
|
+
column_default as "defaultValue",
|
|
64
|
+
COALESCE(pk.primary, false) as "primaryKey"
|
|
65
|
+
FROM information_schema.columns
|
|
66
|
+
LEFT JOIN (
|
|
67
|
+
SELECT a.attname as primary, c.relname
|
|
68
|
+
FROM pg_index i
|
|
69
|
+
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
|
70
|
+
JOIN pg_class c ON c.oid = i.indrelid
|
|
71
|
+
WHERE i.indisprimary
|
|
72
|
+
) pk ON pk.primary = column_name AND pk.relname = $1
|
|
73
|
+
WHERE table_schema = 'public' AND table_name = $1
|
|
74
|
+
ORDER BY ordinal_position`,
|
|
75
|
+
[table]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return result.rows.map((row: any) => ({
|
|
79
|
+
name: row.name,
|
|
80
|
+
type: row.type,
|
|
81
|
+
nullable: row.nullable === 'YES',
|
|
82
|
+
defaultValue: row.defaultValue,
|
|
83
|
+
primaryKey: row.primaryKey,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async executeQuery(sql: string, database?: string): Promise<QueryResult> {
|
|
88
|
+
if (!this.pool) throw new Error('Not connected');
|
|
89
|
+
|
|
90
|
+
// Allow queries with fully qualified table names (schema.table)
|
|
91
|
+
const trimmed = sql.trim().toUpperCase();
|
|
92
|
+
if (!trimmed.startsWith('SELECT')) {
|
|
93
|
+
throw new Error('Only SELECT queries allowed in executeQuery');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const result = await this.pool.query(sql);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
rows: result.rows,
|
|
100
|
+
rowCount: result.rowCount || 0,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async executeSQL(sql: string, database?: string): Promise<QueryResult | { affectedRows: number }> {
|
|
105
|
+
if (!this.pool) throw new Error('Not connected');
|
|
106
|
+
|
|
107
|
+
// Allow SQL execution with database-specific queries
|
|
108
|
+
const result = await this.pool.query(sql);
|
|
109
|
+
|
|
110
|
+
if (result.rowCount !== null && result.command !== 'SELECT') {
|
|
111
|
+
return { affectedRows: result.rowCount };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
rows: result.rows,
|
|
116
|
+
rowCount: result.rowCount || 0,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async close(): Promise<void> {
|
|
121
|
+
if (this.pool) {
|
|
122
|
+
await this.pool.end();
|
|
123
|
+
this.pool = undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|