@cuongph.dev/dbdocgen 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +437 -0
- package/dist/cli/index.cjs +1844 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1830 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.cjs +1751 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +292 -0
- package/dist/index.d.ts +292 -0
- package/dist/index.js +1713 -0
- package/dist/index.js.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,1830 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { readFile as readFile2, writeFile as writeFile5, rm } from "fs/promises";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { resolve } from "path";
|
|
8
|
+
|
|
9
|
+
// src/core/config/loader.ts
|
|
10
|
+
import { cosmiconfig } from "cosmiconfig";
|
|
11
|
+
|
|
12
|
+
// src/core/config/schema.ts
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
var dialectSchema = z.enum([
|
|
15
|
+
"postgres",
|
|
16
|
+
"mysql",
|
|
17
|
+
"mariadb",
|
|
18
|
+
"sqlite",
|
|
19
|
+
"mssql",
|
|
20
|
+
"unknown"
|
|
21
|
+
]);
|
|
22
|
+
var outputFormatSchema = z.enum([
|
|
23
|
+
"excel",
|
|
24
|
+
"markdown",
|
|
25
|
+
"html",
|
|
26
|
+
"diagram",
|
|
27
|
+
"word"
|
|
28
|
+
]);
|
|
29
|
+
var outputLanguageSchema = z.enum(["en", "jp"]);
|
|
30
|
+
var dbdocgenConfigSchema = z.object({
|
|
31
|
+
schema: z.string().default("./schema.sql"),
|
|
32
|
+
dialect: dialectSchema.optional(),
|
|
33
|
+
outDir: z.string().default("./docs/db"),
|
|
34
|
+
output: z.object({
|
|
35
|
+
formats: z.array(outputFormatSchema).default(["excel", "markdown", "html", "diagram", "word"]),
|
|
36
|
+
language: outputLanguageSchema.default("en")
|
|
37
|
+
}).default({})
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// src/core/config/loader.ts
|
|
41
|
+
async function loadConfig(input) {
|
|
42
|
+
const explorer = cosmiconfig("dbdocgen", {
|
|
43
|
+
searchPlaces: ["dbdocgen.config.js", "dbdocgen.config.json", ".dbdocgenrc"]
|
|
44
|
+
});
|
|
45
|
+
const result = input.cliOptions.configPath ? await explorer.load(input.cliOptions.configPath) : await explorer.search(input.cwd);
|
|
46
|
+
const fileConfig = result?.config ?? {};
|
|
47
|
+
const merged = mergeConfig(fileConfig, input.cliOptions);
|
|
48
|
+
return dbdocgenConfigSchema.parse(merged);
|
|
49
|
+
}
|
|
50
|
+
function mergeConfig(fileConfig, cli) {
|
|
51
|
+
return {
|
|
52
|
+
...fileConfig,
|
|
53
|
+
schema: cli.schema ?? fileConfig.schema,
|
|
54
|
+
dialect: cli.dialect ?? fileConfig.dialect,
|
|
55
|
+
outDir: cli.outDir ?? fileConfig.outDir,
|
|
56
|
+
output: {
|
|
57
|
+
...fileConfig.output,
|
|
58
|
+
formats: cli.formats ?? fileConfig.output?.formats
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/core/pipeline/generate-db-docs.ts
|
|
64
|
+
import { readFile } from "fs/promises";
|
|
65
|
+
|
|
66
|
+
// src/exporters/diagram/mermaid-exporter.ts
|
|
67
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
68
|
+
import { join } from "path";
|
|
69
|
+
async function exportMermaidDiagram(doc, options) {
|
|
70
|
+
await mkdir(options.outDir, { recursive: true });
|
|
71
|
+
await writeFile(
|
|
72
|
+
join(options.outDir, "er_diagram.mmd"),
|
|
73
|
+
renderMermaid(doc),
|
|
74
|
+
"utf8"
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
function renderMermaid(doc) {
|
|
78
|
+
const lines = ["erDiagram"];
|
|
79
|
+
for (const warning of doc.warnings) {
|
|
80
|
+
const target = warning.target ? ` (${warning.target})` : "";
|
|
81
|
+
lines.push(
|
|
82
|
+
` %% WARNING [${warning.severity}] ${warning.code}${target}: ${warning.message}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
for (const table of doc.tables) {
|
|
86
|
+
for (const todo of table.reviewTodos) {
|
|
87
|
+
lines.push(` %% TODO [${todo.type}] ${todo.target}: ${todo.issue}`);
|
|
88
|
+
}
|
|
89
|
+
lines.push(` ${table.name} {`);
|
|
90
|
+
for (const column of table.columns) {
|
|
91
|
+
const markers = [
|
|
92
|
+
column.isPrimaryKey ? "PK" : "",
|
|
93
|
+
column.isForeignKey ? "FK" : ""
|
|
94
|
+
].filter(Boolean).join(" ");
|
|
95
|
+
lines.push(
|
|
96
|
+
` ${sanitizeType(column.type)} ${column.name}${markers ? ` "${markers}"` : ""}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
lines.push(" }");
|
|
100
|
+
}
|
|
101
|
+
for (const relationship of doc.relationships.filter(
|
|
102
|
+
(item) => item.source === "schema"
|
|
103
|
+
)) {
|
|
104
|
+
lines.push(
|
|
105
|
+
` ${relationship.toTable} ||--o{ ${relationship.fromTable} : "${relationship.constraintName ?? relationship.fromColumn}"`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return `${lines.join("\n")}
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
function sanitizeType(type) {
|
|
112
|
+
return type.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/exporters/excel/excel-exporter.ts
|
|
116
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
117
|
+
import { join as join2 } from "path";
|
|
118
|
+
import ExcelJS from "exceljs";
|
|
119
|
+
|
|
120
|
+
// src/exporters/shared/output-labels.ts
|
|
121
|
+
var LABELS = {
|
|
122
|
+
en: {
|
|
123
|
+
docTitle: "Database Documentation",
|
|
124
|
+
tableInfoHeading: "Table Info",
|
|
125
|
+
columnsHeading: "Columns",
|
|
126
|
+
metaField: "Field",
|
|
127
|
+
metaValue: "Value",
|
|
128
|
+
tablePhysicalName: "Table Physical Name",
|
|
129
|
+
tableLogicalName: "Table Logical Name",
|
|
130
|
+
schema: "Schema",
|
|
131
|
+
primaryKey: "Primary Key",
|
|
132
|
+
foreignKeys: "Foreign Keys",
|
|
133
|
+
indexes: "Indexes",
|
|
134
|
+
none: "(none)",
|
|
135
|
+
physicalName: "Physical Name",
|
|
136
|
+
logicalName: "Logical Name",
|
|
137
|
+
type: "Type",
|
|
138
|
+
required: "Required",
|
|
139
|
+
defaultValue: "Default Value",
|
|
140
|
+
notes: "Notes",
|
|
141
|
+
yes: "Yes",
|
|
142
|
+
no: "No",
|
|
143
|
+
generatedNote: "Generated by dbdocgen in A5:SQL-style layout.",
|
|
144
|
+
overviewHeading: "Overview",
|
|
145
|
+
tableListHeading: "Table List",
|
|
146
|
+
relationshipsHeading: "Relationships",
|
|
147
|
+
warningsHeading: "Warnings",
|
|
148
|
+
tableLabel: "Table",
|
|
149
|
+
descriptionLabel: "Description",
|
|
150
|
+
dialectLabel: "Dialect",
|
|
151
|
+
tablesLabel: "Tables",
|
|
152
|
+
relationshipsLabel: "Relationships",
|
|
153
|
+
severity: "Severity",
|
|
154
|
+
code: "Code",
|
|
155
|
+
target: "Target",
|
|
156
|
+
message: "Message",
|
|
157
|
+
fromTable: "From Table",
|
|
158
|
+
fromColumn: "From Column",
|
|
159
|
+
toTable: "To Table",
|
|
160
|
+
toColumn: "To Column",
|
|
161
|
+
constraint: "Constraint",
|
|
162
|
+
source: "Source",
|
|
163
|
+
needsReview: "Needs Review",
|
|
164
|
+
columnsCount: "Columns",
|
|
165
|
+
rowNo: "#",
|
|
166
|
+
backToOverview: "\u2190 Overview",
|
|
167
|
+
pkMarker: "PK",
|
|
168
|
+
fkMarker: "FK"
|
|
169
|
+
},
|
|
170
|
+
jp: {
|
|
171
|
+
docTitle: "Database Documentation",
|
|
172
|
+
tableInfoHeading: "Table Info",
|
|
173
|
+
columnsHeading: "Columns",
|
|
174
|
+
metaField: "\u9805\u76EE",
|
|
175
|
+
metaValue: "\u5024",
|
|
176
|
+
tablePhysicalName: "\u30C6\u30FC\u30D6\u30EB\u7269\u7406\u540D",
|
|
177
|
+
tableLogicalName: "\u30C6\u30FC\u30D6\u30EB\u8AD6\u7406\u540D",
|
|
178
|
+
schema: "\u30B9\u30AD\u30FC\u30DE",
|
|
179
|
+
primaryKey: "\u4E3B\u30AD\u30FC",
|
|
180
|
+
foreignKeys: "\u5916\u90E8\u30AD\u30FC",
|
|
181
|
+
indexes: "\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9",
|
|
182
|
+
none: "(none)",
|
|
183
|
+
physicalName: "\u7269\u7406\u540D",
|
|
184
|
+
logicalName: "\u8AD6\u7406\u540D",
|
|
185
|
+
type: "\u578B",
|
|
186
|
+
required: "\u5FC5\u9808",
|
|
187
|
+
defaultValue: "\u30C7\u30D5\u30A9\u30EB\u30C8\u5024",
|
|
188
|
+
notes: "\u5099\u8003",
|
|
189
|
+
yes: "Yes",
|
|
190
|
+
no: "No",
|
|
191
|
+
generatedNote: "Generated by dbdocgen in A5:SQL-style layout.",
|
|
192
|
+
overviewHeading: "Overview",
|
|
193
|
+
tableListHeading: "Table List",
|
|
194
|
+
relationshipsHeading: "Relationships",
|
|
195
|
+
warningsHeading: "Warnings",
|
|
196
|
+
tableLabel: "Table",
|
|
197
|
+
descriptionLabel: "Description",
|
|
198
|
+
dialectLabel: "Dialect",
|
|
199
|
+
tablesLabel: "Tables",
|
|
200
|
+
relationshipsLabel: "Relationships",
|
|
201
|
+
severity: "Severity",
|
|
202
|
+
code: "Code",
|
|
203
|
+
target: "Target",
|
|
204
|
+
message: "Message",
|
|
205
|
+
fromTable: "From Table",
|
|
206
|
+
fromColumn: "From Column",
|
|
207
|
+
toTable: "To Table",
|
|
208
|
+
toColumn: "To Column",
|
|
209
|
+
constraint: "Constraint",
|
|
210
|
+
source: "Source",
|
|
211
|
+
needsReview: "Needs Review",
|
|
212
|
+
columnsCount: "\u5217\u6570",
|
|
213
|
+
rowNo: "No.",
|
|
214
|
+
backToOverview: "\u2190 \u4E00\u89A7",
|
|
215
|
+
pkMarker: "PK",
|
|
216
|
+
fkMarker: "FK"
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
function getOutputLabels(language = "en") {
|
|
220
|
+
return LABELS[language];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/exporters/excel/excel-exporter.ts
|
|
224
|
+
var COLOR = {
|
|
225
|
+
headerBg: "FF4472C4",
|
|
226
|
+
headerFg: "FFFFFFFF",
|
|
227
|
+
metaBg: "FFD9E1F2",
|
|
228
|
+
metaFg: "FF1F3864",
|
|
229
|
+
overviewBg: "FF4472C4",
|
|
230
|
+
overviewFg: "FFFFFFFF",
|
|
231
|
+
altRow: "FFF2F7FF",
|
|
232
|
+
pkBg: "FFFFF3CD",
|
|
233
|
+
fkBg: "FFE8F4FD",
|
|
234
|
+
link: "FF0563C1",
|
|
235
|
+
border: "FFB8CCE4",
|
|
236
|
+
valueBg: "FFFAFBFE"
|
|
237
|
+
};
|
|
238
|
+
var COL_COUNT = 7;
|
|
239
|
+
async function exportExcelDictionary(doc, options) {
|
|
240
|
+
await mkdir2(options.outDir, { recursive: true });
|
|
241
|
+
const workbook = new ExcelJS.Workbook();
|
|
242
|
+
const labels = getOutputLabels(options.language);
|
|
243
|
+
const sheetNames = /* @__PURE__ */ new Map();
|
|
244
|
+
for (const table of doc.tables) {
|
|
245
|
+
sheetNames.set(table.name, buildSheetName(table.name, sheetNames));
|
|
246
|
+
}
|
|
247
|
+
addOverviewSheet(workbook, doc, labels, sheetNames);
|
|
248
|
+
for (const table of doc.tables) {
|
|
249
|
+
const sheetName = sheetNames.get(table.name);
|
|
250
|
+
const sheet = workbook.addWorksheet(sheetName);
|
|
251
|
+
populateTableSheet(sheet, table, doc, labels);
|
|
252
|
+
}
|
|
253
|
+
await workbook.xlsx.writeFile(
|
|
254
|
+
join2(options.outDir, "database_dictionary.xlsx")
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
function addOverviewSheet(workbook, doc, labels, sheetNames) {
|
|
258
|
+
const sheet = workbook.addWorksheet("Overview");
|
|
259
|
+
sheet.mergeCells(1, 1, 1, COL_COUNT);
|
|
260
|
+
const titleCell = sheet.getCell(1, 1);
|
|
261
|
+
titleCell.value = labels.docTitle;
|
|
262
|
+
titleCell.font = { bold: true, size: 14, color: { argb: COLOR.overviewFg } };
|
|
263
|
+
titleCell.fill = solidFill(COLOR.overviewBg);
|
|
264
|
+
titleCell.alignment = { horizontal: "center", vertical: "middle" };
|
|
265
|
+
sheet.getRow(1).height = 30;
|
|
266
|
+
sheet.addRow([]);
|
|
267
|
+
const summary = [
|
|
268
|
+
[labels.dialectLabel, doc.dialect],
|
|
269
|
+
[labels.tablesLabel, doc.tables.length],
|
|
270
|
+
[labels.relationshipsLabel, doc.relationships.length]
|
|
271
|
+
];
|
|
272
|
+
for (const [field, value] of summary) {
|
|
273
|
+
const row = sheet.addRow([field, value]);
|
|
274
|
+
styleMetaRow(row);
|
|
275
|
+
applyBorderToRow(row, 2);
|
|
276
|
+
row.getCell(1).alignment = { vertical: "middle", indent: 1 };
|
|
277
|
+
row.getCell(2).alignment = { vertical: "middle", indent: 1 };
|
|
278
|
+
}
|
|
279
|
+
sheet.addRow([]);
|
|
280
|
+
const sectionRow = sheet.addRow([labels.tableListHeading]);
|
|
281
|
+
sheet.mergeCells(sectionRow.number, 1, sectionRow.number, COL_COUNT);
|
|
282
|
+
const sectionCell = sectionRow.getCell(1);
|
|
283
|
+
sectionCell.font = { bold: true, size: 11, color: { argb: COLOR.metaFg } };
|
|
284
|
+
sectionCell.fill = solidFill(COLOR.metaBg);
|
|
285
|
+
sectionCell.alignment = { vertical: "middle" };
|
|
286
|
+
sectionRow.height = 22;
|
|
287
|
+
const headerRowNum = sectionRow.number + 1;
|
|
288
|
+
const headerRow = sheet.getRow(headerRowNum);
|
|
289
|
+
headerRow.values = [
|
|
290
|
+
labels.rowNo,
|
|
291
|
+
labels.tableLabel,
|
|
292
|
+
labels.tableLogicalName,
|
|
293
|
+
labels.columnsCount,
|
|
294
|
+
labels.primaryKey,
|
|
295
|
+
labels.foreignKeys,
|
|
296
|
+
labels.indexes
|
|
297
|
+
];
|
|
298
|
+
styleColorRow(headerRow, COLOR.overviewBg, COLOR.overviewFg);
|
|
299
|
+
applyBorderToRow(headerRow, COL_COUNT);
|
|
300
|
+
for (const [i, table] of doc.tables.entries()) {
|
|
301
|
+
const indexes = collectTableIndexes(table, doc);
|
|
302
|
+
const targetSheet = sheetNames.get(table.name);
|
|
303
|
+
const row = sheet.addRow([
|
|
304
|
+
i + 1,
|
|
305
|
+
table.name,
|
|
306
|
+
displayValue(table.comment, labels),
|
|
307
|
+
table.columns.length,
|
|
308
|
+
displayValue(table.primaryKeys.join(", "), labels),
|
|
309
|
+
table.foreignKeys.length > 0 ? table.foreignKeys.map((fk) => `${fk.columns.join(",")} \u2192 ${fk.referencedTable}`).join("; ") : labels.none,
|
|
310
|
+
indexes.length > 0 ? indexes.map((idx) => idx.name).join("; ") : labels.none
|
|
311
|
+
]);
|
|
312
|
+
setHyperlink(row.getCell(2), table.name, targetSheet);
|
|
313
|
+
row.getCell(2).font = { bold: true, color: { argb: COLOR.link }, underline: true };
|
|
314
|
+
row.getCell(4).alignment = { horizontal: "center" };
|
|
315
|
+
if (i % 2 === 1) {
|
|
316
|
+
shadeRow(row, COL_COUNT, COLOR.altRow);
|
|
317
|
+
}
|
|
318
|
+
applyBorderToRow(row, COL_COUNT);
|
|
319
|
+
}
|
|
320
|
+
sheet.columns = [
|
|
321
|
+
{ width: 20 },
|
|
322
|
+
{ width: 28 },
|
|
323
|
+
{ width: 30 },
|
|
324
|
+
{ width: 9 },
|
|
325
|
+
{ width: 20 },
|
|
326
|
+
{ width: 38 },
|
|
327
|
+
{ width: 38 }
|
|
328
|
+
];
|
|
329
|
+
sheet.autoFilter = {
|
|
330
|
+
from: { row: headerRowNum, column: 1 },
|
|
331
|
+
to: { row: headerRowNum + doc.tables.length, column: COL_COUNT }
|
|
332
|
+
};
|
|
333
|
+
sheet.views = [{ state: "frozen", ySplit: headerRowNum }];
|
|
334
|
+
}
|
|
335
|
+
function populateTableSheet(sheet, table, doc, labels) {
|
|
336
|
+
const indexes = collectTableIndexes(table, doc);
|
|
337
|
+
sheet.mergeCells(1, 1, 1, 6);
|
|
338
|
+
const titleCell = sheet.getCell(1, 1);
|
|
339
|
+
titleCell.value = table.name;
|
|
340
|
+
titleCell.font = { bold: true, size: 13, color: { argb: COLOR.overviewFg } };
|
|
341
|
+
titleCell.fill = solidFill(COLOR.headerBg);
|
|
342
|
+
titleCell.alignment = { horizontal: "left", vertical: "middle", indent: 1 };
|
|
343
|
+
sheet.getRow(1).height = 26;
|
|
344
|
+
const backRow = sheet.addRow([labels.backToOverview]);
|
|
345
|
+
setHyperlink(backRow.getCell(1), labels.backToOverview, "Overview");
|
|
346
|
+
backRow.getCell(1).font = { color: { argb: COLOR.link }, underline: true, size: 10 };
|
|
347
|
+
sheet.addRow([]);
|
|
348
|
+
const metaData = [
|
|
349
|
+
[labels.tablePhysicalName, table.name],
|
|
350
|
+
[labels.tableLogicalName, displayValue(table.comment, labels)],
|
|
351
|
+
...table.schema ? [[labels.schema, table.schema]] : [],
|
|
352
|
+
[labels.columnsCount, String(table.columns.length)],
|
|
353
|
+
[labels.primaryKey, displayValue(table.primaryKeys.join(", "), labels)],
|
|
354
|
+
[
|
|
355
|
+
labels.foreignKeys,
|
|
356
|
+
table.foreignKeys.length > 0 ? table.foreignKeys.map((fk) => {
|
|
357
|
+
const name = fk.name ? ` (${fk.name})` : "";
|
|
358
|
+
return `${fk.columns.join(", ")} \u2192 ${fk.referencedTable}.${fk.referencedColumns.join(", ")}${name}`;
|
|
359
|
+
}).join("; ") : labels.none
|
|
360
|
+
],
|
|
361
|
+
[
|
|
362
|
+
labels.indexes,
|
|
363
|
+
indexes.length > 0 ? indexes.map(
|
|
364
|
+
(idx) => `${idx.name} (${idx.columns.join(", ")})${idx.unique ? " UNIQUE" : ""}`
|
|
365
|
+
).join("; ") : labels.none
|
|
366
|
+
]
|
|
367
|
+
];
|
|
368
|
+
for (const [field, value] of metaData) {
|
|
369
|
+
const row = sheet.addRow([field, value]);
|
|
370
|
+
styleMetaRow(row);
|
|
371
|
+
applyBorderToRow(row, 2);
|
|
372
|
+
row.getCell(2).alignment = { wrapText: true, vertical: "top" };
|
|
373
|
+
}
|
|
374
|
+
sheet.addRow([]);
|
|
375
|
+
const headerRow = sheet.addRow([
|
|
376
|
+
labels.physicalName,
|
|
377
|
+
labels.logicalName,
|
|
378
|
+
labels.type,
|
|
379
|
+
labels.required,
|
|
380
|
+
labels.defaultValue,
|
|
381
|
+
labels.notes
|
|
382
|
+
]);
|
|
383
|
+
styleColorRow(headerRow, COLOR.headerBg, COLOR.headerFg);
|
|
384
|
+
applyBorderToRow(headerRow, 6);
|
|
385
|
+
const headerRowNum = headerRow.number;
|
|
386
|
+
for (const [i, column] of table.columns.entries()) {
|
|
387
|
+
const markers = [];
|
|
388
|
+
if (column.isPrimaryKey) markers.push(labels.pkMarker);
|
|
389
|
+
if (column.isForeignKey) markers.push(labels.fkMarker);
|
|
390
|
+
const notes = [markers.join(", "), column.description?.value ?? ""].filter(Boolean).join(" | ");
|
|
391
|
+
const row = sheet.addRow([
|
|
392
|
+
column.name,
|
|
393
|
+
displayValue(column.comment, labels),
|
|
394
|
+
column.type,
|
|
395
|
+
column.nullable ? labels.no : labels.yes,
|
|
396
|
+
column.defaultValue ?? "-",
|
|
397
|
+
notes || "-"
|
|
398
|
+
]);
|
|
399
|
+
if (column.isPrimaryKey) {
|
|
400
|
+
shadeRow(row, 6, COLOR.pkBg);
|
|
401
|
+
row.getCell(1).font = { bold: true };
|
|
402
|
+
} else if (column.isForeignKey) {
|
|
403
|
+
shadeRow(row, 6, COLOR.fkBg);
|
|
404
|
+
} else if (i % 2 === 1) {
|
|
405
|
+
shadeRow(row, 6, COLOR.altRow);
|
|
406
|
+
}
|
|
407
|
+
row.getCell(4).alignment = { horizontal: "center" };
|
|
408
|
+
applyBorderToRow(row, 6);
|
|
409
|
+
}
|
|
410
|
+
sheet.columns = [
|
|
411
|
+
{ width: 24 },
|
|
412
|
+
{ width: 28 },
|
|
413
|
+
{ width: 18 },
|
|
414
|
+
{ width: 10 },
|
|
415
|
+
{ width: 18 },
|
|
416
|
+
{ width: 36 }
|
|
417
|
+
];
|
|
418
|
+
sheet.views = [{ state: "frozen", ySplit: headerRowNum }];
|
|
419
|
+
}
|
|
420
|
+
function displayValue(value, labels) {
|
|
421
|
+
const trimmed = value?.trim();
|
|
422
|
+
return trimmed ? trimmed : labels.none;
|
|
423
|
+
}
|
|
424
|
+
function buildSheetName(tableName, existing) {
|
|
425
|
+
const base = tableName.slice(0, 31);
|
|
426
|
+
if (![...existing.values()].includes(base)) return base;
|
|
427
|
+
let suffix = 2;
|
|
428
|
+
while (suffix < 100) {
|
|
429
|
+
const candidate = `${tableName.slice(0, 28)}_${suffix}`;
|
|
430
|
+
if (![...existing.values()].includes(candidate)) return candidate;
|
|
431
|
+
suffix++;
|
|
432
|
+
}
|
|
433
|
+
return base;
|
|
434
|
+
}
|
|
435
|
+
function sheetLocation(sheetName, cellRef = "A1") {
|
|
436
|
+
if (/^[A-Za-z0-9_]+$/.test(sheetName)) {
|
|
437
|
+
return `${sheetName}!${cellRef}`;
|
|
438
|
+
}
|
|
439
|
+
const escaped = sheetName.replace(/'/g, "''");
|
|
440
|
+
return `'${escaped}'!${cellRef}`;
|
|
441
|
+
}
|
|
442
|
+
function setHyperlink(cell, text, sheetName) {
|
|
443
|
+
cell.value = { text, hyperlink: sheetLocation(sheetName) };
|
|
444
|
+
}
|
|
445
|
+
function solidFill(argb) {
|
|
446
|
+
return { type: "pattern", pattern: "solid", fgColor: { argb } };
|
|
447
|
+
}
|
|
448
|
+
function styleMetaRow(row) {
|
|
449
|
+
row.getCell(1).font = { bold: true, color: { argb: COLOR.metaFg } };
|
|
450
|
+
row.getCell(1).fill = solidFill(COLOR.metaBg);
|
|
451
|
+
row.getCell(2).fill = solidFill(COLOR.valueBg);
|
|
452
|
+
}
|
|
453
|
+
function styleColorRow(row, bgArgb, fgArgb) {
|
|
454
|
+
row.eachCell((cell) => {
|
|
455
|
+
cell.font = { bold: true, color: { argb: fgArgb } };
|
|
456
|
+
cell.fill = solidFill(bgArgb);
|
|
457
|
+
cell.alignment = { vertical: "middle", horizontal: "center" };
|
|
458
|
+
});
|
|
459
|
+
row.height = 22;
|
|
460
|
+
}
|
|
461
|
+
function shadeRow(row, colCount, argb) {
|
|
462
|
+
for (let c = 1; c <= colCount; c++) {
|
|
463
|
+
row.getCell(c).fill = solidFill(argb);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function applyBorderToRow(row, colCount) {
|
|
467
|
+
const border = { style: "thin", color: { argb: COLOR.border } };
|
|
468
|
+
for (let c = 1; c <= colCount; c++) {
|
|
469
|
+
row.getCell(c).border = {
|
|
470
|
+
top: border,
|
|
471
|
+
left: border,
|
|
472
|
+
bottom: border,
|
|
473
|
+
right: border
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function collectTableIndexes(table, doc) {
|
|
478
|
+
return [
|
|
479
|
+
...table.indexes,
|
|
480
|
+
...doc.indexes.filter(
|
|
481
|
+
(idx) => idx.table === table.name && !table.indexes.some((tableIdx) => tableIdx.name === idx.name)
|
|
482
|
+
)
|
|
483
|
+
];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/exporters/markdown/markdown-exporter.ts
|
|
487
|
+
import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
|
|
488
|
+
import { join as join3 } from "path";
|
|
489
|
+
|
|
490
|
+
// src/core/sanitize.ts
|
|
491
|
+
function sanitizeFilename(name) {
|
|
492
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/exporters/markdown/markdown-exporter.ts
|
|
496
|
+
async function exportMarkdownDocs(doc, options) {
|
|
497
|
+
try {
|
|
498
|
+
const tablesDir = join3(options.outDir, "tables");
|
|
499
|
+
await mkdir3(tablesDir, { recursive: true });
|
|
500
|
+
const labels = getOutputLabels(options.language);
|
|
501
|
+
for (const table of doc.tables) {
|
|
502
|
+
await writeFile2(
|
|
503
|
+
join3(tablesDir, `${sanitizeFilename(table.name)}.md`),
|
|
504
|
+
renderTableDoc(table, doc, labels),
|
|
505
|
+
"utf8"
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
} catch (err) {
|
|
509
|
+
throw new Error(
|
|
510
|
+
`Failed to export Markdown docs: ${err instanceof Error ? err.message : String(err)}`,
|
|
511
|
+
{ cause: err }
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function renderTableDoc(table, doc, labels) {
|
|
516
|
+
const lines = [];
|
|
517
|
+
const tableIndexes = collectTableIndexes2(table, doc);
|
|
518
|
+
lines.push(`# ${escapeMd(table.name)}`);
|
|
519
|
+
lines.push("");
|
|
520
|
+
lines.push(`## ${labels.tableInfoHeading}`);
|
|
521
|
+
lines.push("");
|
|
522
|
+
lines.push(`| ${labels.metaField} | ${labels.metaValue} |`);
|
|
523
|
+
lines.push("|------|----|");
|
|
524
|
+
lines.push(`| ${labels.tablePhysicalName} | ${escapeMd(table.name)} |`);
|
|
525
|
+
lines.push(`| ${labels.tableLogicalName} | ${escapeMd(table.comment ?? "")} |`);
|
|
526
|
+
lines.push(`| ${labels.schema} | ${escapeMd(table.schema ?? "")} |`);
|
|
527
|
+
lines.push(
|
|
528
|
+
`| ${labels.primaryKey} | ${escapeMd(table.primaryKeys.join(", ") || labels.none)} |`
|
|
529
|
+
);
|
|
530
|
+
lines.push(
|
|
531
|
+
`| ${labels.foreignKeys} | ${escapeMd(
|
|
532
|
+
table.foreignKeys.length > 0 ? table.foreignKeys.map((fk) => {
|
|
533
|
+
const name = fk.name ? ` (${fk.name})` : "";
|
|
534
|
+
return `${fk.columns.join(", ")} -> ${fk.referencedTable}.${fk.referencedColumns.join(", ")}${name}`;
|
|
535
|
+
}).join("; ") : labels.none
|
|
536
|
+
)} |`
|
|
537
|
+
);
|
|
538
|
+
lines.push(
|
|
539
|
+
`| ${labels.indexes} | ${escapeMd(
|
|
540
|
+
tableIndexes.length > 0 ? tableIndexes.map(
|
|
541
|
+
(idx) => `${idx.name} (${idx.columns.join(", ")})${idx.unique ? " UNIQUE" : ""}`
|
|
542
|
+
).join("; ") : labels.none
|
|
543
|
+
)} |`
|
|
544
|
+
);
|
|
545
|
+
lines.push("");
|
|
546
|
+
lines.push(`## ${labels.columnsHeading}`);
|
|
547
|
+
lines.push("");
|
|
548
|
+
lines.push(
|
|
549
|
+
`| ${labels.physicalName} | ${labels.logicalName} | ${labels.type} | ${labels.required} | ${labels.defaultValue} | ${labels.notes} |`
|
|
550
|
+
);
|
|
551
|
+
lines.push("|--------|--------|----|------|--------------|------|");
|
|
552
|
+
for (const col of table.columns) {
|
|
553
|
+
lines.push(
|
|
554
|
+
`| ${escapeMd(col.name)} | ${escapeMd(col.comment ?? "")} | ${escapeMd(col.type)} | ${col.nullable ? labels.no : labels.yes} | ${escapeMd(col.defaultValue ?? "-")} | ${escapeMd(col.description?.value ?? "")} |`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
lines.push("");
|
|
558
|
+
return lines.join("\n");
|
|
559
|
+
}
|
|
560
|
+
function collectTableIndexes2(table, doc) {
|
|
561
|
+
return [
|
|
562
|
+
...table.indexes,
|
|
563
|
+
...doc.indexes.filter(
|
|
564
|
+
(idx) => idx.table === table.name && !table.indexes.some((tableIdx) => tableIdx.name === idx.name)
|
|
565
|
+
)
|
|
566
|
+
];
|
|
567
|
+
}
|
|
568
|
+
function escapeMd(text) {
|
|
569
|
+
return text.replace(/([|*_`\[\]<>#~\\])/g, "\\$1");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/exporters/html/html-exporter.ts
|
|
573
|
+
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
574
|
+
import { join as join4 } from "path";
|
|
575
|
+
async function exportHtmlDocs(doc, options) {
|
|
576
|
+
try {
|
|
577
|
+
const htmlDir = join4(options.outDir, "html");
|
|
578
|
+
const tablesDir = join4(htmlDir, "tables");
|
|
579
|
+
await mkdir4(tablesDir, { recursive: true });
|
|
580
|
+
const labels = getOutputLabels(options.language);
|
|
581
|
+
await writeFile3(
|
|
582
|
+
join4(htmlDir, "index.html"),
|
|
583
|
+
renderIndexPage(doc, labels),
|
|
584
|
+
"utf8"
|
|
585
|
+
);
|
|
586
|
+
for (const table of doc.tables) {
|
|
587
|
+
await writeFile3(
|
|
588
|
+
join4(tablesDir, `${sanitizeFilename(table.name)}.html`),
|
|
589
|
+
renderTablePage(table, doc, labels),
|
|
590
|
+
"utf8"
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
} catch (err) {
|
|
594
|
+
throw new Error(
|
|
595
|
+
`Failed to export HTML docs: ${err instanceof Error ? err.message : String(err)}`,
|
|
596
|
+
{ cause: err }
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
var CSS = `
|
|
601
|
+
:root {
|
|
602
|
+
--bg: #f3f4f6;
|
|
603
|
+
--paper: #ffffff;
|
|
604
|
+
--text: #111827;
|
|
605
|
+
--muted: #4b5563;
|
|
606
|
+
--border: #bfc7d4;
|
|
607
|
+
--accent: #4472c4;
|
|
608
|
+
--accent-light: #dbe5f1;
|
|
609
|
+
--accent-mid: #eef3f8;
|
|
610
|
+
--pk-bg: #fff3cd;
|
|
611
|
+
--fk-bg: #e8f4fd;
|
|
612
|
+
}
|
|
613
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
614
|
+
body {
|
|
615
|
+
font-family: "Yu Gothic UI", "Meiryo", Arial, sans-serif;
|
|
616
|
+
background: var(--bg); color: var(--text); line-height: 1.5;
|
|
617
|
+
padding: 24px;
|
|
618
|
+
}
|
|
619
|
+
a { color: var(--accent); text-decoration: none; }
|
|
620
|
+
a:hover { text-decoration: underline; }
|
|
621
|
+
.sheet {
|
|
622
|
+
max-width: 1200px;
|
|
623
|
+
margin: 0 auto;
|
|
624
|
+
background: var(--paper);
|
|
625
|
+
border: 1px solid var(--border);
|
|
626
|
+
padding: 24px;
|
|
627
|
+
}
|
|
628
|
+
h1 { font-size: 22px; margin-bottom: 16px; color: var(--accent); border-bottom: 2px solid var(--accent-light); padding-bottom: 8px; }
|
|
629
|
+
h2 { font-size: 15px; margin: 20px 0 8px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
|
|
630
|
+
table { width: 100%; border-collapse: collapse; table-layout: fixed; margin-bottom: 16px; }
|
|
631
|
+
th, td {
|
|
632
|
+
border: 1px solid var(--border);
|
|
633
|
+
padding: 7px 10px;
|
|
634
|
+
text-align: left;
|
|
635
|
+
vertical-align: top;
|
|
636
|
+
word-break: break-word;
|
|
637
|
+
font-size: 13px;
|
|
638
|
+
}
|
|
639
|
+
thead th { background: var(--accent); color: #fff; font-weight: 700; }
|
|
640
|
+
.meta th { background: var(--accent-light); font-weight: 700; color: #1f3864; width: 180px; }
|
|
641
|
+
.meta td { background: #fafbfe; }
|
|
642
|
+
.pk td:first-child { font-weight: 700; }
|
|
643
|
+
.pk { background: var(--pk-bg); }
|
|
644
|
+
.fk { background: var(--fk-bg); }
|
|
645
|
+
.badge {
|
|
646
|
+
display: inline-block; font-size: 10px; font-weight: 700;
|
|
647
|
+
padding: 1px 5px; border-radius: 3px; margin-left: 4px; vertical-align: middle;
|
|
648
|
+
}
|
|
649
|
+
.badge-pk { background: #f59e0b; color: #fff; }
|
|
650
|
+
.badge-fk { background: var(--accent); color: #fff; }
|
|
651
|
+
.note { color: var(--muted); font-size: 12px; margin-top: 10px; }
|
|
652
|
+
.back { margin-bottom: 16px; font-size: 13px; }
|
|
653
|
+
.summary { display: flex; gap: 24px; margin-bottom: 20px; }
|
|
654
|
+
.summary-item { background: var(--accent-light); border-radius: 6px; padding: 10px 18px; }
|
|
655
|
+
.summary-item .num { font-size: 24px; font-weight: 700; color: var(--accent); }
|
|
656
|
+
.summary-item .lbl { font-size: 12px; color: var(--muted); }
|
|
657
|
+
.table-list { width: 100%; border-collapse: collapse; margin-top: 8px; }
|
|
658
|
+
.table-list th { background: var(--accent); color: #fff; }
|
|
659
|
+
.table-list tr:nth-child(even) td { background: var(--accent-mid); }
|
|
660
|
+
.table-list td:first-child a { font-weight: 600; }
|
|
661
|
+
`;
|
|
662
|
+
function pageShell(title, body, fromSubdir = false) {
|
|
663
|
+
const base = fromSubdir ? "../" : "";
|
|
664
|
+
return `<!DOCTYPE html>
|
|
665
|
+
<html lang="en">
|
|
666
|
+
<head>
|
|
667
|
+
<meta charset="UTF-8">
|
|
668
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
669
|
+
<title>${esc(title)}</title>
|
|
670
|
+
<style>${CSS} </style>
|
|
671
|
+
</head>
|
|
672
|
+
<body>
|
|
673
|
+
<div class="sheet">
|
|
674
|
+
${body}
|
|
675
|
+
</div>
|
|
676
|
+
</body>
|
|
677
|
+
</html>`;
|
|
678
|
+
}
|
|
679
|
+
function renderIndexPage(doc, labels) {
|
|
680
|
+
let tableRows = "";
|
|
681
|
+
for (const table of doc.tables) {
|
|
682
|
+
const pkCols = table.primaryKeys.join(", ") || labels.none;
|
|
683
|
+
const fkCount = table.foreignKeys.length;
|
|
684
|
+
const fileName = sanitizeFilename(table.name);
|
|
685
|
+
tableRows += ` <tr>
|
|
686
|
+
<td><a href="tables/${fileName}.html">${esc(table.name)}</a></td>
|
|
687
|
+
<td>${esc(table.comment ?? "")}</td>
|
|
688
|
+
<td style="text-align:center">${table.columns.length}</td>
|
|
689
|
+
<td>${esc(pkCols)}</td>
|
|
690
|
+
<td style="text-align:center">${fkCount}</td>
|
|
691
|
+
</tr>
|
|
692
|
+
`;
|
|
693
|
+
}
|
|
694
|
+
const body = `
|
|
695
|
+
<h1>${esc(labels.docTitle)}</h1>
|
|
696
|
+
<div class="summary">
|
|
697
|
+
<div class="summary-item"><div class="num">${doc.tables.length}</div><div class="lbl">${esc(labels.tablesLabel)}</div></div>
|
|
698
|
+
<div class="summary-item"><div class="num">${doc.relationships.length}</div><div class="lbl">${esc(labels.relationshipsLabel)}</div></div>
|
|
699
|
+
<div class="summary-item"><div class="num">${doc.dialect}</div><div class="lbl">${esc(labels.dialectLabel)}</div></div>
|
|
700
|
+
</div>
|
|
701
|
+
<h2>${esc(labels.tableListHeading)}</h2>
|
|
702
|
+
<table class="table-list">
|
|
703
|
+
<thead><tr>
|
|
704
|
+
<th>${esc(labels.tableLabel)}</th>
|
|
705
|
+
<th>${esc(labels.tableLogicalName)}</th>
|
|
706
|
+
<th style="width:70px;text-align:center">Cols</th>
|
|
707
|
+
<th>${esc(labels.primaryKey)}</th>
|
|
708
|
+
<th style="width:50px;text-align:center">FK</th>
|
|
709
|
+
</tr></thead>
|
|
710
|
+
<tbody>
|
|
711
|
+
${tableRows} </tbody>
|
|
712
|
+
</table>
|
|
713
|
+
<p class="note">${esc(labels.generatedNote)}</p>
|
|
714
|
+
`;
|
|
715
|
+
return pageShell(labels.docTitle, body);
|
|
716
|
+
}
|
|
717
|
+
function renderTablePage(table, doc, labels) {
|
|
718
|
+
const indexes = collectTableIndexes3(table, doc);
|
|
719
|
+
const foreignKeys = table.foreignKeys.length ? table.foreignKeys.map((fk) => {
|
|
720
|
+
const name = fk.name ? ` (${fk.name})` : "";
|
|
721
|
+
return `${fk.columns.join(", ")} \u2192 ${fk.referencedTable}.${fk.referencedColumns.join(", ")}${name}`;
|
|
722
|
+
}).join("<br>") : labels.none;
|
|
723
|
+
const indexText = indexes.length ? indexes.map(
|
|
724
|
+
(idx) => `${idx.name} (${idx.columns.join(", ")})${idx.unique ? " UNIQUE" : ""}`
|
|
725
|
+
).join("<br>") : labels.none;
|
|
726
|
+
let colRows = "";
|
|
727
|
+
for (const col of table.columns) {
|
|
728
|
+
const pkBadge = col.isPrimaryKey ? `<span class="badge badge-pk">PK</span>` : "";
|
|
729
|
+
const fkBadge = col.isForeignKey ? `<span class="badge badge-fk">FK</span>` : "";
|
|
730
|
+
const rowClass = col.isPrimaryKey ? "pk" : col.isForeignKey ? "fk" : "";
|
|
731
|
+
const required = col.nullable ? labels.no : labels.yes;
|
|
732
|
+
colRows += ` <tr${rowClass ? ` class="${rowClass}"` : ""}><td>${esc(col.name)}${pkBadge}${fkBadge}</td><td>${esc(col.comment ?? "")}</td><td>${esc(col.type)}</td><td>${required}</td><td>${esc(col.defaultValue ?? "-")}</td><td>${esc(col.description?.value ?? "")}</td></tr>
|
|
733
|
+
`;
|
|
734
|
+
}
|
|
735
|
+
const body = `
|
|
736
|
+
<p class="back"><a href="../index.html">\u2190 ${esc(labels.tableListHeading)}</a></p>
|
|
737
|
+
<h1>${esc(table.name)}</h1>
|
|
738
|
+
<h2>${esc(labels.tableInfoHeading)}</h2>
|
|
739
|
+
<table class="meta">
|
|
740
|
+
<tbody>
|
|
741
|
+
<tr><th>${esc(labels.tablePhysicalName)}</th><td>${esc(table.name)}</td></tr>
|
|
742
|
+
<tr><th>${esc(labels.tableLogicalName)}</th><td>${esc(table.comment ?? "")}</td></tr>
|
|
743
|
+
<tr><th>${esc(labels.schema)}</th><td>${esc(table.schema ?? "")}</td></tr>
|
|
744
|
+
<tr><th>${esc(labels.primaryKey)}</th><td>${esc(table.primaryKeys.join(", ") || labels.none)}</td></tr>
|
|
745
|
+
<tr><th>${esc(labels.foreignKeys)}</th><td>${foreignKeys}</td></tr>
|
|
746
|
+
<tr><th>${esc(labels.indexes)}</th><td>${indexText}</td></tr>
|
|
747
|
+
</tbody>
|
|
748
|
+
</table>
|
|
749
|
+
|
|
750
|
+
<h2>${esc(labels.columnsHeading)}</h2>
|
|
751
|
+
<table class="columns">
|
|
752
|
+
<thead><tr>
|
|
753
|
+
<th>${esc(labels.physicalName)}</th>
|
|
754
|
+
<th>${esc(labels.logicalName)}</th>
|
|
755
|
+
<th>${esc(labels.type)}</th>
|
|
756
|
+
<th>${esc(labels.required)}</th>
|
|
757
|
+
<th>${esc(labels.defaultValue)}</th>
|
|
758
|
+
<th>${esc(labels.notes)}</th>
|
|
759
|
+
</tr></thead>
|
|
760
|
+
<tbody>
|
|
761
|
+
${colRows} </tbody>
|
|
762
|
+
</table>
|
|
763
|
+
<p class="note">${esc(labels.generatedNote)}</p>
|
|
764
|
+
`;
|
|
765
|
+
return pageShell(table.name, body);
|
|
766
|
+
}
|
|
767
|
+
function collectTableIndexes3(table, doc) {
|
|
768
|
+
return [
|
|
769
|
+
...table.indexes,
|
|
770
|
+
...doc.indexes.filter(
|
|
771
|
+
(idx) => idx.table === table.name && !table.indexes.some((tableIdx) => tableIdx.name === idx.name)
|
|
772
|
+
)
|
|
773
|
+
];
|
|
774
|
+
}
|
|
775
|
+
function esc(text) {
|
|
776
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// src/exporters/word/word-exporter.ts
|
|
780
|
+
import { mkdir as mkdir5, writeFile as writeFile4 } from "fs/promises";
|
|
781
|
+
import { join as join5 } from "path";
|
|
782
|
+
import {
|
|
783
|
+
Document,
|
|
784
|
+
Packer,
|
|
785
|
+
Paragraph,
|
|
786
|
+
Table,
|
|
787
|
+
TableCell,
|
|
788
|
+
TableRow,
|
|
789
|
+
TextRun,
|
|
790
|
+
HeadingLevel
|
|
791
|
+
} from "docx";
|
|
792
|
+
async function exportWordDocument(doc, options) {
|
|
793
|
+
try {
|
|
794
|
+
await mkdir5(options.outDir, { recursive: true });
|
|
795
|
+
const labels = getOutputLabels(options.language);
|
|
796
|
+
const children = [];
|
|
797
|
+
children.push(
|
|
798
|
+
new Paragraph({
|
|
799
|
+
heading: HeadingLevel.HEADING_1,
|
|
800
|
+
children: [new TextRun(labels.docTitle)]
|
|
801
|
+
})
|
|
802
|
+
);
|
|
803
|
+
children.push(
|
|
804
|
+
new Paragraph({
|
|
805
|
+
heading: HeadingLevel.HEADING_2,
|
|
806
|
+
children: [new TextRun(labels.overviewHeading)]
|
|
807
|
+
})
|
|
808
|
+
);
|
|
809
|
+
children.push(
|
|
810
|
+
new Paragraph({
|
|
811
|
+
children: [new TextRun(`${labels.dialectLabel}: ${doc.dialect}`)]
|
|
812
|
+
})
|
|
813
|
+
);
|
|
814
|
+
children.push(
|
|
815
|
+
new Paragraph({
|
|
816
|
+
children: [new TextRun(`${labels.tablesLabel}: ${doc.tables.length}`)]
|
|
817
|
+
})
|
|
818
|
+
);
|
|
819
|
+
children.push(
|
|
820
|
+
new Paragraph({
|
|
821
|
+
children: [new TextRun(`${labels.relationshipsLabel}: ${doc.relationships.length}`)]
|
|
822
|
+
})
|
|
823
|
+
);
|
|
824
|
+
children.push(
|
|
825
|
+
new Paragraph({
|
|
826
|
+
heading: HeadingLevel.HEADING_2,
|
|
827
|
+
children: [new TextRun(labels.tableListHeading)]
|
|
828
|
+
})
|
|
829
|
+
);
|
|
830
|
+
if (doc.tables.length > 0) {
|
|
831
|
+
const tableListRows = [
|
|
832
|
+
new TableRow({
|
|
833
|
+
children: [
|
|
834
|
+
new TableCell({
|
|
835
|
+
children: [
|
|
836
|
+
new Paragraph({
|
|
837
|
+
children: [new TextRun({ text: labels.tableLabel, bold: true })]
|
|
838
|
+
})
|
|
839
|
+
]
|
|
840
|
+
}),
|
|
841
|
+
new TableCell({
|
|
842
|
+
children: [
|
|
843
|
+
new Paragraph({
|
|
844
|
+
children: [new TextRun({ text: labels.descriptionLabel, bold: true })]
|
|
845
|
+
})
|
|
846
|
+
]
|
|
847
|
+
})
|
|
848
|
+
]
|
|
849
|
+
})
|
|
850
|
+
];
|
|
851
|
+
for (const table of doc.tables) {
|
|
852
|
+
tableListRows.push(
|
|
853
|
+
new TableRow({
|
|
854
|
+
children: [
|
|
855
|
+
new TableCell({
|
|
856
|
+
children: [
|
|
857
|
+
new Paragraph({ children: [new TextRun(table.name)] })
|
|
858
|
+
]
|
|
859
|
+
}),
|
|
860
|
+
new TableCell({
|
|
861
|
+
children: [
|
|
862
|
+
new Paragraph({
|
|
863
|
+
children: [
|
|
864
|
+
new TextRun(
|
|
865
|
+
table.description?.value ?? table.comment ?? ""
|
|
866
|
+
)
|
|
867
|
+
]
|
|
868
|
+
})
|
|
869
|
+
]
|
|
870
|
+
})
|
|
871
|
+
]
|
|
872
|
+
})
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
children.push(new Table({ rows: tableListRows }));
|
|
876
|
+
}
|
|
877
|
+
for (const table of doc.tables) {
|
|
878
|
+
children.push(...renderTableDetail(table, doc, labels));
|
|
879
|
+
}
|
|
880
|
+
children.push(
|
|
881
|
+
new Paragraph({
|
|
882
|
+
heading: HeadingLevel.HEADING_2,
|
|
883
|
+
children: [new TextRun(labels.relationshipsHeading)]
|
|
884
|
+
})
|
|
885
|
+
);
|
|
886
|
+
if (doc.relationships.length > 0) {
|
|
887
|
+
const relHeaderCells = [
|
|
888
|
+
labels.fromTable,
|
|
889
|
+
labels.fromColumn,
|
|
890
|
+
labels.toTable,
|
|
891
|
+
labels.toColumn,
|
|
892
|
+
labels.constraint,
|
|
893
|
+
labels.source,
|
|
894
|
+
labels.needsReview
|
|
895
|
+
].map(
|
|
896
|
+
(h) => new TableCell({
|
|
897
|
+
children: [
|
|
898
|
+
new Paragraph({
|
|
899
|
+
children: [new TextRun({ text: h, bold: true })]
|
|
900
|
+
})
|
|
901
|
+
]
|
|
902
|
+
})
|
|
903
|
+
);
|
|
904
|
+
const relRows = [new TableRow({ children: relHeaderCells })];
|
|
905
|
+
for (const rel of doc.relationships) {
|
|
906
|
+
relRows.push(
|
|
907
|
+
new TableRow({
|
|
908
|
+
children: [
|
|
909
|
+
new TableCell({
|
|
910
|
+
children: [
|
|
911
|
+
new Paragraph({ children: [new TextRun(rel.fromTable)] })
|
|
912
|
+
]
|
|
913
|
+
}),
|
|
914
|
+
new TableCell({
|
|
915
|
+
children: [
|
|
916
|
+
new Paragraph({ children: [new TextRun(rel.fromColumn)] })
|
|
917
|
+
]
|
|
918
|
+
}),
|
|
919
|
+
new TableCell({
|
|
920
|
+
children: [
|
|
921
|
+
new Paragraph({ children: [new TextRun(rel.toTable)] })
|
|
922
|
+
]
|
|
923
|
+
}),
|
|
924
|
+
new TableCell({
|
|
925
|
+
children: [
|
|
926
|
+
new Paragraph({ children: [new TextRun(rel.toColumn)] })
|
|
927
|
+
]
|
|
928
|
+
}),
|
|
929
|
+
new TableCell({
|
|
930
|
+
children: [
|
|
931
|
+
new Paragraph({
|
|
932
|
+
children: [new TextRun(rel.constraintName ?? "")]
|
|
933
|
+
})
|
|
934
|
+
]
|
|
935
|
+
}),
|
|
936
|
+
new TableCell({
|
|
937
|
+
children: [
|
|
938
|
+
new Paragraph({ children: [new TextRun(rel.source)] })
|
|
939
|
+
]
|
|
940
|
+
}),
|
|
941
|
+
new TableCell({
|
|
942
|
+
children: [
|
|
943
|
+
new Paragraph({
|
|
944
|
+
children: [new TextRun(rel.needsReview ? labels.yes : labels.no)]
|
|
945
|
+
})
|
|
946
|
+
]
|
|
947
|
+
})
|
|
948
|
+
]
|
|
949
|
+
})
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
children.push(new Table({ rows: relRows }));
|
|
953
|
+
} else {
|
|
954
|
+
children.push(
|
|
955
|
+
new Paragraph({
|
|
956
|
+
children: [new TextRun(labels.none)]
|
|
957
|
+
})
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
children.push(
|
|
961
|
+
new Paragraph({
|
|
962
|
+
heading: HeadingLevel.HEADING_2,
|
|
963
|
+
children: [new TextRun(labels.warningsHeading)]
|
|
964
|
+
})
|
|
965
|
+
);
|
|
966
|
+
if (doc.warnings.length > 0) {
|
|
967
|
+
const warnHeaderCells = [
|
|
968
|
+
labels.severity,
|
|
969
|
+
labels.code,
|
|
970
|
+
labels.target,
|
|
971
|
+
labels.message
|
|
972
|
+
].map(
|
|
973
|
+
(h) => new TableCell({
|
|
974
|
+
children: [
|
|
975
|
+
new Paragraph({
|
|
976
|
+
children: [new TextRun({ text: h, bold: true })]
|
|
977
|
+
})
|
|
978
|
+
]
|
|
979
|
+
})
|
|
980
|
+
);
|
|
981
|
+
const warnRows = [new TableRow({ children: warnHeaderCells })];
|
|
982
|
+
for (const warning of doc.warnings) {
|
|
983
|
+
warnRows.push(
|
|
984
|
+
new TableRow({
|
|
985
|
+
children: [
|
|
986
|
+
new TableCell({
|
|
987
|
+
children: [
|
|
988
|
+
new Paragraph({ children: [new TextRun(warning.severity)] })
|
|
989
|
+
]
|
|
990
|
+
}),
|
|
991
|
+
new TableCell({
|
|
992
|
+
children: [
|
|
993
|
+
new Paragraph({ children: [new TextRun(warning.code)] })
|
|
994
|
+
]
|
|
995
|
+
}),
|
|
996
|
+
new TableCell({
|
|
997
|
+
children: [
|
|
998
|
+
new Paragraph({
|
|
999
|
+
children: [new TextRun(warning.target ?? "")]
|
|
1000
|
+
})
|
|
1001
|
+
]
|
|
1002
|
+
}),
|
|
1003
|
+
new TableCell({
|
|
1004
|
+
children: [
|
|
1005
|
+
new Paragraph({ children: [new TextRun(warning.message)] })
|
|
1006
|
+
]
|
|
1007
|
+
})
|
|
1008
|
+
]
|
|
1009
|
+
})
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
children.push(new Table({ rows: warnRows }));
|
|
1013
|
+
} else {
|
|
1014
|
+
children.push(
|
|
1015
|
+
new Paragraph({
|
|
1016
|
+
children: [new TextRun(labels.none)]
|
|
1017
|
+
})
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
const wordDoc = new Document({
|
|
1021
|
+
sections: [{ children }]
|
|
1022
|
+
});
|
|
1023
|
+
const buffer = await Packer.toBuffer(wordDoc);
|
|
1024
|
+
await writeFile4(join5(options.outDir, "database_document.docx"), buffer);
|
|
1025
|
+
} catch (err) {
|
|
1026
|
+
throw new Error(
|
|
1027
|
+
`Failed to export Word document: ${err instanceof Error ? err.message : String(err)}`,
|
|
1028
|
+
{ cause: err }
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
function renderTableDetail(table, doc, labels) {
|
|
1033
|
+
const items = [];
|
|
1034
|
+
const indexes = collectTableIndexes4(table, doc);
|
|
1035
|
+
items.push(
|
|
1036
|
+
new Paragraph({
|
|
1037
|
+
heading: HeadingLevel.HEADING_2,
|
|
1038
|
+
children: [new TextRun(table.name)]
|
|
1039
|
+
})
|
|
1040
|
+
);
|
|
1041
|
+
items.push(
|
|
1042
|
+
new Paragraph({
|
|
1043
|
+
heading: HeadingLevel.HEADING_3,
|
|
1044
|
+
children: [new TextRun(labels.tableInfoHeading)]
|
|
1045
|
+
})
|
|
1046
|
+
);
|
|
1047
|
+
items.push(
|
|
1048
|
+
renderMetaTable([
|
|
1049
|
+
[labels.tablePhysicalName, table.name],
|
|
1050
|
+
[labels.tableLogicalName, table.comment ?? ""],
|
|
1051
|
+
[labels.schema, table.schema ?? ""],
|
|
1052
|
+
[labels.primaryKey, table.primaryKeys.join(", ") || labels.none],
|
|
1053
|
+
[
|
|
1054
|
+
labels.foreignKeys,
|
|
1055
|
+
table.foreignKeys.length > 0 ? table.foreignKeys.map((fk) => {
|
|
1056
|
+
const name = fk.name ? ` (${fk.name})` : "";
|
|
1057
|
+
return `${fk.columns.join(", ")} -> ${fk.referencedTable}.${fk.referencedColumns.join(", ")}${name}`;
|
|
1058
|
+
}).join("; ") : labels.none
|
|
1059
|
+
],
|
|
1060
|
+
[
|
|
1061
|
+
labels.indexes,
|
|
1062
|
+
indexes.length > 0 ? indexes.map(
|
|
1063
|
+
(idx) => `${idx.name} (${idx.columns.join(", ")})${idx.unique ? " UNIQUE" : ""}`
|
|
1064
|
+
).join("; ") : labels.none
|
|
1065
|
+
]
|
|
1066
|
+
])
|
|
1067
|
+
);
|
|
1068
|
+
items.push(
|
|
1069
|
+
new Paragraph({
|
|
1070
|
+
heading: HeadingLevel.HEADING_3,
|
|
1071
|
+
children: [new TextRun(labels.columnsHeading)]
|
|
1072
|
+
})
|
|
1073
|
+
);
|
|
1074
|
+
items.push(renderColumnsTable(table, labels));
|
|
1075
|
+
items.push(
|
|
1076
|
+
new Paragraph({
|
|
1077
|
+
children: [new TextRun("")]
|
|
1078
|
+
})
|
|
1079
|
+
);
|
|
1080
|
+
return items;
|
|
1081
|
+
}
|
|
1082
|
+
function renderColumnsTable(table, labels) {
|
|
1083
|
+
const headerCells = [
|
|
1084
|
+
labels.physicalName,
|
|
1085
|
+
labels.logicalName,
|
|
1086
|
+
labels.type,
|
|
1087
|
+
labels.required,
|
|
1088
|
+
labels.defaultValue,
|
|
1089
|
+
labels.notes
|
|
1090
|
+
].map(
|
|
1091
|
+
(h) => new TableCell({
|
|
1092
|
+
children: [
|
|
1093
|
+
new Paragraph({ children: [new TextRun({ text: h, bold: true })] })
|
|
1094
|
+
]
|
|
1095
|
+
})
|
|
1096
|
+
);
|
|
1097
|
+
const colRows = [new TableRow({ children: headerCells })];
|
|
1098
|
+
for (const col of table.columns) {
|
|
1099
|
+
colRows.push(
|
|
1100
|
+
new TableRow({
|
|
1101
|
+
children: [
|
|
1102
|
+
new TableCell({
|
|
1103
|
+
children: [new Paragraph({ children: [new TextRun(col.name)] })]
|
|
1104
|
+
}),
|
|
1105
|
+
new TableCell({
|
|
1106
|
+
children: [
|
|
1107
|
+
new Paragraph({ children: [new TextRun(col.comment ?? "")] })
|
|
1108
|
+
]
|
|
1109
|
+
}),
|
|
1110
|
+
new TableCell({
|
|
1111
|
+
children: [new Paragraph({ children: [new TextRun(col.type)] })]
|
|
1112
|
+
}),
|
|
1113
|
+
new TableCell({
|
|
1114
|
+
children: [
|
|
1115
|
+
new Paragraph({
|
|
1116
|
+
children: [new TextRun(col.nullable ? labels.no : labels.yes)]
|
|
1117
|
+
})
|
|
1118
|
+
]
|
|
1119
|
+
}),
|
|
1120
|
+
new TableCell({
|
|
1121
|
+
children: [
|
|
1122
|
+
new Paragraph({
|
|
1123
|
+
children: [new TextRun(col.defaultValue ?? "-")]
|
|
1124
|
+
})
|
|
1125
|
+
]
|
|
1126
|
+
}),
|
|
1127
|
+
new TableCell({
|
|
1128
|
+
children: [
|
|
1129
|
+
new Paragraph({
|
|
1130
|
+
children: [new TextRun(col.description?.value ?? "")]
|
|
1131
|
+
})
|
|
1132
|
+
]
|
|
1133
|
+
})
|
|
1134
|
+
]
|
|
1135
|
+
})
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
return new Table({ rows: colRows });
|
|
1139
|
+
}
|
|
1140
|
+
function renderMetaTable(rows) {
|
|
1141
|
+
return new Table({
|
|
1142
|
+
rows: rows.map(
|
|
1143
|
+
([label, value]) => new TableRow({
|
|
1144
|
+
children: [
|
|
1145
|
+
new TableCell({
|
|
1146
|
+
children: [
|
|
1147
|
+
new Paragraph({
|
|
1148
|
+
children: [new TextRun({ text: label, bold: true })]
|
|
1149
|
+
})
|
|
1150
|
+
]
|
|
1151
|
+
}),
|
|
1152
|
+
new TableCell({
|
|
1153
|
+
children: [new Paragraph({ children: [new TextRun(value)] })]
|
|
1154
|
+
})
|
|
1155
|
+
]
|
|
1156
|
+
})
|
|
1157
|
+
)
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
function collectTableIndexes4(table, doc) {
|
|
1161
|
+
return [
|
|
1162
|
+
...table.indexes,
|
|
1163
|
+
...doc.indexes.filter(
|
|
1164
|
+
(idx) => idx.table === table.name && !table.indexes.some((tableIdx) => tableIdx.name === idx.name)
|
|
1165
|
+
)
|
|
1166
|
+
];
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// src/parsers/sql/sql-parser.ts
|
|
1170
|
+
import nodeSqlParser from "node-sql-parser";
|
|
1171
|
+
|
|
1172
|
+
// src/core/warnings.ts
|
|
1173
|
+
function createWarning(code, message, target) {
|
|
1174
|
+
return {
|
|
1175
|
+
code,
|
|
1176
|
+
message,
|
|
1177
|
+
target,
|
|
1178
|
+
severity: "warning"
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// src/parsers/sql/sql-normalizer.ts
|
|
1183
|
+
function normalizeSqlAst(ast, dialect) {
|
|
1184
|
+
const statements = Array.isArray(ast) ? ast : [ast];
|
|
1185
|
+
const tables = [];
|
|
1186
|
+
const indexes = [];
|
|
1187
|
+
const relationships = [];
|
|
1188
|
+
const warnings = [];
|
|
1189
|
+
for (const statement of statements) {
|
|
1190
|
+
if (statement.type === "create" && statement.keyword === "table") {
|
|
1191
|
+
const table = normalizeCreateTable(statement);
|
|
1192
|
+
tables.push(table);
|
|
1193
|
+
const result = relationshipsFromTable(table);
|
|
1194
|
+
relationships.push(...result.relationships);
|
|
1195
|
+
warnings.push(...result.warnings);
|
|
1196
|
+
}
|
|
1197
|
+
if (statement.type === "create" && statement.keyword === "index") {
|
|
1198
|
+
indexes.push(normalizeCreateIndex(statement));
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
for (const index of indexes) {
|
|
1202
|
+
const table = tables.find((candidate) => candidate.name === index.table);
|
|
1203
|
+
table?.indexes.push(index);
|
|
1204
|
+
}
|
|
1205
|
+
return {
|
|
1206
|
+
dialect,
|
|
1207
|
+
tables,
|
|
1208
|
+
relationships,
|
|
1209
|
+
indexes,
|
|
1210
|
+
warnings
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
function normalizeCreateTable(statement) {
|
|
1214
|
+
const tableName = extractTableName(statement.table);
|
|
1215
|
+
const createDefinitions = Array.isArray(statement.create_definitions) ? statement.create_definitions : [];
|
|
1216
|
+
const table = {
|
|
1217
|
+
name: tableName,
|
|
1218
|
+
columns: [],
|
|
1219
|
+
primaryKeys: [],
|
|
1220
|
+
foreignKeys: [],
|
|
1221
|
+
indexes: [],
|
|
1222
|
+
reviewTodos: []
|
|
1223
|
+
};
|
|
1224
|
+
for (const definition of createDefinitions) {
|
|
1225
|
+
if (definition.resource === "column") {
|
|
1226
|
+
const columnName = extractDeepColumnName(definition.column);
|
|
1227
|
+
const isPrimaryKey = hasPrimaryKey(definition);
|
|
1228
|
+
const isNotNull = hasNotNull(definition);
|
|
1229
|
+
table.columns.push({
|
|
1230
|
+
name: columnName,
|
|
1231
|
+
type: normalizeType(definition.definition),
|
|
1232
|
+
nullable: !isNotNull && !isPrimaryKey,
|
|
1233
|
+
defaultValue: extractDefaultFromDef(definition),
|
|
1234
|
+
isPrimaryKey,
|
|
1235
|
+
isForeignKey: false
|
|
1236
|
+
});
|
|
1237
|
+
if (isPrimaryKey) table.primaryKeys.push(columnName);
|
|
1238
|
+
}
|
|
1239
|
+
if (definition.resource === "constraint" && isConstraintType(definition.constraint_type, "PRIMARY KEY")) {
|
|
1240
|
+
table.primaryKeys = extractDeepColumnNames(definition.definition);
|
|
1241
|
+
for (const column of table.columns) {
|
|
1242
|
+
if (table.primaryKeys.includes(column.name)) column.isPrimaryKey = true;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
if (definition.resource === "constraint" && isConstraintType(definition.constraint_type, "FOREIGN KEY")) {
|
|
1246
|
+
const columns = extractDeepColumnNames(definition.definition);
|
|
1247
|
+
const refDef = definition.reference_definition;
|
|
1248
|
+
const referencedTable = extractTableName(refDef?.table);
|
|
1249
|
+
const referencedColumns = extractDeepColumnNames(refDef?.definition);
|
|
1250
|
+
table.foreignKeys.push({
|
|
1251
|
+
name: typeof definition.constraint === "string" ? definition.constraint : void 0,
|
|
1252
|
+
columns,
|
|
1253
|
+
referencedTable,
|
|
1254
|
+
referencedColumns
|
|
1255
|
+
});
|
|
1256
|
+
for (const column of table.columns) {
|
|
1257
|
+
if (columns.includes(column.name)) column.isForeignKey = true;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return table;
|
|
1262
|
+
}
|
|
1263
|
+
function normalizeCreateIndex(statement) {
|
|
1264
|
+
return {
|
|
1265
|
+
name: String(statement.index ?? statement.index_name ?? "unnamed_index"),
|
|
1266
|
+
table: extractTableName(statement.table),
|
|
1267
|
+
columns: extractDeepColumnNames(
|
|
1268
|
+
statement.index_columns ?? statement.columns
|
|
1269
|
+
),
|
|
1270
|
+
unique: Boolean(statement.unique)
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
function relationshipsFromTable(table) {
|
|
1274
|
+
const relationships = [];
|
|
1275
|
+
const warnings = [];
|
|
1276
|
+
for (const foreignKey of table.foreignKeys) {
|
|
1277
|
+
for (let index = 0; index < foreignKey.columns.length; index++) {
|
|
1278
|
+
const column = foreignKey.columns[index];
|
|
1279
|
+
let toColumn;
|
|
1280
|
+
let needsReview = false;
|
|
1281
|
+
if (foreignKey.referencedColumns[index]) {
|
|
1282
|
+
toColumn = foreignKey.referencedColumns[index];
|
|
1283
|
+
} else {
|
|
1284
|
+
toColumn = foreignKey.referencedColumns[0] ?? column;
|
|
1285
|
+
needsReview = true;
|
|
1286
|
+
table.reviewTodos.push({
|
|
1287
|
+
type: "relationship",
|
|
1288
|
+
target: `${table.name}.${column} \u2192 ${foreignKey.referencedTable}`,
|
|
1289
|
+
issue: `Foreign key column "${column}" references table "${foreignKey.referencedTable}" but the referenced column at position ${index} is missing from the schema. Using "${toColumn}" as a best-guess fallback.`,
|
|
1290
|
+
suggestion: `Verify the referenced column name in table "${foreignKey.referencedTable}" and update manually.`,
|
|
1291
|
+
source: "schema"
|
|
1292
|
+
});
|
|
1293
|
+
warnings.push({
|
|
1294
|
+
code: "FK_REFERENCED_COLUMN_GUESS",
|
|
1295
|
+
message: `In table "${table.name}", foreign key column "${column}" references "${foreignKey.referencedTable}" but the referenced column at index ${index} is missing. Falling back to "${toColumn}".`,
|
|
1296
|
+
target: `${table.name}.${column}`,
|
|
1297
|
+
severity: "warning"
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
relationships.push({
|
|
1301
|
+
fromTable: table.name,
|
|
1302
|
+
fromColumn: column,
|
|
1303
|
+
toTable: foreignKey.referencedTable,
|
|
1304
|
+
toColumn,
|
|
1305
|
+
constraintName: foreignKey.name,
|
|
1306
|
+
source: "schema",
|
|
1307
|
+
needsReview
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
return { relationships, warnings };
|
|
1312
|
+
}
|
|
1313
|
+
function extractTableName(value) {
|
|
1314
|
+
if (Array.isArray(value)) return extractTableName(value[0]);
|
|
1315
|
+
if (typeof value === "object" && value !== null) {
|
|
1316
|
+
const object = value;
|
|
1317
|
+
return String(object.table ?? object.tableName ?? object.name ?? "unknown");
|
|
1318
|
+
}
|
|
1319
|
+
return String(value ?? "unknown");
|
|
1320
|
+
}
|
|
1321
|
+
function extractDeepColumnName(value) {
|
|
1322
|
+
if (typeof value !== "object" || value === null)
|
|
1323
|
+
return String(value ?? "unknown");
|
|
1324
|
+
const object = value;
|
|
1325
|
+
if (object.expr && typeof object.expr === "object") {
|
|
1326
|
+
return extractDeepColumnName(object.expr);
|
|
1327
|
+
}
|
|
1328
|
+
if (object.column && typeof object.column === "object") {
|
|
1329
|
+
return extractDeepColumnName(object.column);
|
|
1330
|
+
}
|
|
1331
|
+
if (object.value !== void 0) {
|
|
1332
|
+
return String(object.value);
|
|
1333
|
+
}
|
|
1334
|
+
if (object.column !== void 0) {
|
|
1335
|
+
return String(object.column);
|
|
1336
|
+
}
|
|
1337
|
+
return String(object.name ?? object.tableName ?? "unknown");
|
|
1338
|
+
}
|
|
1339
|
+
function extractDeepColumnNames(value) {
|
|
1340
|
+
if (!Array.isArray(value)) return [];
|
|
1341
|
+
return value.map((item) => extractDeepColumnName(item));
|
|
1342
|
+
}
|
|
1343
|
+
function normalizeType(value) {
|
|
1344
|
+
if (typeof value === "object" && value !== null) {
|
|
1345
|
+
const object = value;
|
|
1346
|
+
return String(
|
|
1347
|
+
object.dataType ?? object.type ?? object.name ?? "unknown"
|
|
1348
|
+
).toLowerCase();
|
|
1349
|
+
}
|
|
1350
|
+
return String(value ?? "unknown").toLowerCase();
|
|
1351
|
+
}
|
|
1352
|
+
function hasPrimaryKey(def) {
|
|
1353
|
+
if (def.primary_key) return true;
|
|
1354
|
+
if (def.constraint_type && isConstraintType(def.constraint_type, "PRIMARY KEY"))
|
|
1355
|
+
return true;
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
function hasNotNull(def) {
|
|
1359
|
+
if (!def.nullable) return false;
|
|
1360
|
+
if (typeof def.nullable === "object" && def.nullable.type === "not null")
|
|
1361
|
+
return true;
|
|
1362
|
+
if (String(def.nullable) === "not null") return true;
|
|
1363
|
+
return false;
|
|
1364
|
+
}
|
|
1365
|
+
function isConstraintType(value, expected) {
|
|
1366
|
+
if (typeof value !== "string") return false;
|
|
1367
|
+
return value.toUpperCase() === expected.toUpperCase();
|
|
1368
|
+
}
|
|
1369
|
+
function extractDefaultFromDef(def) {
|
|
1370
|
+
if (!def.default_val) return void 0;
|
|
1371
|
+
const defaultVal = def.default_val;
|
|
1372
|
+
if (defaultVal.type === "default" && defaultVal.value) {
|
|
1373
|
+
if (typeof defaultVal.value === "object" && defaultVal.value !== null) {
|
|
1374
|
+
const val = defaultVal.value;
|
|
1375
|
+
if (val.type === "function" && val.name) {
|
|
1376
|
+
const name = val.name;
|
|
1377
|
+
const parts = Array.isArray(name.name) ? name.name : [];
|
|
1378
|
+
return parts.map((p) => String(p.value ?? "")).join("");
|
|
1379
|
+
}
|
|
1380
|
+
if (val.type === "single_quote_string") {
|
|
1381
|
+
return `'${String(val.value)}'`;
|
|
1382
|
+
}
|
|
1383
|
+
if (val.value !== void 0) return String(val.value);
|
|
1384
|
+
}
|
|
1385
|
+
return String(defaultVal.value);
|
|
1386
|
+
}
|
|
1387
|
+
if (typeof defaultVal.value === "string") return defaultVal.value;
|
|
1388
|
+
return void 0;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// src/parsers/sql/sql-parser.ts
|
|
1392
|
+
var { Parser } = nodeSqlParser;
|
|
1393
|
+
async function parseSqlSchema(sql, options = {}) {
|
|
1394
|
+
const requestedDialect = options.dialect;
|
|
1395
|
+
const parser = new Parser();
|
|
1396
|
+
const detectedDialect = detectDialect(sql);
|
|
1397
|
+
const attempts = buildDialectAttempts(requestedDialect, detectedDialect);
|
|
1398
|
+
const failures = [];
|
|
1399
|
+
const successes = [];
|
|
1400
|
+
for (const dialect of attempts) {
|
|
1401
|
+
try {
|
|
1402
|
+
const ast = parser.astify(sql, buildAstifyOptions(dialect));
|
|
1403
|
+
const doc = normalizeSqlAst(ast, dialect);
|
|
1404
|
+
successes.push({
|
|
1405
|
+
dialect,
|
|
1406
|
+
doc,
|
|
1407
|
+
score: scoreParsedDoc(doc)
|
|
1408
|
+
});
|
|
1409
|
+
} catch (error) {
|
|
1410
|
+
failures.push({
|
|
1411
|
+
dialect,
|
|
1412
|
+
message: error instanceof Error ? error.message : "Unsupported SQL syntax"
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
const best = pickBestResult(successes, requestedDialect, detectedDialect);
|
|
1417
|
+
if (best) {
|
|
1418
|
+
if (!requestedDialect && best.dialect !== "unknown") {
|
|
1419
|
+
best.doc.warnings.unshift(
|
|
1420
|
+
createWarning(
|
|
1421
|
+
"DIALECT_AUTO_DETECTED",
|
|
1422
|
+
`SQL dialect auto-detected as "${best.dialect}".`
|
|
1423
|
+
)
|
|
1424
|
+
);
|
|
1425
|
+
} else if (requestedDialect && requestedDialect !== best.dialect) {
|
|
1426
|
+
best.doc.warnings.unshift(
|
|
1427
|
+
createWarning(
|
|
1428
|
+
"DIALECT_FALLBACK",
|
|
1429
|
+
`Requested dialect "${requestedDialect}" produced a lower-quality parse, reparsed successfully as "${best.dialect}".`
|
|
1430
|
+
)
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
return best.doc;
|
|
1434
|
+
}
|
|
1435
|
+
return {
|
|
1436
|
+
dialect: requestedDialect ?? "unknown",
|
|
1437
|
+
tables: [],
|
|
1438
|
+
relationships: [],
|
|
1439
|
+
indexes: [],
|
|
1440
|
+
warnings: [
|
|
1441
|
+
createWarning(
|
|
1442
|
+
"UNSUPPORTED_SQL",
|
|
1443
|
+
failures[0]?.message ?? "Unsupported SQL syntax"
|
|
1444
|
+
)
|
|
1445
|
+
]
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
function mapDialect(dialect) {
|
|
1449
|
+
if (dialect === "postgres") return "postgresql";
|
|
1450
|
+
if (dialect === "mysql" || dialect === "mariadb") return "mysql";
|
|
1451
|
+
return void 0;
|
|
1452
|
+
}
|
|
1453
|
+
function buildAstifyOptions(dialect) {
|
|
1454
|
+
const database = mapDialect(dialect);
|
|
1455
|
+
return database ? { database } : void 0;
|
|
1456
|
+
}
|
|
1457
|
+
function buildDialectAttempts(requestedDialect, detectedDialect) {
|
|
1458
|
+
const attempts = [];
|
|
1459
|
+
if (requestedDialect) {
|
|
1460
|
+
attempts.push(requestedDialect);
|
|
1461
|
+
}
|
|
1462
|
+
if (detectedDialect) {
|
|
1463
|
+
attempts.push(detectedDialect);
|
|
1464
|
+
}
|
|
1465
|
+
attempts.push("mysql", "mariadb", "postgres", "unknown");
|
|
1466
|
+
return dedupeDialects(attempts);
|
|
1467
|
+
}
|
|
1468
|
+
function dedupeDialects(dialects) {
|
|
1469
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1470
|
+
const ordered = [];
|
|
1471
|
+
for (const dialect of dialects) {
|
|
1472
|
+
if (seen.has(dialect)) continue;
|
|
1473
|
+
seen.add(dialect);
|
|
1474
|
+
ordered.push(dialect);
|
|
1475
|
+
}
|
|
1476
|
+
return ordered;
|
|
1477
|
+
}
|
|
1478
|
+
function detectDialect(sql) {
|
|
1479
|
+
const source = sql.toUpperCase();
|
|
1480
|
+
const mysqlSignals = [
|
|
1481
|
+
"AUTO_INCREMENT",
|
|
1482
|
+
"ENGINE=",
|
|
1483
|
+
"TINYINT",
|
|
1484
|
+
"UNSIGNED",
|
|
1485
|
+
"ZEROFILL",
|
|
1486
|
+
"CHARACTER SET",
|
|
1487
|
+
"COLLATE "
|
|
1488
|
+
];
|
|
1489
|
+
if (mysqlSignals.some((signal) => source.includes(signal))) {
|
|
1490
|
+
return "mysql";
|
|
1491
|
+
}
|
|
1492
|
+
const postgresSignals = [
|
|
1493
|
+
"SERIAL",
|
|
1494
|
+
"BIGSERIAL",
|
|
1495
|
+
"GENERATED ALWAYS AS IDENTITY",
|
|
1496
|
+
"JSONB",
|
|
1497
|
+
"ILIKE",
|
|
1498
|
+
"CREATE EXTENSION"
|
|
1499
|
+
];
|
|
1500
|
+
if (postgresSignals.some((signal) => source.includes(signal))) {
|
|
1501
|
+
return "postgres";
|
|
1502
|
+
}
|
|
1503
|
+
return void 0;
|
|
1504
|
+
}
|
|
1505
|
+
function pickBestResult(successes, requestedDialect, detectedDialect) {
|
|
1506
|
+
const ranked = [...successes].sort((left, right) => {
|
|
1507
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
1508
|
+
const leftBias = dialectBias(left.dialect, requestedDialect, detectedDialect);
|
|
1509
|
+
const rightBias = dialectBias(
|
|
1510
|
+
right.dialect,
|
|
1511
|
+
requestedDialect,
|
|
1512
|
+
detectedDialect
|
|
1513
|
+
);
|
|
1514
|
+
return rightBias - leftBias;
|
|
1515
|
+
});
|
|
1516
|
+
return ranked[0];
|
|
1517
|
+
}
|
|
1518
|
+
function scoreParsedDoc(doc) {
|
|
1519
|
+
let score = 0;
|
|
1520
|
+
score += doc.tables.length * 100;
|
|
1521
|
+
score += doc.indexes.length * 15;
|
|
1522
|
+
score += doc.relationships.length * 20;
|
|
1523
|
+
score -= doc.warnings.length * 25;
|
|
1524
|
+
for (const table of doc.tables) {
|
|
1525
|
+
score += table.columns.length * 10;
|
|
1526
|
+
score += table.primaryKeys.length * 5;
|
|
1527
|
+
for (const column of table.columns) {
|
|
1528
|
+
if (column.name && column.name !== "unknown") score += 3;
|
|
1529
|
+
if (column.type && column.type !== "unknown") score += 2;
|
|
1530
|
+
if (column.name.includes("[object Object]")) score -= 50;
|
|
1531
|
+
if (column.type.includes("[object Object]")) score -= 30;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
return score;
|
|
1535
|
+
}
|
|
1536
|
+
function dialectBias(dialect, requestedDialect, detectedDialect) {
|
|
1537
|
+
let bias = 0;
|
|
1538
|
+
if (detectedDialect && dialect === detectedDialect) bias += 20;
|
|
1539
|
+
if (requestedDialect && dialect === requestedDialect) bias += 5;
|
|
1540
|
+
if (dialect === "unknown") bias -= 10;
|
|
1541
|
+
return bias;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// src/core/pipeline/generate-db-docs.ts
|
|
1545
|
+
async function generateDbDocs(options) {
|
|
1546
|
+
const progress = (step, message, detail) => {
|
|
1547
|
+
options.onProgress?.({ step, message, detail });
|
|
1548
|
+
};
|
|
1549
|
+
progress("read_schema", "Reading schema file", { schema: options.schema });
|
|
1550
|
+
const sql = await readFile(options.schema, "utf8");
|
|
1551
|
+
progress("parse_schema", "Parsing schema", { dialect: options.dialect ?? "auto-detect" });
|
|
1552
|
+
let doc = await parseSqlSchema(sql, {
|
|
1553
|
+
dialect: options.dialect
|
|
1554
|
+
});
|
|
1555
|
+
progress("schema_parsed", "Schema parsed", {
|
|
1556
|
+
tables: doc.tables.length,
|
|
1557
|
+
warnings: doc.warnings.length,
|
|
1558
|
+
dialect: doc.dialect
|
|
1559
|
+
});
|
|
1560
|
+
const exporters = [];
|
|
1561
|
+
if (options.output.formats.includes("excel")) {
|
|
1562
|
+
exporters.push({
|
|
1563
|
+
format: "excel",
|
|
1564
|
+
fn: () => exportExcelDictionary(doc, {
|
|
1565
|
+
outDir: options.outDir,
|
|
1566
|
+
language: options.output.language
|
|
1567
|
+
})
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
if (options.output.formats.includes("diagram")) {
|
|
1571
|
+
exporters.push({
|
|
1572
|
+
format: "diagram",
|
|
1573
|
+
fn: () => exportMermaidDiagram(doc, { outDir: options.outDir })
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
if (options.output.formats.includes("markdown")) {
|
|
1577
|
+
exporters.push({
|
|
1578
|
+
format: "markdown",
|
|
1579
|
+
fn: () => exportMarkdownDocs(doc, {
|
|
1580
|
+
outDir: options.outDir,
|
|
1581
|
+
language: options.output.language
|
|
1582
|
+
})
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
if (options.output.formats.includes("html")) {
|
|
1586
|
+
exporters.push({
|
|
1587
|
+
format: "html",
|
|
1588
|
+
fn: () => exportHtmlDocs(doc, {
|
|
1589
|
+
outDir: options.outDir,
|
|
1590
|
+
language: options.output.language
|
|
1591
|
+
})
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
if (options.output.formats.includes("word")) {
|
|
1595
|
+
exporters.push({
|
|
1596
|
+
format: "word",
|
|
1597
|
+
fn: () => exportWordDocument(doc, {
|
|
1598
|
+
outDir: options.outDir,
|
|
1599
|
+
language: options.output.language
|
|
1600
|
+
})
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
for (const { format, fn } of exporters) {
|
|
1604
|
+
try {
|
|
1605
|
+
progress(`export_${format}`, `Exporting ${format} output`, {
|
|
1606
|
+
outDir: options.outDir
|
|
1607
|
+
});
|
|
1608
|
+
await fn();
|
|
1609
|
+
progress(`export_${format}_done`, `Exported ${format} output`, {
|
|
1610
|
+
outDir: options.outDir
|
|
1611
|
+
});
|
|
1612
|
+
} catch (err) {
|
|
1613
|
+
console.error(
|
|
1614
|
+
`[dbdocgen] Failed to export ${format} docs:`,
|
|
1615
|
+
err instanceof Error ? err.message : String(err)
|
|
1616
|
+
);
|
|
1617
|
+
progress(`export_${format}_failed`, `Failed to export ${format}`, {
|
|
1618
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
progress("complete", "Generation complete", {
|
|
1623
|
+
tables: doc.tables.length,
|
|
1624
|
+
warnings: doc.warnings.length,
|
|
1625
|
+
outDir: options.outDir
|
|
1626
|
+
});
|
|
1627
|
+
return doc;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// src/cli/index.ts
|
|
1631
|
+
var DEFAULT_CONFIG_PATH = "dbdocgen.config.json";
|
|
1632
|
+
var program = new Command();
|
|
1633
|
+
program.name("dbdocgen").description("Generate database documentation from SQL schema files.").version("0.1.0");
|
|
1634
|
+
program.command("init").description("Create a default config file").option("-f, --force", "Overwrite existing config file").action(async (rawOptions) => {
|
|
1635
|
+
const configPath = resolve(process.cwd(), DEFAULT_CONFIG_PATH);
|
|
1636
|
+
if (existsSync(configPath) && !rawOptions.force) {
|
|
1637
|
+
console.log(`Config already exists at ${configPath}. Use --force to overwrite.`);
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
const defaultConfig = {
|
|
1641
|
+
schema: "./database/schema.sql",
|
|
1642
|
+
output: {
|
|
1643
|
+
formats: ["excel", "markdown", "html", "diagram", "word"],
|
|
1644
|
+
language: "en"
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
await writeFile5(configPath, JSON.stringify(defaultConfig, null, 2), "utf8");
|
|
1648
|
+
console.log(`Created config at ${configPath}`);
|
|
1649
|
+
console.log("Default generate output directory is ./output/db_doc_gen_{yymmddhhmm} unless you pass --out.");
|
|
1650
|
+
console.log("Edit the file to configure your database schema path and output formats.");
|
|
1651
|
+
});
|
|
1652
|
+
var configCommand = program.command("config").description("Manage configuration");
|
|
1653
|
+
configCommand.command("show").description("Show resolved configuration").option("--config <path>", "Config file path").action(async (rawOptions) => {
|
|
1654
|
+
const config = await loadConfig({
|
|
1655
|
+
cwd: process.cwd(),
|
|
1656
|
+
cliOptions: { configPath: rawOptions.config }
|
|
1657
|
+
});
|
|
1658
|
+
console.log(JSON.stringify(config, null, 2));
|
|
1659
|
+
});
|
|
1660
|
+
configCommand.command("validate").description("Validate config file").option("--config <path>", "Config file path").action(async (rawOptions) => {
|
|
1661
|
+
try {
|
|
1662
|
+
const config = await loadConfig({
|
|
1663
|
+
cwd: process.cwd(),
|
|
1664
|
+
cliOptions: { configPath: rawOptions.config }
|
|
1665
|
+
});
|
|
1666
|
+
const result = dbdocgenConfigSchema.safeParse(config);
|
|
1667
|
+
if (result.success) {
|
|
1668
|
+
console.log("Config is valid.");
|
|
1669
|
+
} else {
|
|
1670
|
+
console.error("Config validation failed:");
|
|
1671
|
+
console.error(result.error.format());
|
|
1672
|
+
process.exitCode = 1;
|
|
1673
|
+
}
|
|
1674
|
+
} catch (err) {
|
|
1675
|
+
console.error("Failed to load config:", err instanceof Error ? err.message : err);
|
|
1676
|
+
process.exitCode = 1;
|
|
1677
|
+
}
|
|
1678
|
+
});
|
|
1679
|
+
program.command("generate").description("Generate database documentation").option("--schema <path>", "Path to schema.sql").option("--out <path>", "Output directory").option("--format <formats>", "Comma-separated output formats").option("--config <path>", "Config file path").action(async (rawOptions) => {
|
|
1680
|
+
console.log("[dbdocgen] Loading configuration...");
|
|
1681
|
+
const config = await loadConfig({
|
|
1682
|
+
cwd: process.cwd(),
|
|
1683
|
+
cliOptions: {
|
|
1684
|
+
schema: rawOptions.schema,
|
|
1685
|
+
outDir: rawOptions.out,
|
|
1686
|
+
formats: parseFormats(rawOptions.format),
|
|
1687
|
+
configPath: rawOptions.config
|
|
1688
|
+
}
|
|
1689
|
+
});
|
|
1690
|
+
const outDir = rawOptions.out ?? createTimestampedOutputDir();
|
|
1691
|
+
console.log("[dbdocgen] Configuration loaded");
|
|
1692
|
+
console.log(` schema: ${config.schema}`);
|
|
1693
|
+
console.log(` outDir: ${outDir}`);
|
|
1694
|
+
console.log(` formats: ${config.output.formats.join(", ")}`);
|
|
1695
|
+
console.log(` language: ${config.output.language}`);
|
|
1696
|
+
const doc = await generateDbDocs({
|
|
1697
|
+
schema: config.schema,
|
|
1698
|
+
outDir,
|
|
1699
|
+
dialect: config.dialect,
|
|
1700
|
+
output: {
|
|
1701
|
+
formats: config.output.formats,
|
|
1702
|
+
language: config.output.language
|
|
1703
|
+
},
|
|
1704
|
+
onProgress: (event) => {
|
|
1705
|
+
console.log(`[dbdocgen] ${event.message}`);
|
|
1706
|
+
if (event.detail) {
|
|
1707
|
+
for (const [key, value] of Object.entries(event.detail)) {
|
|
1708
|
+
console.log(` ${key}: ${String(value)}`);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
});
|
|
1713
|
+
if (doc.warnings.length > 0) {
|
|
1714
|
+
console.log(`[dbdocgen] Completed with ${doc.warnings.length} warning(s)`);
|
|
1715
|
+
for (const warning of doc.warnings) {
|
|
1716
|
+
console.log(
|
|
1717
|
+
` [${warning.severity}] ${warning.code}: ${warning.message}`
|
|
1718
|
+
);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
console.log(`Generated database documentation in ${outDir}`);
|
|
1722
|
+
});
|
|
1723
|
+
program.command("validate").description("Validate a SQL schema file without generating docs").option("--schema <path>", "Path to schema.sql").option("--config <path>", "Config file path").action(async (rawOptions) => {
|
|
1724
|
+
const config = await loadConfig({
|
|
1725
|
+
cwd: process.cwd(),
|
|
1726
|
+
cliOptions: {
|
|
1727
|
+
schema: rawOptions.schema,
|
|
1728
|
+
configPath: rawOptions.config
|
|
1729
|
+
}
|
|
1730
|
+
});
|
|
1731
|
+
console.log(`Validating ${config.schema}...`);
|
|
1732
|
+
const sql = await readFile2(config.schema, "utf8");
|
|
1733
|
+
const doc = await parseSqlSchema(sql, { dialect: "postgres" });
|
|
1734
|
+
if (doc.tables.length === 0) {
|
|
1735
|
+
console.log("No tables found in schema.");
|
|
1736
|
+
if (doc.warnings.length > 0) {
|
|
1737
|
+
console.log("\nWarnings:");
|
|
1738
|
+
for (const w of doc.warnings) {
|
|
1739
|
+
console.log(` [${w.code}] ${w.message}`);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
console.log(`
|
|
1745
|
+
Found ${doc.tables.length} table(s):
|
|
1746
|
+
`);
|
|
1747
|
+
for (const table of doc.tables) {
|
|
1748
|
+
console.log(` ${table.name}`);
|
|
1749
|
+
console.log(` Columns: ${table.columns.length}`);
|
|
1750
|
+
console.log(` Primary Keys: ${table.primaryKeys.join(", ") || "(none)"}`);
|
|
1751
|
+
console.log(` Foreign Keys: ${table.foreignKeys.length}`);
|
|
1752
|
+
console.log("");
|
|
1753
|
+
}
|
|
1754
|
+
if (doc.warnings.length > 0) {
|
|
1755
|
+
console.log(`Warnings (${doc.warnings.length}):`);
|
|
1756
|
+
for (const w of doc.warnings) {
|
|
1757
|
+
console.log(` [${w.severity}] ${w.code}: ${w.message}`);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
console.log("Schema validation passed.");
|
|
1761
|
+
});
|
|
1762
|
+
program.command("clean").description("Clean output directory").option("--out <path>", "Output directory to clean").option("--config <path>", "Config file path").action(async (rawOptions) => {
|
|
1763
|
+
const config = await loadConfig({
|
|
1764
|
+
cwd: process.cwd(),
|
|
1765
|
+
cliOptions: {
|
|
1766
|
+
outDir: rawOptions.out,
|
|
1767
|
+
configPath: rawOptions.config
|
|
1768
|
+
}
|
|
1769
|
+
});
|
|
1770
|
+
const outDir = resolve(config.outDir);
|
|
1771
|
+
if (!existsSync(outDir)) {
|
|
1772
|
+
console.log(`Output directory ${outDir} does not exist. Nothing to clean.`);
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
console.log(`Cleaning ${outDir}...`);
|
|
1776
|
+
await rm(outDir, { recursive: true, force: true });
|
|
1777
|
+
console.log("Done.");
|
|
1778
|
+
});
|
|
1779
|
+
program.command("info").description("Show project info and supported features").action(() => {
|
|
1780
|
+
console.log("dbdocgen v0.1.0");
|
|
1781
|
+
console.log("");
|
|
1782
|
+
console.log("Generate database documentation from SQL schema files.");
|
|
1783
|
+
console.log("");
|
|
1784
|
+
console.log("Supported input:");
|
|
1785
|
+
console.log(" - PostgreSQL schema.sql");
|
|
1786
|
+
console.log(" - MySQL / MariaDB schema.sql");
|
|
1787
|
+
console.log("");
|
|
1788
|
+
console.log("Supported output formats:");
|
|
1789
|
+
console.log(" - excel Data Dictionary (.xlsx)");
|
|
1790
|
+
console.log(" - markdown Per-table .md files");
|
|
1791
|
+
console.log(" - html Static HTML documentation");
|
|
1792
|
+
console.log(" - diagram Mermaid ER Diagram (.mmd)");
|
|
1793
|
+
console.log(" - word Word document (.docx)");
|
|
1794
|
+
console.log("");
|
|
1795
|
+
console.log("Commands:");
|
|
1796
|
+
console.log(" init Create a default config file");
|
|
1797
|
+
console.log(" generate Generate documentation");
|
|
1798
|
+
console.log(" validate Validate SQL schema");
|
|
1799
|
+
console.log(" clean Clean output directory");
|
|
1800
|
+
console.log(" config show Show current config");
|
|
1801
|
+
console.log(" config validate Validate config");
|
|
1802
|
+
console.log(" info Show this info");
|
|
1803
|
+
});
|
|
1804
|
+
program.parseAsync().catch((error) => {
|
|
1805
|
+
console.error(error instanceof Error ? error.message : error);
|
|
1806
|
+
process.exitCode = 1;
|
|
1807
|
+
});
|
|
1808
|
+
function parseFormats(value) {
|
|
1809
|
+
if (!value) return void 0;
|
|
1810
|
+
const items = value.split(",").map((item) => item.trim());
|
|
1811
|
+
const valid = [];
|
|
1812
|
+
for (const item of items) {
|
|
1813
|
+
const parsed = outputFormatSchema.safeParse(item);
|
|
1814
|
+
if (parsed.success) {
|
|
1815
|
+
valid.push(parsed.data);
|
|
1816
|
+
} else {
|
|
1817
|
+
console.warn(`Warning: Unrecognized format "${item}" \u2014 must be one of: ${outputFormatSchema.options.join(", ")}`);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
return valid.length > 0 ? valid : void 0;
|
|
1821
|
+
}
|
|
1822
|
+
function createTimestampedOutputDir(date = /* @__PURE__ */ new Date()) {
|
|
1823
|
+
const year = String(date.getFullYear()).slice(-2);
|
|
1824
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1825
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
1826
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
1827
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
1828
|
+
return `./output/db_doc_gen_${year}${month}${day}${hours}${minutes}`;
|
|
1829
|
+
}
|
|
1830
|
+
//# sourceMappingURL=index.js.map
|