sql-dashboard 1.0.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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +494 -0
  3. package/dist/base.driver-BKzf8BxS.d.mts +102 -0
  4. package/dist/base.driver-BdK7obt0.d.ts +102 -0
  5. package/dist/chunk-7YLO3OSN.mjs +19513 -0
  6. package/dist/chunk-BPXOTU3D.js +76 -0
  7. package/dist/chunk-HIQUIRDJ.mjs +76 -0
  8. package/dist/chunk-MTCZXLV5.mjs +232 -0
  9. package/dist/chunk-OCL5Y3AH.mjs +51 -0
  10. package/dist/chunk-OQJUWTZV.js +51 -0
  11. package/dist/chunk-P4QE6SGC.mjs +69 -0
  12. package/dist/chunk-TNHUK2FI.mjs +1033 -0
  13. package/dist/chunk-YGKUVVJT.mjs +5494 -0
  14. package/dist/connection-CzduPMhl.d.mts +68 -0
  15. package/dist/connection-CzduPMhl.d.ts +68 -0
  16. package/dist/dashboard-CkGz4ID-.d.mts +45 -0
  17. package/dist/dashboard-D9xSb-hQ.d.ts +45 -0
  18. package/dist/drivers/mssql.driver.d.mts +26 -0
  19. package/dist/drivers/mssql.driver.d.ts +26 -0
  20. package/dist/drivers/mssql.driver.js +64824 -0
  21. package/dist/drivers/mssql.driver.mjs +8 -0
  22. package/dist/drivers/mysql.driver.d.mts +29 -0
  23. package/dist/drivers/mysql.driver.d.ts +29 -0
  24. package/dist/drivers/mysql.driver.js +19672 -0
  25. package/dist/drivers/mysql.driver.mjs +9 -0
  26. package/dist/drivers/postgres.driver.d.mts +29 -0
  27. package/dist/drivers/postgres.driver.d.ts +29 -0
  28. package/dist/drivers/postgres.driver.js +5588 -0
  29. package/dist/drivers/postgres.driver.mjs +8 -0
  30. package/dist/export/index.d.mts +23 -0
  31. package/dist/export/index.d.ts +23 -0
  32. package/dist/export/index.js +102 -0
  33. package/dist/export/index.mjs +75 -0
  34. package/dist/index.d.mts +579 -0
  35. package/dist/index.d.ts +579 -0
  36. package/dist/index.js +26694 -0
  37. package/dist/index.mjs +26694 -0
  38. package/dist/middleware/express.d.mts +11 -0
  39. package/dist/middleware/express.d.ts +11 -0
  40. package/dist/middleware/express.js +90896 -0
  41. package/dist/middleware/express.mjs +91 -0
  42. package/dist/middleware/fastify.d.mts +10 -0
  43. package/dist/middleware/fastify.d.ts +10 -0
  44. package/dist/middleware/fastify.js +90860 -0
  45. package/dist/middleware/fastify.mjs +54 -0
  46. package/dist/open-JHAWMLA2.mjs +602 -0
  47. package/dist/open-T3PIT3AO.js +602 -0
  48. package/dist/query-BFhJHNeb.d.mts +16 -0
  49. package/dist/query-BFhJHNeb.d.ts +16 -0
  50. package/dist/tedious-LLE7JVQC.js +63830 -0
  51. package/dist/tedious-PHHFLMLD.mjs +63830 -0
  52. package/package.json +131 -0
@@ -0,0 +1,1033 @@
1
+ import {
2
+ MSSQLDriver
3
+ } from "./chunk-MTCZXLV5.mjs";
4
+ import {
5
+ MySQLDriver
6
+ } from "./chunk-7YLO3OSN.mjs";
7
+ import {
8
+ PostgresDriver
9
+ } from "./chunk-YGKUVVJT.mjs";
10
+ import {
11
+ BaseDriver,
12
+ QueryTimer
13
+ } from "./chunk-P4QE6SGC.mjs";
14
+
15
+ // src/engine.ts
16
+ import { EventEmitter } from "eventemitter3";
17
+
18
+ // src/drivers/sqlite.driver.ts
19
+ import initSqlJs from "sql.js";
20
+ import path from "path";
21
+ import fs from "fs";
22
+ var SQLiteDriver = class extends BaseDriver {
23
+ constructor(config) {
24
+ super();
25
+ this.type = "sqlite" /* SQLITE */;
26
+ this.db = null;
27
+ this.sqlJs = null;
28
+ this.memoryMode = config.mode === "memory" || !config.path;
29
+ this.dbPath = config.path || ":memory:";
30
+ this.config = { ...config };
31
+ }
32
+ async connect() {
33
+ try {
34
+ this.sqlJs = await initSqlJs();
35
+ if (this.memoryMode) {
36
+ this.db = new this.sqlJs.Database();
37
+ } else {
38
+ const fullPath = path.resolve(this.dbPath);
39
+ const dir = path.dirname(fullPath);
40
+ if (!fs.existsSync(dir)) {
41
+ fs.mkdirSync(dir, { recursive: true });
42
+ }
43
+ if (fs.existsSync(fullPath)) {
44
+ const buffer = fs.readFileSync(fullPath);
45
+ this.db = new this.sqlJs.Database(buffer);
46
+ } else {
47
+ this.db = new this.sqlJs.Database();
48
+ this.saveToFile();
49
+ }
50
+ }
51
+ this.db.run("PRAGMA journal_mode=WAL;");
52
+ this.db.run("PRAGMA foreign_keys=ON;");
53
+ this.connected = true;
54
+ } catch (error) {
55
+ throw new Error(`Failed to connect to SQLite: ${error.message}`);
56
+ }
57
+ }
58
+ async disconnect() {
59
+ if (this.db) {
60
+ if (!this.memoryMode && this.dbPath !== ":memory:") {
61
+ this.saveToFile();
62
+ }
63
+ this.db.close();
64
+ this.db = null;
65
+ }
66
+ this.connected = false;
67
+ }
68
+ isConnected() {
69
+ return this.connected && this.db !== null;
70
+ }
71
+ saveToFile() {
72
+ if (this.db && !this.memoryMode) {
73
+ const data = this.db.export();
74
+ const buffer = Buffer.from(data);
75
+ fs.writeFileSync(path.resolve(this.dbPath), buffer);
76
+ }
77
+ }
78
+ async executeQuery(sql, params) {
79
+ this.ensureConnected();
80
+ const timer = new QueryTimer();
81
+ try {
82
+ const normalizedSql = sql.trim().replace(/;$/, "");
83
+ const isSelect = /^\s*(SELECT|PRAGMA|SHOW|DESCRIBE|EXPLAIN)/i.test(normalizedSql);
84
+ if (isSelect) {
85
+ const stmt = this.db.prepare(normalizedSql);
86
+ const rows = [];
87
+ const columns = [];
88
+ columns.push(...stmt.getColumnNames());
89
+ if (params && params.length > 0) {
90
+ stmt.bind(params);
91
+ }
92
+ while (stmt.step()) {
93
+ const row = stmt.getAsObject();
94
+ rows.push(row);
95
+ }
96
+ stmt.free();
97
+ return this.createResult(sql, rows, timer.elapsed);
98
+ } else {
99
+ this.db.run(normalizedSql, params);
100
+ const affectedRows = this.db.getRowsModified();
101
+ return this.createResult(sql, [], timer.elapsed, affectedRows);
102
+ }
103
+ } catch (error) {
104
+ return this.createErrorResult(sql, error, timer.elapsed);
105
+ }
106
+ }
107
+ async executeBatch(queries) {
108
+ this.ensureConnected();
109
+ const results = [];
110
+ try {
111
+ this.db.run("BEGIN TRANSACTION;");
112
+ for (const query of queries) {
113
+ const result = await this.executeQuery(query);
114
+ results.push(result);
115
+ if (result.status === "error") {
116
+ this.db.run("ROLLBACK;");
117
+ return results;
118
+ }
119
+ }
120
+ this.db.run("COMMIT;");
121
+ } catch (error) {
122
+ this.db.run("ROLLBACK;");
123
+ results.push(this.createErrorResult("Batch", error, 0));
124
+ }
125
+ return results;
126
+ }
127
+ async getSchema() {
128
+ this.ensureConnected();
129
+ return {
130
+ name: "main",
131
+ tables: await this.getTables(),
132
+ views: await this.getViews(),
133
+ procedures: []
134
+ };
135
+ }
136
+ async getTables() {
137
+ const result = await this.executeQuery(
138
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name;"
139
+ );
140
+ const tables = [];
141
+ for (const row of result.rows) {
142
+ const tableName = row.name;
143
+ const columns = await this.getColumns(tableName);
144
+ const indexes = await this.getIndexes(tableName);
145
+ const foreignKeys = await this.getForeignKeys(tableName);
146
+ const rowCount = await this.getTableRowCount(tableName);
147
+ tables.push({
148
+ name: tableName,
149
+ schema: "main",
150
+ type: "table",
151
+ columns,
152
+ indexes,
153
+ foreignKeys,
154
+ rowCount
155
+ });
156
+ }
157
+ return tables;
158
+ }
159
+ async getTableInfo(tableName) {
160
+ const columns = await this.getColumns(tableName);
161
+ const indexes = await this.getIndexes(tableName);
162
+ const foreignKeys = await this.getForeignKeys(tableName);
163
+ const rowCount = await this.getTableRowCount(tableName);
164
+ return {
165
+ name: tableName,
166
+ schema: "main",
167
+ type: "table",
168
+ columns,
169
+ indexes,
170
+ foreignKeys,
171
+ rowCount
172
+ };
173
+ }
174
+ async getColumns(tableName) {
175
+ const result = await this.executeQuery(`PRAGMA table_info("${tableName}");`);
176
+ return result.rows.map((row) => ({
177
+ name: row.name,
178
+ type: row.type || "TEXT",
179
+ nullable: !row.notnull,
180
+ primaryKey: row.pk === 1,
181
+ defaultValue: row.dflt_value
182
+ }));
183
+ }
184
+ async getIndexes(tableName) {
185
+ const result = await this.executeQuery(`PRAGMA index_list("${tableName}");`);
186
+ const indexes = [];
187
+ for (const row of result.rows) {
188
+ const indexName = row.name;
189
+ const unique = !!row.unique;
190
+ const primary = indexName.startsWith("sqlite_autoindex");
191
+ const indexInfo = await this.executeQuery(`PRAGMA index_info("${indexName}");`);
192
+ const columns = indexInfo.rows.map((r) => r.name);
193
+ indexes.push({
194
+ name: indexName,
195
+ columns,
196
+ unique,
197
+ primary,
198
+ type: primary ? "primary" : unique ? "unique" : "index"
199
+ });
200
+ }
201
+ return indexes;
202
+ }
203
+ async getForeignKeys(tableName) {
204
+ const result = await this.executeQuery(`PRAGMA foreign_key_list("${tableName}");`);
205
+ return result.rows.map((row) => ({
206
+ name: `fk_${tableName}_${row.from}`,
207
+ column: row.from,
208
+ referencedSchema: "main",
209
+ referencedTable: row.table,
210
+ referencedColumn: row.to,
211
+ onDelete: row.on_delete || "NO ACTION",
212
+ onUpdate: row.on_update || "NO ACTION"
213
+ }));
214
+ }
215
+ async getViews() {
216
+ const result = await this.executeQuery(
217
+ "SELECT name, sql FROM sqlite_master WHERE type='view' ORDER BY name;"
218
+ );
219
+ return result.rows.map((row) => ({
220
+ name: row.name,
221
+ schema: "main",
222
+ definition: row.sql || "",
223
+ columns: []
224
+ }));
225
+ }
226
+ async getTableRowCount(tableName) {
227
+ const result = await this.executeQuery(
228
+ `SELECT COUNT(*) as count FROM "${tableName}";`
229
+ );
230
+ return result.rows[0]?.count || 0;
231
+ }
232
+ async getVersion() {
233
+ const result = await this.executeQuery("SELECT sqlite_version() as version;");
234
+ return result.rows[0]?.version || "unknown";
235
+ }
236
+ async getDatabases() {
237
+ return ["main"];
238
+ }
239
+ };
240
+
241
+ // src/drivers/index.ts
242
+ function createDriver(config) {
243
+ switch (config.type) {
244
+ case "sqlite" /* SQLITE */:
245
+ return new SQLiteDriver(config.connection);
246
+ case "mysql" /* MYSQL */:
247
+ return new MySQLDriver(config.connection);
248
+ case "postgres" /* POSTGRES */:
249
+ return new PostgresDriver(config.connection);
250
+ case "mssql" /* MSSQL */:
251
+ return new MSSQLDriver(config.connection);
252
+ default:
253
+ throw new Error(`Unsupported driver type: ${config.type}`);
254
+ }
255
+ }
256
+
257
+ // src/admin/schema.ts
258
+ var SchemaBrowser = class {
259
+ constructor(driver) {
260
+ this.driver = driver;
261
+ }
262
+ async getSchema() {
263
+ return this.driver.getSchema();
264
+ }
265
+ async getTables(filter) {
266
+ const tables = await this.driver.getTables();
267
+ if (filter?.table) {
268
+ return tables.filter((t) => t.name.toLowerCase().includes(filter.table.toLowerCase()));
269
+ }
270
+ return tables;
271
+ }
272
+ async getTable(name) {
273
+ return this.driver.getTableInfo(name);
274
+ }
275
+ async getColumns(tableName) {
276
+ return this.driver.getColumns(tableName);
277
+ }
278
+ async getIndexes(tableName) {
279
+ return this.driver.getIndexes(tableName);
280
+ }
281
+ async getForeignKeys(tableName) {
282
+ return this.driver.getForeignKeys(tableName);
283
+ }
284
+ async getViews() {
285
+ return this.driver.getViews();
286
+ }
287
+ async getTableRowCount(tableName) {
288
+ return this.driver.getTableRowCount(tableName);
289
+ }
290
+ async searchTables(query) {
291
+ const tables = await this.driver.getTables();
292
+ const lower = query.toLowerCase();
293
+ return tables.filter(
294
+ (t) => t.name.toLowerCase().includes(lower)
295
+ );
296
+ }
297
+ async getTableSummary(tableName) {
298
+ const info = await this.getTable(tableName);
299
+ return {
300
+ name: info.name,
301
+ columnCount: info.columns.length,
302
+ indexCount: info.indexes.length,
303
+ foreignKeyCount: info.foreignKeys.length,
304
+ rowCount: info.rowCount || 0,
305
+ size: info.size
306
+ };
307
+ }
308
+ };
309
+
310
+ // src/utils/pagination.ts
311
+ function createPaginatedResult(data, total, params) {
312
+ const page = Math.max(1, params.page ?? 1);
313
+ const pageSize = Math.min(Math.max(1, params.pageSize ?? 50), 1e3);
314
+ return {
315
+ data,
316
+ total,
317
+ page,
318
+ pageSize,
319
+ totalPages: Math.ceil(total / pageSize)
320
+ };
321
+ }
322
+
323
+ // src/admin/history.ts
324
+ var MemoryStorage = class {
325
+ constructor(maxEntries = 500) {
326
+ this.entries = [];
327
+ this.maxEntries = maxEntries;
328
+ }
329
+ getEntries() {
330
+ return [...this.entries];
331
+ }
332
+ addEntry(entry) {
333
+ this.entries.unshift(entry);
334
+ if (this.entries.length > this.maxEntries) {
335
+ this.entries = this.entries.slice(0, this.maxEntries);
336
+ }
337
+ }
338
+ clear() {
339
+ this.entries = [];
340
+ }
341
+ removeEntry(id) {
342
+ this.entries = this.entries.filter((e) => e.id !== id);
343
+ }
344
+ };
345
+ var QueryHistory = class {
346
+ constructor(maxEntries = 500) {
347
+ this.storage = new MemoryStorage(maxEntries);
348
+ }
349
+ record(result, database, user) {
350
+ const entry = {
351
+ id: result.id,
352
+ query: result.query.length > 500 ? result.query.substring(0, 500) + "..." : result.query,
353
+ executedAt: /* @__PURE__ */ new Date(),
354
+ duration: result.duration,
355
+ status: result.status,
356
+ rowCount: result.rowCount,
357
+ database,
358
+ user
359
+ };
360
+ this.storage.addEntry(entry);
361
+ return entry;
362
+ }
363
+ list(params = {}) {
364
+ let entries = this.storage.getEntries();
365
+ if (params.database) {
366
+ entries = entries.filter((e) => e.database === params.database);
367
+ }
368
+ if (params.status) {
369
+ entries = entries.filter((e) => e.status === params.status);
370
+ }
371
+ if (params.search) {
372
+ const lower = params.search.toLowerCase();
373
+ entries = entries.filter((e) => e.query.toLowerCase().includes(lower));
374
+ }
375
+ const total = entries.length;
376
+ const { page = 1, pageSize = 50 } = params;
377
+ const start = (page - 1) * pageSize;
378
+ const data = entries.slice(start, start + pageSize);
379
+ return createPaginatedResult(data, total, { page, pageSize });
380
+ }
381
+ getRecent(limit = 10) {
382
+ return this.storage.getEntries().slice(0, limit);
383
+ }
384
+ getStats() {
385
+ const entries = this.storage.getEntries();
386
+ const successful = entries.filter((e) => e.status === "success").length;
387
+ const totalDuration = entries.reduce((sum, e) => sum + e.duration, 0);
388
+ return {
389
+ totalQueries: entries.length,
390
+ successfulQueries: successful,
391
+ failedQueries: entries.length - successful,
392
+ avgDuration: entries.length > 0 ? totalDuration / entries.length : 0
393
+ };
394
+ }
395
+ clear() {
396
+ this.storage.clear();
397
+ }
398
+ remove(id) {
399
+ this.storage.removeEntry(id);
400
+ }
401
+ };
402
+
403
+ // src/security/readonly.ts
404
+ var ReadOnlyGuard = class {
405
+ constructor(rule = {}) {
406
+ this.rule = {
407
+ enabled: true,
408
+ allowSelect: true,
409
+ allowShow: true,
410
+ allowDescribe: true,
411
+ allowExplain: true,
412
+ bypassUsers: [],
413
+ ...rule
414
+ };
415
+ }
416
+ isAllowed(sql, user) {
417
+ if (!this.rule.enabled) return true;
418
+ if (user && this.rule.bypassUsers?.includes(user)) return true;
419
+ const trimmed = sql.trim().toUpperCase();
420
+ if (this.rule.allowSelect && trimmed.startsWith("SELECT")) return true;
421
+ if (this.rule.allowShow && trimmed.startsWith("SHOW")) return true;
422
+ if (this.rule.allowDescribe && trimmed.startsWith("DESCRIBE")) return true;
423
+ if (this.rule.allowExplain && trimmed.startsWith("EXPLAIN")) return true;
424
+ if (trimmed.startsWith("WITH")) return true;
425
+ if (trimmed.startsWith("PRAGMA")) return true;
426
+ return false;
427
+ }
428
+ checkReadOnly(sql, user) {
429
+ if (this.isAllowed(sql, user)) {
430
+ return { allowed: true };
431
+ }
432
+ return {
433
+ allowed: false,
434
+ reason: "Database is in read-only mode. Write operations are not permitted."
435
+ };
436
+ }
437
+ updateRule(rule) {
438
+ this.rule = { ...this.rule, ...rule };
439
+ }
440
+ };
441
+
442
+ // src/security/ratelimit.ts
443
+ var RateLimiter = class {
444
+ constructor(config = {}) {
445
+ this.store = /* @__PURE__ */ new Map();
446
+ this.config = {
447
+ enabled: true,
448
+ windowMs: 6e4,
449
+ maxQueries: 100,
450
+ maxQueriesPerUser: {},
451
+ errorMessage: "Too many queries. Please slow down.",
452
+ ...config
453
+ };
454
+ this.cleanupInterval = setInterval(() => this.cleanup(), 6e4);
455
+ }
456
+ cleanup() {
457
+ const now = Date.now();
458
+ for (const [key, entry] of this.store.entries()) {
459
+ if (entry.resetAt <= now) {
460
+ this.store.delete(key);
461
+ }
462
+ }
463
+ }
464
+ check(key) {
465
+ if (!this.config.enabled) {
466
+ return { allowed: true, remaining: Infinity, resetAt: 0 };
467
+ }
468
+ const now = Date.now();
469
+ const entry = this.store.get(key);
470
+ if (!entry || entry.resetAt <= now) {
471
+ const maxQueries2 = this.config.maxQueriesPerUser[key] || this.config.maxQueries;
472
+ this.store.set(key, {
473
+ count: 1,
474
+ resetAt: now + this.config.windowMs
475
+ });
476
+ return { allowed: true, remaining: maxQueries2 - 1, resetAt: now + this.config.windowMs };
477
+ }
478
+ const maxQueries = this.config.maxQueriesPerUser[key] || this.config.maxQueries;
479
+ entry.count++;
480
+ if (entry.count > maxQueries) {
481
+ return {
482
+ allowed: false,
483
+ remaining: 0,
484
+ resetAt: entry.resetAt,
485
+ error: this.config.errorMessage
486
+ };
487
+ }
488
+ return { allowed: true, remaining: maxQueries - entry.count, resetAt: entry.resetAt };
489
+ }
490
+ getRemaining(key) {
491
+ const entry = this.store.get(key);
492
+ if (!entry || entry.resetAt <= Date.now()) {
493
+ return this.config.maxQueries;
494
+ }
495
+ const maxQueries = this.config.maxQueriesPerUser[key] || this.config.maxQueries;
496
+ return Math.max(0, maxQueries - entry.count);
497
+ }
498
+ reset(key) {
499
+ if (key) {
500
+ this.store.delete(key);
501
+ } else {
502
+ this.store.clear();
503
+ }
504
+ }
505
+ updateConfig(config) {
506
+ this.config = { ...this.config, ...config };
507
+ }
508
+ destroy() {
509
+ clearInterval(this.cleanupInterval);
510
+ this.store.clear();
511
+ }
512
+ };
513
+
514
+ // src/utils/logger.ts
515
+ var LOG_LEVELS = {
516
+ debug: 0,
517
+ info: 1,
518
+ warn: 2,
519
+ error: 3,
520
+ silent: 4
521
+ };
522
+ var Logger = class {
523
+ constructor(config) {
524
+ this.timers = /* @__PURE__ */ new Map();
525
+ this.config = {
526
+ level: config?.level ?? "info",
527
+ queries: config?.queries ?? true,
528
+ slowQueryThreshold: config?.slowQueryThreshold ?? 1e3,
529
+ format: config?.format ?? "text"
530
+ };
531
+ }
532
+ shouldLog(level) {
533
+ return LOG_LEVELS[level] >= LOG_LEVELS[this.config.level];
534
+ }
535
+ formatMessage(level, message, data) {
536
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
537
+ if (this.config.format === "json") {
538
+ return JSON.stringify({ timestamp, level, message, data });
539
+ }
540
+ const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
541
+ const dataStr = data ? ` ${typeof data === "string" ? data : JSON.stringify(data)}` : "";
542
+ return `${prefix} ${message}${dataStr}`;
543
+ }
544
+ debug(message, data) {
545
+ if (this.shouldLog("debug")) {
546
+ console.debug(this.formatMessage("debug", message, data));
547
+ }
548
+ }
549
+ info(message, data) {
550
+ if (this.shouldLog("info")) {
551
+ console.info(this.formatMessage("info", message, data));
552
+ }
553
+ }
554
+ warn(message, data) {
555
+ if (this.shouldLog("warn")) {
556
+ console.warn(this.formatMessage("warn", message, data));
557
+ }
558
+ }
559
+ error(message, data) {
560
+ if (this.shouldLog("error")) {
561
+ console.error(this.formatMessage("error", message, data));
562
+ }
563
+ }
564
+ query(sql, duration, rowCount) {
565
+ if (this.config.queries && this.shouldLog("info")) {
566
+ const slow = duration >= this.config.slowQueryThreshold ? " [SLOW]" : "";
567
+ this.info(`Query (${duration}ms, ${rowCount} rows)${slow}: ${sql.substring(0, 200)}`);
568
+ if (sql.length > 200) {
569
+ this.debug(`Full query: ${sql}`);
570
+ }
571
+ }
572
+ }
573
+ startTimer(label) {
574
+ this.timers.set(label, Date.now());
575
+ }
576
+ endTimer(label) {
577
+ const start = this.timers.get(label);
578
+ if (start === void 0) return 0;
579
+ const duration = Date.now() - start;
580
+ this.timers.delete(label);
581
+ this.debug(`Timer "${label}": ${duration}ms`);
582
+ return duration;
583
+ }
584
+ updateConfig(config) {
585
+ this.config = { ...this.config, ...config };
586
+ }
587
+ };
588
+
589
+ // src/core/validator.ts
590
+ var STATEMENT_TYPES = [
591
+ "SELECT",
592
+ "INSERT",
593
+ "UPDATE",
594
+ "DELETE",
595
+ "CREATE",
596
+ "ALTER",
597
+ "DROP",
598
+ "TRUNCATE",
599
+ "GRANT",
600
+ "REVOKE",
601
+ "SHOW",
602
+ "DESCRIBE",
603
+ "EXPLAIN",
604
+ "USE",
605
+ "SET",
606
+ "CALL",
607
+ "EXEC",
608
+ "MERGE",
609
+ "REPLACE",
610
+ "RENAME",
611
+ "BEGIN",
612
+ "COMMIT",
613
+ "ROLLBACK",
614
+ "SAVEPOINT",
615
+ "LOCK",
616
+ "UNLOCK",
617
+ "ANALYZE",
618
+ "OPTIMIZE",
619
+ "REPAIR",
620
+ "BACKUP",
621
+ "RESTORE"
622
+ ];
623
+ var DML_STATEMENTS = /* @__PURE__ */ new Set(["INSERT", "UPDATE", "DELETE", "TRUNCATE", "MERGE", "REPLACE"]);
624
+ var DDL_STATEMENTS = /* @__PURE__ */ new Set(["CREATE", "ALTER", "DROP", "RENAME", "TRUNCATE"]);
625
+ var DCL_STATEMENTS = /* @__PURE__ */ new Set(["GRANT", "REVOKE"]);
626
+ var DANGEROUS_STATEMENTS = /* @__PURE__ */ new Set(["DROP", "TRUNCATE", "GRANT", "REVOKE", "ALTER"]);
627
+ function detectStatementType(sql) {
628
+ const trimmed = sql.trim().toUpperCase();
629
+ for (const type of STATEMENT_TYPES) {
630
+ if (trimmed.startsWith(type)) return type;
631
+ }
632
+ const words = trimmed.split(/\s+/);
633
+ return words[0] || "UNKNOWN";
634
+ }
635
+ function isReadOnlyStatement(sql) {
636
+ const type = detectStatementType(sql);
637
+ return !DML_STATEMENTS.has(type) && !DDL_STATEMENTS.has(type) && !DCL_STATEMENTS.has(type) && !DANGEROUS_STATEMENTS.has(type);
638
+ }
639
+ function extractTableNames(sql) {
640
+ const patterns = [
641
+ /(?:FROM|JOIN|INTO|UPDATE|TABLE|FROM|USING)\s+[`"']?(\w+)[`"']?/gi,
642
+ /(?:INSERT\s+(?:INTO\s+)?|REPLACE\s+(?:INTO\s+)?)[`"']?(\w+)[`"']?/gi,
643
+ /(?:DELETE\s+(?:FROM\s+)?)[`"']?(\w+)[`"']?/gi,
644
+ /(?:ALTER\s+TABLE\s+)[`"']?(\w+)[`"']?/gi,
645
+ /(?:DROP\s+(?:TABLE|VIEW|INDEX|PROCEDURE|FUNCTION)\s+)[`"']?(\w+)[`"']?/gi,
646
+ /(?:CREATE\s+(?:TABLE|VIEW|INDEX|PROCEDURE|FUNCTION)\s+)[`"']?(\w+)[`"']?/gi,
647
+ /(?:TRUNCATE\s+TABLE\s+)[`"']?(\w+)[`"']?/gi
648
+ ];
649
+ const tables = /* @__PURE__ */ new Set();
650
+ for (const pattern of patterns) {
651
+ let match;
652
+ while ((match = pattern.exec(sql)) !== null) {
653
+ if (match[1]) tables.add(match[1]);
654
+ }
655
+ }
656
+ return Array.from(tables);
657
+ }
658
+ function validateQuery(sql, security) {
659
+ const errors = [];
660
+ const trimmedSql = sql.trim();
661
+ if (!trimmedSql) {
662
+ errors.push({
663
+ code: "EMPTY_QUERY",
664
+ message: "Query is empty",
665
+ severity: "error"
666
+ });
667
+ return { valid: false, errors, normalizedQuery: sql, statementType: "UNKNOWN", isReadOnly: true, tables: [] };
668
+ }
669
+ if (!trimmedSql.endsWith(";")) {
670
+ trimmedSql + ";";
671
+ }
672
+ const statementType = detectStatementType(trimmedSql);
673
+ const detectedTables = extractTableNames(trimmedSql);
674
+ const readOnly = isReadOnlyStatement(trimmedSql);
675
+ if (security?.maxQueryLength && trimmedSql.length > security.maxQueryLength) {
676
+ errors.push({
677
+ code: "QUERY_TOO_LONG",
678
+ message: `Query exceeds maximum length of ${security.maxQueryLength} characters`,
679
+ severity: "error"
680
+ });
681
+ }
682
+ if (security?.bannedStatements) {
683
+ for (const banned of security.bannedStatements) {
684
+ const bannedUpper = banned.toUpperCase();
685
+ if (trimmedSql.toUpperCase().includes(bannedUpper)) {
686
+ errors.push({
687
+ code: "BANNED_STATEMENT",
688
+ message: `Statement "${bannedUpper}" is not allowed`,
689
+ severity: "error"
690
+ });
691
+ }
692
+ }
693
+ }
694
+ if (security?.readOnly === true && !readOnly && statementType !== "UNKNOWN") {
695
+ errors.push({
696
+ code: "READ_ONLY_MODE",
697
+ message: "Database is in read-only mode. Write operations are not allowed.",
698
+ severity: "error"
699
+ });
700
+ }
701
+ if (security?.requireWhere && ["UPDATE", "DELETE"].includes(statementType)) {
702
+ if (!/\bWHERE\b/i.test(trimmedSql)) {
703
+ errors.push({
704
+ code: "MISSING_WHERE",
705
+ message: `${statementType} without WHERE clause is not allowed`,
706
+ severity: "error"
707
+ });
708
+ }
709
+ }
710
+ return {
711
+ valid: errors.length === 0,
712
+ errors,
713
+ normalizedQuery: trimmedSql,
714
+ statementType,
715
+ isReadOnly: readOnly,
716
+ tables: detectedTables
717
+ };
718
+ }
719
+
720
+ // src/core/sanitizer.ts
721
+ var SQL_INJECTION_PATTERNS = [
722
+ /'.*OR.*'='/i,
723
+ /'.*AND.*'='/i,
724
+ /OR\s+'.*'='.*'/i,
725
+ /AND\s+'.*'='.*'/i,
726
+ /\bOR\s+\d+\s*=\s*\d+/i,
727
+ /\bAND\s+\d+\s*=\s*\d+/i,
728
+ /\bOR\s+\w+\s*=\s*\w+/i,
729
+ /\bAND\s+\w+\s*=\s*\w+/i,
730
+ /--\s*$/m,
731
+ /\/\*.*\*\//,
732
+ /;\s*DROP\s+/i,
733
+ /;\s*DELETE\s+/i,
734
+ /;\s*UPDATE\s+/i,
735
+ /;\s*INSERT\s+/i,
736
+ /;\s*EXEC(?:UTE)?\s+/i,
737
+ /\bUNION\s+ALL\s+SELECT\b/i,
738
+ /\bUNION\s+SELECT\b/i,
739
+ /\bINTO\s+OUTFILE\b/i,
740
+ /\bINTO\s+DUMPFILE\b/i,
741
+ /\bLOAD_FILE\s*\(/i,
742
+ /\bpg_sleep\s*\(/i,
743
+ /\bWAITFOR\s+DELAY\b/i,
744
+ /\bBENCHMARK\s*\(/i,
745
+ /\bSLEEP\s*\(/i
746
+ ];
747
+ var STRING_LITERALS = /'(?:[^'\\]|\\.)*'/g;
748
+ function sanitizeComment(sql) {
749
+ return sql.replace(/--.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/#.*$/gm, "").trim();
750
+ }
751
+ function detectInjection(sql) {
752
+ const noComments = sql.replace(/--.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
753
+ const noStrings = noComments.replace(STRING_LITERALS, "");
754
+ if (SQL_INJECTION_PATTERNS.some((pattern) => pattern.test(noStrings))) {
755
+ return true;
756
+ }
757
+ for (const pattern of SQL_INJECTION_PATTERNS) {
758
+ if (pattern.test(noComments)) {
759
+ return true;
760
+ }
761
+ }
762
+ return false;
763
+ }
764
+ function prepareBatchQueries(sql) {
765
+ const sanitized = sanitizeComment(sql);
766
+ const queries = sanitized.split(";").map((q) => q.trim()).filter((q) => q.length > 0).map((q) => q + ";");
767
+ return queries;
768
+ }
769
+
770
+ // src/core/formatter.ts
771
+ function truncateResult(rows, maxRows) {
772
+ if (rows.length <= maxRows) {
773
+ return { rows, truncated: false };
774
+ }
775
+ return { rows: rows.slice(0, maxRows), truncated: true };
776
+ }
777
+
778
+ // src/engine.ts
779
+ var SQLDashboard = class extends EventEmitter {
780
+ constructor(options) {
781
+ super();
782
+ this.version = "1.0.0";
783
+ this.connected = false;
784
+ this.connectionPromise = null;
785
+ this.config = {
786
+ driver: options.driver,
787
+ security: options.security || {},
788
+ logger: options.logger || { level: "info", queries: true, slowQueryThreshold: 1e3, format: "text" },
789
+ autoConnect: options.autoConnect ?? true,
790
+ version: this.version
791
+ };
792
+ this.logger = new Logger(this.config.logger);
793
+ this.driver = createDriver(this.config.driver);
794
+ this.schema = new SchemaBrowser(this.driver);
795
+ this.history = new QueryHistory();
796
+ this.setupSecurity();
797
+ this.setupEventHandlers();
798
+ if (this.config.autoConnect) {
799
+ this.connect().catch((err) => {
800
+ this.logger.error(`Auto-connect failed: ${err.message}`);
801
+ });
802
+ }
803
+ }
804
+ setupSecurity() {
805
+ const security = this.config.security;
806
+ if (!security) return;
807
+ if (security.readOnly) {
808
+ this.readOnlyGuard = new ReadOnlyGuard(
809
+ typeof security.readOnly === "object" ? security.readOnly : void 0
810
+ );
811
+ }
812
+ if (security.rateLimit) {
813
+ this.rateLimiter = new RateLimiter(security.rateLimit);
814
+ }
815
+ }
816
+ setupEventHandlers() {
817
+ this.on("error", (error) => {
818
+ this.logger.error(`Dashboard error: ${error.message}`);
819
+ });
820
+ }
821
+ async connect() {
822
+ if (this.connected) return;
823
+ if (this.connectionPromise) return this.connectionPromise;
824
+ this.connectionPromise = this.driver.connect().then(() => {
825
+ this.connected = true;
826
+ this.connectionPromise = null;
827
+ this.logger.info(`Connected to ${this.driver.type} database`);
828
+ this.emit("connect");
829
+ }).catch((err) => {
830
+ this.connectionPromise = null;
831
+ this.emit("error", err);
832
+ throw err;
833
+ });
834
+ return this.connectionPromise;
835
+ }
836
+ async disconnect() {
837
+ if (!this.connected) return;
838
+ await this.driver.disconnect();
839
+ this.connected = false;
840
+ this.logger.info("Disconnected from database");
841
+ this.emit("disconnect");
842
+ }
843
+ async query(sql, options) {
844
+ await this.ensureConnected();
845
+ const timer = new QueryTimer();
846
+ const opts = {
847
+ timeout: options?.timeout ?? this.config.security?.queryTimeout ?? 3e4,
848
+ params: options?.params,
849
+ readOnly: options?.readOnly ?? this.config.security?.readOnly === true,
850
+ maxRows: options?.maxRows ?? this.config.security?.maxRows ?? 1e3
851
+ };
852
+ const validation = this.validateQuery(sql, opts);
853
+ if (!validation.valid) {
854
+ const errorResult = {
855
+ id: crypto.randomUUID(),
856
+ status: "error",
857
+ rows: [],
858
+ columns: [],
859
+ rowCount: 0,
860
+ duration: 0,
861
+ query: sql,
862
+ error: validation.errors.map((e) => e.message).join("; ")
863
+ };
864
+ this.emit("error", new Error(errorResult.error), sql);
865
+ return errorResult;
866
+ }
867
+ if (detectInjection(sql)) {
868
+ const errorResult = {
869
+ id: crypto.randomUUID(),
870
+ status: "error",
871
+ rows: [],
872
+ columns: [],
873
+ rowCount: 0,
874
+ duration: 0,
875
+ query: sql,
876
+ error: "Potential SQL injection detected"
877
+ };
878
+ this.emit("error", new Error("SQL injection detected"), sql);
879
+ return errorResult;
880
+ }
881
+ try {
882
+ const queryPromise = opts.params ? this.driver.executeQuery(sql, opts.params) : this.driver.executeQuery(sql);
883
+ const result = await this.withTimeout(queryPromise, opts.timeout);
884
+ const truncated = truncateResult(result.rows, opts.maxRows);
885
+ const finalResult = {
886
+ ...result,
887
+ rows: truncated.rows,
888
+ rowCount: truncated.truncated ? truncated.rows.length : result.rowCount,
889
+ warning: truncated.truncated ? `Results truncated to ${opts.maxRows} rows. Total results: ${result.rowCount}` : void 0
890
+ };
891
+ this.logger.query(sql, finalResult.duration, finalResult.rowCount);
892
+ this.history.record(finalResult, this.getDriverType());
893
+ this.emit("query", finalResult);
894
+ return finalResult;
895
+ } catch (error) {
896
+ const errorResult = this.driver["createErrorResult"](sql, error, timer.elapsed);
897
+ this.history.record(errorResult, this.getDriverType());
898
+ this.emit("error", error, sql);
899
+ if (errorResult.error) {
900
+ this.logger.error(`Query failed: ${errorResult.error}`);
901
+ }
902
+ return errorResult;
903
+ }
904
+ }
905
+ async execute(sql, options) {
906
+ return this.query(sql, options);
907
+ }
908
+ async batch(queries) {
909
+ await this.ensureConnected();
910
+ const results = [];
911
+ for (const query of queries) {
912
+ const result = await this.query(query);
913
+ results.push(result);
914
+ if (result.status === "error") break;
915
+ }
916
+ return results;
917
+ }
918
+ async executeBatch(sql) {
919
+ const queries = prepareBatchQueries(sql);
920
+ return this.batch(queries);
921
+ }
922
+ async transaction(callback) {
923
+ await this.ensureConnected();
924
+ await this.query("BEGIN");
925
+ try {
926
+ const result = await callback(async (sql, params) => {
927
+ return this.query(sql, { params, transaction: true });
928
+ });
929
+ await this.query("COMMIT");
930
+ return result;
931
+ } catch (error) {
932
+ await this.query("ROLLBACK");
933
+ throw error;
934
+ }
935
+ }
936
+ async explain(sql) {
937
+ const explainSql = `EXPLAIN ${sql.trim().replace(/;$/, "")}`;
938
+ return this.query(explainSql);
939
+ }
940
+ async analyze(sql) {
941
+ const analyzeSql = `EXPLAIN ANALYZE ${sql.trim().replace(/;$/, "")}`;
942
+ return this.query(analyzeSql);
943
+ }
944
+ async status() {
945
+ let dbVersion;
946
+ try {
947
+ if (this.connected) {
948
+ dbVersion = await this.driver.getVersion();
949
+ }
950
+ } catch {
951
+ }
952
+ const histStats = this.history.getStats();
953
+ return {
954
+ connected: this.connected,
955
+ driver: this.driver.type,
956
+ version: this.version,
957
+ uptime: process.uptime(),
958
+ history: {
959
+ total: histStats.totalQueries,
960
+ successful: histStats.successfulQueries,
961
+ failed: histStats.failedQueries
962
+ },
963
+ databaseVersion: dbVersion
964
+ };
965
+ }
966
+ async getDriverVersion() {
967
+ await this.ensureConnected();
968
+ return this.driver.getVersion();
969
+ }
970
+ async getDatabases() {
971
+ await this.ensureConnected();
972
+ return this.driver.getDatabases();
973
+ }
974
+ validate(sql) {
975
+ return validateQuery(sql, this.config.security);
976
+ }
977
+ validateQuery(sql, options) {
978
+ const result = validateQuery(sql, this.config.security);
979
+ if (options.readOnly && !result.isReadOnly) {
980
+ result.valid = false;
981
+ result.errors.push({
982
+ code: "READ_ONLY",
983
+ message: "Query is not allowed in read-only mode",
984
+ severity: "error"
985
+ });
986
+ }
987
+ return result;
988
+ }
989
+ async ensureConnected() {
990
+ if (!this.connected) {
991
+ await this.connect();
992
+ }
993
+ }
994
+ async withTimeout(promise, ms) {
995
+ if (ms <= 0) return promise;
996
+ return Promise.race([
997
+ promise,
998
+ new Promise(
999
+ (_, reject) => setTimeout(() => reject(new Error(`Query timed out after ${ms}ms`)), ms)
1000
+ )
1001
+ ]);
1002
+ }
1003
+ getDriverType() {
1004
+ return this.driver.type;
1005
+ }
1006
+ updateSecurity(config) {
1007
+ if (config.readOnly !== void 0) {
1008
+ if (this.readOnlyGuard) {
1009
+ this.readOnlyGuard.updateRule(
1010
+ typeof config.readOnly === "object" ? config.readOnly : { enabled: config.readOnly }
1011
+ );
1012
+ } else if (config.readOnly) {
1013
+ this.readOnlyGuard = new ReadOnlyGuard(
1014
+ typeof config.readOnly === "object" ? config.readOnly : void 0
1015
+ );
1016
+ }
1017
+ }
1018
+ if (config.rateLimit && this.rateLimiter) {
1019
+ this.rateLimiter.updateConfig(config.rateLimit);
1020
+ }
1021
+ this.config.security = { ...this.config.security, ...config };
1022
+ }
1023
+ destroy() {
1024
+ this.rateLimiter?.destroy();
1025
+ this.disconnect().catch(() => {
1026
+ });
1027
+ this.removeAllListeners();
1028
+ }
1029
+ };
1030
+
1031
+ export {
1032
+ SQLDashboard
1033
+ };