@hesed/mysql 0.1.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 (42) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +289 -0
  3. package/bin/dev.cmd +3 -0
  4. package/bin/dev.js +5 -0
  5. package/bin/run.cmd +3 -0
  6. package/bin/run.js +5 -0
  7. package/dist/commands/mysql/auth/add.d.ts +18 -0
  8. package/dist/commands/mysql/auth/add.js +57 -0
  9. package/dist/commands/mysql/auth/test.d.ts +12 -0
  10. package/dist/commands/mysql/auth/test.js +41 -0
  11. package/dist/commands/mysql/auth/update.d.ts +18 -0
  12. package/dist/commands/mysql/auth/update.js +74 -0
  13. package/dist/commands/mysql/describe-table.d.ts +11 -0
  14. package/dist/commands/mysql/describe-table.js +37 -0
  15. package/dist/commands/mysql/explain-query.d.ts +13 -0
  16. package/dist/commands/mysql/explain-query.js +39 -0
  17. package/dist/commands/mysql/list-databases.d.ts +9 -0
  18. package/dist/commands/mysql/list-databases.js +31 -0
  19. package/dist/commands/mysql/list-tables.d.ts +9 -0
  20. package/dist/commands/mysql/list-tables.js +31 -0
  21. package/dist/commands/mysql/query.d.ts +14 -0
  22. package/dist/commands/mysql/query.js +47 -0
  23. package/dist/commands/mysql/show-indexes.d.ts +11 -0
  24. package/dist/commands/mysql/show-indexes.js +37 -0
  25. package/dist/config.d.ts +13 -0
  26. package/dist/config.js +18 -0
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +1 -0
  29. package/dist/mysql/config-loader.d.ts +32 -0
  30. package/dist/mysql/config-loader.js +27 -0
  31. package/dist/mysql/database.d.ts +85 -0
  32. package/dist/mysql/database.js +5 -0
  33. package/dist/mysql/index.d.ts +3 -0
  34. package/dist/mysql/index.js +1 -0
  35. package/dist/mysql/mysql-client.d.ts +60 -0
  36. package/dist/mysql/mysql-client.js +131 -0
  37. package/dist/mysql/mysql-utils.d.ts +68 -0
  38. package/dist/mysql/mysql-utils.js +370 -0
  39. package/dist/mysql/query-validator.d.ts +38 -0
  40. package/dist/mysql/query-validator.js +103 -0
  41. package/oclif.manifest.json +527 -0
  42. package/package.json +102 -0
@@ -0,0 +1,131 @@
1
+ import { readConfig } from '../config.js';
2
+ import { MySQLUtil } from './mysql-utils.js';
3
+ let mysqlUtil = null;
4
+ let cachedConfig = null;
5
+ let cachedConfigDir;
6
+ /**
7
+ * Set the config directory for the singleton client
8
+ * @param dir - Oclif config directory path
9
+ */
10
+ export function setConfigDir(dir) {
11
+ cachedConfigDir = dir;
12
+ }
13
+ /**
14
+ * Initialize (or return cached) MySQLUtil
15
+ */
16
+ async function initMySQL() {
17
+ if (mysqlUtil)
18
+ return mysqlUtil;
19
+ if (!cachedConfigDir) {
20
+ throw new Error('MySQL client not initialized. Call setConfigDir() before running commands.');
21
+ }
22
+ const jsonConfig = await readConfig(cachedConfigDir, console.error);
23
+ if (!jsonConfig) {
24
+ throw new Error('Missing connection config. Run "mq mysql auth add" to create a config.');
25
+ }
26
+ cachedConfig = {
27
+ defaultFormat: 'table',
28
+ defaultProfile: jsonConfig.defaultProfile,
29
+ profiles: jsonConfig.profiles,
30
+ safety: {
31
+ blacklistedOperations: ['DROP DATABASE'],
32
+ defaultLimit: 100,
33
+ requireConfirmationFor: ['DELETE', 'UPDATE', 'DROP', 'TRUNCATE', 'ALTER'],
34
+ },
35
+ };
36
+ mysqlUtil = new MySQLUtil(cachedConfig);
37
+ return mysqlUtil;
38
+ }
39
+ /**
40
+ * Get the loaded MySQL config, initializing if needed
41
+ */
42
+ export async function getMySQLConfig() {
43
+ if (!cachedConfig) {
44
+ await initMySQL();
45
+ }
46
+ return cachedConfig;
47
+ }
48
+ /**
49
+ * Execute SQL query
50
+ * @param query - SQL query to execute
51
+ * @param profile - Database profile name
52
+ * @param format - Output format
53
+ * @param skipConfirmation - Skip confirmation for destructive operations
54
+ */
55
+ export async function executeQuery(query, profile, format = 'table', skipConfirmation = false) {
56
+ return (await initMySQL()).executeQuery(profile, query, format, skipConfirmation);
57
+ }
58
+ /**
59
+ * List all databases
60
+ * @param profile - Database profile name
61
+ */
62
+ export async function listDatabases(profile) {
63
+ return (await initMySQL()).listDatabases(profile);
64
+ }
65
+ /**
66
+ * List all tables in current database
67
+ * @param profile - Database profile name
68
+ */
69
+ export async function listTables(profile) {
70
+ return (await initMySQL()).listTables(profile);
71
+ }
72
+ /**
73
+ * Describe table structure
74
+ * @param profile - Database profile name
75
+ * @param table - Table name
76
+ * @param format - Output format
77
+ */
78
+ export async function describeTable(profile, table, format = 'table') {
79
+ return (await initMySQL()).describeTable(profile, table, format);
80
+ }
81
+ /**
82
+ * Show table indexes
83
+ * @param profile - Database profile name
84
+ * @param table - Table name
85
+ * @param format - Output format
86
+ */
87
+ export async function showIndexes(profile, table, format = 'table') {
88
+ return (await initMySQL()).showIndexes(profile, table, format);
89
+ }
90
+ /**
91
+ * Explain query execution plan
92
+ * @param profile - Database profile name
93
+ * @param query - SQL query to explain
94
+ * @param format - Output format
95
+ */
96
+ export async function explainQuery(profile, query, format = 'table') {
97
+ return (await initMySQL()).explainQuery(profile, query, format);
98
+ }
99
+ /**
100
+ * Test a connection directly with profile options (without loading JSON config)
101
+ * @param profile - Database connection profile options
102
+ */
103
+ export async function testDirectConnection(profile) {
104
+ const tempConfig = {
105
+ defaultFormat: 'table',
106
+ defaultProfile: '_auth',
107
+ profiles: { _auth: profile },
108
+ safety: {
109
+ blacklistedOperations: [],
110
+ defaultLimit: 100,
111
+ requireConfirmationFor: [],
112
+ },
113
+ };
114
+ const tempUtil = new MySQLUtil(tempConfig);
115
+ try {
116
+ return await tempUtil.testConnection('_auth');
117
+ }
118
+ finally {
119
+ await tempUtil.closeAll();
120
+ }
121
+ }
122
+ /**
123
+ * Close all connections
124
+ */
125
+ export async function closeConnections() {
126
+ if (mysqlUtil) {
127
+ await mysqlUtil.closeAll();
128
+ mysqlUtil = null;
129
+ cachedConfig = null;
130
+ }
131
+ }
@@ -0,0 +1,68 @@
1
+ import type { FieldPacket, RowDataPacket } from 'mysql2/promise';
2
+ import type { MySQLConfig } from './config-loader.js';
3
+ import type { ConnectionTestResult, DatabaseListResult, DatabaseUtil, ExplainResult, IndexResult, OutputFormat, QueryResult, TableListResult, TableStructureResult } from './database.js';
4
+ /**
5
+ * MySQL Database Utility
6
+ * Provides core database operations with safety validation and formatting
7
+ */
8
+ export declare class MySQLUtil implements DatabaseUtil {
9
+ private config;
10
+ private connectionPool;
11
+ constructor(config: MySQLConfig);
12
+ /**
13
+ * Close all connections
14
+ */
15
+ closeAll(): Promise<void>;
16
+ /**
17
+ * Describe table structure
18
+ */
19
+ describeTable(profileName: string, table: string, format?: 'json' | 'table' | 'toon'): Promise<TableStructureResult>;
20
+ /**
21
+ * Validate and execute a SQL query
22
+ */
23
+ executeQuery(profileName: string, query: string, format?: OutputFormat, skipConfirmation?: boolean): Promise<QueryResult>;
24
+ /**
25
+ * Explain query execution plan
26
+ */
27
+ explainQuery(profileName: string, query: string, format?: 'json' | 'table' | 'toon'): Promise<ExplainResult>;
28
+ /**
29
+ * Format query results as CSV
30
+ */
31
+ formatAsCsv(rows: RowDataPacket[], fields: FieldPacket[]): string;
32
+ /**
33
+ * Format query results as JSON
34
+ */
35
+ formatAsJson(rows: RowDataPacket[]): string;
36
+ /**
37
+ * Format query results as table
38
+ */
39
+ formatAsTable(rows: RowDataPacket[], fields: FieldPacket[]): string;
40
+ /**
41
+ * Format query results as TOON
42
+ */
43
+ formatAsToon(rows: RowDataPacket[]): string;
44
+ /**
45
+ * List all databases
46
+ */
47
+ listDatabases(profileName: string): Promise<DatabaseListResult>;
48
+ /**
49
+ * List all tables in current database
50
+ */
51
+ listTables(profileName: string): Promise<TableListResult>;
52
+ /**
53
+ * Show table indexes
54
+ */
55
+ showIndexes(profileName: string, table: string, format?: 'json' | 'table' | 'toon'): Promise<IndexResult>;
56
+ /**
57
+ * Test database connection
58
+ */
59
+ testConnection(profileName: string): Promise<ConnectionTestResult>;
60
+ /**
61
+ * Format rows for SELECT/SHOW/DESCRIBE/EXPLAIN query result
62
+ */
63
+ private formatSelectResult;
64
+ /**
65
+ * Get or create MySQL connection for a profile
66
+ */
67
+ private getConnection;
68
+ }
@@ -0,0 +1,370 @@
1
+ import { encode } from '@toon-format/toon';
2
+ import mysql from 'mysql2/promise';
3
+ import { getMySQLConnectionOptions } from './config-loader.js';
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
+ export class MySQLUtil {
10
+ config;
11
+ connectionPool;
12
+ constructor(config) {
13
+ this.config = config;
14
+ this.connectionPool = new Map();
15
+ }
16
+ /**
17
+ * Close all connections
18
+ */
19
+ async closeAll() {
20
+ await Promise.all([...this.connectionPool.values()].map((conn) => conn.end()));
21
+ this.connectionPool.clear();
22
+ }
23
+ /**
24
+ * Describe table structure
25
+ */
26
+ async describeTable(profileName, table, format = 'table') {
27
+ try {
28
+ const connection = await this.getConnection(profileName);
29
+ 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
+ return {
41
+ result,
42
+ structure: rows,
43
+ success: true,
44
+ };
45
+ }
46
+ catch (error) {
47
+ const errorMessage = error instanceof Error ? error.message : String(error);
48
+ return {
49
+ error: `ERROR: ${errorMessage}`,
50
+ success: false,
51
+ };
52
+ }
53
+ }
54
+ /**
55
+ * Validate and execute a SQL query
56
+ */
57
+ async executeQuery(profileName, query, format = 'table', skipConfirmation = false) {
58
+ const blacklistCheck = checkBlacklist(query, this.config.safety.blacklistedOperations);
59
+ if (!blacklistCheck.allowed) {
60
+ return {
61
+ error: `${blacklistCheck.reason}\n\nThis operation is blocked by safety rules and cannot be executed.`,
62
+ success: false,
63
+ };
64
+ }
65
+ if (!skipConfirmation) {
66
+ const confirmationCheck = requiresConfirmation(query, this.config.safety.requireConfirmationFor);
67
+ if (confirmationCheck.required) {
68
+ return {
69
+ message: `${confirmationCheck.message}\nQuery: ${query}`,
70
+ requiresConfirmation: true,
71
+ success: false,
72
+ };
73
+ }
74
+ }
75
+ const warnings = analyzeQuery(query);
76
+ let warningText = '';
77
+ if (warnings.length > 0) {
78
+ warningText =
79
+ 'Query Analysis:\n' +
80
+ warnings.map((w) => ` [${w.level.toUpperCase()}] ${w.message}\n → ${w.suggestion}`).join('\n') +
81
+ '\n\n';
82
+ }
83
+ let finalQuery = query;
84
+ const queryType = getQueryType(query);
85
+ if (queryType === 'SELECT') {
86
+ finalQuery = applyDefaultLimit(query, this.config.safety.defaultLimit);
87
+ if (finalQuery !== query) {
88
+ warningText += `Applied default LIMIT ${this.config.safety.defaultLimit}\n\n`;
89
+ }
90
+ }
91
+ try {
92
+ const connection = await this.getConnection(profileName);
93
+ const [rows, fields] = await connection.query(finalQuery);
94
+ let result = '';
95
+ if (queryType === 'SELECT' || queryType === 'SHOW' || queryType === 'DESCRIBE' || queryType === 'EXPLAIN') {
96
+ result += this.formatSelectResult(rows, fields, format);
97
+ }
98
+ else {
99
+ const okPacket = rows;
100
+ const affectedRows = okPacket.affectedRows ?? 0;
101
+ const insertId = okPacket.insertId ?? null;
102
+ result += `Query executed successfully.\n`;
103
+ result += `Affected rows: ${affectedRows}\n`;
104
+ if (insertId) {
105
+ result += `Insert ID: ${insertId}\n`;
106
+ }
107
+ }
108
+ return {
109
+ result: warningText + result,
110
+ success: true,
111
+ };
112
+ }
113
+ catch (error) {
114
+ const errorMessage = error instanceof Error ? error.message : String(error);
115
+ return {
116
+ error: `ERROR: ${errorMessage}`,
117
+ success: false,
118
+ };
119
+ }
120
+ }
121
+ /**
122
+ * Explain query execution plan
123
+ */
124
+ async explainQuery(profileName, query, format = 'table') {
125
+ try {
126
+ const connection = await this.getConnection(profileName);
127
+ 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
+ return {
139
+ plan: rows,
140
+ result,
141
+ success: true,
142
+ };
143
+ }
144
+ catch (error) {
145
+ const errorMessage = error instanceof Error ? error.message : String(error);
146
+ return {
147
+ error: `ERROR: ${errorMessage}`,
148
+ success: false,
149
+ };
150
+ }
151
+ }
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
+ async listDatabases(profileName) {
237
+ try {
238
+ const connection = await this.getConnection(profileName);
239
+ const [rows] = await connection.query('SHOW DATABASES');
240
+ const databases = rows.map((row) => row.Database);
241
+ return {
242
+ databases,
243
+ result: `Databases:\n${databases.map((db) => ` • ${db}`).join('\n')}`,
244
+ success: true,
245
+ };
246
+ }
247
+ catch (error) {
248
+ const errorMessage = error instanceof Error ? error.message : String(error);
249
+ return {
250
+ error: `ERROR: ${errorMessage}`,
251
+ success: false,
252
+ };
253
+ }
254
+ }
255
+ /**
256
+ * List all tables in current database
257
+ */
258
+ async listTables(profileName) {
259
+ try {
260
+ const connection = await this.getConnection(profileName);
261
+ const [rows] = await connection.query('SHOW TABLES');
262
+ const rowsArray = rows;
263
+ const tableKey = Object.keys(rowsArray[0])[0];
264
+ const tables = rowsArray.map((row) => row[tableKey]);
265
+ return {
266
+ result: `Tables in database:\n${tables.map((table) => ` • ${table}`).join('\n')}`,
267
+ success: true,
268
+ tables,
269
+ };
270
+ }
271
+ catch (error) {
272
+ const errorMessage = error instanceof Error ? error.message : String(error);
273
+ return {
274
+ error: `ERROR: ${errorMessage}`,
275
+ success: false,
276
+ };
277
+ }
278
+ }
279
+ /**
280
+ * Show table indexes
281
+ */
282
+ async showIndexes(profileName, table, format = 'table') {
283
+ try {
284
+ const connection = await this.getConnection(profileName);
285
+ 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
+ return {
297
+ indexes: rows,
298
+ result,
299
+ success: true,
300
+ };
301
+ }
302
+ catch (error) {
303
+ const errorMessage = error instanceof Error ? error.message : String(error);
304
+ return {
305
+ error: `ERROR: ${errorMessage}`,
306
+ success: false,
307
+ };
308
+ }
309
+ }
310
+ /**
311
+ * Test database connection
312
+ */
313
+ async testConnection(profileName) {
314
+ try {
315
+ const connection = await this.getConnection(profileName);
316
+ const [rows] = await connection.query('SELECT VERSION() as version, DATABASE() as current_database');
317
+ const info = rows[0];
318
+ return {
319
+ database: info.current_database,
320
+ result: `Connection successful!\n\nProfile: ${profileName}\nMySQL Version: ${info.version}\nCurrent Database: ${info.current_database}`,
321
+ success: true,
322
+ version: info.version,
323
+ };
324
+ }
325
+ catch (error) {
326
+ const errorMessage = error instanceof Error ? error.message : String(error);
327
+ return {
328
+ error: `ERROR: ${errorMessage}`,
329
+ success: false,
330
+ };
331
+ }
332
+ }
333
+ /**
334
+ * Format rows for SELECT/SHOW/DESCRIBE/EXPLAIN query result
335
+ */
336
+ formatSelectResult(rows, fields, format) {
337
+ 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;
351
+ }
352
+ default: {
353
+ result += this.formatAsTable(rows, fields);
354
+ }
355
+ }
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);
364
+ }
365
+ const options = getMySQLConnectionOptions(this.config, profileName);
366
+ const connection = await mysql.createConnection(options);
367
+ this.connectionPool.set(profileName, connection);
368
+ return connection;
369
+ }
370
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Query Validation and Safety Module
3
+ * Provides SQL query analysis and safety checks
4
+ */
5
+ interface BlacklistCheckResult {
6
+ allowed: boolean;
7
+ reason?: string;
8
+ }
9
+ interface ConfirmationCheckResult {
10
+ message?: string;
11
+ required: boolean;
12
+ }
13
+ interface QueryWarning {
14
+ level: 'info' | 'warning';
15
+ message: string;
16
+ suggestion: string;
17
+ }
18
+ /**
19
+ * Check if query contains blacklisted operations
20
+ */
21
+ export declare function checkBlacklist(query: string, blacklistedOperations: string[]): BlacklistCheckResult;
22
+ /**
23
+ * Check if query requires user confirmation
24
+ */
25
+ export declare function requiresConfirmation(query: string, confirmationOperations: string[]): ConfirmationCheckResult;
26
+ /**
27
+ * Get query type (SELECT, INSERT, UPDATE, etc.)
28
+ */
29
+ export declare function getQueryType(query: string): string;
30
+ /**
31
+ * Analyze query for potential issues and provide warnings
32
+ */
33
+ export declare function analyzeQuery(query: string): QueryWarning[];
34
+ /**
35
+ * Apply default LIMIT to SELECT queries if not present
36
+ */
37
+ export declare function applyDefaultLimit(query: string, defaultLimit: number): string;
38
+ export {};