@hesed/mysql 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 (37) hide show
  1. package/README.md +107 -32
  2. package/dist/commands/mysql/auth/add.d.ts +2 -18
  3. package/dist/commands/mysql/auth/add.js +16 -57
  4. package/dist/commands/mysql/auth/delete.d.ts +2 -0
  5. package/dist/commands/mysql/auth/delete.js +2 -0
  6. package/dist/commands/mysql/auth/list.d.ts +2 -0
  7. package/dist/commands/mysql/auth/list.js +2 -0
  8. package/dist/commands/mysql/auth/profile.d.ts +2 -0
  9. package/dist/commands/mysql/auth/profile.js +2 -0
  10. package/dist/commands/mysql/auth/test.d.ts +2 -12
  11. package/dist/commands/mysql/auth/test.js +16 -41
  12. package/dist/commands/mysql/auth/update.d.ts +2 -18
  13. package/dist/commands/mysql/auth/update.js +16 -74
  14. package/dist/commands/mysql/databases.js +2 -10
  15. package/dist/commands/mysql/describe-table.js +2 -10
  16. package/dist/commands/mysql/explain-query.js +2 -10
  17. package/dist/commands/mysql/indexes.js +2 -10
  18. package/dist/commands/mysql/query.js +2 -10
  19. package/dist/commands/mysql/tables.js +2 -10
  20. package/dist/mysql/config-loader.d.ts +8 -17
  21. package/dist/mysql/config-loader.js +0 -7
  22. package/dist/mysql/database.d.ts +0 -31
  23. package/dist/mysql/database.js +0 -4
  24. package/dist/mysql/formatters.d.ts +5 -0
  25. package/dist/mysql/formatters.js +76 -0
  26. package/dist/mysql/index.d.ts +1 -3
  27. package/dist/mysql/index.js +1 -1
  28. package/dist/mysql/mysql-client.d.ts +8 -57
  29. package/dist/mysql/mysql-client.js +44 -101
  30. package/dist/mysql/mysql-utils.d.ts +2 -52
  31. package/dist/mysql/mysql-utils.js +31 -177
  32. package/dist/mysql/query-validator.d.ts +0 -19
  33. package/dist/mysql/query-validator.js +0 -19
  34. package/oclif.manifest.json +176 -59
  35. package/package.json +3 -2
  36. package/dist/config.d.ts +0 -13
  37. package/dist/config.js +0 -18
@@ -1,44 +1,25 @@
1
- import { encode } from '@toon-format/toon';
2
1
  import mysql from 'mysql2/promise';
3
2
  import { getMySQLConnectionOptions } from './config-loader.js';
3
+ import { FORMATTERS } from './formatters.js';
4
4
  import { analyzeQuery, applyDefaultLimit, checkBlacklist, getQueryType, requiresConfirmation } from './query-validator.js';
5
- /**
6
- * MySQL Database Utility
7
- * Provides core database operations with safety validation and formatting
8
- */
9
5
  export class MySQLUtil {
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((conn) => conn.end()));
21
- this.connectionPool.clear();
13
+ const entries = [...this.connections.values()];
14
+ this.connections.clear();
15
+ await Promise.allSettled(entries.map(async (connPromise) => (await connPromise).end()));
22
16
  }
23
- /**
24
- * Describe table structure
25
- */
26
17
  async describeTable(profileName, table, format = 'table') {
27
18
  try {
28
19
  const connection = await this.getConnection(profileName);
29
20
  const [rows, fields] = await connection.query(`DESCRIBE ${table}`);
30
- let result = '';
31
- if (format === 'json') {
32
- result += this.formatAsJson(rows);
33
- }
34
- else if (format === 'toon') {
35
- result += this.formatAsToon(rows);
36
- }
37
- else {
38
- result += this.formatAsTable(rows, fields);
39
- }
40
21
  return {
41
- result,
22
+ result: this.formatRows(rows, fields, format),
42
23
  structure: rows,
43
24
  success: true,
44
25
  };
@@ -51,9 +32,6 @@ export class MySQLUtil {
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) {
@@ -118,26 +96,13 @@ export class MySQLUtil {
118
96
  };
119
97
  }
120
98
  }
121
- /**
122
- * Explain query execution plan
123
- */
124
99
  async explainQuery(profileName, query, format = 'table') {
125
100
  try {
126
101
  const connection = await this.getConnection(profileName);
127
102
  const [rows, fields] = await connection.query(`EXPLAIN ${query}`);
128
- let result = '';
129
- if (format === 'json') {
130
- result += this.formatAsJson(rows);
131
- }
132
- else if (format === 'toon') {
133
- result += this.formatAsToon(rows);
134
- }
135
- else {
136
- result += this.formatAsTable(rows, fields);
137
- }
138
103
  return {
139
104
  plan: rows,
140
- result,
105
+ result: this.formatRows(rows, fields, format),
141
106
  success: true,
142
107
  };
143
108
  }
@@ -149,90 +114,6 @@ export class MySQLUtil {
149
114
  };
150
115
  }
151
116
  }
152
- /**
153
- * Format query results as CSV
154
- */
155
- formatAsCsv(rows, fields) {
156
- if (!rows || rows.length === 0) {
157
- return '';
158
- }
159
- const columnNames = fields.map((f) => f.name);
160
- let csv = columnNames.join(',') + '\n';
161
- for (const row of rows) {
162
- const values = columnNames.map((name) => {
163
- const value = row[name] ?? '';
164
- const str = String(value);
165
- if (str.includes(',') || str.includes('"') || str.includes('\n')) {
166
- return '"' + str.replaceAll('"', '""') + '"';
167
- }
168
- return str;
169
- });
170
- csv += values.join(',') + '\n';
171
- }
172
- return csv;
173
- }
174
- /**
175
- * Format query results as JSON
176
- */
177
- formatAsJson(rows) {
178
- return JSON.stringify(rows, null, 2);
179
- }
180
- /**
181
- * Format query results as table
182
- */
183
- formatAsTable(rows, fields) {
184
- if (!rows || rows.length === 0) {
185
- return 'No results';
186
- }
187
- const columnNames = fields.map((f) => f.name);
188
- const columnWidths = columnNames.map((name) => {
189
- const dataWidth = Math.max(...rows.map((row) => String(row[name] ?? '').length));
190
- return Math.max(name.length, dataWidth, 3);
191
- });
192
- let table = '┌' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┬') + '┐\n';
193
- table += '│ ' + columnNames.map((name, i) => name.padEnd(columnWidths[i])).join(' │ ') + ' │\n';
194
- table += '├' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┼') + '┤\n';
195
- for (const row of rows) {
196
- table +=
197
- '│ ' +
198
- columnNames
199
- .map((name, i) => {
200
- const value = row[name] ?? 'NULL';
201
- return String(value).padEnd(columnWidths[i]);
202
- })
203
- .join(' │ ') +
204
- ' │\n';
205
- }
206
- table += '└' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┴') + '┘';
207
- return table;
208
- }
209
- /**
210
- * Format query results as TOON
211
- */
212
- formatAsToon(rows) {
213
- if (!rows || rows.length === 0) {
214
- return '';
215
- }
216
- const serializedRows = rows.map((row) => {
217
- const serialized = {};
218
- for (const [key, value] of Object.entries(row)) {
219
- if (value instanceof Date) {
220
- serialized[key] = Number.isNaN(value.getTime()) ? null : value.toISOString();
221
- }
222
- else if (Buffer.isBuffer(value)) {
223
- serialized[key] = value.toString('base64');
224
- }
225
- else {
226
- serialized[key] = value;
227
- }
228
- }
229
- return serialized;
230
- });
231
- return encode(serializedRows);
232
- }
233
- /**
234
- * List all databases
235
- */
236
117
  async listDatabases(profileName) {
237
118
  try {
238
119
  const connection = await this.getConnection(profileName);
@@ -252,9 +133,6 @@ export class MySQLUtil {
252
133
  };
253
134
  }
254
135
  }
255
- /**
256
- * List all tables in current database
257
- */
258
136
  async listTables(profileName) {
259
137
  try {
260
138
  const connection = await this.getConnection(profileName);
@@ -276,26 +154,13 @@ export class MySQLUtil {
276
154
  };
277
155
  }
278
156
  }
279
- /**
280
- * Show table indexes
281
- */
282
157
  async showIndexes(profileName, table, format = 'table') {
283
158
  try {
284
159
  const connection = await this.getConnection(profileName);
285
160
  const [rows, fields] = await connection.query(`SHOW INDEXES FROM ${table}`);
286
- let result = '';
287
- if (format === 'json') {
288
- result += this.formatAsJson(rows);
289
- }
290
- else if (format === 'toon') {
291
- result += this.formatAsToon(rows);
292
- }
293
- else {
294
- result += this.formatAsTable(rows, fields);
295
- }
296
161
  return {
297
162
  indexes: rows,
298
- result,
163
+ result: this.formatRows(rows, fields, format),
299
164
  success: true,
300
165
  };
301
166
  }
@@ -307,9 +172,6 @@ export class MySQLUtil {
307
172
  };
308
173
  }
309
174
  }
310
- /**
311
- * Test database connection
312
- */
313
175
  async testConnection(profileName) {
314
176
  try {
315
177
  const connection = await this.getConnection(profileName);
@@ -330,41 +192,33 @@ export class MySQLUtil {
330
192
  };
331
193
  }
332
194
  }
333
- /**
334
- * Format rows for SELECT/SHOW/DESCRIBE/EXPLAIN query result
335
- */
195
+ formatRows(rows, fields, format) {
196
+ return FORMATTERS[format](rows, fields);
197
+ }
336
198
  formatSelectResult(rows, fields, format) {
337
199
  const rowCount = Array.isArray(rows) ? rows.length : 0;
338
- let result = `Query executed successfully. Rows returned: ${rowCount}\n\n`;
339
- switch (format) {
340
- case 'csv': {
341
- result += this.formatAsCsv(rows, fields);
342
- break;
343
- }
344
- case 'json': {
345
- result += this.formatAsJson(rows);
346
- break;
347
- }
348
- case 'toon': {
349
- result += this.formatAsToon(rows);
350
- break;
200
+ return `Query executed successfully. Rows returned: ${rowCount}\n\n` + this.formatRows(rows, fields, format);
201
+ }
202
+ async getConnection(profileName) {
203
+ const existing = this.connections.get(profileName);
204
+ if (existing) {
205
+ try {
206
+ const conn = await existing;
207
+ await conn.ping();
208
+ return conn;
351
209
  }
352
- default: {
353
- result += this.formatAsTable(rows, fields);
210
+ catch {
211
+ this.connections.delete(profileName);
354
212
  }
355
213
  }
356
- return result;
357
- }
358
- /**
359
- * Get or create MySQL connection for a profile
360
- */
361
- async getConnection(profileName) {
362
- if (this.connectionPool.has(profileName)) {
363
- return this.connectionPool.get(profileName);
214
+ const connPromise = mysql.createConnection(getMySQLConnectionOptions(this.config, profileName));
215
+ this.connections.set(profileName, connPromise);
216
+ try {
217
+ return await connPromise;
218
+ }
219
+ catch (error) {
220
+ this.connections.delete(profileName);
221
+ throw error;
364
222
  }
365
- const options = getMySQLConnectionOptions(this.config, profileName);
366
- const connection = await mysql.createConnection(options);
367
- this.connectionPool.set(profileName, connection);
368
- return connection;
369
223
  }
370
224
  }
@@ -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,9 +24,6 @@ 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];
@@ -58,9 +45,6 @@ export function getQueryType(query) {
58
45
  }
59
46
  return 'UNKNOWN';
60
47
  }
61
- /**
62
- * Analyze query for potential issues and provide warnings
63
- */
64
48
  export function analyzeQuery(query) {
65
49
  const warnings = [];
66
50
  const normalizedQuery = query.trim().toUpperCase();
@@ -91,9 +75,6 @@ export function analyzeQuery(query) {
91
75
  }
92
76
  return warnings;
93
77
  }
94
- /**
95
- * Apply default LIMIT to SELECT queries if not present
96
- */
97
78
  export function applyDefaultLimit(query, defaultLimit) {
98
79
  const normalizedQuery = query.trim().toUpperCase();
99
80
  if (normalizedQuery.startsWith('SELECT') && !normalizedQuery.includes('LIMIT')) {