mcp-database-inspector 2.0.1

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 (105) hide show
  1. package/README.md +197 -0
  2. package/dist/database/connection.d.ts +13 -0
  3. package/dist/database/connection.d.ts.map +1 -0
  4. package/dist/database/connection.js +155 -0
  5. package/dist/database/connection.js.map +1 -0
  6. package/dist/database/manager.d.ts +28 -0
  7. package/dist/database/manager.d.ts.map +1 -0
  8. package/dist/database/manager.js +621 -0
  9. package/dist/database/manager.js.map +1 -0
  10. package/dist/database/postgres-connection.d.ts +10 -0
  11. package/dist/database/postgres-connection.d.ts.map +1 -0
  12. package/dist/database/postgres-connection.js +113 -0
  13. package/dist/database/postgres-connection.js.map +1 -0
  14. package/dist/database/types.d.ts +84 -0
  15. package/dist/database/types.d.ts.map +1 -0
  16. package/dist/database/types.js +6 -0
  17. package/dist/database/types.js.map +1 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +120 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/server.d.ts +14 -0
  23. package/dist/server.d.ts.map +1 -0
  24. package/dist/server.js +186 -0
  25. package/dist/server.js.map +1 -0
  26. package/dist/test-defaults.d.ts +2 -0
  27. package/dist/test-defaults.d.ts.map +1 -0
  28. package/dist/test-defaults.js +57 -0
  29. package/dist/test-defaults.js.map +1 -0
  30. package/dist/tools/analyze-query.d.ts +27 -0
  31. package/dist/tools/analyze-query.d.ts.map +1 -0
  32. package/dist/tools/analyze-query.js +71 -0
  33. package/dist/tools/analyze-query.js.map +1 -0
  34. package/dist/tools/execute-query.d.ts +33 -0
  35. package/dist/tools/execute-query.d.ts.map +1 -0
  36. package/dist/tools/execute-query.js +57 -0
  37. package/dist/tools/execute-query.js.map +1 -0
  38. package/dist/tools/get-foreign-keys.d.ts +38 -0
  39. package/dist/tools/get-foreign-keys.d.ts.map +1 -0
  40. package/dist/tools/get-foreign-keys.js +391 -0
  41. package/dist/tools/get-foreign-keys.js.map +1 -0
  42. package/dist/tools/get-indexes.d.ts +38 -0
  43. package/dist/tools/get-indexes.d.ts.map +1 -0
  44. package/dist/tools/get-indexes.js +472 -0
  45. package/dist/tools/get-indexes.js.map +1 -0
  46. package/dist/tools/information-schema-query.d.ts +33 -0
  47. package/dist/tools/information-schema-query.d.ts.map +1 -0
  48. package/dist/tools/information-schema-query.js +76 -0
  49. package/dist/tools/information-schema-query.js.map +1 -0
  50. package/dist/tools/inspect-table.d.ts +38 -0
  51. package/dist/tools/inspect-table.d.ts.map +1 -0
  52. package/dist/tools/inspect-table.js +351 -0
  53. package/dist/tools/inspect-table.js.map +1 -0
  54. package/dist/tools/list-databases.d.ts +14 -0
  55. package/dist/tools/list-databases.d.ts.map +1 -0
  56. package/dist/tools/list-databases.js +83 -0
  57. package/dist/tools/list-databases.js.map +1 -0
  58. package/dist/tools/list-tables.d.ts +19 -0
  59. package/dist/tools/list-tables.d.ts.map +1 -0
  60. package/dist/tools/list-tables.js +130 -0
  61. package/dist/tools/list-tables.js.map +1 -0
  62. package/dist/utils/errors.d.ts +32 -0
  63. package/dist/utils/errors.d.ts.map +1 -0
  64. package/dist/utils/errors.js +98 -0
  65. package/dist/utils/errors.js.map +1 -0
  66. package/dist/utils/logger.d.ts +28 -0
  67. package/dist/utils/logger.d.ts.map +1 -0
  68. package/dist/utils/logger.js +132 -0
  69. package/dist/utils/logger.js.map +1 -0
  70. package/dist/validators/input-validator.d.ts +76 -0
  71. package/dist/validators/input-validator.d.ts.map +1 -0
  72. package/dist/validators/input-validator.js +295 -0
  73. package/dist/validators/input-validator.js.map +1 -0
  74. package/dist/validators/query-validator.d.ts +19 -0
  75. package/dist/validators/query-validator.d.ts.map +1 -0
  76. package/dist/validators/query-validator.js +229 -0
  77. package/dist/validators/query-validator.js.map +1 -0
  78. package/enhanced_sql_prompt.md +324 -0
  79. package/examples/claude-config.json +23 -0
  80. package/examples/roo-config.json +16 -0
  81. package/package.json +42 -0
  82. package/src/database/connection.ts +165 -0
  83. package/src/database/manager.ts +682 -0
  84. package/src/database/postgres-connection.ts +123 -0
  85. package/src/database/types.ts +93 -0
  86. package/src/index.ts +136 -0
  87. package/src/server.ts +254 -0
  88. package/src/test-defaults.ts +63 -0
  89. package/src/tools/analyze-query.test.ts +100 -0
  90. package/src/tools/analyze-query.ts +112 -0
  91. package/src/tools/execute-query.ts +91 -0
  92. package/src/tools/get-foreign-keys.test.ts +51 -0
  93. package/src/tools/get-foreign-keys.ts +488 -0
  94. package/src/tools/get-indexes.test.ts +51 -0
  95. package/src/tools/get-indexes.ts +570 -0
  96. package/src/tools/information-schema-query.ts +125 -0
  97. package/src/tools/inspect-table.test.ts +59 -0
  98. package/src/tools/inspect-table.ts +440 -0
  99. package/src/tools/list-databases.ts +119 -0
  100. package/src/tools/list-tables.ts +181 -0
  101. package/src/utils/errors.ts +103 -0
  102. package/src/utils/logger.ts +158 -0
  103. package/src/validators/input-validator.ts +318 -0
  104. package/src/validators/query-validator.ts +267 -0
  105. package/tsconfig.json +30 -0
@@ -0,0 +1,318 @@
1
+ import { z } from 'zod';
2
+ import { ValidationResult } from '../database/types.js';
3
+
4
+ export class InputValidator {
5
+ // Schema for database connection URLs
6
+ static readonly connectionUrlSchema = z.string()
7
+ .url()
8
+ .refine(url => url.startsWith('mysql://') || url.startsWith('postgresql://') || url.startsWith('postgres://'), {
9
+ message: 'URL must start with mysql://, postgresql://, or postgres://'
10
+ })
11
+ .refine(url => {
12
+ try {
13
+ const parsed = new URL(url);
14
+ return parsed.hostname && parsed.username && parsed.password;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }, {
19
+ message: 'URL must contain hostname, username, and password'
20
+ });
21
+
22
+ // Schema for database names
23
+ static readonly databaseNameSchema = z.string()
24
+ .min(1, 'Database name cannot be empty')
25
+ .max(64, 'Database name cannot exceed 64 characters')
26
+ .regex(/^[a-zA-Z_][a-zA-Z0-9_$-]*$/, 'Invalid database name format');
27
+
28
+ // Schema for table names
29
+ static readonly tableNameSchema = z.string()
30
+ .min(1, 'Table name cannot be empty')
31
+ .max(64, 'Table name cannot exceed 64 characters')
32
+ .regex(/^[a-zA-Z_][a-zA-Z0-9_$-]*$|^`[^`]+`$/, 'Invalid table name format');
33
+
34
+ // Schema for column names
35
+ static readonly columnNameSchema = z.string()
36
+ .min(1, 'Column name cannot be empty')
37
+ .max(64, 'Column name cannot exceed 64 characters')
38
+ .regex(/^[a-zA-Z_][a-zA-Z0-9_$-]*$|^`[^`]+`$/, 'Invalid column name format');
39
+
40
+ // Schema for general text input
41
+ static readonly textInputSchema = z.string()
42
+ .max(10000, 'Input too long')
43
+ .refine(text => !text.includes('\0'), {
44
+ message: 'Input cannot contain null bytes'
45
+ });
46
+
47
+ /**
48
+ * Validate a connection URL
49
+ */
50
+ static validateConnectionUrl(url: string): ValidationResult {
51
+ try {
52
+ this.connectionUrlSchema.parse(url);
53
+ return { isValid: true };
54
+ } catch (error) {
55
+ if (error instanceof z.ZodError) {
56
+ return {
57
+ isValid: false,
58
+ error: error.issues.map(e => e.message).join(', ')
59
+ };
60
+ }
61
+ return {
62
+ isValid: false,
63
+ error: 'Invalid connection URL format'
64
+ };
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Validate a database name
70
+ */
71
+ static validateDatabaseName(name: string): ValidationResult {
72
+ try {
73
+ this.databaseNameSchema.parse(name);
74
+ return { isValid: true };
75
+ } catch (error) {
76
+ if (error instanceof z.ZodError) {
77
+ return {
78
+ isValid: false,
79
+ error: error.issues.map(e => e.message).join(', ')
80
+ };
81
+ }
82
+ return {
83
+ isValid: false,
84
+ error: 'Invalid database name'
85
+ };
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Validate a table name
91
+ */
92
+ static validateTableName(name: string): ValidationResult {
93
+ try {
94
+ this.tableNameSchema.parse(name);
95
+ return { isValid: true };
96
+ } catch (error) {
97
+ if (error instanceof z.ZodError) {
98
+ return {
99
+ isValid: false,
100
+ error: error.issues.map(e => e.message).join(', ')
101
+ };
102
+ }
103
+ return {
104
+ isValid: false,
105
+ error: 'Invalid table name'
106
+ };
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Validate a column name
112
+ */
113
+ static validateColumnName(name: string): ValidationResult {
114
+ try {
115
+ this.columnNameSchema.parse(name);
116
+ return { isValid: true };
117
+ } catch (error) {
118
+ if (error instanceof z.ZodError) {
119
+ return {
120
+ isValid: false,
121
+ error: error.issues.map(e => e.message).join(', ')
122
+ };
123
+ }
124
+ return {
125
+ isValid: false,
126
+ error: 'Invalid column name'
127
+ };
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Validate text input
133
+ */
134
+ static validateTextInput(text: string): ValidationResult {
135
+ try {
136
+ this.textInputSchema.parse(text);
137
+ return { isValid: true };
138
+ } catch (error) {
139
+ if (error instanceof z.ZodError) {
140
+ return {
141
+ isValid: false,
142
+ error: error.issues.map(e => e.message).join(', ')
143
+ };
144
+ }
145
+ return {
146
+ isValid: false,
147
+ error: 'Invalid text input'
148
+ };
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Sanitize string input by removing dangerous characters
154
+ */
155
+ static sanitizeString(input: string): string {
156
+ if (!input) return '';
157
+
158
+ return input
159
+ // Remove null bytes
160
+ .replace(/\0/g, '')
161
+ // Remove control characters except tab, newline, carriage return
162
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
163
+ // Trim whitespace
164
+ .trim();
165
+ }
166
+
167
+ /**
168
+ * Escape MySQL identifiers (table names, column names)
169
+ */
170
+ static escapeIdentifier(identifier: string): string {
171
+ if (!identifier) return '';
172
+
173
+ // If already quoted, return as is
174
+ if (identifier.startsWith('`') && identifier.endsWith('`')) {
175
+ return identifier;
176
+ }
177
+
178
+ // Remove any existing backticks and escape them
179
+ const cleaned = identifier.replace(/`/g, '``');
180
+ return `\`${cleaned}\``;
181
+ }
182
+
183
+ /**
184
+ * Validate tool arguments based on schema
185
+ */
186
+ static validateToolArgs<T>(args: unknown, schema: z.ZodSchema<T>): ValidationResult & { data?: T } {
187
+ try {
188
+ const data = schema.parse(args);
189
+ return { isValid: true, data };
190
+ } catch (error) {
191
+ if (error instanceof z.ZodError) {
192
+ return {
193
+ isValid: false,
194
+ error: `Invalid arguments: ${error.issues.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`
195
+ };
196
+ }
197
+ return {
198
+ isValid: false,
199
+ error: 'Invalid arguments format'
200
+ };
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Validate that a string represents a valid number
206
+ */
207
+ static validateNumeric(value: string, options?: { min?: number; max?: number; integer?: boolean }): ValidationResult {
208
+ const num = Number(value);
209
+
210
+ if (isNaN(num)) {
211
+ return { isValid: false, error: 'Value must be a valid number' };
212
+ }
213
+
214
+ if (options?.integer && !Number.isInteger(num)) {
215
+ return { isValid: false, error: 'Value must be an integer' };
216
+ }
217
+
218
+ if (options?.min !== undefined && num < options.min) {
219
+ return { isValid: false, error: `Value must be at least ${options.min}` };
220
+ }
221
+
222
+ if (options?.max !== undefined && num > options.max) {
223
+ return { isValid: false, error: `Value must be at most ${options.max}` };
224
+ }
225
+
226
+ return { isValid: true };
227
+ }
228
+
229
+ /**
230
+ * Validate an array of values
231
+ */
232
+ static validateArray<T>(
233
+ values: unknown[],
234
+ itemValidator: (item: unknown) => ValidationResult & { data?: T }
235
+ ): ValidationResult & { data?: T[] } {
236
+ if (!Array.isArray(values)) {
237
+ return { isValid: false, error: 'Value must be an array' };
238
+ }
239
+
240
+ const validatedItems: T[] = [];
241
+ const errors: string[] = [];
242
+
243
+ for (let i = 0; i < values.length; i++) {
244
+ const result = itemValidator(values[i]);
245
+ if (result.isValid && result.data !== undefined) {
246
+ validatedItems.push(result.data);
247
+ } else {
248
+ errors.push(`Item ${i}: ${result.error || 'Validation failed'}`);
249
+ }
250
+ }
251
+
252
+ if (errors.length > 0) {
253
+ return { isValid: false, error: errors.join(', ') };
254
+ }
255
+
256
+ return { isValid: true, data: validatedItems };
257
+ }
258
+
259
+ /**
260
+ * Validate email format (for potential user management features)
261
+ */
262
+ static validateEmail(email: string): ValidationResult {
263
+ const emailSchema = z.string().email();
264
+ try {
265
+ emailSchema.parse(email);
266
+ return { isValid: true };
267
+ } catch (error) {
268
+ return { isValid: false, error: 'Invalid email format' };
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Validate URL format
274
+ */
275
+ static validateUrl(url: string): ValidationResult {
276
+ const urlSchema = z.string().url();
277
+ try {
278
+ urlSchema.parse(url);
279
+ return { isValid: true };
280
+ } catch (error) {
281
+ return { isValid: false, error: 'Invalid URL format' };
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Check if a string contains only safe characters for logging
287
+ */
288
+ static isSafeForLogging(text: string): boolean {
289
+ // Check for sensitive patterns that shouldn't be logged
290
+ const sensitivePatterns = [
291
+ /password\s*[=:]\s*[^\s&]+/gi,
292
+ /pwd\s*[=:]\s*[^\s&]+/gi,
293
+ /secret\s*[=:]\s*[^\s&]+/gi,
294
+ /token\s*[=:]\s*[^\s&]+/gi,
295
+ /key\s*[=:]\s*[^\s&]+/gi,
296
+ /(mysql|postgresql?):\/\/[^@]+:[^@]+@/gi, // Connection strings with credentials
297
+ ];
298
+
299
+ return !sensitivePatterns.some(pattern => pattern.test(text));
300
+ }
301
+
302
+ /**
303
+ * Sanitize text for safe logging by masking sensitive information
304
+ */
305
+ static sanitizeForLogging(text: string): string {
306
+ if (!text) return '';
307
+
308
+ return text
309
+ // Mask passwords in URLs
310
+ .replace(/((?:mysql|postgresql?):\/\/[^:]+:)[^@]+(@)/gi, '$1***$2')
311
+ // Mask password parameters
312
+ .replace(/(password\s*[=:]\s*)[^\s&]+/gi, '$1***')
313
+ .replace(/(pwd\s*[=:]\s*)[^\s&]+/gi, '$1***')
314
+ .replace(/(secret\s*[=:]\s*)[^\s&]+/gi, '$1***')
315
+ .replace(/(token\s*[=:]\s*)[^\s&]+/gi, '$1***')
316
+ .replace(/(key\s*[=:]\s*)[^\s&]+/gi, '$1***');
317
+ }
318
+ }
@@ -0,0 +1,267 @@
1
+ import { DatabaseType, ValidationResult } from '../database/types.js';
2
+
3
+ export class QueryValidator {
4
+ // Keywords that are forbidden in queries
5
+ private static readonly FORBIDDEN_KEYWORDS = [
6
+ 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE',
7
+ 'ALTER', 'TRUNCATE', 'REPLACE', 'MERGE', 'CALL',
8
+ 'EXEC', 'EXECUTE', 'LOAD', 'IMPORT', 'BULK',
9
+ 'GRANT', 'REVOKE', 'SET', 'USE', 'START',
10
+ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SAVEPOINT',
11
+ 'LOCK', 'UNLOCK', 'FLUSH', 'RESET', 'PURGE',
12
+ 'KILL', 'SHUTDOWN', 'RESTART', 'COPY'
13
+ ];
14
+
15
+ // Allowed keywords for read-only operations
16
+ private static readonly ALLOWED_KEYWORDS = [
17
+ 'SELECT', 'SHOW', 'DESCRIBE', 'DESC', 'EXPLAIN',
18
+ 'ANALYZE', 'CHECK', 'CHECKSUM', 'OPTIMIZE', 'WITH', 'VALUES'
19
+ ];
20
+
21
+ // Dangerous functions that should be blocked
22
+ private static readonly FORBIDDEN_FUNCTIONS = [
23
+ 'LOAD_FILE', 'INTO OUTFILE', 'INTO DUMPFILE',
24
+ 'SYSTEM', 'USER_DEFINED_FUNCTION', 'BENCHMARK',
25
+ 'PG_READ_FILE', 'PG_LS_DIR', 'PG_EXECUTE'
26
+ ];
27
+
28
+ static validateQuery(query: string, type: DatabaseType = DatabaseType.MySQL): ValidationResult {
29
+ if (!query || query.trim().length === 0) {
30
+ return {
31
+ isValid: false,
32
+ error: 'Query cannot be empty'
33
+ };
34
+ }
35
+
36
+ const normalizedQuery = this.normalizeQuery(query);
37
+
38
+ // Check for forbidden keywords
39
+ const forbiddenCheck = this.checkForbiddenKeywords(normalizedQuery);
40
+ if (!forbiddenCheck.isValid) {
41
+ return forbiddenCheck;
42
+ }
43
+
44
+ // Check for forbidden functions
45
+ const functionCheck = this.checkForbiddenFunctions(normalizedQuery);
46
+ if (!functionCheck.isValid) {
47
+ return functionCheck;
48
+ }
49
+
50
+ // Check if query starts with an allowed keyword
51
+ const allowedCheck = this.checkAllowedStart(normalizedQuery);
52
+ if (!allowedCheck.isValid) {
53
+ return allowedCheck;
54
+ }
55
+
56
+ // Check for SQL injection patterns
57
+ const injectionCheck = this.checkSqlInjectionPatterns(normalizedQuery, type);
58
+ if (!injectionCheck.isValid) {
59
+ return injectionCheck;
60
+ }
61
+
62
+ // Check for suspicious patterns
63
+ const suspiciousCheck = this.checkSuspiciousPatterns(normalizedQuery);
64
+ if (!suspiciousCheck.isValid) {
65
+ return suspiciousCheck;
66
+ }
67
+
68
+ return {
69
+ isValid: true,
70
+ warnings: this.getWarnings(normalizedQuery)
71
+ };
72
+ }
73
+
74
+ private static normalizeQuery(query: string): string {
75
+ // Remove comments and normalize whitespace
76
+ return query
77
+ .replace(/--[^\r\n]*/g, '') // Remove -- comments
78
+ .replace(/\/\*[\s\S]*?\*\//g, '') // Remove /* */ comments
79
+ .replace(/\s+/g, ' ') // Normalize whitespace
80
+ .trim()
81
+ .toUpperCase();
82
+ }
83
+
84
+ private static checkForbiddenKeywords(query: string): ValidationResult {
85
+ for (const keyword of this.FORBIDDEN_KEYWORDS) {
86
+ // Use word boundaries to avoid false positives
87
+ const regex = new RegExp(`\\b${keyword}\\b`, 'i');
88
+ if (regex.test(query)) {
89
+ return {
90
+ isValid: false,
91
+ error: `Forbidden keyword detected: ${keyword}. Only read-only operations are allowed.`
92
+ };
93
+ }
94
+ }
95
+ return { isValid: true };
96
+ }
97
+
98
+ private static checkForbiddenFunctions(query: string): ValidationResult {
99
+ for (const func of this.FORBIDDEN_FUNCTIONS) {
100
+ if (query.includes(func)) {
101
+ return {
102
+ isValid: false,
103
+ error: `Forbidden function detected: ${func}. This function is not allowed for security reasons.`
104
+ };
105
+ }
106
+ }
107
+ return { isValid: true };
108
+ }
109
+
110
+ private static checkAllowedStart(query: string): ValidationResult {
111
+ const firstWord = query.split(' ')[0];
112
+
113
+ if (!this.ALLOWED_KEYWORDS.includes(firstWord)) {
114
+ return {
115
+ isValid: false,
116
+ error: `Query must start with one of: ${this.ALLOWED_KEYWORDS.join(', ')}`
117
+ };
118
+ }
119
+
120
+ return { isValid: true };
121
+ }
122
+
123
+ private static checkSqlInjectionPatterns(query: string, type: DatabaseType): ValidationResult {
124
+ const suspiciousPatterns = [
125
+ /;\s*(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)/i, // Multiple statements
126
+ /UNION\s+(ALL\s+)?SELECT/i, // Union-based injection
127
+ /'\s*(OR|AND)\s*'[^']*'\s*=/i, // Quote-based injection
128
+ /'\s*(OR|AND)\s*\d+\s*=\s*\d+/i, // Numeric injection
129
+ /CONCAT\s*\(\s*0x[0-9a-f]+/i, // Hex concatenation
130
+ /(SLEEP|BENCHMARK)\s*\(/i, // Time-based attacks
131
+ ];
132
+
133
+ if (type === DatabaseType.MySQL) {
134
+ suspiciousPatterns.push(/INFORMATION_SCHEMA\.\w+\s+(WHERE|AND|OR)/i);
135
+ }
136
+
137
+ for (const pattern of suspiciousPatterns) {
138
+ if (pattern.test(query)) {
139
+ return {
140
+ isValid: false,
141
+ error: 'Query contains suspicious patterns that may indicate SQL injection'
142
+ };
143
+ }
144
+ }
145
+
146
+ return { isValid: true };
147
+ }
148
+
149
+ private static checkSuspiciousPatterns(query: string): ValidationResult {
150
+ // Check for extremely long queries (potential DoS)
151
+ if (query.length > 10000) {
152
+ return {
153
+ isValid: false,
154
+ error: 'Query is too long. Maximum allowed length is 10,000 characters.'
155
+ };
156
+ }
157
+
158
+ // Check for excessive nesting
159
+ const nestedCount = (query.match(/\(/g) || []).length;
160
+ if (nestedCount > 50) {
161
+ return {
162
+ isValid: false,
163
+ error: 'Query has too many nested expressions. Maximum allowed is 50.'
164
+ };
165
+ }
166
+
167
+ return { isValid: true };
168
+ }
169
+
170
+ private static getWarnings(query: string): string[] {
171
+ const warnings: string[] = [];
172
+
173
+ // Warn about potentially slow operations
174
+ if (query.includes('SELECT *')) {
175
+ warnings.push('Using SELECT * may return large result sets. Consider specifying specific columns.');
176
+ }
177
+
178
+ if (query.includes('ORDER BY') && !query.includes('LIMIT')) {
179
+ warnings.push('ORDER BY without LIMIT may be slow on large tables.');
180
+ }
181
+
182
+ if (query.includes('LIKE %') || query.includes("LIKE '%")) {
183
+ warnings.push('Leading wildcard in LIKE patterns may cause slow queries.');
184
+ }
185
+
186
+ // Check for cross-joins
187
+ if (query.match(/FROM\s+\w+\s*,\s*\w+/i) && !query.includes('WHERE')) {
188
+ warnings.push('Potential cartesian product detected. Consider adding WHERE conditions.');
189
+ }
190
+
191
+ return warnings;
192
+ }
193
+
194
+ // Validate table and column names to prevent injection through identifiers
195
+ static validateIdentifier(identifier: string): ValidationResult {
196
+ if (!identifier || identifier.trim().length === 0) {
197
+ return {
198
+ isValid: false,
199
+ error: 'Identifier cannot be empty'
200
+ };
201
+ }
202
+
203
+ // MySQL/PostgreSQL identifier rules (simplified common)
204
+ const validIdentifier = /^[a-zA-Z_][a-zA-Z0-9_$]*$|^`[^`]+`$/;
205
+
206
+ if (!validIdentifier.test(identifier.trim())) {
207
+ return {
208
+ isValid: false,
209
+ error: 'Invalid identifier format. Use only letters, numbers, underscore, and dollar sign.'
210
+ };
211
+ }
212
+
213
+ // Check length (common limit is 64 characters)
214
+ const cleanIdentifier = identifier.replace(/[`"]/g, '');
215
+ if (cleanIdentifier.length > 64) {
216
+ return {
217
+ isValid: false,
218
+ error: 'Identifier too long. Maximum length is 64 characters.'
219
+ };
220
+ }
221
+
222
+ return { isValid: true };
223
+ }
224
+
225
+ // Sanitize user input
226
+ static sanitizeInput(input: string): string {
227
+ if (!input) return '';
228
+
229
+ // Remove null bytes and control characters
230
+ return input
231
+ .replace(/\0/g, '')
232
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
233
+ .trim();
234
+ }
235
+
236
+ // Check if a query is a simple read operation
237
+ static isSimpleReadQuery(query: string): boolean {
238
+ const normalized = this.normalizeQuery(query);
239
+ const firstWord = normalized.split(' ')[0];
240
+
241
+ return ['SELECT', 'SHOW', 'DESCRIBE', 'DESC', 'EXPLAIN', 'WITH', 'VALUES'].includes(firstWord);
242
+ }
243
+
244
+ // Estimate query complexity
245
+ static getQueryComplexity(query: string): 'low' | 'medium' | 'high' {
246
+ const normalized = this.normalizeQuery(query);
247
+ let complexity = 0;
248
+
249
+ // Count joins
250
+ complexity += (normalized.match(/\bJOIN\b/g) || []).length * 2;
251
+
252
+ // Count subqueries
253
+ complexity += (normalized.match(/\bSELECT\b/g) || []).length - 1;
254
+
255
+ // Count aggregation functions
256
+ complexity += (normalized.match(/\b(COUNT|SUM|AVG|MAX|MIN|GROUP_CONCAT)\b/g) || []).length;
257
+
258
+ // Count sorting and grouping
259
+ if (normalized.includes('ORDER BY')) complexity += 1;
260
+ if (normalized.includes('GROUP BY')) complexity += 2;
261
+ if (normalized.includes('HAVING')) complexity += 1;
262
+
263
+ if (complexity <= 2) return 'low';
264
+ if (complexity <= 6) return 'medium';
265
+ return 'high';
266
+ }
267
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true,
13
+ "declarationMap": true,
14
+ "sourceMap": true,
15
+ "resolveJsonModule": true,
16
+ "allowSyntheticDefaultImports": true,
17
+ "experimentalDecorators": true,
18
+ "emitDecoratorMetadata": true,
19
+ "lib": ["ES2022", "DOM"]
20
+ },
21
+ "include": [
22
+ "src/**/*"
23
+ ],
24
+ "exclude": [
25
+ "node_modules",
26
+ "dist",
27
+ "**/*.test.ts",
28
+ "**/*.spec.ts"
29
+ ]
30
+ }