@berthojoris/mcp-mysql-server 1.14.1 → 1.16.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.
@@ -0,0 +1,820 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DocumentationGeneratorTools = void 0;
7
+ const connection_1 = __importDefault(require("../db/connection"));
8
+ const config_1 = require("../config/config");
9
+ /**
10
+ * AI-Powered Documentation Generator
11
+ * Automatic database documentation generation with business glossary
12
+ */
13
+ class DocumentationGeneratorTools {
14
+ constructor(security) {
15
+ this.db = connection_1.default.getInstance();
16
+ this.security = security;
17
+ }
18
+ /**
19
+ * Validate database access - ensures only the connected database can be accessed
20
+ */
21
+ validateDatabaseAccess(requestedDatabase) {
22
+ const connectedDatabase = config_1.dbConfig.database;
23
+ if (!connectedDatabase) {
24
+ return {
25
+ valid: false,
26
+ database: "",
27
+ error: "No database specified in connection string. Cannot access any database.",
28
+ };
29
+ }
30
+ if (!requestedDatabase) {
31
+ return {
32
+ valid: true,
33
+ database: connectedDatabase,
34
+ };
35
+ }
36
+ if (requestedDatabase !== connectedDatabase) {
37
+ return {
38
+ valid: false,
39
+ database: "",
40
+ error: `Access denied. You can only access the connected database '${connectedDatabase}'. Requested database '${requestedDatabase}' is not allowed.`,
41
+ };
42
+ }
43
+ return {
44
+ valid: true,
45
+ database: connectedDatabase,
46
+ };
47
+ }
48
+ /**
49
+ * Generate comprehensive database documentation
50
+ */
51
+ async generateDocumentation(params) {
52
+ try {
53
+ const dbValidation = this.validateDatabaseAccess(params?.database);
54
+ if (!dbValidation.valid) {
55
+ return { status: "error", error: dbValidation.error };
56
+ }
57
+ const { scope = "database", table_name, include_business_glossary = true, format = "markdown", include_examples = true, include_statistics = true, } = params;
58
+ const database = dbValidation.database;
59
+ // Validate table_name if provided
60
+ if (table_name && !this.security.validateIdentifier(table_name).valid) {
61
+ return { status: "error", error: "Invalid table name" };
62
+ }
63
+ // Gather schema information
64
+ let tables = [];
65
+ if (scope === "table" && table_name) {
66
+ tables = await this.db.query(`SELECT TABLE_NAME, TABLE_COMMENT, ENGINE, TABLE_ROWS, CREATE_TIME, UPDATE_TIME
67
+ FROM INFORMATION_SCHEMA.TABLES
68
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND TABLE_TYPE = 'BASE TABLE'`, [database, table_name]);
69
+ }
70
+ else {
71
+ tables = await this.db.query(`SELECT TABLE_NAME, TABLE_COMMENT, ENGINE, TABLE_ROWS, CREATE_TIME, UPDATE_TIME
72
+ FROM INFORMATION_SCHEMA.TABLES
73
+ WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE'
74
+ ORDER BY TABLE_NAME`, [database]);
75
+ }
76
+ if (tables.length === 0) {
77
+ return {
78
+ status: "error",
79
+ error: scope === "table" && table_name
80
+ ? `Table '${table_name}' not found`
81
+ : "No tables found in the database",
82
+ };
83
+ }
84
+ const tableNames = tables.map((t) => t.TABLE_NAME);
85
+ const placeholders = tableNames.map(() => "?").join(",");
86
+ // Get columns
87
+ const columns = await this.db.query(`SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY,
88
+ COLUMN_DEFAULT, EXTRA, COLUMN_COMMENT, CHARACTER_MAXIMUM_LENGTH
89
+ FROM INFORMATION_SCHEMA.COLUMNS
90
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME IN (${placeholders})
91
+ ORDER BY TABLE_NAME, ORDINAL_POSITION`, [database, ...tableNames]);
92
+ // Get foreign keys
93
+ const foreignKeys = await this.db.query(`SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME,
94
+ REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
95
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
96
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME IN (${placeholders})
97
+ AND REFERENCED_TABLE_NAME IS NOT NULL`, [database, ...tableNames]);
98
+ // Get indexes
99
+ const indexes = await this.db.query(`SELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME, NON_UNIQUE, SEQ_IN_INDEX
100
+ FROM INFORMATION_SCHEMA.STATISTICS
101
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME IN (${placeholders})
102
+ ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX`, [database, ...tableNames]);
103
+ // Generate business glossary
104
+ let businessGlossary = new Map();
105
+ if (include_business_glossary) {
106
+ businessGlossary = this.generateBusinessGlossary(columns);
107
+ }
108
+ // Get sample data and statistics if requested
109
+ let tableStats = new Map();
110
+ if (include_statistics) {
111
+ for (const table of tables) {
112
+ try {
113
+ const stats = await this.getTableStatistics(database, table.TABLE_NAME);
114
+ tableStats.set(table.TABLE_NAME, stats);
115
+ }
116
+ catch {
117
+ // Skip if error
118
+ }
119
+ }
120
+ }
121
+ // Generate documentation
122
+ let content = "";
123
+ switch (format) {
124
+ case "markdown":
125
+ content = this.generateMarkdown(database, tables, columns, foreignKeys, indexes, businessGlossary, tableStats, include_examples);
126
+ break;
127
+ case "html":
128
+ content = this.generateHTML(database, tables, columns, foreignKeys, indexes, businessGlossary, tableStats, include_examples);
129
+ break;
130
+ case "json":
131
+ content = this.generateJSON(database, tables, columns, foreignKeys, indexes, businessGlossary, tableStats);
132
+ break;
133
+ }
134
+ return {
135
+ status: "success",
136
+ data: {
137
+ format,
138
+ scope,
139
+ content,
140
+ metadata: {
141
+ generated_at: new Date().toISOString(),
142
+ database,
143
+ tables_documented: tables.length,
144
+ columns_documented: columns.length,
145
+ },
146
+ },
147
+ };
148
+ }
149
+ catch (error) {
150
+ return {
151
+ status: "error",
152
+ error: error.message,
153
+ };
154
+ }
155
+ }
156
+ /**
157
+ * Generate data dictionary for a specific table
158
+ */
159
+ async generateDataDictionary(params) {
160
+ try {
161
+ const dbValidation = this.validateDatabaseAccess(params?.database);
162
+ if (!dbValidation.valid) {
163
+ return { status: "error", error: dbValidation.error };
164
+ }
165
+ const { table_name, include_sample_values = true, include_constraints = true, } = params;
166
+ const database = dbValidation.database;
167
+ // Validate table name
168
+ if (!this.security.validateIdentifier(table_name).valid) {
169
+ return { status: "error", error: "Invalid table name" };
170
+ }
171
+ // Get table info
172
+ const tableInfo = await this.db.query(`SELECT TABLE_COMMENT FROM INFORMATION_SCHEMA.TABLES
173
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?`, [database, table_name]);
174
+ if (tableInfo.length === 0) {
175
+ return { status: "error", error: `Table '${table_name}' not found` };
176
+ }
177
+ // Get columns
178
+ const columnsResult = await this.db.query(`SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY,
179
+ COLUMN_DEFAULT, EXTRA, COLUMN_COMMENT
180
+ FROM INFORMATION_SCHEMA.COLUMNS
181
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
182
+ ORDER BY ORDINAL_POSITION`, [database, table_name]);
183
+ // Get foreign keys
184
+ const fkResult = await this.db.query(`SELECT COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
185
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
186
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND REFERENCED_TABLE_NAME IS NOT NULL`, [database, table_name]);
187
+ // Get indexes
188
+ const indexResult = await this.db.query(`SELECT INDEX_NAME, COLUMN_NAME, NON_UNIQUE
189
+ FROM INFORMATION_SCHEMA.STATISTICS
190
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
191
+ ORDER BY INDEX_NAME, SEQ_IN_INDEX`, [database, table_name]);
192
+ // Get check constraints (MySQL 8.0.16+)
193
+ let checkConstraints = [];
194
+ if (include_constraints) {
195
+ try {
196
+ checkConstraints = await this.db.query(`SELECT CONSTRAINT_NAME, CHECK_CLAUSE
197
+ FROM INFORMATION_SCHEMA.CHECK_CONSTRAINTS
198
+ WHERE CONSTRAINT_SCHEMA = ?`, [database]);
199
+ }
200
+ catch {
201
+ // Ignore if check constraints are not supported
202
+ }
203
+ }
204
+ // Build column information
205
+ const columns = await Promise.all(columnsResult.map(async (col) => {
206
+ const constraints = [];
207
+ if (col.COLUMN_KEY === "PRI")
208
+ constraints.push("PRIMARY KEY");
209
+ if (col.COLUMN_KEY === "UNI")
210
+ constraints.push("UNIQUE");
211
+ if (col.IS_NULLABLE === "NO")
212
+ constraints.push("NOT NULL");
213
+ if (col.EXTRA.includes("auto_increment"))
214
+ constraints.push("AUTO_INCREMENT");
215
+ // Check for foreign key
216
+ const fk = fkResult.find((f) => f.COLUMN_NAME === col.COLUMN_NAME);
217
+ if (fk)
218
+ constraints.push(`FOREIGN KEY -> ${fk.REFERENCED_TABLE_NAME}`);
219
+ // Get sample values
220
+ let sampleValues;
221
+ if (include_sample_values) {
222
+ try {
223
+ const samples = await this.db.query(`SELECT DISTINCT \`${col.COLUMN_NAME}\`
224
+ FROM \`${database}\`.\`${table_name}\`
225
+ WHERE \`${col.COLUMN_NAME}\` IS NOT NULL
226
+ LIMIT 5`);
227
+ sampleValues = samples.map((s) => s[col.COLUMN_NAME]);
228
+ }
229
+ catch {
230
+ // Ignore errors
231
+ }
232
+ }
233
+ return {
234
+ name: col.COLUMN_NAME,
235
+ data_type: col.COLUMN_TYPE,
236
+ description: col.COLUMN_COMMENT ||
237
+ this.inferColumnDescription(col.COLUMN_NAME, col.DATA_TYPE),
238
+ constraints,
239
+ is_nullable: col.IS_NULLABLE === "YES",
240
+ default_value: col.COLUMN_DEFAULT,
241
+ sample_values: sampleValues,
242
+ business_term: this.inferBusinessTerm(col.COLUMN_NAME),
243
+ };
244
+ }));
245
+ // Build primary key
246
+ const primaryKey = columnsResult
247
+ .filter((c) => c.COLUMN_KEY === "PRI")
248
+ .map((c) => c.COLUMN_NAME);
249
+ // Build foreign keys
250
+ const foreignKeys = fkResult.map((fk) => ({
251
+ column: fk.COLUMN_NAME,
252
+ references_table: fk.REFERENCED_TABLE_NAME,
253
+ references_column: fk.REFERENCED_COLUMN_NAME,
254
+ }));
255
+ // Build indexes (group by index name)
256
+ const indexMap = new Map();
257
+ for (const idx of indexResult) {
258
+ if (!indexMap.has(idx.INDEX_NAME)) {
259
+ indexMap.set(idx.INDEX_NAME, {
260
+ columns: [],
261
+ is_unique: idx.NON_UNIQUE === 0,
262
+ });
263
+ }
264
+ indexMap.get(idx.INDEX_NAME).columns.push(idx.COLUMN_NAME);
265
+ }
266
+ const indexes = Array.from(indexMap.entries()).map(([name, info]) => ({
267
+ name,
268
+ columns: info.columns,
269
+ is_unique: info.is_unique,
270
+ }));
271
+ return {
272
+ status: "success",
273
+ data: {
274
+ table_name,
275
+ description: tableInfo[0].TABLE_COMMENT ||
276
+ this.inferTableDescription(table_name),
277
+ columns,
278
+ primary_key: primaryKey,
279
+ foreign_keys: foreignKeys,
280
+ indexes,
281
+ },
282
+ };
283
+ }
284
+ catch (error) {
285
+ return {
286
+ status: "error",
287
+ error: error.message,
288
+ };
289
+ }
290
+ }
291
+ /**
292
+ * Generate a business glossary from the schema
293
+ */
294
+ async generateBusinessGlossaryReport(params) {
295
+ try {
296
+ const dbValidation = this.validateDatabaseAccess(params?.database);
297
+ if (!dbValidation.valid) {
298
+ return { status: "error", error: dbValidation.error };
299
+ }
300
+ const { include_descriptions = true, group_by = "category", } = params;
301
+ const database = dbValidation.database;
302
+ // Get all columns
303
+ const columns = await this.db.query(`SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT
304
+ FROM INFORMATION_SCHEMA.COLUMNS
305
+ WHERE TABLE_SCHEMA = ?
306
+ ORDER BY TABLE_NAME, ORDINAL_POSITION`, [database]);
307
+ const glossary = [];
308
+ const categorySet = new Set();
309
+ const termsByName = new Map();
310
+ for (const col of columns) {
311
+ const businessTerm = this.inferBusinessTerm(col.COLUMN_NAME);
312
+ const category = this.inferCategory(col.COLUMN_NAME, col.DATA_TYPE);
313
+ const description = col.COLUMN_COMMENT ||
314
+ (include_descriptions
315
+ ? this.inferColumnDescription(col.COLUMN_NAME, col.DATA_TYPE)
316
+ : "");
317
+ categorySet.add(category);
318
+ // Track related terms (same normalized name across tables)
319
+ const normalizedName = this.normalizeColumnName(col.COLUMN_NAME);
320
+ if (!termsByName.has(normalizedName)) {
321
+ termsByName.set(normalizedName, []);
322
+ }
323
+ termsByName
324
+ .get(normalizedName)
325
+ .push(`${col.TABLE_NAME}.${col.COLUMN_NAME}`);
326
+ glossary.push({
327
+ term: businessTerm,
328
+ technical_name: col.COLUMN_NAME,
329
+ source_table: col.TABLE_NAME,
330
+ data_type: col.DATA_TYPE,
331
+ description,
332
+ category,
333
+ });
334
+ }
335
+ // Add related terms
336
+ for (const entry of glossary) {
337
+ const normalizedName = this.normalizeColumnName(entry.technical_name);
338
+ const related = termsByName.get(normalizedName) || [];
339
+ if (related.length > 1) {
340
+ entry.related_terms = related.filter((r) => r !== `${entry.source_table}.${entry.technical_name}`);
341
+ }
342
+ }
343
+ // Sort based on group_by
344
+ if (group_by === "alphabetical") {
345
+ glossary.sort((a, b) => a.term.localeCompare(b.term));
346
+ }
347
+ else if (group_by === "category") {
348
+ glossary.sort((a, b) => {
349
+ const catCompare = a.category.localeCompare(b.category);
350
+ if (catCompare !== 0)
351
+ return catCompare;
352
+ return a.term.localeCompare(b.term);
353
+ });
354
+ }
355
+ else if (group_by === "table") {
356
+ glossary.sort((a, b) => {
357
+ const tableCompare = a.source_table.localeCompare(b.source_table);
358
+ if (tableCompare !== 0)
359
+ return tableCompare;
360
+ return a.term.localeCompare(b.term);
361
+ });
362
+ }
363
+ return {
364
+ status: "success",
365
+ data: {
366
+ glossary,
367
+ categories: Array.from(categorySet).sort(),
368
+ total_terms: glossary.length,
369
+ },
370
+ };
371
+ }
372
+ catch (error) {
373
+ return {
374
+ status: "error",
375
+ error: error.message,
376
+ };
377
+ }
378
+ }
379
+ // ==================== Private Helper Methods ====================
380
+ /**
381
+ * Generate business glossary from columns
382
+ */
383
+ generateBusinessGlossary(columns) {
384
+ const glossary = new Map();
385
+ for (const col of columns) {
386
+ const term = this.inferBusinessTerm(col.COLUMN_NAME);
387
+ const description = col.COLUMN_COMMENT ||
388
+ this.inferColumnDescription(col.COLUMN_NAME, col.DATA_TYPE);
389
+ glossary.set(col.COLUMN_NAME, `${term}: ${description}`);
390
+ }
391
+ return glossary;
392
+ }
393
+ /**
394
+ * Get table statistics
395
+ */
396
+ async getTableStatistics(database, tableName) {
397
+ const result = await this.db.query(`SELECT COUNT(*) as row_count FROM \`${database}\`.\`${tableName}\``);
398
+ return {
399
+ row_count: result[0]?.row_count || 0,
400
+ };
401
+ }
402
+ /**
403
+ * Generate Markdown documentation
404
+ */
405
+ generateMarkdown(database, tables, columns, foreignKeys, indexes, businessGlossary, tableStats, includeExamples) {
406
+ const lines = [];
407
+ lines.push(`# Database Documentation: ${database}`);
408
+ lines.push("");
409
+ lines.push(`*Generated on ${new Date().toISOString()}*`);
410
+ lines.push("");
411
+ lines.push("## Table of Contents");
412
+ lines.push("");
413
+ for (const table of tables) {
414
+ lines.push(`- [${table.TABLE_NAME}](#${table.TABLE_NAME.toLowerCase()})`);
415
+ }
416
+ lines.push("");
417
+ // Relationships overview
418
+ if (foreignKeys.length > 0) {
419
+ lines.push("## Relationships");
420
+ lines.push("");
421
+ lines.push("```mermaid");
422
+ lines.push("erDiagram");
423
+ for (const fk of foreignKeys) {
424
+ lines.push(` ${fk.TABLE_NAME} ||--o{ ${fk.REFERENCED_TABLE_NAME} : "${fk.COLUMN_NAME}"`);
425
+ }
426
+ lines.push("```");
427
+ lines.push("");
428
+ }
429
+ // Table documentation
430
+ for (const table of tables) {
431
+ lines.push(`## ${table.TABLE_NAME}`);
432
+ lines.push("");
433
+ if (table.TABLE_COMMENT) {
434
+ lines.push(`> ${table.TABLE_COMMENT}`);
435
+ lines.push("");
436
+ }
437
+ const stats = tableStats.get(table.TABLE_NAME);
438
+ if (stats) {
439
+ lines.push(`**Rows:** ${stats.row_count} | **Engine:** ${table.ENGINE || "N/A"}`);
440
+ lines.push("");
441
+ }
442
+ // Columns table
443
+ lines.push("### Columns");
444
+ lines.push("");
445
+ lines.push("| Column | Type | Nullable | Key | Description |");
446
+ lines.push("|--------|------|----------|-----|-------------|");
447
+ const tableCols = columns.filter((c) => c.TABLE_NAME === table.TABLE_NAME);
448
+ for (const col of tableCols) {
449
+ const keyBadge = col.COLUMN_KEY === "PRI"
450
+ ? "🔑 PK"
451
+ : col.COLUMN_KEY === "UNI"
452
+ ? "🔒 UNI"
453
+ : col.COLUMN_KEY === "MUL"
454
+ ? "🔗 FK"
455
+ : "";
456
+ const description = col.COLUMN_COMMENT ||
457
+ businessGlossary.get(col.COLUMN_NAME)?.split(": ")[1] ||
458
+ "-";
459
+ lines.push(`| \`${col.COLUMN_NAME}\` | ${col.COLUMN_TYPE} | ${col.IS_NULLABLE} | ${keyBadge} | ${description} |`);
460
+ }
461
+ lines.push("");
462
+ // Foreign keys
463
+ const tableFKs = foreignKeys.filter((fk) => fk.TABLE_NAME === table.TABLE_NAME);
464
+ if (tableFKs.length > 0) {
465
+ lines.push("### Foreign Keys");
466
+ lines.push("");
467
+ for (const fk of tableFKs) {
468
+ lines.push(`- \`${fk.COLUMN_NAME}\` → \`${fk.REFERENCED_TABLE_NAME}.${fk.REFERENCED_COLUMN_NAME}\``);
469
+ }
470
+ lines.push("");
471
+ }
472
+ // Indexes
473
+ const tableIndexes = indexes.filter((idx) => idx.TABLE_NAME === table.TABLE_NAME);
474
+ if (tableIndexes.length > 0) {
475
+ lines.push("### Indexes");
476
+ lines.push("");
477
+ const indexGroups = new Map();
478
+ for (const idx of tableIndexes) {
479
+ if (!indexGroups.has(idx.INDEX_NAME)) {
480
+ indexGroups.set(idx.INDEX_NAME, []);
481
+ }
482
+ indexGroups.get(idx.INDEX_NAME).push(idx.COLUMN_NAME);
483
+ }
484
+ for (const [indexName, cols] of indexGroups) {
485
+ const isUnique = tableIndexes.find((i) => i.INDEX_NAME === indexName)?.NON_UNIQUE === 0;
486
+ lines.push(`- **${indexName}**${isUnique ? " (UNIQUE)" : ""}: ${cols.join(", ")}`);
487
+ }
488
+ lines.push("");
489
+ }
490
+ // Example queries
491
+ if (includeExamples) {
492
+ lines.push("### Example Queries");
493
+ lines.push("");
494
+ lines.push("```sql");
495
+ lines.push(`-- Select all from ${table.TABLE_NAME}`);
496
+ lines.push(`SELECT * FROM \`${database}\`.\`${table.TABLE_NAME}\` LIMIT 10;`);
497
+ lines.push("");
498
+ lines.push(`-- Count records`);
499
+ lines.push(`SELECT COUNT(*) FROM \`${database}\`.\`${table.TABLE_NAME}\`;`);
500
+ lines.push("```");
501
+ lines.push("");
502
+ }
503
+ lines.push("---");
504
+ lines.push("");
505
+ }
506
+ // Business glossary
507
+ if (businessGlossary.size > 0) {
508
+ lines.push("## Business Glossary");
509
+ lines.push("");
510
+ lines.push("| Term | Description |");
511
+ lines.push("|------|-------------|");
512
+ const sortedTerms = Array.from(businessGlossary.entries()).sort((a, b) => a[0].localeCompare(b[0]));
513
+ for (const [term, desc] of sortedTerms.slice(0, 50)) {
514
+ lines.push(`| ${term} | ${desc} |`);
515
+ }
516
+ lines.push("");
517
+ }
518
+ return lines.join("\n");
519
+ }
520
+ /**
521
+ * Generate HTML documentation
522
+ */
523
+ generateHTML(database, tables, columns, foreignKeys, indexes, businessGlossary, tableStats, includeExamples) {
524
+ const html = [];
525
+ html.push("<!DOCTYPE html>");
526
+ html.push('<html lang="en">');
527
+ html.push("<head>");
528
+ html.push(' <meta charset="UTF-8">');
529
+ html.push(' <meta name="viewport" content="width=device-width, initial-scale=1.0">');
530
+ html.push(` <title>Database Documentation: ${database}</title>`);
531
+ html.push(" <style>");
532
+ html.push(" body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }");
533
+ html.push(" h1 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }");
534
+ html.push(" h2 { color: #34495e; margin-top: 30px; }");
535
+ html.push(" h3 { color: #7f8c8d; }");
536
+ html.push(" table { width: 100%; border-collapse: collapse; margin: 15px 0; }");
537
+ html.push(" th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }");
538
+ html.push(" th { background: #3498db; color: white; }");
539
+ html.push(" tr:nth-child(even) { background: #f9f9f9; }");
540
+ html.push(" code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; }");
541
+ html.push(" pre { background: #2c3e50; color: #ecf0f1; padding: 15px; border-radius: 5px; overflow-x: auto; }");
542
+ html.push(" .badge { display: inline-block; padding: 3px 8px; border-radius: 3px; font-size: 12px; }");
543
+ html.push(" .badge-pk { background: #e74c3c; color: white; }");
544
+ html.push(" .badge-fk { background: #3498db; color: white; }");
545
+ html.push(" .badge-uni { background: #9b59b6; color: white; }");
546
+ html.push(" .toc { background: #f8f9fa; padding: 15px; border-radius: 5px; }");
547
+ html.push(" .toc a { color: #3498db; text-decoration: none; }");
548
+ html.push(" .toc a:hover { text-decoration: underline; }");
549
+ html.push(" </style>");
550
+ html.push("</head>");
551
+ html.push("<body>");
552
+ html.push(` <h1>📊 Database Documentation: ${database}</h1>`);
553
+ html.push(` <p><em>Generated on ${new Date().toISOString()}</em></p>`);
554
+ // Table of contents
555
+ html.push(' <div class="toc">');
556
+ html.push(" <h3>📑 Table of Contents</h3>");
557
+ html.push(" <ul>");
558
+ for (const table of tables) {
559
+ html.push(` <li><a href="#${table.TABLE_NAME}">${table.TABLE_NAME}</a></li>`);
560
+ }
561
+ html.push(" </ul>");
562
+ html.push(" </div>");
563
+ // Tables documentation
564
+ for (const table of tables) {
565
+ html.push(` <h2 id="${table.TABLE_NAME}">📋 ${table.TABLE_NAME}</h2>`);
566
+ if (table.TABLE_COMMENT) {
567
+ html.push(` <blockquote>${table.TABLE_COMMENT}</blockquote>`);
568
+ }
569
+ const stats = tableStats.get(table.TABLE_NAME);
570
+ if (stats) {
571
+ html.push(` <p><strong>Rows:</strong> ${stats.row_count} | <strong>Engine:</strong> ${table.ENGINE || "N/A"}</p>`);
572
+ }
573
+ // Columns
574
+ html.push(" <h3>Columns</h3>");
575
+ html.push(" <table>");
576
+ html.push(" <tr><th>Column</th><th>Type</th><th>Nullable</th><th>Key</th><th>Description</th></tr>");
577
+ const tableCols = columns.filter((c) => c.TABLE_NAME === table.TABLE_NAME);
578
+ for (const col of tableCols) {
579
+ let keyBadge = "";
580
+ if (col.COLUMN_KEY === "PRI") {
581
+ keyBadge = '<span class="badge badge-pk">PK</span>';
582
+ }
583
+ else if (col.COLUMN_KEY === "UNI") {
584
+ keyBadge = '<span class="badge badge-uni">UNI</span>';
585
+ }
586
+ else if (col.COLUMN_KEY === "MUL") {
587
+ keyBadge = '<span class="badge badge-fk">FK</span>';
588
+ }
589
+ const description = col.COLUMN_COMMENT ||
590
+ businessGlossary.get(col.COLUMN_NAME)?.split(": ")[1] ||
591
+ "-";
592
+ html.push(` <tr><td><code>${col.COLUMN_NAME}</code></td><td>${col.COLUMN_TYPE}</td><td>${col.IS_NULLABLE}</td><td>${keyBadge}</td><td>${description}</td></tr>`);
593
+ }
594
+ html.push(" </table>");
595
+ // Example queries
596
+ if (includeExamples) {
597
+ html.push(" <h3>Example Queries</h3>");
598
+ html.push(" <pre>");
599
+ html.push(`-- Select all from ${table.TABLE_NAME}`);
600
+ html.push(`SELECT * FROM \`${database}\`.\`${table.TABLE_NAME}\` LIMIT 10;`);
601
+ html.push("");
602
+ html.push("-- Count records");
603
+ html.push(`SELECT COUNT(*) FROM \`${database}\`.\`${table.TABLE_NAME}\`;`);
604
+ html.push(" </pre>");
605
+ }
606
+ html.push(" <hr>");
607
+ }
608
+ html.push("</body>");
609
+ html.push("</html>");
610
+ return html.join("\n");
611
+ }
612
+ /**
613
+ * Generate JSON documentation
614
+ */
615
+ generateJSON(database, tables, columns, foreignKeys, indexes, businessGlossary, tableStats) {
616
+ const doc = {
617
+ database,
618
+ generated_at: new Date().toISOString(),
619
+ tables: tables.map((table) => {
620
+ const tableCols = columns.filter((c) => c.TABLE_NAME === table.TABLE_NAME);
621
+ const tableFKs = foreignKeys.filter((fk) => fk.TABLE_NAME === table.TABLE_NAME);
622
+ const tableIdxs = indexes.filter((idx) => idx.TABLE_NAME === table.TABLE_NAME);
623
+ return {
624
+ name: table.TABLE_NAME,
625
+ comment: table.TABLE_COMMENT || null,
626
+ engine: table.ENGINE,
627
+ stats: tableStats.get(table.TABLE_NAME) || null,
628
+ columns: tableCols.map((col) => ({
629
+ name: col.COLUMN_NAME,
630
+ type: col.COLUMN_TYPE,
631
+ data_type: col.DATA_TYPE,
632
+ is_nullable: col.IS_NULLABLE === "YES",
633
+ column_key: col.COLUMN_KEY || null,
634
+ default_value: col.COLUMN_DEFAULT,
635
+ extra: col.EXTRA || null,
636
+ comment: col.COLUMN_COMMENT || null,
637
+ business_term: businessGlossary.get(col.COLUMN_NAME)?.split(": ")[0] || null,
638
+ })),
639
+ foreign_keys: tableFKs.map((fk) => ({
640
+ column: fk.COLUMN_NAME,
641
+ references_table: fk.REFERENCED_TABLE_NAME,
642
+ references_column: fk.REFERENCED_COLUMN_NAME,
643
+ })),
644
+ indexes: this.groupIndexes(tableIdxs),
645
+ };
646
+ }),
647
+ relationships: foreignKeys.map((fk) => ({
648
+ from_table: fk.TABLE_NAME,
649
+ from_column: fk.COLUMN_NAME,
650
+ to_table: fk.REFERENCED_TABLE_NAME,
651
+ to_column: fk.REFERENCED_COLUMN_NAME,
652
+ })),
653
+ };
654
+ return JSON.stringify(doc, null, 2);
655
+ }
656
+ /**
657
+ * Group indexes by name
658
+ */
659
+ groupIndexes(indexes) {
660
+ const groups = new Map();
661
+ for (const idx of indexes) {
662
+ if (!groups.has(idx.INDEX_NAME)) {
663
+ groups.set(idx.INDEX_NAME, {
664
+ columns: [],
665
+ is_unique: idx.NON_UNIQUE === 0,
666
+ });
667
+ }
668
+ groups.get(idx.INDEX_NAME).columns.push(idx.COLUMN_NAME);
669
+ }
670
+ return Array.from(groups.entries()).map(([name, info]) => ({
671
+ name,
672
+ columns: info.columns,
673
+ is_unique: info.is_unique,
674
+ }));
675
+ }
676
+ /**
677
+ * Infer business term from column name
678
+ */
679
+ inferBusinessTerm(columnName) {
680
+ // Convert snake_case or camelCase to Title Case
681
+ return columnName
682
+ .replace(/_/g, " ")
683
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
684
+ .replace(/\b\w/g, (c) => c.toUpperCase());
685
+ }
686
+ /**
687
+ * Infer column description from name and type
688
+ */
689
+ inferColumnDescription(columnName, dataType) {
690
+ const name = columnName.toLowerCase();
691
+ // Common patterns
692
+ if (name === "id" || name.endsWith("_id")) {
693
+ return "Unique identifier";
694
+ }
695
+ if (name.includes("created")) {
696
+ return "Record creation timestamp";
697
+ }
698
+ if (name.includes("updated") || name.includes("modified")) {
699
+ return "Last update timestamp";
700
+ }
701
+ if (name.includes("deleted")) {
702
+ return "Soft delete indicator/timestamp";
703
+ }
704
+ if (name.includes("email")) {
705
+ return "Email address";
706
+ }
707
+ if (name.includes("phone") || name.includes("mobile")) {
708
+ return "Phone number";
709
+ }
710
+ if (name.includes("name")) {
711
+ return "Name or title";
712
+ }
713
+ if (name.includes("description") || name.includes("desc")) {
714
+ return "Detailed description";
715
+ }
716
+ if (name.includes("status")) {
717
+ return "Status indicator";
718
+ }
719
+ if (name.includes("type")) {
720
+ return "Type or category";
721
+ }
722
+ if (name.includes("price") || name.includes("amount") || name.includes("cost")) {
723
+ return "Monetary value";
724
+ }
725
+ if (name.includes("count") || name.includes("quantity") || name.includes("qty")) {
726
+ return "Numeric count";
727
+ }
728
+ if (name.includes("is_") || name.includes("has_") || name.startsWith("is") || name.startsWith("has")) {
729
+ return "Boolean flag";
730
+ }
731
+ if (name.includes("url") || name.includes("link")) {
732
+ return "URL or web link";
733
+ }
734
+ if (name.includes("address")) {
735
+ return "Physical or mailing address";
736
+ }
737
+ if (name.includes("date")) {
738
+ return "Date value";
739
+ }
740
+ // Type-based defaults
741
+ if (dataType.includes("text") || dataType.includes("blob")) {
742
+ return "Extended text content";
743
+ }
744
+ if (dataType.includes("int") || dataType.includes("decimal") || dataType.includes("float")) {
745
+ return "Numeric value";
746
+ }
747
+ if (dataType.includes("date") || dataType.includes("time")) {
748
+ return "Date/time value";
749
+ }
750
+ if (dataType.includes("enum") || dataType.includes("set")) {
751
+ return "Enumerated value";
752
+ }
753
+ return "Data field";
754
+ }
755
+ /**
756
+ * Infer table description from name
757
+ */
758
+ inferTableDescription(tableName) {
759
+ const name = tableName.toLowerCase();
760
+ if (name.includes("user"))
761
+ return "Stores user account information";
762
+ if (name.includes("order"))
763
+ return "Stores order records";
764
+ if (name.includes("product"))
765
+ return "Stores product catalog";
766
+ if (name.includes("customer"))
767
+ return "Stores customer information";
768
+ if (name.includes("invoice"))
769
+ return "Stores invoice records";
770
+ if (name.includes("payment"))
771
+ return "Stores payment transactions";
772
+ if (name.includes("log"))
773
+ return "Stores activity/event logs";
774
+ if (name.includes("config") || name.includes("setting"))
775
+ return "Stores configuration settings";
776
+ if (name.includes("session"))
777
+ return "Stores session data";
778
+ if (name.includes("cache"))
779
+ return "Stores cached data";
780
+ if (name.includes("migration"))
781
+ return "Stores database migration history";
782
+ return `Stores ${this.inferBusinessTerm(tableName).toLowerCase()} data`;
783
+ }
784
+ /**
785
+ * Infer category from column name and type
786
+ */
787
+ inferCategory(columnName, dataType) {
788
+ const name = columnName.toLowerCase();
789
+ if (name === "id" || name.endsWith("_id"))
790
+ return "Identifiers";
791
+ if (name.includes("created") || name.includes("updated") || name.includes("deleted") || name.includes("date") || name.includes("time"))
792
+ return "Timestamps";
793
+ if (name.includes("email") || name.includes("phone") || name.includes("address"))
794
+ return "Contact";
795
+ if (name.includes("name") || name.includes("title"))
796
+ return "Names";
797
+ if (name.includes("price") || name.includes("amount") || name.includes("cost") || name.includes("total"))
798
+ return "Financial";
799
+ if (name.includes("status") || name.includes("type") || name.includes("category"))
800
+ return "Classification";
801
+ if (name.startsWith("is_") || name.startsWith("has_"))
802
+ return "Flags";
803
+ if (name.includes("description") || name.includes("notes") || name.includes("comment"))
804
+ return "Text Content";
805
+ if (name.includes("url") || name.includes("link") || name.includes("path"))
806
+ return "References";
807
+ return "General";
808
+ }
809
+ /**
810
+ * Normalize column name for comparison
811
+ */
812
+ normalizeColumnName(name) {
813
+ return name
814
+ .toLowerCase()
815
+ .replace(/^(fk_|pk_|idx_)/, "")
816
+ .replace(/_id$/, "")
817
+ .replace(/[_-]/g, "");
818
+ }
819
+ }
820
+ exports.DocumentationGeneratorTools = DocumentationGeneratorTools;