@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.
- package/README.md +107 -32
- package/dist/commands/mysql/auth/add.d.ts +2 -18
- package/dist/commands/mysql/auth/add.js +16 -57
- package/dist/commands/mysql/auth/delete.d.ts +2 -0
- package/dist/commands/mysql/auth/delete.js +2 -0
- package/dist/commands/mysql/auth/list.d.ts +2 -0
- package/dist/commands/mysql/auth/list.js +2 -0
- package/dist/commands/mysql/auth/profile.d.ts +2 -0
- package/dist/commands/mysql/auth/profile.js +2 -0
- package/dist/commands/mysql/auth/test.d.ts +2 -12
- package/dist/commands/mysql/auth/test.js +16 -41
- package/dist/commands/mysql/auth/update.d.ts +2 -18
- package/dist/commands/mysql/auth/update.js +16 -74
- package/dist/commands/mysql/databases.js +2 -10
- package/dist/commands/mysql/describe-table.js +2 -10
- package/dist/commands/mysql/explain-query.js +2 -10
- package/dist/commands/mysql/indexes.js +2 -10
- package/dist/commands/mysql/query.js +2 -10
- package/dist/commands/mysql/tables.js +2 -10
- package/dist/mysql/config-loader.d.ts +8 -17
- package/dist/mysql/config-loader.js +0 -7
- package/dist/mysql/database.d.ts +0 -31
- package/dist/mysql/database.js +0 -4
- package/dist/mysql/formatters.d.ts +5 -0
- package/dist/mysql/formatters.js +76 -0
- package/dist/mysql/index.d.ts +1 -3
- package/dist/mysql/index.js +1 -1
- package/dist/mysql/mysql-client.d.ts +8 -57
- package/dist/mysql/mysql-client.js +44 -101
- package/dist/mysql/mysql-utils.d.ts +2 -52
- package/dist/mysql/mysql-utils.js +31 -177
- package/dist/mysql/query-validator.d.ts +0 -19
- package/dist/mysql/query-validator.js +0 -19
- 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,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
|
-
|
|
7
|
+
connections;
|
|
12
8
|
constructor(config) {
|
|
13
9
|
this.config = config;
|
|
14
|
-
this.
|
|
10
|
+
this.connections = new Map();
|
|
15
11
|
}
|
|
16
|
-
/**
|
|
17
|
-
* Close all connections
|
|
18
|
-
*/
|
|
19
12
|
async closeAll() {
|
|
20
|
-
|
|
21
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
353
|
-
|
|
210
|
+
catch {
|
|
211
|
+
this.connections.delete(profileName);
|
|
354
212
|
}
|
|
355
213
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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')) {
|