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