@hesed/psql 0.2.2 → 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 +1 -55
  29. package/dist/psql/postgres-utils.js +8 -168
  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,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: output,
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: output,
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: output,
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
- * Format rows for SELECT/EXPLAIN query result
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
- let result = `Query executed successfully. Rows returned: ${rowCount}\n\n`;
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 = ['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')) {