@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.
Files changed (35) hide show
  1. package/README.md +107 -32
  2. package/dist/commands/psql/auth/add.d.ts +2 -18
  3. package/dist/commands/psql/auth/add.js +16 -57
  4. package/dist/commands/psql/auth/delete.d.ts +2 -0
  5. package/dist/commands/psql/auth/delete.js +2 -0
  6. package/dist/commands/psql/auth/list.d.ts +2 -0
  7. package/dist/commands/psql/auth/list.js +2 -0
  8. package/dist/commands/psql/auth/profile.d.ts +2 -0
  9. package/dist/commands/psql/auth/profile.js +2 -0
  10. package/dist/commands/psql/auth/test.d.ts +2 -12
  11. package/dist/commands/psql/auth/test.js +16 -41
  12. package/dist/commands/psql/auth/update.d.ts +2 -18
  13. package/dist/commands/psql/auth/update.js +16 -74
  14. package/dist/commands/psql/databases.js +2 -10
  15. package/dist/commands/psql/describe-table.js +2 -10
  16. package/dist/commands/psql/explain-query.js +2 -10
  17. package/dist/commands/psql/indexes.js +2 -10
  18. package/dist/commands/psql/query.js +2 -10
  19. package/dist/commands/psql/tables.js +2 -10
  20. package/dist/psql/config-loader.d.ts +8 -14
  21. package/dist/psql/config-loader.js +0 -7
  22. package/dist/psql/formatters.d.ts +8 -0
  23. package/dist/psql/formatters.js +76 -0
  24. package/dist/psql/index.d.ts +1 -2
  25. package/dist/psql/index.js +1 -1
  26. package/dist/psql/postgres-client.d.ts +8 -57
  27. package/dist/psql/postgres-client.js +44 -101
  28. package/dist/psql/postgres-utils.d.ts +2 -56
  29. package/dist/psql/postgres-utils.js +36 -179
  30. package/dist/psql/query-validator.d.ts +0 -19
  31. package/dist/psql/query-validator.js +13 -20
  32. package/oclif.manifest.json +176 -59
  33. package/package.json +2 -1
  34. package/dist/config.d.ts +0 -13
  35. 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
- connectionPool;
7
+ connections;
12
8
  constructor(config) {
13
9
  this.config = config;
14
- this.connectionPool = new Map();
10
+ this.connections = new Map();
15
11
  }
16
- /**
17
- * Close all connections
18
- */
19
12
  async closeAll() {
20
- await Promise.all([...this.connectionPool.values()].map((c) => c.end()));
21
- this.connectionPool.clear();
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: output,
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: output,
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: output,
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
- * Format rows for SELECT/EXPLAIN query result
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
- 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;
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
- if (this.connectionPool.has(profileName)) {
356
- return this.connectionPool.get(profileName);
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 = ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'ALTER', 'TRUNCATE', 'EXPLAIN'];
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')) {