@hesed/psql 0.2.2 → 0.4.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 +17 -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 +17 -41
- package/dist/commands/psql/auth/update.d.ts +2 -18
- package/dist/commands/psql/auth/update.js +17 -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 +1 -55
- package/dist/psql/postgres-utils.js +8 -168
- 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 +3 -2
- package/dist/config.d.ts +0 -13
- package/dist/config.js +0 -18
|
@@ -1,11 +1,7 @@
|
|
|
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;
|
|
@@ -13,33 +9,17 @@ export class PostgreSQLUtil {
|
|
|
13
9
|
this.config = config;
|
|
14
10
|
this.connections = new Map();
|
|
15
11
|
}
|
|
16
|
-
/**
|
|
17
|
-
* Close all connections
|
|
18
|
-
*/
|
|
19
12
|
async closeAll() {
|
|
20
13
|
const entries = [...this.connections.values()];
|
|
21
14
|
this.connections.clear();
|
|
22
15
|
await Promise.allSettled(entries.map(async (clientPromise) => (await clientPromise).end()));
|
|
23
16
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Describe table structure
|
|
26
|
-
*/
|
|
27
17
|
async describeTable(profileName, table, format = 'table') {
|
|
28
18
|
try {
|
|
29
19
|
const client = await this.getConnection(profileName);
|
|
30
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`);
|
|
31
|
-
let output = '';
|
|
32
|
-
if (format === 'json') {
|
|
33
|
-
output += this.formatAsJson(result.rows);
|
|
34
|
-
}
|
|
35
|
-
else if (format === 'toon') {
|
|
36
|
-
output += this.formatAsToon(result.rows);
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
output += this.formatAsTable(result.rows, result.fields);
|
|
40
|
-
}
|
|
41
21
|
return {
|
|
42
|
-
result:
|
|
22
|
+
result: this.formatRows(result.rows, result.fields, format),
|
|
43
23
|
structure: result.rows,
|
|
44
24
|
success: true,
|
|
45
25
|
};
|
|
@@ -52,9 +32,6 @@ export class PostgreSQLUtil {
|
|
|
52
32
|
};
|
|
53
33
|
}
|
|
54
34
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Validate and execute a SQL query
|
|
57
|
-
*/
|
|
58
35
|
async executeQuery(profileName, query, format = 'table', skipConfirmation = false) {
|
|
59
36
|
const blacklistCheck = checkBlacklist(query, this.config.safety.blacklistedOperations);
|
|
60
37
|
if (!blacklistCheck.allowed) {
|
|
@@ -114,26 +91,13 @@ export class PostgreSQLUtil {
|
|
|
114
91
|
};
|
|
115
92
|
}
|
|
116
93
|
}
|
|
117
|
-
/**
|
|
118
|
-
* Explain query execution plan
|
|
119
|
-
*/
|
|
120
94
|
async explainQuery(profileName, query, format = 'table') {
|
|
121
95
|
try {
|
|
122
96
|
const client = await this.getConnection(profileName);
|
|
123
97
|
const result = await client.query(`EXPLAIN ${query}`);
|
|
124
|
-
let output = '';
|
|
125
|
-
if (format === 'json') {
|
|
126
|
-
output += this.formatAsJson(result.rows);
|
|
127
|
-
}
|
|
128
|
-
else if (format === 'toon') {
|
|
129
|
-
output += this.formatAsToon(result.rows);
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
output += this.formatAsTable(result.rows, result.fields);
|
|
133
|
-
}
|
|
134
98
|
return {
|
|
135
99
|
plan: result.rows,
|
|
136
|
-
result:
|
|
100
|
+
result: this.formatRows(result.rows, result.fields, format),
|
|
137
101
|
success: true,
|
|
138
102
|
};
|
|
139
103
|
}
|
|
@@ -145,90 +109,6 @@ export class PostgreSQLUtil {
|
|
|
145
109
|
};
|
|
146
110
|
}
|
|
147
111
|
}
|
|
148
|
-
/**
|
|
149
|
-
* Format query results as CSV
|
|
150
|
-
*/
|
|
151
|
-
formatAsCsv(rows, fields) {
|
|
152
|
-
if (!rows || rows.length === 0) {
|
|
153
|
-
return '';
|
|
154
|
-
}
|
|
155
|
-
const columnNames = fields.map((f) => f.name);
|
|
156
|
-
let csv = columnNames.join(',') + '\n';
|
|
157
|
-
for (const row of rows) {
|
|
158
|
-
const values = columnNames.map((name) => {
|
|
159
|
-
const value = row[name] ?? '';
|
|
160
|
-
const str = String(value);
|
|
161
|
-
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
162
|
-
return '"' + str.replaceAll('"', '""') + '"';
|
|
163
|
-
}
|
|
164
|
-
return str;
|
|
165
|
-
});
|
|
166
|
-
csv += values.join(',') + '\n';
|
|
167
|
-
}
|
|
168
|
-
return csv;
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Format query results as JSON
|
|
172
|
-
*/
|
|
173
|
-
formatAsJson(rows) {
|
|
174
|
-
return JSON.stringify(rows, null, 2);
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Format query results as table
|
|
178
|
-
*/
|
|
179
|
-
formatAsTable(rows, fields) {
|
|
180
|
-
if (!rows || rows.length === 0) {
|
|
181
|
-
return 'No results';
|
|
182
|
-
}
|
|
183
|
-
const columnNames = fields.map((f) => f.name);
|
|
184
|
-
const columnWidths = columnNames.map((name) => {
|
|
185
|
-
const dataWidth = Math.max(...rows.map((row) => String(row[name] ?? '').length));
|
|
186
|
-
return Math.max(name.length, dataWidth, 3);
|
|
187
|
-
});
|
|
188
|
-
let table = '┌' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┬') + '┐\n';
|
|
189
|
-
table += '│ ' + columnNames.map((name, i) => name.padEnd(columnWidths[i])).join(' │ ') + ' │\n';
|
|
190
|
-
table += '├' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┼') + '┤\n';
|
|
191
|
-
for (const row of rows) {
|
|
192
|
-
table +=
|
|
193
|
-
'│ ' +
|
|
194
|
-
columnNames
|
|
195
|
-
.map((name, i) => {
|
|
196
|
-
const value = row[name] ?? 'NULL';
|
|
197
|
-
return String(value).padEnd(columnWidths[i]);
|
|
198
|
-
})
|
|
199
|
-
.join(' │ ') +
|
|
200
|
-
' │\n';
|
|
201
|
-
}
|
|
202
|
-
table += '└' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┴') + '┘';
|
|
203
|
-
return table;
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Format query results as TOON
|
|
207
|
-
*/
|
|
208
|
-
formatAsToon(rows) {
|
|
209
|
-
if (!rows || rows.length === 0) {
|
|
210
|
-
return '';
|
|
211
|
-
}
|
|
212
|
-
const serializedRows = rows.map((row) => {
|
|
213
|
-
const serialized = {};
|
|
214
|
-
for (const [key, value] of Object.entries(row)) {
|
|
215
|
-
if (value instanceof Date) {
|
|
216
|
-
serialized[key] = Number.isNaN(value.getTime()) ? null : value.toISOString();
|
|
217
|
-
}
|
|
218
|
-
else if (Buffer.isBuffer(value)) {
|
|
219
|
-
serialized[key] = value.toString('base64');
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
serialized[key] = value;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
return serialized;
|
|
226
|
-
});
|
|
227
|
-
return encode(serializedRows);
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* List all databases
|
|
231
|
-
*/
|
|
232
112
|
async listDatabases(profileName) {
|
|
233
113
|
try {
|
|
234
114
|
const client = await this.getConnection(profileName);
|
|
@@ -248,9 +128,6 @@ export class PostgreSQLUtil {
|
|
|
248
128
|
};
|
|
249
129
|
}
|
|
250
130
|
}
|
|
251
|
-
/**
|
|
252
|
-
* List all tables in current database
|
|
253
|
-
*/
|
|
254
131
|
async listTables(profileName) {
|
|
255
132
|
try {
|
|
256
133
|
const client = await this.getConnection(profileName);
|
|
@@ -270,26 +147,13 @@ export class PostgreSQLUtil {
|
|
|
270
147
|
};
|
|
271
148
|
}
|
|
272
149
|
}
|
|
273
|
-
/**
|
|
274
|
-
* Show table indexes
|
|
275
|
-
*/
|
|
276
150
|
async showIndexes(profileName, table, format = 'table') {
|
|
277
151
|
try {
|
|
278
152
|
const client = await this.getConnection(profileName);
|
|
279
153
|
const result = await client.query(`SELECT indexname, indexdef FROM pg_indexes WHERE tablename = '${table}' AND schemaname = 'public'`);
|
|
280
|
-
let output = '';
|
|
281
|
-
if (format === 'json') {
|
|
282
|
-
output += this.formatAsJson(result.rows);
|
|
283
|
-
}
|
|
284
|
-
else if (format === 'toon') {
|
|
285
|
-
output += this.formatAsToon(result.rows);
|
|
286
|
-
}
|
|
287
|
-
else {
|
|
288
|
-
output += this.formatAsTable(result.rows, result.fields);
|
|
289
|
-
}
|
|
290
154
|
return {
|
|
291
155
|
indexes: result.rows,
|
|
292
|
-
result:
|
|
156
|
+
result: this.formatRows(result.rows, result.fields, format),
|
|
293
157
|
success: true,
|
|
294
158
|
};
|
|
295
159
|
}
|
|
@@ -301,9 +165,6 @@ export class PostgreSQLUtil {
|
|
|
301
165
|
};
|
|
302
166
|
}
|
|
303
167
|
}
|
|
304
|
-
/**
|
|
305
|
-
* Test database connection
|
|
306
|
-
*/
|
|
307
168
|
async testConnection(profileName) {
|
|
308
169
|
try {
|
|
309
170
|
const client = await this.getConnection(profileName);
|
|
@@ -324,34 +185,13 @@ export class PostgreSQLUtil {
|
|
|
324
185
|
};
|
|
325
186
|
}
|
|
326
187
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
188
|
+
formatRows(rows, fields, format) {
|
|
189
|
+
return FORMATTERS[format](rows, fields);
|
|
190
|
+
}
|
|
330
191
|
formatSelectResult(rows, fields, format) {
|
|
331
192
|
const rowCount = Array.isArray(rows) ? rows.length : 0;
|
|
332
|
-
|
|
333
|
-
switch (format) {
|
|
334
|
-
case 'csv': {
|
|
335
|
-
result += this.formatAsCsv(rows, fields);
|
|
336
|
-
break;
|
|
337
|
-
}
|
|
338
|
-
case 'json': {
|
|
339
|
-
result += this.formatAsJson(rows);
|
|
340
|
-
break;
|
|
341
|
-
}
|
|
342
|
-
case 'toon': {
|
|
343
|
-
result += this.formatAsToon(rows);
|
|
344
|
-
break;
|
|
345
|
-
}
|
|
346
|
-
default: {
|
|
347
|
-
result += this.formatAsTable(rows, fields);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
return result;
|
|
193
|
+
return `Query executed successfully. Rows returned: ${rowCount}\n\n` + this.formatRows(rows, fields, format);
|
|
351
194
|
}
|
|
352
|
-
/**
|
|
353
|
-
* Get or create PostgreSQL client for a profile
|
|
354
|
-
*/
|
|
355
195
|
async getConnection(profileName) {
|
|
356
196
|
const existing = this.connections.get(profileName);
|
|
357
197
|
if (existing) {
|
|
@@ -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')) {
|