@hesed/psql 0.1.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/LICENSE +202 -0
- package/README.md +293 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/psql/auth/add.d.ts +18 -0
- package/dist/commands/psql/auth/add.js +57 -0
- package/dist/commands/psql/auth/test.d.ts +12 -0
- package/dist/commands/psql/auth/test.js +41 -0
- package/dist/commands/psql/auth/update.d.ts +18 -0
- package/dist/commands/psql/auth/update.js +74 -0
- package/dist/commands/psql/databases.d.ts +9 -0
- package/dist/commands/psql/databases.js +31 -0
- package/dist/commands/psql/describe-table.d.ts +13 -0
- package/dist/commands/psql/describe-table.js +39 -0
- package/dist/commands/psql/explain-query.d.ts +13 -0
- package/dist/commands/psql/explain-query.js +39 -0
- package/dist/commands/psql/indexes.d.ts +13 -0
- package/dist/commands/psql/indexes.js +39 -0
- package/dist/commands/psql/query.d.ts +14 -0
- package/dist/commands/psql/query.js +47 -0
- package/dist/commands/psql/tables.d.ts +9 -0
- package/dist/commands/psql/tables.js +31 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +18 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/psql/config-loader.d.ts +28 -0
- package/dist/psql/config-loader.js +26 -0
- package/dist/psql/database.d.ts +85 -0
- package/dist/psql/database.js +5 -0
- package/dist/psql/index.d.ts +3 -0
- package/dist/psql/index.js +1 -0
- package/dist/psql/postgres-client.d.ts +60 -0
- package/dist/psql/postgres-client.js +131 -0
- package/dist/psql/postgres-utils.d.ts +72 -0
- package/dist/psql/postgres-utils.js +364 -0
- package/dist/psql/query-validator.d.ts +38 -0
- package/dist/psql/query-validator.js +91 -0
- package/oclif.manifest.json +521 -0
- package/package.json +103 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { readConfig } from '../config.js';
|
|
2
|
+
import { PostgreSQLUtil } from './postgres-utils.js';
|
|
3
|
+
let pgUtil = null;
|
|
4
|
+
let cachedConfig = null;
|
|
5
|
+
let cachedConfigDir;
|
|
6
|
+
/**
|
|
7
|
+
* Set the config directory for the singleton client
|
|
8
|
+
* @param dir - Oclif config directory path
|
|
9
|
+
*/
|
|
10
|
+
export function setConfigDir(dir) {
|
|
11
|
+
cachedConfigDir = dir;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Initialize (or return cached) PostgreSQLUtil
|
|
15
|
+
*/
|
|
16
|
+
async function initPg() {
|
|
17
|
+
if (pgUtil)
|
|
18
|
+
return pgUtil;
|
|
19
|
+
if (!cachedConfigDir) {
|
|
20
|
+
throw new Error('PostgreSQL client not initialized. Call setConfigDir() before running commands.');
|
|
21
|
+
}
|
|
22
|
+
const jsonConfig = await readConfig(cachedConfigDir, console.error);
|
|
23
|
+
if (!jsonConfig) {
|
|
24
|
+
throw new Error('Missing connection config. Run "pg psql auth add" to create a config.');
|
|
25
|
+
}
|
|
26
|
+
cachedConfig = {
|
|
27
|
+
defaultFormat: 'table',
|
|
28
|
+
defaultProfile: jsonConfig.defaultProfile,
|
|
29
|
+
profiles: jsonConfig.profiles,
|
|
30
|
+
safety: {
|
|
31
|
+
blacklistedOperations: ['DROP DATABASE'],
|
|
32
|
+
defaultLimit: 100,
|
|
33
|
+
requireConfirmationFor: ['DELETE', 'UPDATE', 'DROP', 'TRUNCATE', 'ALTER'],
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
pgUtil = new PostgreSQLUtil(cachedConfig);
|
|
37
|
+
return pgUtil;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get the loaded PostgreSQL config, initializing if needed
|
|
41
|
+
*/
|
|
42
|
+
export async function getPgConfig() {
|
|
43
|
+
if (!cachedConfig) {
|
|
44
|
+
await initPg();
|
|
45
|
+
}
|
|
46
|
+
return cachedConfig;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Execute SQL query
|
|
50
|
+
* @param query - SQL query to execute
|
|
51
|
+
* @param profile - Database profile name
|
|
52
|
+
* @param format - Output format
|
|
53
|
+
* @param skipConfirmation - Skip confirmation for destructive operations
|
|
54
|
+
*/
|
|
55
|
+
export async function executeQuery(query, profile, format = 'table', skipConfirmation = false) {
|
|
56
|
+
return (await initPg()).executeQuery(profile, query, format, skipConfirmation);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* List all databases
|
|
60
|
+
* @param profile - Database profile name
|
|
61
|
+
*/
|
|
62
|
+
export async function listDatabases(profile) {
|
|
63
|
+
return (await initPg()).listDatabases(profile);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* List all tables in current database
|
|
67
|
+
* @param profile - Database profile name
|
|
68
|
+
*/
|
|
69
|
+
export async function listTables(profile) {
|
|
70
|
+
return (await initPg()).listTables(profile);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Describe table structure
|
|
74
|
+
* @param profile - Database profile name
|
|
75
|
+
* @param table - Table name
|
|
76
|
+
* @param format - Output format
|
|
77
|
+
*/
|
|
78
|
+
export async function describeTable(profile, table, format = 'table') {
|
|
79
|
+
return (await initPg()).describeTable(profile, table, format);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Show table indexes
|
|
83
|
+
* @param profile - Database profile name
|
|
84
|
+
* @param table - Table name
|
|
85
|
+
* @param format - Output format
|
|
86
|
+
*/
|
|
87
|
+
export async function showIndexes(profile, table, format = 'table') {
|
|
88
|
+
return (await initPg()).showIndexes(profile, table, format);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Explain query execution plan
|
|
92
|
+
* @param profile - Database profile name
|
|
93
|
+
* @param query - SQL query to explain
|
|
94
|
+
* @param format - Output format
|
|
95
|
+
*/
|
|
96
|
+
export async function explainQuery(profile, query, format = 'table') {
|
|
97
|
+
return (await initPg()).explainQuery(profile, query, format);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Test a connection directly with profile options (without loading JSON config)
|
|
101
|
+
* @param profile - Database connection profile options
|
|
102
|
+
*/
|
|
103
|
+
export async function testDirectConnection(profile) {
|
|
104
|
+
const tempConfig = {
|
|
105
|
+
defaultFormat: 'table',
|
|
106
|
+
defaultProfile: '_auth',
|
|
107
|
+
profiles: { _auth: profile },
|
|
108
|
+
safety: {
|
|
109
|
+
blacklistedOperations: [],
|
|
110
|
+
defaultLimit: 100,
|
|
111
|
+
requireConfirmationFor: [],
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
const tempUtil = new PostgreSQLUtil(tempConfig);
|
|
115
|
+
try {
|
|
116
|
+
return await tempUtil.testConnection('_auth');
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
await tempUtil.closeAll();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Close all connections
|
|
124
|
+
*/
|
|
125
|
+
export async function closeConnections() {
|
|
126
|
+
if (pgUtil) {
|
|
127
|
+
await pgUtil.closeAll();
|
|
128
|
+
pgUtil = null;
|
|
129
|
+
cachedConfig = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { PgConfig } from './config-loader.js';
|
|
2
|
+
import type { ConnectionTestResult, DatabaseListResult, DatabaseUtil, ExplainResult, IndexResult, OutputFormat, QueryResult, TableListResult, TableStructureResult } from './database.js';
|
|
3
|
+
type PgRow = Record<string, unknown>;
|
|
4
|
+
type PgField = {
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* PostgreSQL Database Utility
|
|
9
|
+
* Provides core database operations with safety validation and formatting
|
|
10
|
+
*/
|
|
11
|
+
export declare class PostgreSQLUtil implements DatabaseUtil {
|
|
12
|
+
private config;
|
|
13
|
+
private connectionPool;
|
|
14
|
+
constructor(config: PgConfig);
|
|
15
|
+
/**
|
|
16
|
+
* Close all connections
|
|
17
|
+
*/
|
|
18
|
+
closeAll(): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Describe table structure
|
|
21
|
+
*/
|
|
22
|
+
describeTable(profileName: string, table: string, format?: 'json' | 'table' | 'toon'): Promise<TableStructureResult>;
|
|
23
|
+
/**
|
|
24
|
+
* Validate and execute a SQL query
|
|
25
|
+
*/
|
|
26
|
+
executeQuery(profileName: string, query: string, format?: OutputFormat, skipConfirmation?: boolean): Promise<QueryResult>;
|
|
27
|
+
/**
|
|
28
|
+
* Explain query execution plan
|
|
29
|
+
*/
|
|
30
|
+
explainQuery(profileName: string, query: string, format?: 'json' | 'table' | 'toon'): Promise<ExplainResult>;
|
|
31
|
+
/**
|
|
32
|
+
* Format query results as CSV
|
|
33
|
+
*/
|
|
34
|
+
formatAsCsv(rows: PgRow[], fields: PgField[]): string;
|
|
35
|
+
/**
|
|
36
|
+
* Format query results as JSON
|
|
37
|
+
*/
|
|
38
|
+
formatAsJson(rows: PgRow[]): string;
|
|
39
|
+
/**
|
|
40
|
+
* Format query results as table
|
|
41
|
+
*/
|
|
42
|
+
formatAsTable(rows: PgRow[], fields: PgField[]): string;
|
|
43
|
+
/**
|
|
44
|
+
* Format query results as TOON
|
|
45
|
+
*/
|
|
46
|
+
formatAsToon(rows: PgRow[]): string;
|
|
47
|
+
/**
|
|
48
|
+
* List all databases
|
|
49
|
+
*/
|
|
50
|
+
listDatabases(profileName: string): Promise<DatabaseListResult>;
|
|
51
|
+
/**
|
|
52
|
+
* List all tables in current database
|
|
53
|
+
*/
|
|
54
|
+
listTables(profileName: string): Promise<TableListResult>;
|
|
55
|
+
/**
|
|
56
|
+
* Show table indexes
|
|
57
|
+
*/
|
|
58
|
+
showIndexes(profileName: string, table: string, format?: 'json' | 'table' | 'toon'): Promise<IndexResult>;
|
|
59
|
+
/**
|
|
60
|
+
* Test database connection
|
|
61
|
+
*/
|
|
62
|
+
testConnection(profileName: string): Promise<ConnectionTestResult>;
|
|
63
|
+
/**
|
|
64
|
+
* Format rows for SELECT/EXPLAIN query result
|
|
65
|
+
*/
|
|
66
|
+
private formatSelectResult;
|
|
67
|
+
/**
|
|
68
|
+
* Get or create PostgreSQL client for a profile
|
|
69
|
+
*/
|
|
70
|
+
private getConnection;
|
|
71
|
+
}
|
|
72
|
+
export {};
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { encode } from '@toon-format/toon';
|
|
2
|
+
import pg from 'pg';
|
|
3
|
+
import { getPgConnectionOptions } from './config-loader.js';
|
|
4
|
+
import { analyzeQuery, applyDefaultLimit, checkBlacklist, getQueryType, requiresConfirmation } from './query-validator.js';
|
|
5
|
+
/**
|
|
6
|
+
* PostgreSQL Database Utility
|
|
7
|
+
* Provides core database operations with safety validation and formatting
|
|
8
|
+
*/
|
|
9
|
+
export class PostgreSQLUtil {
|
|
10
|
+
config;
|
|
11
|
+
connectionPool;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.connectionPool = new Map();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Close all connections
|
|
18
|
+
*/
|
|
19
|
+
async closeAll() {
|
|
20
|
+
await Promise.all([...this.connectionPool.values()].map((c) => c.end()));
|
|
21
|
+
this.connectionPool.clear();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Describe table structure
|
|
25
|
+
*/
|
|
26
|
+
async describeTable(profileName, table, format = 'table') {
|
|
27
|
+
try {
|
|
28
|
+
const client = await this.getConnection(profileName);
|
|
29
|
+
const result = await client.query(`SELECT column_name, data_type, character_maximum_length, is_nullable, column_default FROM information_schema.columns WHERE table_name = '${table}' AND table_schema = 'public' ORDER BY ordinal_position`);
|
|
30
|
+
let output = '';
|
|
31
|
+
if (format === 'json') {
|
|
32
|
+
output += this.formatAsJson(result.rows);
|
|
33
|
+
}
|
|
34
|
+
else if (format === 'toon') {
|
|
35
|
+
output += this.formatAsToon(result.rows);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
output += this.formatAsTable(result.rows, result.fields);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
result: output,
|
|
42
|
+
structure: result.rows,
|
|
43
|
+
success: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
48
|
+
return {
|
|
49
|
+
error: `ERROR: ${errorMessage}`,
|
|
50
|
+
success: false,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Validate and execute a SQL query
|
|
56
|
+
*/
|
|
57
|
+
async executeQuery(profileName, query, format = 'table', skipConfirmation = false) {
|
|
58
|
+
const blacklistCheck = checkBlacklist(query, this.config.safety.blacklistedOperations);
|
|
59
|
+
if (!blacklistCheck.allowed) {
|
|
60
|
+
return {
|
|
61
|
+
error: `${blacklistCheck.reason}\n\nThis operation is blocked by safety rules and cannot be executed.`,
|
|
62
|
+
success: false,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (!skipConfirmation) {
|
|
66
|
+
const confirmationCheck = requiresConfirmation(query, this.config.safety.requireConfirmationFor);
|
|
67
|
+
if (confirmationCheck.required) {
|
|
68
|
+
return {
|
|
69
|
+
message: `${confirmationCheck.message}\nQuery: ${query}`,
|
|
70
|
+
requiresConfirmation: true,
|
|
71
|
+
success: false,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const warnings = analyzeQuery(query);
|
|
76
|
+
let warningText = '';
|
|
77
|
+
if (warnings.length > 0) {
|
|
78
|
+
warningText =
|
|
79
|
+
'Query Analysis:\n' +
|
|
80
|
+
warnings.map((w) => ` [${w.level.toUpperCase()}] ${w.message}\n → ${w.suggestion}`).join('\n') +
|
|
81
|
+
'\n\n';
|
|
82
|
+
}
|
|
83
|
+
let finalQuery = query;
|
|
84
|
+
const queryType = getQueryType(query);
|
|
85
|
+
if (queryType === 'SELECT') {
|
|
86
|
+
finalQuery = applyDefaultLimit(query, this.config.safety.defaultLimit);
|
|
87
|
+
if (finalQuery !== query) {
|
|
88
|
+
warningText += `Applied default LIMIT ${this.config.safety.defaultLimit}\n\n`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const client = await this.getConnection(profileName);
|
|
93
|
+
const result = await client.query(finalQuery);
|
|
94
|
+
let output = '';
|
|
95
|
+
if (result.rows.length > 0 || result.command === 'SELECT' || result.command === 'EXPLAIN') {
|
|
96
|
+
output += this.formatSelectResult(result.rows, result.fields, format);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const affectedRows = result.rowCount ?? 0;
|
|
100
|
+
output += `Query executed successfully.\n`;
|
|
101
|
+
output += `Affected rows: ${affectedRows}\n`;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
result: warningText + output,
|
|
105
|
+
success: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
110
|
+
return {
|
|
111
|
+
error: `ERROR: ${errorMessage}`,
|
|
112
|
+
success: false,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Explain query execution plan
|
|
118
|
+
*/
|
|
119
|
+
async explainQuery(profileName, query, format = 'table') {
|
|
120
|
+
try {
|
|
121
|
+
const client = await this.getConnection(profileName);
|
|
122
|
+
const result = await client.query(`EXPLAIN ${query}`);
|
|
123
|
+
let output = '';
|
|
124
|
+
if (format === 'json') {
|
|
125
|
+
output += this.formatAsJson(result.rows);
|
|
126
|
+
}
|
|
127
|
+
else if (format === 'toon') {
|
|
128
|
+
output += this.formatAsToon(result.rows);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
output += this.formatAsTable(result.rows, result.fields);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
plan: result.rows,
|
|
135
|
+
result: output,
|
|
136
|
+
success: true,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
141
|
+
return {
|
|
142
|
+
error: `ERROR: ${errorMessage}`,
|
|
143
|
+
success: false,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Format query results as CSV
|
|
149
|
+
*/
|
|
150
|
+
formatAsCsv(rows, fields) {
|
|
151
|
+
if (!rows || rows.length === 0) {
|
|
152
|
+
return '';
|
|
153
|
+
}
|
|
154
|
+
const columnNames = fields.map((f) => f.name);
|
|
155
|
+
let csv = columnNames.join(',') + '\n';
|
|
156
|
+
for (const row of rows) {
|
|
157
|
+
const values = columnNames.map((name) => {
|
|
158
|
+
const value = row[name] ?? '';
|
|
159
|
+
const str = String(value);
|
|
160
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
161
|
+
return '"' + str.replaceAll('"', '""') + '"';
|
|
162
|
+
}
|
|
163
|
+
return str;
|
|
164
|
+
});
|
|
165
|
+
csv += values.join(',') + '\n';
|
|
166
|
+
}
|
|
167
|
+
return csv;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Format query results as JSON
|
|
171
|
+
*/
|
|
172
|
+
formatAsJson(rows) {
|
|
173
|
+
return JSON.stringify(rows, null, 2);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Format query results as table
|
|
177
|
+
*/
|
|
178
|
+
formatAsTable(rows, fields) {
|
|
179
|
+
if (!rows || rows.length === 0) {
|
|
180
|
+
return 'No results';
|
|
181
|
+
}
|
|
182
|
+
const columnNames = fields.map((f) => f.name);
|
|
183
|
+
const columnWidths = columnNames.map((name) => {
|
|
184
|
+
const dataWidth = Math.max(...rows.map((row) => String(row[name] ?? '').length));
|
|
185
|
+
return Math.max(name.length, dataWidth, 3);
|
|
186
|
+
});
|
|
187
|
+
let table = '┌' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┬') + '┐\n';
|
|
188
|
+
table += '│ ' + columnNames.map((name, i) => name.padEnd(columnWidths[i])).join(' │ ') + ' │\n';
|
|
189
|
+
table += '├' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┼') + '┤\n';
|
|
190
|
+
for (const row of rows) {
|
|
191
|
+
table +=
|
|
192
|
+
'│ ' +
|
|
193
|
+
columnNames
|
|
194
|
+
.map((name, i) => {
|
|
195
|
+
const value = row[name] ?? 'NULL';
|
|
196
|
+
return String(value).padEnd(columnWidths[i]);
|
|
197
|
+
})
|
|
198
|
+
.join(' │ ') +
|
|
199
|
+
' │\n';
|
|
200
|
+
}
|
|
201
|
+
table += '└' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┴') + '┘';
|
|
202
|
+
return table;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Format query results as TOON
|
|
206
|
+
*/
|
|
207
|
+
formatAsToon(rows) {
|
|
208
|
+
if (!rows || rows.length === 0) {
|
|
209
|
+
return '';
|
|
210
|
+
}
|
|
211
|
+
const serializedRows = rows.map((row) => {
|
|
212
|
+
const serialized = {};
|
|
213
|
+
for (const [key, value] of Object.entries(row)) {
|
|
214
|
+
if (value instanceof Date) {
|
|
215
|
+
serialized[key] = Number.isNaN(value.getTime()) ? null : value.toISOString();
|
|
216
|
+
}
|
|
217
|
+
else if (Buffer.isBuffer(value)) {
|
|
218
|
+
serialized[key] = value.toString('base64');
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
serialized[key] = value;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return serialized;
|
|
225
|
+
});
|
|
226
|
+
return encode(serializedRows);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* List all databases
|
|
230
|
+
*/
|
|
231
|
+
async listDatabases(profileName) {
|
|
232
|
+
try {
|
|
233
|
+
const client = await this.getConnection(profileName);
|
|
234
|
+
const result = await client.query('SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname');
|
|
235
|
+
const databases = result.rows.map((row) => row.datname);
|
|
236
|
+
return {
|
|
237
|
+
databases,
|
|
238
|
+
result: `Databases:\n${databases.map((db) => ` • ${db}`).join('\n')}`,
|
|
239
|
+
success: true,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
244
|
+
return {
|
|
245
|
+
error: `ERROR: ${errorMessage}`,
|
|
246
|
+
success: false,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* List all tables in current database
|
|
252
|
+
*/
|
|
253
|
+
async listTables(profileName) {
|
|
254
|
+
try {
|
|
255
|
+
const client = await this.getConnection(profileName);
|
|
256
|
+
const result = await client.query("SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename");
|
|
257
|
+
const tables = result.rows.map((row) => row.tablename);
|
|
258
|
+
return {
|
|
259
|
+
result: `Tables in database:\n${tables.map((table) => ` • ${table}`).join('\n')}`,
|
|
260
|
+
success: true,
|
|
261
|
+
tables,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
266
|
+
return {
|
|
267
|
+
error: `ERROR: ${errorMessage}`,
|
|
268
|
+
success: false,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Show table indexes
|
|
274
|
+
*/
|
|
275
|
+
async showIndexes(profileName, table, format = 'table') {
|
|
276
|
+
try {
|
|
277
|
+
const client = await this.getConnection(profileName);
|
|
278
|
+
const result = await client.query(`SELECT indexname, indexdef FROM pg_indexes WHERE tablename = '${table}' AND schemaname = 'public'`);
|
|
279
|
+
let output = '';
|
|
280
|
+
if (format === 'json') {
|
|
281
|
+
output += this.formatAsJson(result.rows);
|
|
282
|
+
}
|
|
283
|
+
else if (format === 'toon') {
|
|
284
|
+
output += this.formatAsToon(result.rows);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
output += this.formatAsTable(result.rows, result.fields);
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
indexes: result.rows,
|
|
291
|
+
result: output,
|
|
292
|
+
success: true,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
297
|
+
return {
|
|
298
|
+
error: `ERROR: ${errorMessage}`,
|
|
299
|
+
success: false,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Test database connection
|
|
305
|
+
*/
|
|
306
|
+
async testConnection(profileName) {
|
|
307
|
+
try {
|
|
308
|
+
const client = await this.getConnection(profileName);
|
|
309
|
+
const result = await client.query('SELECT version() as version, current_database() as current_database');
|
|
310
|
+
const info = result.rows[0];
|
|
311
|
+
return {
|
|
312
|
+
database: info.current_database,
|
|
313
|
+
result: `Connection successful!\n\nProfile: ${profileName}\nPostgreSQL Version: ${info.version}\nCurrent Database: ${info.current_database}`,
|
|
314
|
+
success: true,
|
|
315
|
+
version: info.version,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
320
|
+
return {
|
|
321
|
+
error: `ERROR: ${errorMessage}`,
|
|
322
|
+
success: false,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Format rows for SELECT/EXPLAIN query result
|
|
328
|
+
*/
|
|
329
|
+
formatSelectResult(rows, fields, format) {
|
|
330
|
+
const rowCount = Array.isArray(rows) ? rows.length : 0;
|
|
331
|
+
let result = `Query executed successfully. Rows returned: ${rowCount}\n\n`;
|
|
332
|
+
switch (format) {
|
|
333
|
+
case 'csv': {
|
|
334
|
+
result += this.formatAsCsv(rows, fields);
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
case 'json': {
|
|
338
|
+
result += this.formatAsJson(rows);
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
case 'toon': {
|
|
342
|
+
result += this.formatAsToon(rows);
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
default: {
|
|
346
|
+
result += this.formatAsTable(rows, fields);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Get or create PostgreSQL client for a profile
|
|
353
|
+
*/
|
|
354
|
+
async getConnection(profileName) {
|
|
355
|
+
if (this.connectionPool.has(profileName)) {
|
|
356
|
+
return this.connectionPool.get(profileName);
|
|
357
|
+
}
|
|
358
|
+
const options = getPgConnectionOptions(this.config, profileName);
|
|
359
|
+
const client = new pg.Client(options);
|
|
360
|
+
await client.connect();
|
|
361
|
+
this.connectionPool.set(profileName, client);
|
|
362
|
+
return client;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Validation and Safety Module
|
|
3
|
+
* Provides SQL query analysis and safety checks
|
|
4
|
+
*/
|
|
5
|
+
interface BlacklistCheckResult {
|
|
6
|
+
allowed: boolean;
|
|
7
|
+
reason?: string;
|
|
8
|
+
}
|
|
9
|
+
interface ConfirmationCheckResult {
|
|
10
|
+
message?: string;
|
|
11
|
+
required: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface QueryWarning {
|
|
14
|
+
level: 'info' | 'warning';
|
|
15
|
+
message: string;
|
|
16
|
+
suggestion: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Check if query contains blacklisted operations
|
|
20
|
+
*/
|
|
21
|
+
export declare function checkBlacklist(query: string, blacklistedOperations: string[]): BlacklistCheckResult;
|
|
22
|
+
/**
|
|
23
|
+
* Check if query requires user confirmation
|
|
24
|
+
*/
|
|
25
|
+
export declare function requiresConfirmation(query: string, confirmationOperations: string[]): ConfirmationCheckResult;
|
|
26
|
+
/**
|
|
27
|
+
* Get query type (SELECT, INSERT, UPDATE, etc.)
|
|
28
|
+
*/
|
|
29
|
+
export declare function getQueryType(query: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Analyze query for potential issues and provide warnings
|
|
32
|
+
*/
|
|
33
|
+
export declare function analyzeQuery(query: string): QueryWarning[];
|
|
34
|
+
/**
|
|
35
|
+
* Apply default LIMIT to SELECT queries if not present
|
|
36
|
+
*/
|
|
37
|
+
export declare function applyDefaultLimit(query: string, defaultLimit: number): string;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Validation and Safety Module
|
|
3
|
+
* Provides SQL query analysis and safety checks
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Check if query contains blacklisted operations
|
|
7
|
+
*/
|
|
8
|
+
export function checkBlacklist(query, blacklistedOperations) {
|
|
9
|
+
const normalizedQuery = query.trim().toUpperCase();
|
|
10
|
+
for (const operation of blacklistedOperations) {
|
|
11
|
+
const normalizedOp = operation.toUpperCase();
|
|
12
|
+
if (normalizedQuery.includes(normalizedOp)) {
|
|
13
|
+
return {
|
|
14
|
+
allowed: false,
|
|
15
|
+
reason: `Operation "${operation}" is blacklisted and not allowed`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return { allowed: true };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check if query requires user confirmation
|
|
23
|
+
*/
|
|
24
|
+
export function requiresConfirmation(query, confirmationOperations) {
|
|
25
|
+
const normalizedQuery = query.trim().toUpperCase();
|
|
26
|
+
for (const operation of confirmationOperations) {
|
|
27
|
+
const normalizedOp = operation.toUpperCase();
|
|
28
|
+
if (normalizedQuery.startsWith(normalizedOp) || normalizedQuery.includes(` ${normalizedOp} `)) {
|
|
29
|
+
return {
|
|
30
|
+
message: `This query contains a destructive operation: ${operation}`,
|
|
31
|
+
required: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { required: false };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get query type (SELECT, INSERT, UPDATE, etc.)
|
|
39
|
+
*/
|
|
40
|
+
export function getQueryType(query) {
|
|
41
|
+
const normalizedQuery = query.trim().toUpperCase();
|
|
42
|
+
const firstWord = normalizedQuery.split(/\s+/)[0];
|
|
43
|
+
const knownTypes = ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'ALTER', 'TRUNCATE', 'EXPLAIN'];
|
|
44
|
+
if (knownTypes.includes(firstWord)) {
|
|
45
|
+
return firstWord;
|
|
46
|
+
}
|
|
47
|
+
return 'UNKNOWN';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Analyze query for potential issues and provide warnings
|
|
51
|
+
*/
|
|
52
|
+
export function analyzeQuery(query) {
|
|
53
|
+
const warnings = [];
|
|
54
|
+
const normalizedQuery = query.trim().toUpperCase();
|
|
55
|
+
// Check for missing WHERE clause in UPDATE/DELETE
|
|
56
|
+
if ((normalizedQuery.startsWith('UPDATE') || normalizedQuery.startsWith('DELETE')) &&
|
|
57
|
+
!normalizedQuery.includes('WHERE')) {
|
|
58
|
+
warnings.push({
|
|
59
|
+
level: 'warning',
|
|
60
|
+
message: 'Missing WHERE clause in UPDATE/DELETE query',
|
|
61
|
+
suggestion: 'This will affect all rows in the table. Add a WHERE clause to limit scope.',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// Check for SELECT * (potential performance issue)
|
|
65
|
+
if (normalizedQuery.includes('SELECT *')) {
|
|
66
|
+
warnings.push({
|
|
67
|
+
level: 'info',
|
|
68
|
+
message: 'Using SELECT * may impact performance',
|
|
69
|
+
suggestion: 'Consider selecting only the columns you need.',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// Check for missing LIMIT in SELECT
|
|
73
|
+
if (normalizedQuery.startsWith('SELECT') && !normalizedQuery.includes('LIMIT')) {
|
|
74
|
+
warnings.push({
|
|
75
|
+
level: 'info',
|
|
76
|
+
message: 'SELECT query without LIMIT',
|
|
77
|
+
suggestion: 'Consider adding a LIMIT clause to prevent large result sets.',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return warnings;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Apply default LIMIT to SELECT queries if not present
|
|
84
|
+
*/
|
|
85
|
+
export function applyDefaultLimit(query, defaultLimit) {
|
|
86
|
+
const normalizedQuery = query.trim().toUpperCase();
|
|
87
|
+
if (normalizedQuery.startsWith('SELECT') && !normalizedQuery.includes('LIMIT')) {
|
|
88
|
+
return `${query.trim()} LIMIT ${defaultLimit}`;
|
|
89
|
+
}
|
|
90
|
+
return query;
|
|
91
|
+
}
|