@cuongph.dev/dbdocgen 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -37
- package/dist/cli/index.cjs +1080 -163
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +1090 -172
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +1086 -198
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1095 -206
- package/dist/index.js.map +1 -1
- package/package.json +15 -11
package/dist/cli/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import { readFile as readFile2, writeFile as
|
|
5
|
+
import { readFile as readFile2, readdir, rm, writeFile as writeFile6 } from "fs/promises";
|
|
6
6
|
import { existsSync } from "fs";
|
|
7
7
|
import { resolve } from "path";
|
|
8
8
|
|
|
@@ -30,7 +30,7 @@ var outputLanguageSchema = z.enum(["en", "jp"]);
|
|
|
30
30
|
var dbdocgenConfigSchema = z.object({
|
|
31
31
|
schema: z.string().default("./schema.sql"),
|
|
32
32
|
dialect: dialectSchema.optional(),
|
|
33
|
-
outDir: z.string().default("./
|
|
33
|
+
outDir: z.string().default("./output"),
|
|
34
34
|
output: z.object({
|
|
35
35
|
formats: z.array(outputFormatSchema).default(["excel", "markdown", "html", "diagram", "word"]),
|
|
36
36
|
language: outputLanguageSchema.default("en")
|
|
@@ -113,7 +113,7 @@ function sanitizeType(type) {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
// src/exporters/excel/excel-exporter.ts
|
|
116
|
-
import { mkdir as mkdir2 } from "fs/promises";
|
|
116
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
117
117
|
import { join as join2 } from "path";
|
|
118
118
|
import ExcelJS from "exceljs";
|
|
119
119
|
|
|
@@ -137,6 +137,10 @@ var LABELS = {
|
|
|
137
137
|
type: "Type",
|
|
138
138
|
required: "Required",
|
|
139
139
|
defaultValue: "Default Value",
|
|
140
|
+
size: "Size",
|
|
141
|
+
minValue: "Min",
|
|
142
|
+
maxValue: "Max",
|
|
143
|
+
unique: "Unique",
|
|
140
144
|
notes: "Notes",
|
|
141
145
|
yes: "Yes",
|
|
142
146
|
no: "No",
|
|
@@ -165,7 +169,15 @@ var LABELS = {
|
|
|
165
169
|
rowNo: "#",
|
|
166
170
|
backToOverview: "\u2190 Overview",
|
|
167
171
|
pkMarker: "PK",
|
|
168
|
-
fkMarker: "FK"
|
|
172
|
+
fkMarker: "FK",
|
|
173
|
+
erDiagramHeading: "ER Diagram",
|
|
174
|
+
erDiagramSheet: "ER Diagram",
|
|
175
|
+
viewErDiagram: "View interactive ER diagram (html/er-diagram.html)",
|
|
176
|
+
zoomIn: "Zoom in",
|
|
177
|
+
zoomOut: "Zoom out",
|
|
178
|
+
zoomReset: "Reset",
|
|
179
|
+
zoomFit: "Fit",
|
|
180
|
+
panZoomHint: "Drag to pan \xB7 Scroll to zoom"
|
|
169
181
|
},
|
|
170
182
|
jp: {
|
|
171
183
|
docTitle: "Database Documentation",
|
|
@@ -185,6 +197,10 @@ var LABELS = {
|
|
|
185
197
|
type: "\u578B",
|
|
186
198
|
required: "\u5FC5\u9808",
|
|
187
199
|
defaultValue: "\u30C7\u30D5\u30A9\u30EB\u30C8\u5024",
|
|
200
|
+
size: "\u6841\u6570",
|
|
201
|
+
minValue: "\u6700\u5C0F\u5024",
|
|
202
|
+
maxValue: "\u6700\u5927\u5024",
|
|
203
|
+
unique: "\u4E00\u610F",
|
|
188
204
|
notes: "\u5099\u8003",
|
|
189
205
|
yes: "Yes",
|
|
190
206
|
no: "No",
|
|
@@ -213,13 +229,533 @@ var LABELS = {
|
|
|
213
229
|
rowNo: "No.",
|
|
214
230
|
backToOverview: "\u2190 \u4E00\u89A7",
|
|
215
231
|
pkMarker: "PK",
|
|
216
|
-
fkMarker: "FK"
|
|
232
|
+
fkMarker: "FK",
|
|
233
|
+
erDiagramHeading: "ER Diagram",
|
|
234
|
+
erDiagramSheet: "ER Diagram",
|
|
235
|
+
viewErDiagram: "\u30A4\u30F3\u30BF\u30E9\u30AF\u30C6\u30A3\u30D6ER\u56F3 (html/er-diagram.html)",
|
|
236
|
+
zoomIn: "\u62E1\u5927",
|
|
237
|
+
zoomOut: "\u7E2E\u5C0F",
|
|
238
|
+
zoomReset: "\u30EA\u30BB\u30C3\u30C8",
|
|
239
|
+
zoomFit: "\u5168\u4F53\u8868\u793A",
|
|
240
|
+
panZoomHint: "\u30C9\u30E9\u30C3\u30B0\u3067\u79FB\u52D5 \xB7 \u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u62E1\u5927\u7E2E\u5C0F"
|
|
217
241
|
}
|
|
218
242
|
};
|
|
219
243
|
function getOutputLabels(language = "en") {
|
|
220
244
|
return LABELS[language];
|
|
221
245
|
}
|
|
222
246
|
|
|
247
|
+
// src/exporters/shared/column-definition.ts
|
|
248
|
+
var A5_COLUMN_COUNT = 10;
|
|
249
|
+
function columnDefinitionHeaders(labels) {
|
|
250
|
+
return [
|
|
251
|
+
labels.physicalName,
|
|
252
|
+
labels.logicalName,
|
|
253
|
+
labels.type,
|
|
254
|
+
labels.size,
|
|
255
|
+
labels.required,
|
|
256
|
+
labels.defaultValue,
|
|
257
|
+
labels.minValue,
|
|
258
|
+
labels.maxValue,
|
|
259
|
+
labels.unique,
|
|
260
|
+
labels.notes
|
|
261
|
+
];
|
|
262
|
+
}
|
|
263
|
+
function formatColumnNotes(column, labels) {
|
|
264
|
+
const parts = [];
|
|
265
|
+
if (column.isPrimaryKey) parts.push(labels.pkMarker);
|
|
266
|
+
if (column.isForeignKey) parts.push(labels.fkMarker);
|
|
267
|
+
if (column.constraintNotes?.length) parts.push(...column.constraintNotes);
|
|
268
|
+
if (column.description?.value) parts.push(column.description.value);
|
|
269
|
+
return parts.join(", ") || labels.none;
|
|
270
|
+
}
|
|
271
|
+
function columnDefinitionRow(column, labels) {
|
|
272
|
+
return [
|
|
273
|
+
column.name,
|
|
274
|
+
column.comment ?? "",
|
|
275
|
+
column.type,
|
|
276
|
+
column.size ?? labels.none,
|
|
277
|
+
column.nullable ? labels.no : labels.yes,
|
|
278
|
+
column.defaultValue ?? labels.none,
|
|
279
|
+
column.minValue ?? labels.none,
|
|
280
|
+
column.maxValue ?? labels.none,
|
|
281
|
+
column.isUnique ? labels.yes : labels.no,
|
|
282
|
+
formatColumnNotes(column, labels)
|
|
283
|
+
];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/exporters/diagram/er-diagram-embed.ts
|
|
287
|
+
function getErDiagramMermaid(doc) {
|
|
288
|
+
return renderMermaid(doc);
|
|
289
|
+
}
|
|
290
|
+
function renderErDiagramHtmlPage(mermaidSource, labels) {
|
|
291
|
+
const escaped = mermaidSource.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
292
|
+
return `<!DOCTYPE html>
|
|
293
|
+
<html lang="en">
|
|
294
|
+
<head>
|
|
295
|
+
<meta charset="UTF-8">
|
|
296
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
297
|
+
<title>${esc(labels.erDiagramHeading)}</title>
|
|
298
|
+
<style>
|
|
299
|
+
body { margin: 0; font-family: "Yu Gothic UI", "Meiryo", Arial, sans-serif; background: #f3f4f6; }
|
|
300
|
+
.toolbar {
|
|
301
|
+
background: #4472c4; color: #fff; padding: 10px 16px;
|
|
302
|
+
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
|
303
|
+
}
|
|
304
|
+
.toolbar a { color: #fff; text-decoration: underline; }
|
|
305
|
+
.toolbar .spacer { flex: 1; }
|
|
306
|
+
.toolbar .hint { opacity: 0.9; font-size: 13px; }
|
|
307
|
+
.toolbar button {
|
|
308
|
+
background: #fff; color: #2f5597; border: none; border-radius: 4px;
|
|
309
|
+
padding: 6px 12px; font-size: 13px; cursor: pointer; font-weight: 600;
|
|
310
|
+
}
|
|
311
|
+
.toolbar button:hover { background: #e8eef8; }
|
|
312
|
+
.viewport {
|
|
313
|
+
position: relative; height: calc(100vh - 52px); margin: 12px;
|
|
314
|
+
background: #fff; border: 1px solid #bfc7d4; border-radius: 4px;
|
|
315
|
+
overflow: hidden; cursor: grab; touch-action: none;
|
|
316
|
+
}
|
|
317
|
+
.viewport.dragging { cursor: grabbing; }
|
|
318
|
+
.canvas {
|
|
319
|
+
position: absolute; left: 0; top: 0; transform-origin: 0 0;
|
|
320
|
+
padding: 24px;
|
|
321
|
+
}
|
|
322
|
+
.mermaid { min-width: 320px; }
|
|
323
|
+
.mermaid svg { max-width: none !important; height: auto !important; }
|
|
324
|
+
</style>
|
|
325
|
+
</head>
|
|
326
|
+
<body>
|
|
327
|
+
<div class="toolbar">
|
|
328
|
+
<strong>${esc(labels.erDiagramHeading)}</strong>
|
|
329
|
+
<a href="index.html">\u2190 ${esc(labels.tableListHeading)}</a>
|
|
330
|
+
<span class="spacer"></span>
|
|
331
|
+
<span class="hint">${esc(labels.panZoomHint)}</span>
|
|
332
|
+
<button type="button" id="zoom-out" title="${esc(labels.zoomOut)}">\u2212</button>
|
|
333
|
+
<button type="button" id="zoom-reset" title="${esc(labels.zoomReset)}">${esc(labels.zoomReset)}</button>
|
|
334
|
+
<button type="button" id="zoom-in" title="${esc(labels.zoomIn)}">+</button>
|
|
335
|
+
<button type="button" id="zoom-fit" title="${esc(labels.zoomFit)}">${esc(labels.zoomFit)}</button>
|
|
336
|
+
</div>
|
|
337
|
+
<div class="viewport" id="viewport">
|
|
338
|
+
<div class="canvas" id="canvas">
|
|
339
|
+
<pre class="mermaid">${escaped}</pre>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
<script type="module">
|
|
343
|
+
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
|
|
344
|
+
|
|
345
|
+
mermaid.initialize({
|
|
346
|
+
startOnLoad: false,
|
|
347
|
+
theme: "default",
|
|
348
|
+
er: { useMaxWidth: false }
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
await mermaid.run({ querySelector: ".mermaid" });
|
|
352
|
+
setupPanZoom(
|
|
353
|
+
document.getElementById("viewport"),
|
|
354
|
+
document.getElementById("canvas")
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
function setupPanZoom(viewport, canvas) {
|
|
358
|
+
let scale = 1;
|
|
359
|
+
let tx = 40;
|
|
360
|
+
let ty = 40;
|
|
361
|
+
let dragging = false;
|
|
362
|
+
let lastX = 0;
|
|
363
|
+
let lastY = 0;
|
|
364
|
+
|
|
365
|
+
function apply() {
|
|
366
|
+
canvas.style.transform = "translate(" + tx + "px," + ty + "px) scale(" + scale + ")";
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function zoomAt(factor, cx, cy) {
|
|
370
|
+
const next = Math.min(4, Math.max(0.15, scale * factor));
|
|
371
|
+
const ratio = next / scale;
|
|
372
|
+
tx = cx - (cx - tx) * ratio;
|
|
373
|
+
ty = cy - (cy - ty) * ratio;
|
|
374
|
+
scale = next;
|
|
375
|
+
apply();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function fitToView() {
|
|
379
|
+
const svg = canvas.querySelector("svg");
|
|
380
|
+
if (!svg) return;
|
|
381
|
+
const box = svg.getBBox();
|
|
382
|
+
const pad = 32;
|
|
383
|
+
const vw = viewport.clientWidth;
|
|
384
|
+
const vh = viewport.clientHeight;
|
|
385
|
+
scale = Math.min(
|
|
386
|
+
(vw - pad * 2) / Math.max(box.width, 1),
|
|
387
|
+
(vh - pad * 2) / Math.max(box.height, 1),
|
|
388
|
+
1.5
|
|
389
|
+
);
|
|
390
|
+
tx = (vw - box.width * scale) / 2 - box.x * scale;
|
|
391
|
+
ty = (vh - box.height * scale) / 2 - box.y * scale;
|
|
392
|
+
apply();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
viewport.addEventListener("wheel", (e) => {
|
|
396
|
+
e.preventDefault();
|
|
397
|
+
const rect = viewport.getBoundingClientRect();
|
|
398
|
+
const cx = e.clientX - rect.left;
|
|
399
|
+
const cy = e.clientY - rect.top;
|
|
400
|
+
zoomAt(e.deltaY < 0 ? 1.12 : 0.89, cx, cy);
|
|
401
|
+
}, { passive: false });
|
|
402
|
+
|
|
403
|
+
viewport.addEventListener("mousedown", (e) => {
|
|
404
|
+
if (e.button !== 0) return;
|
|
405
|
+
dragging = true;
|
|
406
|
+
lastX = e.clientX;
|
|
407
|
+
lastY = e.clientY;
|
|
408
|
+
viewport.classList.add("dragging");
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
window.addEventListener("mousemove", (e) => {
|
|
412
|
+
if (!dragging) return;
|
|
413
|
+
tx += e.clientX - lastX;
|
|
414
|
+
ty += e.clientY - lastY;
|
|
415
|
+
lastX = e.clientX;
|
|
416
|
+
lastY = e.clientY;
|
|
417
|
+
apply();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
window.addEventListener("mouseup", () => {
|
|
421
|
+
dragging = false;
|
|
422
|
+
viewport.classList.remove("dragging");
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
document.getElementById("zoom-in").addEventListener("click", () => {
|
|
426
|
+
zoomAt(1.2, viewport.clientWidth / 2, viewport.clientHeight / 2);
|
|
427
|
+
});
|
|
428
|
+
document.getElementById("zoom-out").addEventListener("click", () => {
|
|
429
|
+
zoomAt(1 / 1.2, viewport.clientWidth / 2, viewport.clientHeight / 2);
|
|
430
|
+
});
|
|
431
|
+
document.getElementById("zoom-reset").addEventListener("click", () => {
|
|
432
|
+
scale = 1;
|
|
433
|
+
tx = 40;
|
|
434
|
+
ty = 40;
|
|
435
|
+
apply();
|
|
436
|
+
});
|
|
437
|
+
document.getElementById("zoom-fit").addEventListener("click", fitToView);
|
|
438
|
+
|
|
439
|
+
fitToView();
|
|
440
|
+
}
|
|
441
|
+
</script>
|
|
442
|
+
</body>
|
|
443
|
+
</html>`;
|
|
444
|
+
}
|
|
445
|
+
function renderErDiagramMarkdown(mermaidSource, labels) {
|
|
446
|
+
return [
|
|
447
|
+
`# ${labels.erDiagramHeading}`,
|
|
448
|
+
"",
|
|
449
|
+
"```mermaid",
|
|
450
|
+
mermaidSource.trimEnd(),
|
|
451
|
+
"```",
|
|
452
|
+
""
|
|
453
|
+
].join("\n");
|
|
454
|
+
}
|
|
455
|
+
function esc(text) {
|
|
456
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/exporters/diagram/er-diagram-layout.ts
|
|
460
|
+
import ELK from "elkjs/lib/elk.bundled.js";
|
|
461
|
+
var BOX_W = 200;
|
|
462
|
+
var HEADER_H = 28;
|
|
463
|
+
var LINE_H = 14;
|
|
464
|
+
var MAX_COLS_SHOWN = 6;
|
|
465
|
+
var COMPACT_THRESHOLD = 18;
|
|
466
|
+
var PAD = 24;
|
|
467
|
+
var CLUSTER_GAP = 56;
|
|
468
|
+
function isCompactLayout(tableCount) {
|
|
469
|
+
return tableCount >= COMPACT_THRESHOLD;
|
|
470
|
+
}
|
|
471
|
+
function getVisibleErColumns(table) {
|
|
472
|
+
const prioritized = [
|
|
473
|
+
...table.columns.filter((column) => column.isPrimaryKey),
|
|
474
|
+
...table.columns.filter(
|
|
475
|
+
(column) => column.isForeignKey && !column.isPrimaryKey
|
|
476
|
+
),
|
|
477
|
+
...table.columns.filter(
|
|
478
|
+
(column) => !column.isPrimaryKey && !column.isForeignKey
|
|
479
|
+
)
|
|
480
|
+
];
|
|
481
|
+
const unique = prioritized.filter(
|
|
482
|
+
(column, index, columns) => columns.findIndex((item) => item.name === column.name) === index
|
|
483
|
+
);
|
|
484
|
+
return unique.slice(0, MAX_COLS_SHOWN);
|
|
485
|
+
}
|
|
486
|
+
function measureTableBox(table, _compact = false) {
|
|
487
|
+
const visible = getVisibleErColumns(table);
|
|
488
|
+
const extra = table.columns.length > visible.length ? 1 : 0;
|
|
489
|
+
return {
|
|
490
|
+
w: BOX_W,
|
|
491
|
+
h: HEADER_H + (visible.length + extra) * LINE_H + 8
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function buildAdjacency(doc) {
|
|
495
|
+
const names = new Set(doc.tables.map((t) => t.name));
|
|
496
|
+
const adj = /* @__PURE__ */ new Map();
|
|
497
|
+
for (const name of names) adj.set(name, /* @__PURE__ */ new Set());
|
|
498
|
+
for (const rel of doc.relationships.filter((r) => r.source === "schema")) {
|
|
499
|
+
if (!names.has(rel.fromTable) || !names.has(rel.toTable)) continue;
|
|
500
|
+
adj.get(rel.fromTable).add(rel.toTable);
|
|
501
|
+
adj.get(rel.toTable).add(rel.fromTable);
|
|
502
|
+
}
|
|
503
|
+
return adj;
|
|
504
|
+
}
|
|
505
|
+
function connectedComponents(tableNames, adj) {
|
|
506
|
+
const visited = /* @__PURE__ */ new Set();
|
|
507
|
+
const components = [];
|
|
508
|
+
for (const name of tableNames) {
|
|
509
|
+
if (visited.has(name)) continue;
|
|
510
|
+
const stack = [name];
|
|
511
|
+
const component = [];
|
|
512
|
+
visited.add(name);
|
|
513
|
+
while (stack.length > 0) {
|
|
514
|
+
const current = stack.pop();
|
|
515
|
+
component.push(current);
|
|
516
|
+
for (const next of adj.get(current) ?? []) {
|
|
517
|
+
if (!visited.has(next)) {
|
|
518
|
+
visited.add(next);
|
|
519
|
+
stack.push(next);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
component.sort();
|
|
524
|
+
components.push(component);
|
|
525
|
+
}
|
|
526
|
+
return components.sort((a, b) => b.length - a.length);
|
|
527
|
+
}
|
|
528
|
+
function sectionToPoints(section) {
|
|
529
|
+
return [section.startPoint, ...section.bendPoints ?? [], section.endPoint];
|
|
530
|
+
}
|
|
531
|
+
function extractEdges(layouted) {
|
|
532
|
+
const edges = [];
|
|
533
|
+
for (const edge of layouted.edges ?? []) {
|
|
534
|
+
for (const section of edge.sections ?? []) {
|
|
535
|
+
edges.push({
|
|
536
|
+
id: edge.id,
|
|
537
|
+
points: sectionToPoints(section)
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return edges;
|
|
542
|
+
}
|
|
543
|
+
async function layoutComponent(doc, tableNames, compact) {
|
|
544
|
+
const elk = new ELK();
|
|
545
|
+
const nameSet = new Set(tableNames);
|
|
546
|
+
const tables = doc.tables.filter((t) => nameSet.has(t.name));
|
|
547
|
+
const direction = tables.length >= 8 ? "DOWN" : "RIGHT";
|
|
548
|
+
const children = tables.map((table) => {
|
|
549
|
+
const { w, h } = measureTableBox(table, compact);
|
|
550
|
+
return { id: table.name, width: w, height: h };
|
|
551
|
+
});
|
|
552
|
+
const edges = [];
|
|
553
|
+
const seen = /* @__PURE__ */ new Set();
|
|
554
|
+
for (const rel of doc.relationships.filter((r) => r.source === "schema")) {
|
|
555
|
+
if (!nameSet.has(rel.fromTable) || !nameSet.has(rel.toTable)) continue;
|
|
556
|
+
const key = `${rel.fromTable}->${rel.toTable}`;
|
|
557
|
+
if (seen.has(key)) continue;
|
|
558
|
+
seen.add(key);
|
|
559
|
+
edges.push({
|
|
560
|
+
id: key,
|
|
561
|
+
sources: [rel.fromTable],
|
|
562
|
+
targets: [rel.toTable]
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
const graph = {
|
|
566
|
+
id: "root",
|
|
567
|
+
layoutOptions: {
|
|
568
|
+
"elk.algorithm": "layered",
|
|
569
|
+
"elk.direction": direction,
|
|
570
|
+
"elk.edgeRouting": "ORTHOGONAL",
|
|
571
|
+
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
|
|
572
|
+
"elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
|
|
573
|
+
"elk.layered.spacing.nodeNodeBetweenLayers": compact ? "56" : "80",
|
|
574
|
+
"elk.spacing.nodeNode": compact ? "32" : "48",
|
|
575
|
+
"elk.spacing.edgeNode": "24",
|
|
576
|
+
"elk.padding": `[top=${PAD},left=${PAD},bottom=${PAD},right=${PAD}]`
|
|
577
|
+
},
|
|
578
|
+
children,
|
|
579
|
+
edges
|
|
580
|
+
};
|
|
581
|
+
const layouted = await elk.layout(graph);
|
|
582
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
583
|
+
let minX = Infinity;
|
|
584
|
+
let minY = Infinity;
|
|
585
|
+
let maxX = -Infinity;
|
|
586
|
+
let maxY = -Infinity;
|
|
587
|
+
for (const child of layouted.children ?? []) {
|
|
588
|
+
const table = tables.find((t) => t.name === child.id);
|
|
589
|
+
if (!table) continue;
|
|
590
|
+
const { w, h } = measureTableBox(table, compact);
|
|
591
|
+
const x = child.x ?? 0;
|
|
592
|
+
const y = child.y ?? 0;
|
|
593
|
+
const box = { x, y, w, h };
|
|
594
|
+
boxes.set(child.id, box);
|
|
595
|
+
minX = Math.min(minX, x);
|
|
596
|
+
minY = Math.min(minY, y);
|
|
597
|
+
maxX = Math.max(maxX, x + w);
|
|
598
|
+
maxY = Math.max(maxY, y + h);
|
|
599
|
+
}
|
|
600
|
+
const width = Math.ceil(maxX - minX + PAD * 2);
|
|
601
|
+
const height = Math.ceil(maxY - minY + PAD * 2);
|
|
602
|
+
const dx = PAD - minX;
|
|
603
|
+
const dy = PAD - minY;
|
|
604
|
+
for (const [name, box] of boxes) {
|
|
605
|
+
boxes.set(name, { x: box.x + dx, y: box.y + dy, w: box.w, h: box.h });
|
|
606
|
+
}
|
|
607
|
+
const shiftedEdges = extractEdges(layouted).map((edge) => ({
|
|
608
|
+
...edge,
|
|
609
|
+
points: edge.points.map((p) => ({ x: p.x + dx, y: p.y + dy }))
|
|
610
|
+
}));
|
|
611
|
+
return { boxes, edges: shiftedEdges, width, height };
|
|
612
|
+
}
|
|
613
|
+
function shiftLayout(boxes, edges, offsetX, offsetY) {
|
|
614
|
+
for (const [name, box] of boxes) {
|
|
615
|
+
boxes.set(name, { ...box, x: box.x + offsetX, y: box.y + offsetY });
|
|
616
|
+
}
|
|
617
|
+
for (const edge of edges) {
|
|
618
|
+
for (const p of edge.points) {
|
|
619
|
+
p.x += offsetX;
|
|
620
|
+
p.y += offsetY;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
async function layoutErDiagram(doc) {
|
|
625
|
+
const tables = doc.tables;
|
|
626
|
+
if (tables.length === 0) {
|
|
627
|
+
return {
|
|
628
|
+
boxes: /* @__PURE__ */ new Map(),
|
|
629
|
+
edges: [],
|
|
630
|
+
compact: false,
|
|
631
|
+
width: 400,
|
|
632
|
+
height: 80
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
const compact = isCompactLayout(tables.length);
|
|
636
|
+
const adj = buildAdjacency(doc);
|
|
637
|
+
const components = connectedComponents(
|
|
638
|
+
tables.map((t) => t.name),
|
|
639
|
+
adj
|
|
640
|
+
);
|
|
641
|
+
const mergedBoxes = /* @__PURE__ */ new Map();
|
|
642
|
+
const mergedEdges = [];
|
|
643
|
+
const clusterCols = components.length <= 1 ? 1 : components.length <= 4 ? 2 : 3;
|
|
644
|
+
let tileX = 0;
|
|
645
|
+
let tileY = 0;
|
|
646
|
+
let rowHeight = 0;
|
|
647
|
+
let maxWidth = PAD;
|
|
648
|
+
let maxHeight = PAD;
|
|
649
|
+
for (const [i, component] of components.entries()) {
|
|
650
|
+
const laid = await layoutComponent(doc, component, compact);
|
|
651
|
+
shiftLayout(laid.boxes, laid.edges, tileX, tileY);
|
|
652
|
+
for (const [name, box] of laid.boxes) mergedBoxes.set(name, box);
|
|
653
|
+
mergedEdges.push(...laid.edges);
|
|
654
|
+
rowHeight = Math.max(rowHeight, laid.height);
|
|
655
|
+
tileX += laid.width + CLUSTER_GAP;
|
|
656
|
+
maxWidth = Math.max(maxWidth, tileX);
|
|
657
|
+
maxHeight = Math.max(maxHeight, tileY + laid.height);
|
|
658
|
+
if ((i + 1) % clusterCols === 0) {
|
|
659
|
+
tileX = 0;
|
|
660
|
+
tileY += rowHeight + CLUSTER_GAP;
|
|
661
|
+
rowHeight = 0;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
boxes: mergedBoxes,
|
|
666
|
+
edges: mergedEdges,
|
|
667
|
+
compact,
|
|
668
|
+
width: Math.ceil(maxWidth),
|
|
669
|
+
height: Math.ceil(maxHeight + PAD)
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/exporters/diagram/er-diagram-svg.ts
|
|
674
|
+
async function renderErDiagramSvg(doc) {
|
|
675
|
+
const tables = doc.tables;
|
|
676
|
+
if (tables.length === 0) {
|
|
677
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="80"><text x="10" y="40" font-family="Arial,sans-serif" font-size="14">No tables</text></svg>`;
|
|
678
|
+
}
|
|
679
|
+
const layout = await layoutErDiagram(doc);
|
|
680
|
+
return buildErDiagramSvg(doc, layout);
|
|
681
|
+
}
|
|
682
|
+
async function renderErDiagramPng(doc) {
|
|
683
|
+
const tables = doc.tables;
|
|
684
|
+
if (tables.length === 0) {
|
|
685
|
+
const svg2 = await renderErDiagramSvg(doc);
|
|
686
|
+
const sharp2 = (await import("sharp")).default;
|
|
687
|
+
const buffer2 = await sharp2(Buffer.from(svg2)).png().toBuffer();
|
|
688
|
+
return { buffer: buffer2, width: 400, height: 80 };
|
|
689
|
+
}
|
|
690
|
+
const layout = await layoutErDiagram(doc);
|
|
691
|
+
const svg = buildErDiagramSvg(doc, layout);
|
|
692
|
+
const sharp = (await import("sharp")).default;
|
|
693
|
+
const buffer = await sharp(Buffer.from(svg)).png().toBuffer();
|
|
694
|
+
return { buffer, width: layout.width, height: layout.height };
|
|
695
|
+
}
|
|
696
|
+
function buildErDiagramSvg(doc, layout) {
|
|
697
|
+
const { boxes, edges, width, height } = layout;
|
|
698
|
+
const parts = [
|
|
699
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" font-family="Arial,sans-serif" font-size="11">`,
|
|
700
|
+
`<rect width="100%" height="100%" fill="#ffffff"/>`,
|
|
701
|
+
`<defs><marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#5b7aa6"/></marker></defs>`,
|
|
702
|
+
`<g class="edges">`
|
|
703
|
+
];
|
|
704
|
+
for (const edge of edges) {
|
|
705
|
+
if (edge.points.length < 2) continue;
|
|
706
|
+
const d = pointsToPath(edge.points);
|
|
707
|
+
parts.push(
|
|
708
|
+
`<path d="${d}" fill="none" stroke="#7d96b8" stroke-width="1.25" marker-end="url(#arrow)"/>`
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
parts.push(`</g><g class="nodes">`);
|
|
712
|
+
for (const table of doc.tables) {
|
|
713
|
+
const box = boxes.get(table.name);
|
|
714
|
+
parts.push(...renderTableBox(table, box));
|
|
715
|
+
}
|
|
716
|
+
parts.push("</g></svg>");
|
|
717
|
+
return parts.join("");
|
|
718
|
+
}
|
|
719
|
+
function pointsToPath(points) {
|
|
720
|
+
return points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ");
|
|
721
|
+
}
|
|
722
|
+
function renderTableBox(table, box) {
|
|
723
|
+
const parts = [
|
|
724
|
+
`<rect x="${box.x}" y="${box.y}" width="${box.w}" height="${box.h}" fill="#f8fafc" stroke="#4472c4" stroke-width="1.5" rx="4"/>`,
|
|
725
|
+
`<rect x="${box.x}" y="${box.y}" width="${box.w}" height="${HEADER_H}" fill="#4472c4" rx="4"/>`,
|
|
726
|
+
`<rect x="${box.x}" y="${box.y + HEADER_H - 4}" width="${box.w}" height="4" fill="#4472c4"/>`,
|
|
727
|
+
`<text x="${box.x + 8}" y="${box.y + 18}" fill="#ffffff" font-weight="bold">${escapeXml(table.name)}</text>`
|
|
728
|
+
];
|
|
729
|
+
let cy = box.y + HEADER_H + 14;
|
|
730
|
+
const visible = getVisibleErColumns(table);
|
|
731
|
+
for (const col of visible) {
|
|
732
|
+
const marker = col.isPrimaryKey ? " PK" : col.isForeignKey ? " FK" : "";
|
|
733
|
+
parts.push(
|
|
734
|
+
`<text x="${box.x + 8}" y="${cy}" fill="#333333">${escapeXml(col.name)} : ${escapeXml(shortType(col.type))}${marker}</text>`
|
|
735
|
+
);
|
|
736
|
+
cy += LINE_H;
|
|
737
|
+
}
|
|
738
|
+
if (table.columns.length > visible.length) {
|
|
739
|
+
parts.push(
|
|
740
|
+
`<text x="${box.x + 8}" y="${cy}" fill="#666666">... +${table.columns.length - visible.length} more</text>`
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
return parts;
|
|
744
|
+
}
|
|
745
|
+
function shortType(type) {
|
|
746
|
+
return type.length > 18 ? `${type.slice(0, 15)}...` : type;
|
|
747
|
+
}
|
|
748
|
+
function escapeXml(text) {
|
|
749
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
750
|
+
}
|
|
751
|
+
function fitErDiagramToBox(width, height, maxWidth, maxHeight) {
|
|
752
|
+
const scale = Math.min(maxWidth / width, maxHeight / height, 1);
|
|
753
|
+
return {
|
|
754
|
+
width: Math.max(1, Math.round(width * scale)),
|
|
755
|
+
height: Math.max(1, Math.round(height * scale))
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
223
759
|
// src/exporters/excel/excel-exporter.ts
|
|
224
760
|
var COLOR = {
|
|
225
761
|
headerBg: "FF4472C4",
|
|
@@ -245,6 +781,9 @@ async function exportExcelDictionary(doc, options) {
|
|
|
245
781
|
sheetNames.set(table.name, buildSheetName(table.name, sheetNames));
|
|
246
782
|
}
|
|
247
783
|
addOverviewSheet(workbook, doc, labels, sheetNames);
|
|
784
|
+
if (doc.tables.length > 0) {
|
|
785
|
+
await addErDiagramSheet(workbook, doc, labels, options.outDir);
|
|
786
|
+
}
|
|
248
787
|
for (const table of doc.tables) {
|
|
249
788
|
const sheetName = sheetNames.get(table.name);
|
|
250
789
|
const sheet = workbook.addWorksheet(sheetName);
|
|
@@ -332,6 +871,51 @@ function addOverviewSheet(workbook, doc, labels, sheetNames) {
|
|
|
332
871
|
};
|
|
333
872
|
sheet.views = [{ state: "frozen", ySplit: headerRowNum }];
|
|
334
873
|
}
|
|
874
|
+
async function addErDiagramSheet(workbook, doc, labels, outDir) {
|
|
875
|
+
const sheet = workbook.addWorksheet(labels.erDiagramSheet);
|
|
876
|
+
sheet.mergeCells(1, 1, 1, 6);
|
|
877
|
+
const titleCell = sheet.getCell(1, 1);
|
|
878
|
+
titleCell.value = labels.erDiagramHeading;
|
|
879
|
+
titleCell.font = { bold: true, size: 14, color: { argb: COLOR.overviewFg } };
|
|
880
|
+
titleCell.fill = solidFill(COLOR.overviewBg);
|
|
881
|
+
titleCell.alignment = { horizontal: "center", vertical: "middle" };
|
|
882
|
+
sheet.getRow(1).height = 28;
|
|
883
|
+
let nextRow = 3;
|
|
884
|
+
try {
|
|
885
|
+
const { buffer: png, width, height } = await renderErDiagramPng(doc);
|
|
886
|
+
await writeFile2(join2(outDir, "er_diagram.png"), png);
|
|
887
|
+
const imageId = workbook.addImage({
|
|
888
|
+
base64: png.toString("base64"),
|
|
889
|
+
extension: "png"
|
|
890
|
+
});
|
|
891
|
+
const fitted = fitErDiagramToBox(width, height, 1100, 1200);
|
|
892
|
+
sheet.addImage(imageId, {
|
|
893
|
+
tl: { col: 0, row: 2 },
|
|
894
|
+
ext: fitted
|
|
895
|
+
});
|
|
896
|
+
nextRow = Math.max(28, Math.ceil(fitted.height / 18) + 4);
|
|
897
|
+
} catch {
|
|
898
|
+
sheet.getCell(3, 1).value = labels.viewErDiagram;
|
|
899
|
+
nextRow = 5;
|
|
900
|
+
}
|
|
901
|
+
const mermaid = getErDiagramMermaid(doc);
|
|
902
|
+
sheet.getCell(nextRow, 1).value = "Mermaid source";
|
|
903
|
+
sheet.getCell(nextRow, 1).font = { bold: true, color: { argb: COLOR.metaFg } };
|
|
904
|
+
nextRow += 1;
|
|
905
|
+
sheet.mergeCells(nextRow, 1, nextRow + 20, 6);
|
|
906
|
+
const sourceCell = sheet.getCell(nextRow, 1);
|
|
907
|
+
sourceCell.value = mermaid;
|
|
908
|
+
sourceCell.alignment = { wrapText: true, vertical: "top" };
|
|
909
|
+
sourceCell.font = { name: "Courier New", size: 9 };
|
|
910
|
+
sheet.columns = [
|
|
911
|
+
{ width: 24 },
|
|
912
|
+
{ width: 24 },
|
|
913
|
+
{ width: 24 },
|
|
914
|
+
{ width: 24 },
|
|
915
|
+
{ width: 24 },
|
|
916
|
+
{ width: 24 }
|
|
917
|
+
];
|
|
918
|
+
}
|
|
335
919
|
function populateTableSheet(sheet, table, doc, labels) {
|
|
336
920
|
const indexes = collectTableIndexes(table, doc);
|
|
337
921
|
sheet.mergeCells(1, 1, 1, 6);
|
|
@@ -372,48 +956,39 @@ function populateTableSheet(sheet, table, doc, labels) {
|
|
|
372
956
|
row.getCell(2).alignment = { wrapText: true, vertical: "top" };
|
|
373
957
|
}
|
|
374
958
|
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
|
-
]);
|
|
959
|
+
const headerRow = sheet.addRow(columnDefinitionHeaders(labels));
|
|
383
960
|
styleColorRow(headerRow, COLOR.headerBg, COLOR.headerFg);
|
|
384
|
-
applyBorderToRow(headerRow,
|
|
961
|
+
applyBorderToRow(headerRow, A5_COLUMN_COUNT);
|
|
385
962
|
const headerRowNum = headerRow.number;
|
|
386
963
|
for (const [i, column] of table.columns.entries()) {
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
column.name,
|
|
393
|
-
displayValue(column.comment, labels),
|
|
394
|
-
column.type,
|
|
395
|
-
column.nullable ? labels.no : labels.yes,
|
|
396
|
-
column.defaultValue ?? "-",
|
|
397
|
-
notes || "-"
|
|
398
|
-
]);
|
|
964
|
+
const row = sheet.addRow(
|
|
965
|
+
columnDefinitionRow(column, labels).map(
|
|
966
|
+
(value, index) => index === 1 ? displayValue(value, labels) : value
|
|
967
|
+
)
|
|
968
|
+
);
|
|
399
969
|
if (column.isPrimaryKey) {
|
|
400
|
-
shadeRow(row,
|
|
970
|
+
shadeRow(row, A5_COLUMN_COUNT, COLOR.pkBg);
|
|
401
971
|
row.getCell(1).font = { bold: true };
|
|
402
972
|
} else if (column.isForeignKey) {
|
|
403
|
-
shadeRow(row,
|
|
973
|
+
shadeRow(row, A5_COLUMN_COUNT, COLOR.fkBg);
|
|
404
974
|
} else if (i % 2 === 1) {
|
|
405
|
-
shadeRow(row,
|
|
975
|
+
shadeRow(row, A5_COLUMN_COUNT, COLOR.altRow);
|
|
406
976
|
}
|
|
407
|
-
row.getCell(
|
|
408
|
-
|
|
977
|
+
row.getCell(5).alignment = { horizontal: "center" };
|
|
978
|
+
row.getCell(9).alignment = { horizontal: "center" };
|
|
979
|
+
applyBorderToRow(row, A5_COLUMN_COUNT);
|
|
409
980
|
}
|
|
410
981
|
sheet.columns = [
|
|
982
|
+
{ width: 22 },
|
|
411
983
|
{ width: 24 },
|
|
412
|
-
{ width:
|
|
413
|
-
{ width: 18 },
|
|
984
|
+
{ width: 16 },
|
|
414
985
|
{ width: 10 },
|
|
415
|
-
{ width:
|
|
416
|
-
{ width:
|
|
986
|
+
{ width: 8 },
|
|
987
|
+
{ width: 14 },
|
|
988
|
+
{ width: 8 },
|
|
989
|
+
{ width: 8 },
|
|
990
|
+
{ width: 8 },
|
|
991
|
+
{ width: 28 }
|
|
417
992
|
];
|
|
418
993
|
sheet.views = [{ state: "frozen", ySplit: headerRowNum }];
|
|
419
994
|
}
|
|
@@ -484,7 +1059,7 @@ function collectTableIndexes(table, doc) {
|
|
|
484
1059
|
}
|
|
485
1060
|
|
|
486
1061
|
// src/exporters/markdown/markdown-exporter.ts
|
|
487
|
-
import { mkdir as mkdir3, writeFile as
|
|
1062
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
488
1063
|
import { join as join3 } from "path";
|
|
489
1064
|
|
|
490
1065
|
// src/core/sanitize.ts
|
|
@@ -498,8 +1073,16 @@ async function exportMarkdownDocs(doc, options) {
|
|
|
498
1073
|
const tablesDir = join3(options.outDir, "tables");
|
|
499
1074
|
await mkdir3(tablesDir, { recursive: true });
|
|
500
1075
|
const labels = getOutputLabels(options.language);
|
|
1076
|
+
if (doc.tables.length > 0) {
|
|
1077
|
+
const mermaid = getErDiagramMermaid(doc);
|
|
1078
|
+
await writeFile3(
|
|
1079
|
+
join3(options.outDir, "ER_DIAGRAM.md"),
|
|
1080
|
+
renderErDiagramMarkdown(mermaid, labels),
|
|
1081
|
+
"utf8"
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
501
1084
|
for (const table of doc.tables) {
|
|
502
|
-
await
|
|
1085
|
+
await writeFile3(
|
|
503
1086
|
join3(tablesDir, `${sanitizeFilename(table.name)}.md`),
|
|
504
1087
|
renderTableDoc(table, doc, labels),
|
|
505
1088
|
"utf8"
|
|
@@ -546,12 +1129,12 @@ function renderTableDoc(table, doc, labels) {
|
|
|
546
1129
|
lines.push(`## ${labels.columnsHeading}`);
|
|
547
1130
|
lines.push("");
|
|
548
1131
|
lines.push(
|
|
549
|
-
`| ${labels.
|
|
1132
|
+
`| ${columnDefinitionHeaders(labels).join(" | ")} |`
|
|
550
1133
|
);
|
|
551
|
-
lines.push("
|
|
1134
|
+
lines.push(`|${columnDefinitionHeaders(labels).map(() => "--------").join("|")}|`);
|
|
552
1135
|
for (const col of table.columns) {
|
|
553
1136
|
lines.push(
|
|
554
|
-
`| ${
|
|
1137
|
+
`| ${columnDefinitionRow(col, labels).map((value) => escapeMd(value)).join(" | ")} |`
|
|
555
1138
|
);
|
|
556
1139
|
}
|
|
557
1140
|
lines.push("");
|
|
@@ -570,7 +1153,7 @@ function escapeMd(text) {
|
|
|
570
1153
|
}
|
|
571
1154
|
|
|
572
1155
|
// src/exporters/html/html-exporter.ts
|
|
573
|
-
import { mkdir as mkdir4, writeFile as
|
|
1156
|
+
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
574
1157
|
import { join as join4 } from "path";
|
|
575
1158
|
async function exportHtmlDocs(doc, options) {
|
|
576
1159
|
try {
|
|
@@ -578,13 +1161,21 @@ async function exportHtmlDocs(doc, options) {
|
|
|
578
1161
|
const tablesDir = join4(htmlDir, "tables");
|
|
579
1162
|
await mkdir4(tablesDir, { recursive: true });
|
|
580
1163
|
const labels = getOutputLabels(options.language);
|
|
581
|
-
await
|
|
1164
|
+
await writeFile4(
|
|
582
1165
|
join4(htmlDir, "index.html"),
|
|
583
1166
|
renderIndexPage(doc, labels),
|
|
584
1167
|
"utf8"
|
|
585
1168
|
);
|
|
1169
|
+
if (doc.tables.length > 0) {
|
|
1170
|
+
const mermaid = getErDiagramMermaid(doc);
|
|
1171
|
+
await writeFile4(
|
|
1172
|
+
join4(htmlDir, "er-diagram.html"),
|
|
1173
|
+
renderErDiagramHtmlPage(mermaid, labels),
|
|
1174
|
+
"utf8"
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
586
1177
|
for (const table of doc.tables) {
|
|
587
|
-
await
|
|
1178
|
+
await writeFile4(
|
|
588
1179
|
join4(tablesDir, `${sanitizeFilename(table.name)}.html`),
|
|
589
1180
|
renderTablePage(table, doc, labels),
|
|
590
1181
|
"utf8"
|
|
@@ -666,7 +1257,7 @@ function pageShell(title, body, fromSubdir = false) {
|
|
|
666
1257
|
<head>
|
|
667
1258
|
<meta charset="UTF-8">
|
|
668
1259
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
669
|
-
<title>${
|
|
1260
|
+
<title>${esc2(title)}</title>
|
|
670
1261
|
<style>${CSS} </style>
|
|
671
1262
|
</head>
|
|
672
1263
|
<body>
|
|
@@ -683,34 +1274,35 @@ function renderIndexPage(doc, labels) {
|
|
|
683
1274
|
const fkCount = table.foreignKeys.length;
|
|
684
1275
|
const fileName = sanitizeFilename(table.name);
|
|
685
1276
|
tableRows += ` <tr>
|
|
686
|
-
<td><a href="tables/${fileName}.html">${
|
|
687
|
-
<td>${
|
|
1277
|
+
<td><a href="tables/${fileName}.html">${esc2(table.name)}</a></td>
|
|
1278
|
+
<td>${esc2(table.comment ?? "")}</td>
|
|
688
1279
|
<td style="text-align:center">${table.columns.length}</td>
|
|
689
|
-
<td>${
|
|
1280
|
+
<td>${esc2(pkCols)}</td>
|
|
690
1281
|
<td style="text-align:center">${fkCount}</td>
|
|
691
1282
|
</tr>
|
|
692
1283
|
`;
|
|
693
1284
|
}
|
|
694
1285
|
const body = `
|
|
695
|
-
<h1>${
|
|
1286
|
+
<h1>${esc2(labels.docTitle)}</h1>
|
|
696
1287
|
<div class="summary">
|
|
697
|
-
<div class="summary-item"><div class="num">${doc.tables.length}</div><div class="lbl">${
|
|
698
|
-
<div class="summary-item"><div class="num">${doc.relationships.length}</div><div class="lbl">${
|
|
699
|
-
<div class="summary-item"><div class="num">${doc.dialect}</div><div class="lbl">${
|
|
1288
|
+
<div class="summary-item"><div class="num">${doc.tables.length}</div><div class="lbl">${esc2(labels.tablesLabel)}</div></div>
|
|
1289
|
+
<div class="summary-item"><div class="num">${doc.relationships.length}</div><div class="lbl">${esc2(labels.relationshipsLabel)}</div></div>
|
|
1290
|
+
<div class="summary-item"><div class="num">${doc.dialect}</div><div class="lbl">${esc2(labels.dialectLabel)}</div></div>
|
|
700
1291
|
</div>
|
|
701
|
-
<
|
|
1292
|
+
<p class="back"><a href="er-diagram.html">${esc2(labels.erDiagramHeading)} \u2192</a></p>
|
|
1293
|
+
<h2>${esc2(labels.tableListHeading)}</h2>
|
|
702
1294
|
<table class="table-list">
|
|
703
1295
|
<thead><tr>
|
|
704
|
-
<th>${
|
|
705
|
-
<th>${
|
|
1296
|
+
<th>${esc2(labels.tableLabel)}</th>
|
|
1297
|
+
<th>${esc2(labels.tableLogicalName)}</th>
|
|
706
1298
|
<th style="width:70px;text-align:center">Cols</th>
|
|
707
|
-
<th>${
|
|
1299
|
+
<th>${esc2(labels.primaryKey)}</th>
|
|
708
1300
|
<th style="width:50px;text-align:center">FK</th>
|
|
709
1301
|
</tr></thead>
|
|
710
1302
|
<tbody>
|
|
711
1303
|
${tableRows} </tbody>
|
|
712
1304
|
</table>
|
|
713
|
-
<p class="note">${
|
|
1305
|
+
<p class="note">${esc2(labels.generatedNote)}</p>
|
|
714
1306
|
`;
|
|
715
1307
|
return pageShell(labels.docTitle, body);
|
|
716
1308
|
}
|
|
@@ -728,39 +1320,34 @@ function renderTablePage(table, doc, labels) {
|
|
|
728
1320
|
const pkBadge = col.isPrimaryKey ? `<span class="badge badge-pk">PK</span>` : "";
|
|
729
1321
|
const fkBadge = col.isForeignKey ? `<span class="badge badge-fk">FK</span>` : "";
|
|
730
1322
|
const rowClass = col.isPrimaryKey ? "pk" : col.isForeignKey ? "fk" : "";
|
|
731
|
-
const
|
|
732
|
-
colRows += ` <tr${rowClass ? ` class="${rowClass}"` : ""}><td>${
|
|
1323
|
+
const cells = columnDefinitionRow(col, labels);
|
|
1324
|
+
colRows += ` <tr${rowClass ? ` class="${rowClass}"` : ""}><td>${esc2(cells[0] ?? "")}${pkBadge}${fkBadge}</td>` + cells.slice(1).map((cell) => `<td>${esc2(cell)}</td>`).join("") + `</tr>
|
|
733
1325
|
`;
|
|
734
1326
|
}
|
|
735
1327
|
const body = `
|
|
736
|
-
<p class="back"><a href="../index.html">\u2190 ${
|
|
737
|
-
<h1>${
|
|
738
|
-
<h2>${
|
|
1328
|
+
<p class="back"><a href="../index.html">\u2190 ${esc2(labels.tableListHeading)}</a></p>
|
|
1329
|
+
<h1>${esc2(table.name)}</h1>
|
|
1330
|
+
<h2>${esc2(labels.tableInfoHeading)}</h2>
|
|
739
1331
|
<table class="meta">
|
|
740
1332
|
<tbody>
|
|
741
|
-
<tr><th>${
|
|
742
|
-
<tr><th>${
|
|
743
|
-
<tr><th>${
|
|
744
|
-
<tr><th>${
|
|
745
|
-
<tr><th>${
|
|
746
|
-
<tr><th>${
|
|
1333
|
+
<tr><th>${esc2(labels.tablePhysicalName)}</th><td>${esc2(table.name)}</td></tr>
|
|
1334
|
+
<tr><th>${esc2(labels.tableLogicalName)}</th><td>${esc2(table.comment ?? "")}</td></tr>
|
|
1335
|
+
<tr><th>${esc2(labels.schema)}</th><td>${esc2(table.schema ?? "")}</td></tr>
|
|
1336
|
+
<tr><th>${esc2(labels.primaryKey)}</th><td>${esc2(table.primaryKeys.join(", ") || labels.none)}</td></tr>
|
|
1337
|
+
<tr><th>${esc2(labels.foreignKeys)}</th><td>${foreignKeys}</td></tr>
|
|
1338
|
+
<tr><th>${esc2(labels.indexes)}</th><td>${indexText}</td></tr>
|
|
747
1339
|
</tbody>
|
|
748
1340
|
</table>
|
|
749
1341
|
|
|
750
|
-
<h2>${
|
|
1342
|
+
<h2>${esc2(labels.columnsHeading)}</h2>
|
|
751
1343
|
<table class="columns">
|
|
752
1344
|
<thead><tr>
|
|
753
|
-
|
|
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>
|
|
1345
|
+
${columnDefinitionHeaders(labels).map((header) => `<th>${esc2(header)}</th>`).join("\n ")}
|
|
759
1346
|
</tr></thead>
|
|
760
1347
|
<tbody>
|
|
761
1348
|
${colRows} </tbody>
|
|
762
1349
|
</table>
|
|
763
|
-
<p class="note">${
|
|
1350
|
+
<p class="note">${esc2(labels.generatedNote)}</p>
|
|
764
1351
|
`;
|
|
765
1352
|
return pageShell(table.name, body);
|
|
766
1353
|
}
|
|
@@ -772,12 +1359,12 @@ function collectTableIndexes3(table, doc) {
|
|
|
772
1359
|
)
|
|
773
1360
|
];
|
|
774
1361
|
}
|
|
775
|
-
function
|
|
1362
|
+
function esc2(text) {
|
|
776
1363
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
777
1364
|
}
|
|
778
1365
|
|
|
779
1366
|
// src/exporters/word/word-exporter.ts
|
|
780
|
-
import { mkdir as mkdir5, writeFile as
|
|
1367
|
+
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
781
1368
|
import { join as join5 } from "path";
|
|
782
1369
|
import {
|
|
783
1370
|
Document,
|
|
@@ -787,7 +1374,8 @@ import {
|
|
|
787
1374
|
TableCell,
|
|
788
1375
|
TableRow,
|
|
789
1376
|
TextRun,
|
|
790
|
-
HeadingLevel
|
|
1377
|
+
HeadingLevel,
|
|
1378
|
+
ImageRun
|
|
791
1379
|
} from "docx";
|
|
792
1380
|
async function exportWordDocument(doc, options) {
|
|
793
1381
|
try {
|
|
@@ -821,6 +1409,53 @@ async function exportWordDocument(doc, options) {
|
|
|
821
1409
|
children: [new TextRun(`${labels.relationshipsLabel}: ${doc.relationships.length}`)]
|
|
822
1410
|
})
|
|
823
1411
|
);
|
|
1412
|
+
if (doc.tables.length > 0) {
|
|
1413
|
+
children.push(
|
|
1414
|
+
new Paragraph({
|
|
1415
|
+
heading: HeadingLevel.HEADING_2,
|
|
1416
|
+
children: [new TextRun(labels.erDiagramHeading)]
|
|
1417
|
+
})
|
|
1418
|
+
);
|
|
1419
|
+
try {
|
|
1420
|
+
const { buffer: png, width, height } = await renderErDiagramPng(doc);
|
|
1421
|
+
await writeFile5(join5(options.outDir, "er_diagram.png"), png);
|
|
1422
|
+
const fitted = fitErDiagramToBox(width, height, 620, 900);
|
|
1423
|
+
children.push(
|
|
1424
|
+
new Paragraph({
|
|
1425
|
+
children: [
|
|
1426
|
+
new ImageRun({
|
|
1427
|
+
data: png,
|
|
1428
|
+
transformation: fitted,
|
|
1429
|
+
type: "png"
|
|
1430
|
+
})
|
|
1431
|
+
]
|
|
1432
|
+
})
|
|
1433
|
+
);
|
|
1434
|
+
} catch {
|
|
1435
|
+
children.push(
|
|
1436
|
+
new Paragraph({
|
|
1437
|
+
children: [new TextRun(labels.viewErDiagram)]
|
|
1438
|
+
})
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
const mermaid = getErDiagramMermaid(doc);
|
|
1442
|
+
children.push(
|
|
1443
|
+
new Paragraph({
|
|
1444
|
+
children: [new TextRun({ text: "Mermaid source", bold: true })]
|
|
1445
|
+
})
|
|
1446
|
+
);
|
|
1447
|
+
children.push(
|
|
1448
|
+
new Paragraph({
|
|
1449
|
+
children: [
|
|
1450
|
+
new TextRun({
|
|
1451
|
+
text: mermaid,
|
|
1452
|
+
font: "Courier New",
|
|
1453
|
+
size: 18
|
|
1454
|
+
})
|
|
1455
|
+
]
|
|
1456
|
+
})
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
824
1459
|
children.push(
|
|
825
1460
|
new Paragraph({
|
|
826
1461
|
heading: HeadingLevel.HEADING_2,
|
|
@@ -1021,7 +1656,7 @@ async function exportWordDocument(doc, options) {
|
|
|
1021
1656
|
sections: [{ children }]
|
|
1022
1657
|
});
|
|
1023
1658
|
const buffer = await Packer.toBuffer(wordDoc);
|
|
1024
|
-
await
|
|
1659
|
+
await writeFile5(join5(options.outDir, "database_document.docx"), buffer);
|
|
1025
1660
|
} catch (err) {
|
|
1026
1661
|
throw new Error(
|
|
1027
1662
|
`Failed to export Word document: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -1080,14 +1715,7 @@ function renderTableDetail(table, doc, labels) {
|
|
|
1080
1715
|
return items;
|
|
1081
1716
|
}
|
|
1082
1717
|
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(
|
|
1718
|
+
const headerCells = columnDefinitionHeaders(labels).map(
|
|
1091
1719
|
(h) => new TableCell({
|
|
1092
1720
|
children: [
|
|
1093
1721
|
new Paragraph({ children: [new TextRun({ text: h, bold: true })] })
|
|
@@ -1098,40 +1726,11 @@ function renderColumnsTable(table, labels) {
|
|
|
1098
1726
|
for (const col of table.columns) {
|
|
1099
1727
|
colRows.push(
|
|
1100
1728
|
new TableRow({
|
|
1101
|
-
children:
|
|
1102
|
-
new TableCell({
|
|
1103
|
-
children: [new Paragraph({ children: [new TextRun(
|
|
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
|
-
]
|
|
1729
|
+
children: columnDefinitionRow(col, labels).map(
|
|
1730
|
+
(value) => new TableCell({
|
|
1731
|
+
children: [new Paragraph({ children: [new TextRun(value)] })]
|
|
1133
1732
|
})
|
|
1134
|
-
|
|
1733
|
+
)
|
|
1135
1734
|
})
|
|
1136
1735
|
);
|
|
1137
1736
|
}
|
|
@@ -1179,6 +1778,221 @@ function createWarning(code, message, target) {
|
|
|
1179
1778
|
};
|
|
1180
1779
|
}
|
|
1181
1780
|
|
|
1781
|
+
// src/parsers/sql/column-meta.ts
|
|
1782
|
+
function normalizeColumnType(definition) {
|
|
1783
|
+
if (typeof definition !== "object" || definition === null) {
|
|
1784
|
+
return { type: String(definition ?? "unknown").toLowerCase() };
|
|
1785
|
+
}
|
|
1786
|
+
const def = definition;
|
|
1787
|
+
const base = String(def.dataType ?? def.type ?? def.name ?? "unknown").toLowerCase();
|
|
1788
|
+
if (String(def.dataType ?? "").toUpperCase() === "ENUM") {
|
|
1789
|
+
const values = extractEnumValues(def.expr);
|
|
1790
|
+
if (values.length > 0) {
|
|
1791
|
+
const joined = values.join(", ");
|
|
1792
|
+
return {
|
|
1793
|
+
type: `${base}(${values.map((v) => `'${v}'`).join(",")})`,
|
|
1794
|
+
size: String(values.length)
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
if (def.length !== void 0 && def.length !== null) {
|
|
1799
|
+
const length = formatAstValue(def.length);
|
|
1800
|
+
if (def.scale !== void 0 && def.scale !== null) {
|
|
1801
|
+
const scale = formatAstValue(def.scale);
|
|
1802
|
+
return {
|
|
1803
|
+
type: `${base}(${length},${scale})`,
|
|
1804
|
+
size: `${length},${scale}`
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
return {
|
|
1808
|
+
type: `${base}(${length})`,
|
|
1809
|
+
size: length
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
const suffix = Array.isArray(def.suffix) ? def.suffix.map((item) => formatAstValue(item)).filter(Boolean).join(" ") : def.suffix ? formatAstValue(def.suffix) : "";
|
|
1813
|
+
return {
|
|
1814
|
+
type: suffix ? `${base} ${suffix}`.trim() : base
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
function extractColumnComment(definition) {
|
|
1818
|
+
const comment = definition.comment;
|
|
1819
|
+
if (!comment) return void 0;
|
|
1820
|
+
const value = comment.value;
|
|
1821
|
+
if (value?.value !== void 0) return String(value.value);
|
|
1822
|
+
if (comment.value !== void 0 && typeof comment.value === "string") {
|
|
1823
|
+
return comment.value;
|
|
1824
|
+
}
|
|
1825
|
+
return void 0;
|
|
1826
|
+
}
|
|
1827
|
+
function hasColumnUnique(definition) {
|
|
1828
|
+
return definition.unique === "unique" || definition.unique === true;
|
|
1829
|
+
}
|
|
1830
|
+
function extractColumnConstraintNotes(definition) {
|
|
1831
|
+
const notes = [];
|
|
1832
|
+
if (definition.auto_increment === "auto_increment" || definition.auto_increment === true) {
|
|
1833
|
+
notes.push("AUTO_INCREMENT");
|
|
1834
|
+
}
|
|
1835
|
+
if (definition.on_update) {
|
|
1836
|
+
notes.push(`ON UPDATE ${formatOnUpdate(definition.on_update)}`);
|
|
1837
|
+
}
|
|
1838
|
+
const generated = definition.generated;
|
|
1839
|
+
if (generated) {
|
|
1840
|
+
const storage = String(generated.storage_type ?? "virtual").toUpperCase();
|
|
1841
|
+
const expression = stringifyGeneratedExpression(generated.expr);
|
|
1842
|
+
notes.push(
|
|
1843
|
+
expression ? `GENERATED ALWAYS ${storage}: ${expression}` : `GENERATED ALWAYS ${storage}`
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
const enumValues = extractEnumValues(definition.definition?.expr);
|
|
1847
|
+
if (enumValues.length > 0) {
|
|
1848
|
+
notes.push(`ENUM: ${enumValues.join(", ")}`);
|
|
1849
|
+
}
|
|
1850
|
+
return notes;
|
|
1851
|
+
}
|
|
1852
|
+
function extractEnumValues(expr) {
|
|
1853
|
+
if (!expr || typeof expr !== "object") return [];
|
|
1854
|
+
const object = expr;
|
|
1855
|
+
if (object.type !== "expr_list" || !Array.isArray(object.value)) return [];
|
|
1856
|
+
return object.value.map((item) => {
|
|
1857
|
+
if (!item || typeof item !== "object") return "";
|
|
1858
|
+
const entry = item;
|
|
1859
|
+
return entry.value !== void 0 ? String(entry.value) : "";
|
|
1860
|
+
}).filter(Boolean);
|
|
1861
|
+
}
|
|
1862
|
+
function formatOnUpdate(value) {
|
|
1863
|
+
if (!value || typeof value !== "object") return String(value ?? "");
|
|
1864
|
+
const object = value;
|
|
1865
|
+
if (object.type === "function" && object.name) {
|
|
1866
|
+
const name = object.name;
|
|
1867
|
+
const parts = Array.isArray(name.name) ? name.name : [];
|
|
1868
|
+
return parts.map((part) => String(part.value ?? "")).join("") || "CURRENT_TIMESTAMP";
|
|
1869
|
+
}
|
|
1870
|
+
return formatAstValue(value);
|
|
1871
|
+
}
|
|
1872
|
+
function stringifyGeneratedExpression(expr) {
|
|
1873
|
+
if (!expr || typeof expr !== "object") return void 0;
|
|
1874
|
+
const text = stringifyExpression(expr);
|
|
1875
|
+
return text === "check" ? void 0 : text;
|
|
1876
|
+
}
|
|
1877
|
+
function extractCheckBounds(expression, columnName) {
|
|
1878
|
+
const result = {};
|
|
1879
|
+
walkCheckExpression(expression, columnName, result);
|
|
1880
|
+
return result;
|
|
1881
|
+
}
|
|
1882
|
+
function walkCheckExpression(expression, columnName, result) {
|
|
1883
|
+
if (!expression || typeof expression !== "object") return;
|
|
1884
|
+
const expr = expression;
|
|
1885
|
+
if (expr.type === "binary_expr") {
|
|
1886
|
+
const operator = String(expr.operator ?? "").toUpperCase();
|
|
1887
|
+
if (operator === "AND" || operator === "OR") {
|
|
1888
|
+
walkCheckExpression(expr.left, columnName, result);
|
|
1889
|
+
walkCheckExpression(expr.right, columnName, result);
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
const columnRef = findColumnRef(expr.left) ?? findColumnRef(expr.right);
|
|
1893
|
+
if (columnRef !== columnName) return;
|
|
1894
|
+
const bound = readBound(expr, columnName);
|
|
1895
|
+
if (!bound) return;
|
|
1896
|
+
if (bound.kind === "min") {
|
|
1897
|
+
result.minValue = mergeBound(result.minValue, bound.value, "max");
|
|
1898
|
+
} else {
|
|
1899
|
+
result.maxValue = mergeBound(result.maxValue, bound.value, "min");
|
|
1900
|
+
}
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
result.expression ??= stringifyExpression(expr);
|
|
1904
|
+
}
|
|
1905
|
+
function readBound(expr, columnName) {
|
|
1906
|
+
const operator = String(expr.operator ?? "");
|
|
1907
|
+
const left = expr.left;
|
|
1908
|
+
const right = expr.right;
|
|
1909
|
+
if (findColumnRef(left) === columnName) {
|
|
1910
|
+
if (operator === ">=" || operator === ">") {
|
|
1911
|
+
return { kind: "min", value: formatAstValue(right) };
|
|
1912
|
+
}
|
|
1913
|
+
if (operator === "<=" || operator === "<") {
|
|
1914
|
+
return { kind: "max", value: formatAstValue(right) };
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
if (findColumnRef(right) === columnName) {
|
|
1918
|
+
if (operator === ">=" || operator === ">") {
|
|
1919
|
+
return { kind: "max", value: formatAstValue(left) };
|
|
1920
|
+
}
|
|
1921
|
+
if (operator === "<=" || operator === "<") {
|
|
1922
|
+
return { kind: "min", value: formatAstValue(left) };
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
return void 0;
|
|
1926
|
+
}
|
|
1927
|
+
function mergeBound(current, next, pick) {
|
|
1928
|
+
if (!current) return next;
|
|
1929
|
+
const currentNum = Number(current);
|
|
1930
|
+
const nextNum = Number(next);
|
|
1931
|
+
if (!Number.isNaN(currentNum) && !Number.isNaN(nextNum)) {
|
|
1932
|
+
return pick === "min" ? String(Math.max(currentNum, nextNum)) : String(Math.min(currentNum, nextNum));
|
|
1933
|
+
}
|
|
1934
|
+
return next;
|
|
1935
|
+
}
|
|
1936
|
+
function findColumnRef(value) {
|
|
1937
|
+
if (!value || typeof value !== "object") return void 0;
|
|
1938
|
+
const expr = value;
|
|
1939
|
+
if (expr.type === "column_ref" && expr.column) {
|
|
1940
|
+
return String(expr.column);
|
|
1941
|
+
}
|
|
1942
|
+
return void 0;
|
|
1943
|
+
}
|
|
1944
|
+
function formatAstValue(value) {
|
|
1945
|
+
if (value === null || value === void 0) return "";
|
|
1946
|
+
if (typeof value === "object") {
|
|
1947
|
+
const object = value;
|
|
1948
|
+
if (object.value !== void 0) return String(object.value);
|
|
1949
|
+
if (object.dataType) return normalizeColumnType(object).type;
|
|
1950
|
+
}
|
|
1951
|
+
return String(value);
|
|
1952
|
+
}
|
|
1953
|
+
function stringifyExpression(expr) {
|
|
1954
|
+
if (expr.type === "binary_expr") {
|
|
1955
|
+
const left = stringifyExpression(expr.left ?? {});
|
|
1956
|
+
const right = stringifyExpression(expr.right ?? {});
|
|
1957
|
+
return `${left} ${expr.operator} ${right}`.trim();
|
|
1958
|
+
}
|
|
1959
|
+
if (expr.type === "column_ref") return String(expr.column ?? "");
|
|
1960
|
+
if (expr.value !== void 0) return formatAstValue(expr);
|
|
1961
|
+
return "check";
|
|
1962
|
+
}
|
|
1963
|
+
function extractConstraintColumnNames(definition) {
|
|
1964
|
+
return extractDeepColumnNames(definition);
|
|
1965
|
+
}
|
|
1966
|
+
function extractDeepColumnNames(value) {
|
|
1967
|
+
if (!Array.isArray(value)) return [];
|
|
1968
|
+
return value.map((item) => {
|
|
1969
|
+
if (typeof item !== "object" || item === null) return String(item ?? "unknown");
|
|
1970
|
+
const object = item;
|
|
1971
|
+
if (object.column !== void 0) return String(object.column);
|
|
1972
|
+
if (object.expr) return extractDeepColumnName(object.expr);
|
|
1973
|
+
return String(object.name ?? "unknown");
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
function extractDeepColumnName(value) {
|
|
1977
|
+
if (typeof value !== "object" || value === null) {
|
|
1978
|
+
return String(value ?? "unknown");
|
|
1979
|
+
}
|
|
1980
|
+
const object = value;
|
|
1981
|
+
if (object.expr) return extractDeepColumnName(object.expr);
|
|
1982
|
+
if (object.column && typeof object.column === "object") {
|
|
1983
|
+
return extractDeepColumnName(object.column);
|
|
1984
|
+
}
|
|
1985
|
+
if (object.column !== void 0) return String(object.column);
|
|
1986
|
+
return String(object.name ?? "unknown");
|
|
1987
|
+
}
|
|
1988
|
+
function stringifyCheckDefinition(definition) {
|
|
1989
|
+
if (!Array.isArray(definition) || definition.length === 0) return void 0;
|
|
1990
|
+
if (definition.length === 1) {
|
|
1991
|
+
return stringifyExpression(definition[0]);
|
|
1992
|
+
}
|
|
1993
|
+
return definition.map((item) => stringifyExpression(item)).filter(Boolean).join("; ");
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1182
1996
|
// src/parsers/sql/sql-normalizer.ts
|
|
1183
1997
|
function normalizeSqlAst(ast, dialect) {
|
|
1184
1998
|
const statements = Array.isArray(ast) ? ast : [ast];
|
|
@@ -1200,7 +2014,11 @@ function normalizeSqlAst(ast, dialect) {
|
|
|
1200
2014
|
}
|
|
1201
2015
|
for (const index of indexes) {
|
|
1202
2016
|
const table = tables.find((candidate) => candidate.name === index.table);
|
|
1203
|
-
table
|
|
2017
|
+
if (!table) continue;
|
|
2018
|
+
table.indexes.push(index);
|
|
2019
|
+
if (index.unique) {
|
|
2020
|
+
markColumnsUnique(table, index.columns, `INDEX ${index.name}`);
|
|
2021
|
+
}
|
|
1204
2022
|
}
|
|
1205
2023
|
return {
|
|
1206
2024
|
dialect,
|
|
@@ -1222,31 +2040,49 @@ function normalizeCreateTable(statement) {
|
|
|
1222
2040
|
reviewTodos: []
|
|
1223
2041
|
};
|
|
1224
2042
|
for (const definition of createDefinitions) {
|
|
1225
|
-
if (definition.resource
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
2043
|
+
if (definition.resource !== "column") continue;
|
|
2044
|
+
const columnName = extractDeepColumnName2(definition.column);
|
|
2045
|
+
const isPrimaryKey = hasPrimaryKey(definition);
|
|
2046
|
+
const isNotNull = hasNotNull(definition);
|
|
2047
|
+
const { type, size } = normalizeColumnType(definition.definition);
|
|
2048
|
+
const check = definition.check;
|
|
2049
|
+
const bounds = check?.definition ? extractCheckBounds(check.definition, columnName) : {};
|
|
2050
|
+
const constraintNotes = extractColumnConstraintNotes(definition);
|
|
2051
|
+
if (check?.definition) {
|
|
2052
|
+
const expression = stringifyCheckDefinition(check.definition);
|
|
2053
|
+
if (expression && (!bounds.minValue || !bounds.maxValue)) {
|
|
2054
|
+
constraintNotes.push(`CHECK: ${expression}`);
|
|
2055
|
+
}
|
|
1238
2056
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
2057
|
+
table.columns.push({
|
|
2058
|
+
name: columnName,
|
|
2059
|
+
type,
|
|
2060
|
+
size,
|
|
2061
|
+
nullable: !isNotNull && !isPrimaryKey,
|
|
2062
|
+
defaultValue: extractDefaultFromDef(definition),
|
|
2063
|
+
minValue: bounds.minValue,
|
|
2064
|
+
maxValue: bounds.maxValue,
|
|
2065
|
+
isUnique: hasColumnUnique(definition),
|
|
2066
|
+
isPrimaryKey,
|
|
2067
|
+
isForeignKey: false,
|
|
2068
|
+
comment: extractColumnComment(definition),
|
|
2069
|
+
constraintNotes: constraintNotes.length > 0 ? constraintNotes : void 0
|
|
2070
|
+
});
|
|
2071
|
+
if (isPrimaryKey) table.primaryKeys.push(columnName);
|
|
2072
|
+
}
|
|
2073
|
+
for (const definition of createDefinitions) {
|
|
2074
|
+
if (definition.resource !== "constraint") continue;
|
|
2075
|
+
if (isConstraintType(definition.constraint_type, "PRIMARY KEY")) {
|
|
2076
|
+
table.primaryKeys = extractDeepColumnNames2(definition.definition);
|
|
1241
2077
|
for (const column of table.columns) {
|
|
1242
2078
|
if (table.primaryKeys.includes(column.name)) column.isPrimaryKey = true;
|
|
1243
2079
|
}
|
|
1244
2080
|
}
|
|
1245
|
-
if (
|
|
1246
|
-
const columns =
|
|
2081
|
+
if (isConstraintType(definition.constraint_type, "FOREIGN KEY")) {
|
|
2082
|
+
const columns = extractDeepColumnNames2(definition.definition);
|
|
1247
2083
|
const refDef = definition.reference_definition;
|
|
1248
2084
|
const referencedTable = extractTableName(refDef?.table);
|
|
1249
|
-
const referencedColumns =
|
|
2085
|
+
const referencedColumns = extractDeepColumnNames2(refDef?.definition);
|
|
1250
2086
|
table.foreignKeys.push({
|
|
1251
2087
|
name: typeof definition.constraint === "string" ? definition.constraint : void 0,
|
|
1252
2088
|
columns,
|
|
@@ -1257,14 +2093,71 @@ function normalizeCreateTable(statement) {
|
|
|
1257
2093
|
if (columns.includes(column.name)) column.isForeignKey = true;
|
|
1258
2094
|
}
|
|
1259
2095
|
}
|
|
2096
|
+
if (isConstraintType(definition.constraint_type, "UNIQUE")) {
|
|
2097
|
+
const columns = extractConstraintColumnNames(definition.definition);
|
|
2098
|
+
const label = typeof definition.constraint === "string" ? definition.constraint : "UNIQUE";
|
|
2099
|
+
markColumnsUnique(table, columns, label);
|
|
2100
|
+
}
|
|
2101
|
+
if (isConstraintType(definition.constraint_type, "CHECK")) {
|
|
2102
|
+
applyTableCheckConstraint(table, definition);
|
|
2103
|
+
}
|
|
1260
2104
|
}
|
|
1261
2105
|
return table;
|
|
1262
2106
|
}
|
|
2107
|
+
function markColumnsUnique(table, columns, label) {
|
|
2108
|
+
const composite = columns.length > 1;
|
|
2109
|
+
for (const columnName of columns) {
|
|
2110
|
+
const column = table.columns.find((item) => item.name === columnName);
|
|
2111
|
+
if (!column) continue;
|
|
2112
|
+
column.isUnique = true;
|
|
2113
|
+
if (composite) {
|
|
2114
|
+
addConstraintNote(
|
|
2115
|
+
column,
|
|
2116
|
+
`UNIQUE (${label}: ${columns.join(", ")})`
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
function applyTableCheckConstraint(table, definition) {
|
|
2122
|
+
const expression = stringifyCheckDefinition(definition.definition);
|
|
2123
|
+
if (!expression) return;
|
|
2124
|
+
const referencedColumns = /* @__PURE__ */ new Set();
|
|
2125
|
+
for (const column of table.columns) {
|
|
2126
|
+
const bounds = extractCheckBounds(definition.definition, column.name);
|
|
2127
|
+
if (bounds.minValue) column.minValue = bounds.minValue;
|
|
2128
|
+
if (bounds.maxValue) column.maxValue = bounds.maxValue;
|
|
2129
|
+
if (bounds.minValue || bounds.maxValue) {
|
|
2130
|
+
referencedColumns.add(column.name);
|
|
2131
|
+
continue;
|
|
2132
|
+
}
|
|
2133
|
+
if (expression.includes(column.name)) {
|
|
2134
|
+
referencedColumns.add(column.name);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
if (referencedColumns.size === 0) {
|
|
2138
|
+
for (const column of table.columns) {
|
|
2139
|
+
addConstraintNote(column, `CHECK: ${expression}`);
|
|
2140
|
+
}
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
for (const columnName of referencedColumns) {
|
|
2144
|
+
const column = table.columns.find((item) => item.name === columnName);
|
|
2145
|
+
if (!column) continue;
|
|
2146
|
+
if (!column.minValue && !column.maxValue) {
|
|
2147
|
+
addConstraintNote(column, `CHECK: ${expression}`);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
function addConstraintNote(column, note) {
|
|
2152
|
+
const notes = column.constraintNotes ?? [];
|
|
2153
|
+
if (!notes.includes(note)) notes.push(note);
|
|
2154
|
+
column.constraintNotes = notes;
|
|
2155
|
+
}
|
|
1263
2156
|
function normalizeCreateIndex(statement) {
|
|
1264
2157
|
return {
|
|
1265
2158
|
name: String(statement.index ?? statement.index_name ?? "unnamed_index"),
|
|
1266
2159
|
table: extractTableName(statement.table),
|
|
1267
|
-
columns:
|
|
2160
|
+
columns: extractDeepColumnNames2(
|
|
1268
2161
|
statement.index_columns ?? statement.columns
|
|
1269
2162
|
),
|
|
1270
2163
|
unique: Boolean(statement.unique)
|
|
@@ -1318,15 +2211,15 @@ function extractTableName(value) {
|
|
|
1318
2211
|
}
|
|
1319
2212
|
return String(value ?? "unknown");
|
|
1320
2213
|
}
|
|
1321
|
-
function
|
|
2214
|
+
function extractDeepColumnName2(value) {
|
|
1322
2215
|
if (typeof value !== "object" || value === null)
|
|
1323
2216
|
return String(value ?? "unknown");
|
|
1324
2217
|
const object = value;
|
|
1325
2218
|
if (object.expr && typeof object.expr === "object") {
|
|
1326
|
-
return
|
|
2219
|
+
return extractDeepColumnName2(object.expr);
|
|
1327
2220
|
}
|
|
1328
2221
|
if (object.column && typeof object.column === "object") {
|
|
1329
|
-
return
|
|
2222
|
+
return extractDeepColumnName2(object.column);
|
|
1330
2223
|
}
|
|
1331
2224
|
if (object.value !== void 0) {
|
|
1332
2225
|
return String(object.value);
|
|
@@ -1336,18 +2229,9 @@ function extractDeepColumnName(value) {
|
|
|
1336
2229
|
}
|
|
1337
2230
|
return String(object.name ?? object.tableName ?? "unknown");
|
|
1338
2231
|
}
|
|
1339
|
-
function
|
|
2232
|
+
function extractDeepColumnNames2(value) {
|
|
1340
2233
|
if (!Array.isArray(value)) return [];
|
|
1341
|
-
return value.map((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();
|
|
2234
|
+
return value.map((item) => extractDeepColumnName2(item));
|
|
1351
2235
|
}
|
|
1352
2236
|
function hasPrimaryKey(def) {
|
|
1353
2237
|
if (def.primary_key) return true;
|
|
@@ -1627,6 +2511,27 @@ async function generateDbDocs(options) {
|
|
|
1627
2511
|
return doc;
|
|
1628
2512
|
}
|
|
1629
2513
|
|
|
2514
|
+
// src/core/output-path.ts
|
|
2515
|
+
import { basename, join as join6 } from "path";
|
|
2516
|
+
var OUTPUT_RUN_DIR_PREFIX = "db_doc_gen_";
|
|
2517
|
+
function createTimestampedRunName(date = /* @__PURE__ */ new Date()) {
|
|
2518
|
+
const year = String(date.getFullYear()).slice(-2);
|
|
2519
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
2520
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
2521
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
2522
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
2523
|
+
return `${OUTPUT_RUN_DIR_PREFIX}${year}${month}${day}${hours}${minutes}`;
|
|
2524
|
+
}
|
|
2525
|
+
function resolveGenerateOutDir(parentDir, date = /* @__PURE__ */ new Date()) {
|
|
2526
|
+
return join6(parentDir, createTimestampedRunName(date));
|
|
2527
|
+
}
|
|
2528
|
+
function isOutputRunDir(path) {
|
|
2529
|
+
return basename(path).startsWith(OUTPUT_RUN_DIR_PREFIX);
|
|
2530
|
+
}
|
|
2531
|
+
function isOutputRunDirName(name) {
|
|
2532
|
+
return name.startsWith(OUTPUT_RUN_DIR_PREFIX);
|
|
2533
|
+
}
|
|
2534
|
+
|
|
1630
2535
|
// src/cli/index.ts
|
|
1631
2536
|
var DEFAULT_CONFIG_PATH = "dbdocgen.config.json";
|
|
1632
2537
|
var program = new Command();
|
|
@@ -1639,14 +2544,17 @@ program.command("init").description("Create a default config file").option("-f,
|
|
|
1639
2544
|
}
|
|
1640
2545
|
const defaultConfig = {
|
|
1641
2546
|
schema: "./database/schema.sql",
|
|
2547
|
+
outDir: "./output",
|
|
1642
2548
|
output: {
|
|
1643
2549
|
formats: ["excel", "markdown", "html", "diagram", "word"],
|
|
1644
2550
|
language: "en"
|
|
1645
2551
|
}
|
|
1646
2552
|
};
|
|
1647
|
-
await
|
|
2553
|
+
await writeFile6(configPath, JSON.stringify(defaultConfig, null, 2), "utf8");
|
|
1648
2554
|
console.log(`Created config at ${configPath}`);
|
|
1649
|
-
console.log(
|
|
2555
|
+
console.log(
|
|
2556
|
+
"Each generate run writes to {outDir}/db_doc_gen_{yymmddhhmm} (outDir defaults to ./output)."
|
|
2557
|
+
);
|
|
1650
2558
|
console.log("Edit the file to configure your database schema path and output formats.");
|
|
1651
2559
|
});
|
|
1652
2560
|
var configCommand = program.command("config").description("Manage configuration");
|
|
@@ -1676,7 +2584,7 @@ configCommand.command("validate").description("Validate config file").option("--
|
|
|
1676
2584
|
process.exitCode = 1;
|
|
1677
2585
|
}
|
|
1678
2586
|
});
|
|
1679
|
-
program.command("generate").description("Generate database documentation").option("--schema <path>", "Path to schema.sql").option("--out <path>", "
|
|
2587
|
+
program.command("generate").description("Generate database documentation").option("--schema <path>", "Path to schema.sql").option("--out <path>", "Parent output directory (run folder: db_doc_gen_{yymmddhhmm})").option("--format <formats>", "Comma-separated output formats").option("--config <path>", "Config file path").action(async (rawOptions) => {
|
|
1680
2588
|
console.log("[dbdocgen] Loading configuration...");
|
|
1681
2589
|
const config = await loadConfig({
|
|
1682
2590
|
cwd: process.cwd(),
|
|
@@ -1687,9 +2595,11 @@ program.command("generate").description("Generate database documentation").optio
|
|
|
1687
2595
|
configPath: rawOptions.config
|
|
1688
2596
|
}
|
|
1689
2597
|
});
|
|
1690
|
-
const
|
|
2598
|
+
const parentOutDir = config.outDir;
|
|
2599
|
+
const outDir = resolveGenerateOutDir(parentOutDir);
|
|
1691
2600
|
console.log("[dbdocgen] Configuration loaded");
|
|
1692
2601
|
console.log(` schema: ${config.schema}`);
|
|
2602
|
+
console.log(` outputParent: ${parentOutDir}`);
|
|
1693
2603
|
console.log(` outDir: ${outDir}`);
|
|
1694
2604
|
console.log(` formats: ${config.output.formats.join(", ")}`);
|
|
1695
2605
|
console.log(` language: ${config.output.language}`);
|
|
@@ -1759,7 +2669,7 @@ Found ${doc.tables.length} table(s):
|
|
|
1759
2669
|
}
|
|
1760
2670
|
console.log("Schema validation passed.");
|
|
1761
2671
|
});
|
|
1762
|
-
program.command("clean").description("Clean output directory").option("--out <path>", "
|
|
2672
|
+
program.command("clean").description("Clean output directory").option("--out <path>", "Parent output directory or a specific db_doc_gen_* run folder").option("--config <path>", "Config file path").action(async (rawOptions) => {
|
|
1763
2673
|
const config = await loadConfig({
|
|
1764
2674
|
cwd: process.cwd(),
|
|
1765
2675
|
cliOptions: {
|
|
@@ -1767,13 +2677,29 @@ program.command("clean").description("Clean output directory").option("--out <pa
|
|
|
1767
2677
|
configPath: rawOptions.config
|
|
1768
2678
|
}
|
|
1769
2679
|
});
|
|
1770
|
-
const
|
|
1771
|
-
if (!existsSync(
|
|
1772
|
-
console.log(`Output
|
|
2680
|
+
const target = resolve(config.outDir);
|
|
2681
|
+
if (!existsSync(target)) {
|
|
2682
|
+
console.log(`Output path ${target} does not exist. Nothing to clean.`);
|
|
2683
|
+
return;
|
|
2684
|
+
}
|
|
2685
|
+
if (isOutputRunDir(target)) {
|
|
2686
|
+
console.log(`Cleaning ${target}...`);
|
|
2687
|
+
await rm(target, { recursive: true, force: true });
|
|
2688
|
+
console.log("Done.");
|
|
1773
2689
|
return;
|
|
1774
2690
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
2691
|
+
const entries = await readdir(target, { withFileTypes: true });
|
|
2692
|
+
const runDirs = entries.filter(
|
|
2693
|
+
(entry) => entry.isDirectory() && isOutputRunDirName(entry.name)
|
|
2694
|
+
);
|
|
2695
|
+
if (runDirs.length === 0) {
|
|
2696
|
+
console.log(`No db_doc_gen_* folders found under ${target}.`);
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
console.log(`Cleaning ${runDirs.length} run folder(s) under ${target}...`);
|
|
2700
|
+
for (const entry of runDirs) {
|
|
2701
|
+
await rm(resolve(target, entry.name), { recursive: true, force: true });
|
|
2702
|
+
}
|
|
1777
2703
|
console.log("Done.");
|
|
1778
2704
|
});
|
|
1779
2705
|
program.command("info").description("Show project info and supported features").action(() => {
|
|
@@ -1819,12 +2745,4 @@ function parseFormats(value) {
|
|
|
1819
2745
|
}
|
|
1820
2746
|
return valid.length > 0 ? valid : void 0;
|
|
1821
2747
|
}
|
|
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
2748
|
//# sourceMappingURL=index.js.map
|