@hesed/psql 0.2.1 → 0.3.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 +107 -32
- package/dist/commands/psql/auth/add.d.ts +2 -18
- package/dist/commands/psql/auth/add.js +16 -57
- package/dist/commands/psql/auth/delete.d.ts +2 -0
- package/dist/commands/psql/auth/delete.js +2 -0
- package/dist/commands/psql/auth/list.d.ts +2 -0
- package/dist/commands/psql/auth/list.js +2 -0
- package/dist/commands/psql/auth/profile.d.ts +2 -0
- package/dist/commands/psql/auth/profile.js +2 -0
- package/dist/commands/psql/auth/test.d.ts +2 -12
- package/dist/commands/psql/auth/test.js +16 -41
- package/dist/commands/psql/auth/update.d.ts +2 -18
- package/dist/commands/psql/auth/update.js +16 -74
- package/dist/commands/psql/databases.js +2 -10
- package/dist/commands/psql/describe-table.js +2 -10
- package/dist/commands/psql/explain-query.js +2 -10
- package/dist/commands/psql/indexes.js +2 -10
- package/dist/commands/psql/query.js +2 -10
- package/dist/commands/psql/tables.js +2 -10
- package/dist/psql/config-loader.d.ts +8 -14
- package/dist/psql/config-loader.js +0 -7
- package/dist/psql/formatters.d.ts +8 -0
- package/dist/psql/formatters.js +76 -0
- package/dist/psql/index.d.ts +1 -2
- package/dist/psql/index.js +1 -1
- package/dist/psql/postgres-client.d.ts +8 -57
- package/dist/psql/postgres-client.js +44 -101
- package/dist/psql/postgres-utils.d.ts +2 -56
- package/dist/psql/postgres-utils.js +36 -179
- package/dist/psql/query-validator.d.ts +0 -19
- package/dist/psql/query-validator.js +13 -20
- package/oclif.manifest.json +176 -59
- package/package.json +2 -1
- package/dist/config.d.ts +0 -13
- package/dist/config.js +0 -18
|
@@ -1,44 +1,25 @@
|
|
|
1
|
-
import { encode } from '@toon-format/toon';
|
|
2
1
|
import pg from 'pg';
|
|
3
2
|
import { getPgConnectionOptions } from './config-loader.js';
|
|
3
|
+
import { FORMATTERS } from './formatters.js';
|
|
4
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
5
|
export class PostgreSQLUtil {
|
|
10
6
|
config;
|
|
11
|
-
|
|
7
|
+
connections;
|
|
12
8
|
constructor(config) {
|
|
13
9
|
this.config = config;
|
|
14
|
-
this.
|
|
10
|
+
this.connections = new Map();
|
|
15
11
|
}
|
|
16
|
-
/**
|
|
17
|
-
* Close all connections
|
|
18
|
-
*/
|
|
19
12
|
async closeAll() {
|
|
20
|
-
|
|
21
|
-
this.
|
|
13
|
+
const entries = [...this.connections.values()];
|
|
14
|
+
this.connections.clear();
|
|
15
|
+
await Promise.allSettled(entries.map(async (clientPromise) => (await clientPromise).end()));
|
|
22
16
|
}
|
|
23
|
-
/**
|
|
24
|
-
* Describe table structure
|
|
25
|
-
*/
|
|
26
17
|
async describeTable(profileName, table, format = 'table') {
|
|
27
18
|
try {
|
|
28
19
|
const client = await this.getConnection(profileName);
|
|
29
20
|
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
21
|
return {
|
|
41
|
-
result:
|
|
22
|
+
result: this.formatRows(result.rows, result.fields, format),
|
|
42
23
|
structure: result.rows,
|
|
43
24
|
success: true,
|
|
44
25
|
};
|
|
@@ -51,9 +32,6 @@ export class PostgreSQLUtil {
|
|
|
51
32
|
};
|
|
52
33
|
}
|
|
53
34
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Validate and execute a SQL query
|
|
56
|
-
*/
|
|
57
35
|
async executeQuery(profileName, query, format = 'table', skipConfirmation = false) {
|
|
58
36
|
const blacklistCheck = checkBlacklist(query, this.config.safety.blacklistedOperations);
|
|
59
37
|
if (!blacklistCheck.allowed) {
|
|
@@ -113,26 +91,13 @@ export class PostgreSQLUtil {
|
|
|
113
91
|
};
|
|
114
92
|
}
|
|
115
93
|
}
|
|
116
|
-
/**
|
|
117
|
-
* Explain query execution plan
|
|
118
|
-
*/
|
|
119
94
|
async explainQuery(profileName, query, format = 'table') {
|
|
120
95
|
try {
|
|
121
96
|
const client = await this.getConnection(profileName);
|
|
122
97
|
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
98
|
return {
|
|
134
99
|
plan: result.rows,
|
|
135
|
-
result:
|
|
100
|
+
result: this.formatRows(result.rows, result.fields, format),
|
|
136
101
|
success: true,
|
|
137
102
|
};
|
|
138
103
|
}
|
|
@@ -144,90 +109,6 @@ export class PostgreSQLUtil {
|
|
|
144
109
|
};
|
|
145
110
|
}
|
|
146
111
|
}
|
|
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
112
|
async listDatabases(profileName) {
|
|
232
113
|
try {
|
|
233
114
|
const client = await this.getConnection(profileName);
|
|
@@ -247,9 +128,6 @@ export class PostgreSQLUtil {
|
|
|
247
128
|
};
|
|
248
129
|
}
|
|
249
130
|
}
|
|
250
|
-
/**
|
|
251
|
-
* List all tables in current database
|
|
252
|
-
*/
|
|
253
131
|
async listTables(profileName) {
|
|
254
132
|
try {
|
|
255
133
|
const client = await this.getConnection(profileName);
|
|
@@ -269,26 +147,13 @@ export class PostgreSQLUtil {
|
|
|
269
147
|
};
|
|
270
148
|
}
|
|
271
149
|
}
|
|
272
|
-
/**
|
|
273
|
-
* Show table indexes
|
|
274
|
-
*/
|
|
275
150
|
async showIndexes(profileName, table, format = 'table') {
|
|
276
151
|
try {
|
|
277
152
|
const client = await this.getConnection(profileName);
|
|
278
153
|
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
154
|
return {
|
|
290
155
|
indexes: result.rows,
|
|
291
|
-
result:
|
|
156
|
+
result: this.formatRows(result.rows, result.fields, format),
|
|
292
157
|
success: true,
|
|
293
158
|
};
|
|
294
159
|
}
|
|
@@ -300,9 +165,6 @@ export class PostgreSQLUtil {
|
|
|
300
165
|
};
|
|
301
166
|
}
|
|
302
167
|
}
|
|
303
|
-
/**
|
|
304
|
-
* Test database connection
|
|
305
|
-
*/
|
|
306
168
|
async testConnection(profileName) {
|
|
307
169
|
try {
|
|
308
170
|
const client = await this.getConnection(profileName);
|
|
@@ -323,42 +185,37 @@ export class PostgreSQLUtil {
|
|
|
323
185
|
};
|
|
324
186
|
}
|
|
325
187
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
188
|
+
formatRows(rows, fields, format) {
|
|
189
|
+
return FORMATTERS[format](rows, fields);
|
|
190
|
+
}
|
|
329
191
|
formatSelectResult(rows, fields, format) {
|
|
330
192
|
const rowCount = Array.isArray(rows) ? rows.length : 0;
|
|
331
|
-
|
|
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;
|
|
193
|
+
return `Query executed successfully. Rows returned: ${rowCount}\n\n` + this.formatRows(rows, fields, format);
|
|
350
194
|
}
|
|
351
|
-
/**
|
|
352
|
-
* Get or create PostgreSQL client for a profile
|
|
353
|
-
*/
|
|
354
195
|
async getConnection(profileName) {
|
|
355
|
-
|
|
356
|
-
|
|
196
|
+
const existing = this.connections.get(profileName);
|
|
197
|
+
if (existing) {
|
|
198
|
+
try {
|
|
199
|
+
const client = await existing;
|
|
200
|
+
await client.query('SELECT 1');
|
|
201
|
+
return client;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
this.connections.delete(profileName);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const clientPromise = (async () => {
|
|
208
|
+
const client = new pg.Client(getPgConnectionOptions(this.config, profileName));
|
|
209
|
+
await client.connect();
|
|
210
|
+
return client;
|
|
211
|
+
})();
|
|
212
|
+
this.connections.set(profileName, clientPromise);
|
|
213
|
+
try {
|
|
214
|
+
return await clientPromise;
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
this.connections.delete(profileName);
|
|
218
|
+
throw error;
|
|
357
219
|
}
|
|
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
220
|
}
|
|
364
221
|
}
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Query Validation and Safety Module
|
|
3
|
-
* Provides SQL query analysis and safety checks
|
|
4
|
-
*/
|
|
5
1
|
interface BlacklistCheckResult {
|
|
6
2
|
allowed: boolean;
|
|
7
3
|
reason?: string;
|
|
@@ -15,24 +11,9 @@ interface QueryWarning {
|
|
|
15
11
|
message: string;
|
|
16
12
|
suggestion: string;
|
|
17
13
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Check if query contains blacklisted operations
|
|
20
|
-
*/
|
|
21
14
|
export declare function checkBlacklist(query: string, blacklistedOperations: string[]): BlacklistCheckResult;
|
|
22
|
-
/**
|
|
23
|
-
* Check if query requires user confirmation
|
|
24
|
-
*/
|
|
25
15
|
export declare function requiresConfirmation(query: string, confirmationOperations: string[]): ConfirmationCheckResult;
|
|
26
|
-
/**
|
|
27
|
-
* Get query type (SELECT, INSERT, UPDATE, etc.)
|
|
28
|
-
*/
|
|
29
16
|
export declare function getQueryType(query: string): string;
|
|
30
|
-
/**
|
|
31
|
-
* Analyze query for potential issues and provide warnings
|
|
32
|
-
*/
|
|
33
17
|
export declare function analyzeQuery(query: string): QueryWarning[];
|
|
34
|
-
/**
|
|
35
|
-
* Apply default LIMIT to SELECT queries if not present
|
|
36
|
-
*/
|
|
37
18
|
export declare function applyDefaultLimit(query: string, defaultLimit: number): string;
|
|
38
19
|
export {};
|
|
@@ -1,10 +1,3 @@
|
|
|
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
1
|
export function checkBlacklist(query, blacklistedOperations) {
|
|
9
2
|
const normalizedQuery = query.trim().toUpperCase();
|
|
10
3
|
for (const operation of blacklistedOperations) {
|
|
@@ -18,9 +11,6 @@ export function checkBlacklist(query, blacklistedOperations) {
|
|
|
18
11
|
}
|
|
19
12
|
return { allowed: true };
|
|
20
13
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Check if query requires user confirmation
|
|
23
|
-
*/
|
|
24
14
|
export function requiresConfirmation(query, confirmationOperations) {
|
|
25
15
|
const normalizedQuery = query.trim().toUpperCase();
|
|
26
16
|
for (const operation of confirmationOperations) {
|
|
@@ -34,21 +24,27 @@ export function requiresConfirmation(query, confirmationOperations) {
|
|
|
34
24
|
}
|
|
35
25
|
return { required: false };
|
|
36
26
|
}
|
|
37
|
-
/**
|
|
38
|
-
* Get query type (SELECT, INSERT, UPDATE, etc.)
|
|
39
|
-
*/
|
|
40
27
|
export function getQueryType(query) {
|
|
41
28
|
const normalizedQuery = query.trim().toUpperCase();
|
|
42
29
|
const firstWord = normalizedQuery.split(/\s+/)[0];
|
|
43
|
-
const knownTypes = [
|
|
30
|
+
const knownTypes = [
|
|
31
|
+
'SELECT',
|
|
32
|
+
'INSERT',
|
|
33
|
+
'UPDATE',
|
|
34
|
+
'DELETE',
|
|
35
|
+
'DROP',
|
|
36
|
+
'CREATE',
|
|
37
|
+
'ALTER',
|
|
38
|
+
'TRUNCATE',
|
|
39
|
+
'SHOW',
|
|
40
|
+
'DESCRIBE',
|
|
41
|
+
'EXPLAIN',
|
|
42
|
+
];
|
|
44
43
|
if (knownTypes.includes(firstWord)) {
|
|
45
44
|
return firstWord;
|
|
46
45
|
}
|
|
47
46
|
return 'UNKNOWN';
|
|
48
47
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Analyze query for potential issues and provide warnings
|
|
51
|
-
*/
|
|
52
48
|
export function analyzeQuery(query) {
|
|
53
49
|
const warnings = [];
|
|
54
50
|
const normalizedQuery = query.trim().toUpperCase();
|
|
@@ -79,9 +75,6 @@ export function analyzeQuery(query) {
|
|
|
79
75
|
}
|
|
80
76
|
return warnings;
|
|
81
77
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Apply default LIMIT to SELECT queries if not present
|
|
84
|
-
*/
|
|
85
78
|
export function applyDefaultLimit(query, defaultLimit) {
|
|
86
79
|
const normalizedQuery = query.trim().toUpperCase();
|
|
87
80
|
if (normalizedQuery.startsWith('SELECT') && !normalizedQuery.includes('LIMIT')) {
|