@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.cjs
CHANGED
|
@@ -27,7 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
var import_commander = require("commander");
|
|
28
28
|
var import_promises7 = require("fs/promises");
|
|
29
29
|
var import_node_fs = require("fs");
|
|
30
|
-
var
|
|
30
|
+
var import_node_path7 = require("path");
|
|
31
31
|
|
|
32
32
|
// src/core/config/loader.ts
|
|
33
33
|
var import_cosmiconfig = require("cosmiconfig");
|
|
@@ -53,7 +53,7 @@ var outputLanguageSchema = import_zod.z.enum(["en", "jp"]);
|
|
|
53
53
|
var dbdocgenConfigSchema = import_zod.z.object({
|
|
54
54
|
schema: import_zod.z.string().default("./schema.sql"),
|
|
55
55
|
dialect: dialectSchema.optional(),
|
|
56
|
-
outDir: import_zod.z.string().default("./
|
|
56
|
+
outDir: import_zod.z.string().default("./output"),
|
|
57
57
|
output: import_zod.z.object({
|
|
58
58
|
formats: import_zod.z.array(outputFormatSchema).default(["excel", "markdown", "html", "diagram", "word"]),
|
|
59
59
|
language: outputLanguageSchema.default("en")
|
|
@@ -160,6 +160,10 @@ var LABELS = {
|
|
|
160
160
|
type: "Type",
|
|
161
161
|
required: "Required",
|
|
162
162
|
defaultValue: "Default Value",
|
|
163
|
+
size: "Size",
|
|
164
|
+
minValue: "Min",
|
|
165
|
+
maxValue: "Max",
|
|
166
|
+
unique: "Unique",
|
|
163
167
|
notes: "Notes",
|
|
164
168
|
yes: "Yes",
|
|
165
169
|
no: "No",
|
|
@@ -188,7 +192,15 @@ var LABELS = {
|
|
|
188
192
|
rowNo: "#",
|
|
189
193
|
backToOverview: "\u2190 Overview",
|
|
190
194
|
pkMarker: "PK",
|
|
191
|
-
fkMarker: "FK"
|
|
195
|
+
fkMarker: "FK",
|
|
196
|
+
erDiagramHeading: "ER Diagram",
|
|
197
|
+
erDiagramSheet: "ER Diagram",
|
|
198
|
+
viewErDiagram: "View interactive ER diagram (html/er-diagram.html)",
|
|
199
|
+
zoomIn: "Zoom in",
|
|
200
|
+
zoomOut: "Zoom out",
|
|
201
|
+
zoomReset: "Reset",
|
|
202
|
+
zoomFit: "Fit",
|
|
203
|
+
panZoomHint: "Drag to pan \xB7 Scroll to zoom"
|
|
192
204
|
},
|
|
193
205
|
jp: {
|
|
194
206
|
docTitle: "Database Documentation",
|
|
@@ -208,6 +220,10 @@ var LABELS = {
|
|
|
208
220
|
type: "\u578B",
|
|
209
221
|
required: "\u5FC5\u9808",
|
|
210
222
|
defaultValue: "\u30C7\u30D5\u30A9\u30EB\u30C8\u5024",
|
|
223
|
+
size: "\u6841\u6570",
|
|
224
|
+
minValue: "\u6700\u5C0F\u5024",
|
|
225
|
+
maxValue: "\u6700\u5927\u5024",
|
|
226
|
+
unique: "\u4E00\u610F",
|
|
211
227
|
notes: "\u5099\u8003",
|
|
212
228
|
yes: "Yes",
|
|
213
229
|
no: "No",
|
|
@@ -236,13 +252,533 @@ var LABELS = {
|
|
|
236
252
|
rowNo: "No.",
|
|
237
253
|
backToOverview: "\u2190 \u4E00\u89A7",
|
|
238
254
|
pkMarker: "PK",
|
|
239
|
-
fkMarker: "FK"
|
|
255
|
+
fkMarker: "FK",
|
|
256
|
+
erDiagramHeading: "ER Diagram",
|
|
257
|
+
erDiagramSheet: "ER Diagram",
|
|
258
|
+
viewErDiagram: "\u30A4\u30F3\u30BF\u30E9\u30AF\u30C6\u30A3\u30D6ER\u56F3 (html/er-diagram.html)",
|
|
259
|
+
zoomIn: "\u62E1\u5927",
|
|
260
|
+
zoomOut: "\u7E2E\u5C0F",
|
|
261
|
+
zoomReset: "\u30EA\u30BB\u30C3\u30C8",
|
|
262
|
+
zoomFit: "\u5168\u4F53\u8868\u793A",
|
|
263
|
+
panZoomHint: "\u30C9\u30E9\u30C3\u30B0\u3067\u79FB\u52D5 \xB7 \u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u62E1\u5927\u7E2E\u5C0F"
|
|
240
264
|
}
|
|
241
265
|
};
|
|
242
266
|
function getOutputLabels(language = "en") {
|
|
243
267
|
return LABELS[language];
|
|
244
268
|
}
|
|
245
269
|
|
|
270
|
+
// src/exporters/shared/column-definition.ts
|
|
271
|
+
var A5_COLUMN_COUNT = 10;
|
|
272
|
+
function columnDefinitionHeaders(labels) {
|
|
273
|
+
return [
|
|
274
|
+
labels.physicalName,
|
|
275
|
+
labels.logicalName,
|
|
276
|
+
labels.type,
|
|
277
|
+
labels.size,
|
|
278
|
+
labels.required,
|
|
279
|
+
labels.defaultValue,
|
|
280
|
+
labels.minValue,
|
|
281
|
+
labels.maxValue,
|
|
282
|
+
labels.unique,
|
|
283
|
+
labels.notes
|
|
284
|
+
];
|
|
285
|
+
}
|
|
286
|
+
function formatColumnNotes(column, labels) {
|
|
287
|
+
const parts = [];
|
|
288
|
+
if (column.isPrimaryKey) parts.push(labels.pkMarker);
|
|
289
|
+
if (column.isForeignKey) parts.push(labels.fkMarker);
|
|
290
|
+
if (column.constraintNotes?.length) parts.push(...column.constraintNotes);
|
|
291
|
+
if (column.description?.value) parts.push(column.description.value);
|
|
292
|
+
return parts.join(", ") || labels.none;
|
|
293
|
+
}
|
|
294
|
+
function columnDefinitionRow(column, labels) {
|
|
295
|
+
return [
|
|
296
|
+
column.name,
|
|
297
|
+
column.comment ?? "",
|
|
298
|
+
column.type,
|
|
299
|
+
column.size ?? labels.none,
|
|
300
|
+
column.nullable ? labels.no : labels.yes,
|
|
301
|
+
column.defaultValue ?? labels.none,
|
|
302
|
+
column.minValue ?? labels.none,
|
|
303
|
+
column.maxValue ?? labels.none,
|
|
304
|
+
column.isUnique ? labels.yes : labels.no,
|
|
305
|
+
formatColumnNotes(column, labels)
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/exporters/diagram/er-diagram-embed.ts
|
|
310
|
+
function getErDiagramMermaid(doc) {
|
|
311
|
+
return renderMermaid(doc);
|
|
312
|
+
}
|
|
313
|
+
function renderErDiagramHtmlPage(mermaidSource, labels) {
|
|
314
|
+
const escaped = mermaidSource.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
315
|
+
return `<!DOCTYPE html>
|
|
316
|
+
<html lang="en">
|
|
317
|
+
<head>
|
|
318
|
+
<meta charset="UTF-8">
|
|
319
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
320
|
+
<title>${esc(labels.erDiagramHeading)}</title>
|
|
321
|
+
<style>
|
|
322
|
+
body { margin: 0; font-family: "Yu Gothic UI", "Meiryo", Arial, sans-serif; background: #f3f4f6; }
|
|
323
|
+
.toolbar {
|
|
324
|
+
background: #4472c4; color: #fff; padding: 10px 16px;
|
|
325
|
+
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
|
326
|
+
}
|
|
327
|
+
.toolbar a { color: #fff; text-decoration: underline; }
|
|
328
|
+
.toolbar .spacer { flex: 1; }
|
|
329
|
+
.toolbar .hint { opacity: 0.9; font-size: 13px; }
|
|
330
|
+
.toolbar button {
|
|
331
|
+
background: #fff; color: #2f5597; border: none; border-radius: 4px;
|
|
332
|
+
padding: 6px 12px; font-size: 13px; cursor: pointer; font-weight: 600;
|
|
333
|
+
}
|
|
334
|
+
.toolbar button:hover { background: #e8eef8; }
|
|
335
|
+
.viewport {
|
|
336
|
+
position: relative; height: calc(100vh - 52px); margin: 12px;
|
|
337
|
+
background: #fff; border: 1px solid #bfc7d4; border-radius: 4px;
|
|
338
|
+
overflow: hidden; cursor: grab; touch-action: none;
|
|
339
|
+
}
|
|
340
|
+
.viewport.dragging { cursor: grabbing; }
|
|
341
|
+
.canvas {
|
|
342
|
+
position: absolute; left: 0; top: 0; transform-origin: 0 0;
|
|
343
|
+
padding: 24px;
|
|
344
|
+
}
|
|
345
|
+
.mermaid { min-width: 320px; }
|
|
346
|
+
.mermaid svg { max-width: none !important; height: auto !important; }
|
|
347
|
+
</style>
|
|
348
|
+
</head>
|
|
349
|
+
<body>
|
|
350
|
+
<div class="toolbar">
|
|
351
|
+
<strong>${esc(labels.erDiagramHeading)}</strong>
|
|
352
|
+
<a href="index.html">\u2190 ${esc(labels.tableListHeading)}</a>
|
|
353
|
+
<span class="spacer"></span>
|
|
354
|
+
<span class="hint">${esc(labels.panZoomHint)}</span>
|
|
355
|
+
<button type="button" id="zoom-out" title="${esc(labels.zoomOut)}">\u2212</button>
|
|
356
|
+
<button type="button" id="zoom-reset" title="${esc(labels.zoomReset)}">${esc(labels.zoomReset)}</button>
|
|
357
|
+
<button type="button" id="zoom-in" title="${esc(labels.zoomIn)}">+</button>
|
|
358
|
+
<button type="button" id="zoom-fit" title="${esc(labels.zoomFit)}">${esc(labels.zoomFit)}</button>
|
|
359
|
+
</div>
|
|
360
|
+
<div class="viewport" id="viewport">
|
|
361
|
+
<div class="canvas" id="canvas">
|
|
362
|
+
<pre class="mermaid">${escaped}</pre>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
<script type="module">
|
|
366
|
+
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
|
|
367
|
+
|
|
368
|
+
mermaid.initialize({
|
|
369
|
+
startOnLoad: false,
|
|
370
|
+
theme: "default",
|
|
371
|
+
er: { useMaxWidth: false }
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
await mermaid.run({ querySelector: ".mermaid" });
|
|
375
|
+
setupPanZoom(
|
|
376
|
+
document.getElementById("viewport"),
|
|
377
|
+
document.getElementById("canvas")
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
function setupPanZoom(viewport, canvas) {
|
|
381
|
+
let scale = 1;
|
|
382
|
+
let tx = 40;
|
|
383
|
+
let ty = 40;
|
|
384
|
+
let dragging = false;
|
|
385
|
+
let lastX = 0;
|
|
386
|
+
let lastY = 0;
|
|
387
|
+
|
|
388
|
+
function apply() {
|
|
389
|
+
canvas.style.transform = "translate(" + tx + "px," + ty + "px) scale(" + scale + ")";
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function zoomAt(factor, cx, cy) {
|
|
393
|
+
const next = Math.min(4, Math.max(0.15, scale * factor));
|
|
394
|
+
const ratio = next / scale;
|
|
395
|
+
tx = cx - (cx - tx) * ratio;
|
|
396
|
+
ty = cy - (cy - ty) * ratio;
|
|
397
|
+
scale = next;
|
|
398
|
+
apply();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function fitToView() {
|
|
402
|
+
const svg = canvas.querySelector("svg");
|
|
403
|
+
if (!svg) return;
|
|
404
|
+
const box = svg.getBBox();
|
|
405
|
+
const pad = 32;
|
|
406
|
+
const vw = viewport.clientWidth;
|
|
407
|
+
const vh = viewport.clientHeight;
|
|
408
|
+
scale = Math.min(
|
|
409
|
+
(vw - pad * 2) / Math.max(box.width, 1),
|
|
410
|
+
(vh - pad * 2) / Math.max(box.height, 1),
|
|
411
|
+
1.5
|
|
412
|
+
);
|
|
413
|
+
tx = (vw - box.width * scale) / 2 - box.x * scale;
|
|
414
|
+
ty = (vh - box.height * scale) / 2 - box.y * scale;
|
|
415
|
+
apply();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
viewport.addEventListener("wheel", (e) => {
|
|
419
|
+
e.preventDefault();
|
|
420
|
+
const rect = viewport.getBoundingClientRect();
|
|
421
|
+
const cx = e.clientX - rect.left;
|
|
422
|
+
const cy = e.clientY - rect.top;
|
|
423
|
+
zoomAt(e.deltaY < 0 ? 1.12 : 0.89, cx, cy);
|
|
424
|
+
}, { passive: false });
|
|
425
|
+
|
|
426
|
+
viewport.addEventListener("mousedown", (e) => {
|
|
427
|
+
if (e.button !== 0) return;
|
|
428
|
+
dragging = true;
|
|
429
|
+
lastX = e.clientX;
|
|
430
|
+
lastY = e.clientY;
|
|
431
|
+
viewport.classList.add("dragging");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
window.addEventListener("mousemove", (e) => {
|
|
435
|
+
if (!dragging) return;
|
|
436
|
+
tx += e.clientX - lastX;
|
|
437
|
+
ty += e.clientY - lastY;
|
|
438
|
+
lastX = e.clientX;
|
|
439
|
+
lastY = e.clientY;
|
|
440
|
+
apply();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
window.addEventListener("mouseup", () => {
|
|
444
|
+
dragging = false;
|
|
445
|
+
viewport.classList.remove("dragging");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
document.getElementById("zoom-in").addEventListener("click", () => {
|
|
449
|
+
zoomAt(1.2, viewport.clientWidth / 2, viewport.clientHeight / 2);
|
|
450
|
+
});
|
|
451
|
+
document.getElementById("zoom-out").addEventListener("click", () => {
|
|
452
|
+
zoomAt(1 / 1.2, viewport.clientWidth / 2, viewport.clientHeight / 2);
|
|
453
|
+
});
|
|
454
|
+
document.getElementById("zoom-reset").addEventListener("click", () => {
|
|
455
|
+
scale = 1;
|
|
456
|
+
tx = 40;
|
|
457
|
+
ty = 40;
|
|
458
|
+
apply();
|
|
459
|
+
});
|
|
460
|
+
document.getElementById("zoom-fit").addEventListener("click", fitToView);
|
|
461
|
+
|
|
462
|
+
fitToView();
|
|
463
|
+
}
|
|
464
|
+
</script>
|
|
465
|
+
</body>
|
|
466
|
+
</html>`;
|
|
467
|
+
}
|
|
468
|
+
function renderErDiagramMarkdown(mermaidSource, labels) {
|
|
469
|
+
return [
|
|
470
|
+
`# ${labels.erDiagramHeading}`,
|
|
471
|
+
"",
|
|
472
|
+
"```mermaid",
|
|
473
|
+
mermaidSource.trimEnd(),
|
|
474
|
+
"```",
|
|
475
|
+
""
|
|
476
|
+
].join("\n");
|
|
477
|
+
}
|
|
478
|
+
function esc(text) {
|
|
479
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/exporters/diagram/er-diagram-layout.ts
|
|
483
|
+
var import_elk_bundled = __toESM(require("elkjs/lib/elk.bundled.js"), 1);
|
|
484
|
+
var BOX_W = 200;
|
|
485
|
+
var HEADER_H = 28;
|
|
486
|
+
var LINE_H = 14;
|
|
487
|
+
var MAX_COLS_SHOWN = 6;
|
|
488
|
+
var COMPACT_THRESHOLD = 18;
|
|
489
|
+
var PAD = 24;
|
|
490
|
+
var CLUSTER_GAP = 56;
|
|
491
|
+
function isCompactLayout(tableCount) {
|
|
492
|
+
return tableCount >= COMPACT_THRESHOLD;
|
|
493
|
+
}
|
|
494
|
+
function getVisibleErColumns(table) {
|
|
495
|
+
const prioritized = [
|
|
496
|
+
...table.columns.filter((column) => column.isPrimaryKey),
|
|
497
|
+
...table.columns.filter(
|
|
498
|
+
(column) => column.isForeignKey && !column.isPrimaryKey
|
|
499
|
+
),
|
|
500
|
+
...table.columns.filter(
|
|
501
|
+
(column) => !column.isPrimaryKey && !column.isForeignKey
|
|
502
|
+
)
|
|
503
|
+
];
|
|
504
|
+
const unique = prioritized.filter(
|
|
505
|
+
(column, index, columns) => columns.findIndex((item) => item.name === column.name) === index
|
|
506
|
+
);
|
|
507
|
+
return unique.slice(0, MAX_COLS_SHOWN);
|
|
508
|
+
}
|
|
509
|
+
function measureTableBox(table, _compact = false) {
|
|
510
|
+
const visible = getVisibleErColumns(table);
|
|
511
|
+
const extra = table.columns.length > visible.length ? 1 : 0;
|
|
512
|
+
return {
|
|
513
|
+
w: BOX_W,
|
|
514
|
+
h: HEADER_H + (visible.length + extra) * LINE_H + 8
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function buildAdjacency(doc) {
|
|
518
|
+
const names = new Set(doc.tables.map((t) => t.name));
|
|
519
|
+
const adj = /* @__PURE__ */ new Map();
|
|
520
|
+
for (const name of names) adj.set(name, /* @__PURE__ */ new Set());
|
|
521
|
+
for (const rel of doc.relationships.filter((r) => r.source === "schema")) {
|
|
522
|
+
if (!names.has(rel.fromTable) || !names.has(rel.toTable)) continue;
|
|
523
|
+
adj.get(rel.fromTable).add(rel.toTable);
|
|
524
|
+
adj.get(rel.toTable).add(rel.fromTable);
|
|
525
|
+
}
|
|
526
|
+
return adj;
|
|
527
|
+
}
|
|
528
|
+
function connectedComponents(tableNames, adj) {
|
|
529
|
+
const visited = /* @__PURE__ */ new Set();
|
|
530
|
+
const components = [];
|
|
531
|
+
for (const name of tableNames) {
|
|
532
|
+
if (visited.has(name)) continue;
|
|
533
|
+
const stack = [name];
|
|
534
|
+
const component = [];
|
|
535
|
+
visited.add(name);
|
|
536
|
+
while (stack.length > 0) {
|
|
537
|
+
const current = stack.pop();
|
|
538
|
+
component.push(current);
|
|
539
|
+
for (const next of adj.get(current) ?? []) {
|
|
540
|
+
if (!visited.has(next)) {
|
|
541
|
+
visited.add(next);
|
|
542
|
+
stack.push(next);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
component.sort();
|
|
547
|
+
components.push(component);
|
|
548
|
+
}
|
|
549
|
+
return components.sort((a, b) => b.length - a.length);
|
|
550
|
+
}
|
|
551
|
+
function sectionToPoints(section) {
|
|
552
|
+
return [section.startPoint, ...section.bendPoints ?? [], section.endPoint];
|
|
553
|
+
}
|
|
554
|
+
function extractEdges(layouted) {
|
|
555
|
+
const edges = [];
|
|
556
|
+
for (const edge of layouted.edges ?? []) {
|
|
557
|
+
for (const section of edge.sections ?? []) {
|
|
558
|
+
edges.push({
|
|
559
|
+
id: edge.id,
|
|
560
|
+
points: sectionToPoints(section)
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return edges;
|
|
565
|
+
}
|
|
566
|
+
async function layoutComponent(doc, tableNames, compact) {
|
|
567
|
+
const elk = new import_elk_bundled.default();
|
|
568
|
+
const nameSet = new Set(tableNames);
|
|
569
|
+
const tables = doc.tables.filter((t) => nameSet.has(t.name));
|
|
570
|
+
const direction = tables.length >= 8 ? "DOWN" : "RIGHT";
|
|
571
|
+
const children = tables.map((table) => {
|
|
572
|
+
const { w, h } = measureTableBox(table, compact);
|
|
573
|
+
return { id: table.name, width: w, height: h };
|
|
574
|
+
});
|
|
575
|
+
const edges = [];
|
|
576
|
+
const seen = /* @__PURE__ */ new Set();
|
|
577
|
+
for (const rel of doc.relationships.filter((r) => r.source === "schema")) {
|
|
578
|
+
if (!nameSet.has(rel.fromTable) || !nameSet.has(rel.toTable)) continue;
|
|
579
|
+
const key = `${rel.fromTable}->${rel.toTable}`;
|
|
580
|
+
if (seen.has(key)) continue;
|
|
581
|
+
seen.add(key);
|
|
582
|
+
edges.push({
|
|
583
|
+
id: key,
|
|
584
|
+
sources: [rel.fromTable],
|
|
585
|
+
targets: [rel.toTable]
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
const graph = {
|
|
589
|
+
id: "root",
|
|
590
|
+
layoutOptions: {
|
|
591
|
+
"elk.algorithm": "layered",
|
|
592
|
+
"elk.direction": direction,
|
|
593
|
+
"elk.edgeRouting": "ORTHOGONAL",
|
|
594
|
+
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
|
|
595
|
+
"elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
|
|
596
|
+
"elk.layered.spacing.nodeNodeBetweenLayers": compact ? "56" : "80",
|
|
597
|
+
"elk.spacing.nodeNode": compact ? "32" : "48",
|
|
598
|
+
"elk.spacing.edgeNode": "24",
|
|
599
|
+
"elk.padding": `[top=${PAD},left=${PAD},bottom=${PAD},right=${PAD}]`
|
|
600
|
+
},
|
|
601
|
+
children,
|
|
602
|
+
edges
|
|
603
|
+
};
|
|
604
|
+
const layouted = await elk.layout(graph);
|
|
605
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
606
|
+
let minX = Infinity;
|
|
607
|
+
let minY = Infinity;
|
|
608
|
+
let maxX = -Infinity;
|
|
609
|
+
let maxY = -Infinity;
|
|
610
|
+
for (const child of layouted.children ?? []) {
|
|
611
|
+
const table = tables.find((t) => t.name === child.id);
|
|
612
|
+
if (!table) continue;
|
|
613
|
+
const { w, h } = measureTableBox(table, compact);
|
|
614
|
+
const x = child.x ?? 0;
|
|
615
|
+
const y = child.y ?? 0;
|
|
616
|
+
const box = { x, y, w, h };
|
|
617
|
+
boxes.set(child.id, box);
|
|
618
|
+
minX = Math.min(minX, x);
|
|
619
|
+
minY = Math.min(minY, y);
|
|
620
|
+
maxX = Math.max(maxX, x + w);
|
|
621
|
+
maxY = Math.max(maxY, y + h);
|
|
622
|
+
}
|
|
623
|
+
const width = Math.ceil(maxX - minX + PAD * 2);
|
|
624
|
+
const height = Math.ceil(maxY - minY + PAD * 2);
|
|
625
|
+
const dx = PAD - minX;
|
|
626
|
+
const dy = PAD - minY;
|
|
627
|
+
for (const [name, box] of boxes) {
|
|
628
|
+
boxes.set(name, { x: box.x + dx, y: box.y + dy, w: box.w, h: box.h });
|
|
629
|
+
}
|
|
630
|
+
const shiftedEdges = extractEdges(layouted).map((edge) => ({
|
|
631
|
+
...edge,
|
|
632
|
+
points: edge.points.map((p) => ({ x: p.x + dx, y: p.y + dy }))
|
|
633
|
+
}));
|
|
634
|
+
return { boxes, edges: shiftedEdges, width, height };
|
|
635
|
+
}
|
|
636
|
+
function shiftLayout(boxes, edges, offsetX, offsetY) {
|
|
637
|
+
for (const [name, box] of boxes) {
|
|
638
|
+
boxes.set(name, { ...box, x: box.x + offsetX, y: box.y + offsetY });
|
|
639
|
+
}
|
|
640
|
+
for (const edge of edges) {
|
|
641
|
+
for (const p of edge.points) {
|
|
642
|
+
p.x += offsetX;
|
|
643
|
+
p.y += offsetY;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
async function layoutErDiagram(doc) {
|
|
648
|
+
const tables = doc.tables;
|
|
649
|
+
if (tables.length === 0) {
|
|
650
|
+
return {
|
|
651
|
+
boxes: /* @__PURE__ */ new Map(),
|
|
652
|
+
edges: [],
|
|
653
|
+
compact: false,
|
|
654
|
+
width: 400,
|
|
655
|
+
height: 80
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
const compact = isCompactLayout(tables.length);
|
|
659
|
+
const adj = buildAdjacency(doc);
|
|
660
|
+
const components = connectedComponents(
|
|
661
|
+
tables.map((t) => t.name),
|
|
662
|
+
adj
|
|
663
|
+
);
|
|
664
|
+
const mergedBoxes = /* @__PURE__ */ new Map();
|
|
665
|
+
const mergedEdges = [];
|
|
666
|
+
const clusterCols = components.length <= 1 ? 1 : components.length <= 4 ? 2 : 3;
|
|
667
|
+
let tileX = 0;
|
|
668
|
+
let tileY = 0;
|
|
669
|
+
let rowHeight = 0;
|
|
670
|
+
let maxWidth = PAD;
|
|
671
|
+
let maxHeight = PAD;
|
|
672
|
+
for (const [i, component] of components.entries()) {
|
|
673
|
+
const laid = await layoutComponent(doc, component, compact);
|
|
674
|
+
shiftLayout(laid.boxes, laid.edges, tileX, tileY);
|
|
675
|
+
for (const [name, box] of laid.boxes) mergedBoxes.set(name, box);
|
|
676
|
+
mergedEdges.push(...laid.edges);
|
|
677
|
+
rowHeight = Math.max(rowHeight, laid.height);
|
|
678
|
+
tileX += laid.width + CLUSTER_GAP;
|
|
679
|
+
maxWidth = Math.max(maxWidth, tileX);
|
|
680
|
+
maxHeight = Math.max(maxHeight, tileY + laid.height);
|
|
681
|
+
if ((i + 1) % clusterCols === 0) {
|
|
682
|
+
tileX = 0;
|
|
683
|
+
tileY += rowHeight + CLUSTER_GAP;
|
|
684
|
+
rowHeight = 0;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return {
|
|
688
|
+
boxes: mergedBoxes,
|
|
689
|
+
edges: mergedEdges,
|
|
690
|
+
compact,
|
|
691
|
+
width: Math.ceil(maxWidth),
|
|
692
|
+
height: Math.ceil(maxHeight + PAD)
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// src/exporters/diagram/er-diagram-svg.ts
|
|
697
|
+
async function renderErDiagramSvg(doc) {
|
|
698
|
+
const tables = doc.tables;
|
|
699
|
+
if (tables.length === 0) {
|
|
700
|
+
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>`;
|
|
701
|
+
}
|
|
702
|
+
const layout = await layoutErDiagram(doc);
|
|
703
|
+
return buildErDiagramSvg(doc, layout);
|
|
704
|
+
}
|
|
705
|
+
async function renderErDiagramPng(doc) {
|
|
706
|
+
const tables = doc.tables;
|
|
707
|
+
if (tables.length === 0) {
|
|
708
|
+
const svg2 = await renderErDiagramSvg(doc);
|
|
709
|
+
const sharp2 = (await import("sharp")).default;
|
|
710
|
+
const buffer2 = await sharp2(Buffer.from(svg2)).png().toBuffer();
|
|
711
|
+
return { buffer: buffer2, width: 400, height: 80 };
|
|
712
|
+
}
|
|
713
|
+
const layout = await layoutErDiagram(doc);
|
|
714
|
+
const svg = buildErDiagramSvg(doc, layout);
|
|
715
|
+
const sharp = (await import("sharp")).default;
|
|
716
|
+
const buffer = await sharp(Buffer.from(svg)).png().toBuffer();
|
|
717
|
+
return { buffer, width: layout.width, height: layout.height };
|
|
718
|
+
}
|
|
719
|
+
function buildErDiagramSvg(doc, layout) {
|
|
720
|
+
const { boxes, edges, width, height } = layout;
|
|
721
|
+
const parts = [
|
|
722
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" font-family="Arial,sans-serif" font-size="11">`,
|
|
723
|
+
`<rect width="100%" height="100%" fill="#ffffff"/>`,
|
|
724
|
+
`<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>`,
|
|
725
|
+
`<g class="edges">`
|
|
726
|
+
];
|
|
727
|
+
for (const edge of edges) {
|
|
728
|
+
if (edge.points.length < 2) continue;
|
|
729
|
+
const d = pointsToPath(edge.points);
|
|
730
|
+
parts.push(
|
|
731
|
+
`<path d="${d}" fill="none" stroke="#7d96b8" stroke-width="1.25" marker-end="url(#arrow)"/>`
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
parts.push(`</g><g class="nodes">`);
|
|
735
|
+
for (const table of doc.tables) {
|
|
736
|
+
const box = boxes.get(table.name);
|
|
737
|
+
parts.push(...renderTableBox(table, box));
|
|
738
|
+
}
|
|
739
|
+
parts.push("</g></svg>");
|
|
740
|
+
return parts.join("");
|
|
741
|
+
}
|
|
742
|
+
function pointsToPath(points) {
|
|
743
|
+
return points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ");
|
|
744
|
+
}
|
|
745
|
+
function renderTableBox(table, box) {
|
|
746
|
+
const parts = [
|
|
747
|
+
`<rect x="${box.x}" y="${box.y}" width="${box.w}" height="${box.h}" fill="#f8fafc" stroke="#4472c4" stroke-width="1.5" rx="4"/>`,
|
|
748
|
+
`<rect x="${box.x}" y="${box.y}" width="${box.w}" height="${HEADER_H}" fill="#4472c4" rx="4"/>`,
|
|
749
|
+
`<rect x="${box.x}" y="${box.y + HEADER_H - 4}" width="${box.w}" height="4" fill="#4472c4"/>`,
|
|
750
|
+
`<text x="${box.x + 8}" y="${box.y + 18}" fill="#ffffff" font-weight="bold">${escapeXml(table.name)}</text>`
|
|
751
|
+
];
|
|
752
|
+
let cy = box.y + HEADER_H + 14;
|
|
753
|
+
const visible = getVisibleErColumns(table);
|
|
754
|
+
for (const col of visible) {
|
|
755
|
+
const marker = col.isPrimaryKey ? " PK" : col.isForeignKey ? " FK" : "";
|
|
756
|
+
parts.push(
|
|
757
|
+
`<text x="${box.x + 8}" y="${cy}" fill="#333333">${escapeXml(col.name)} : ${escapeXml(shortType(col.type))}${marker}</text>`
|
|
758
|
+
);
|
|
759
|
+
cy += LINE_H;
|
|
760
|
+
}
|
|
761
|
+
if (table.columns.length > visible.length) {
|
|
762
|
+
parts.push(
|
|
763
|
+
`<text x="${box.x + 8}" y="${cy}" fill="#666666">... +${table.columns.length - visible.length} more</text>`
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
return parts;
|
|
767
|
+
}
|
|
768
|
+
function shortType(type) {
|
|
769
|
+
return type.length > 18 ? `${type.slice(0, 15)}...` : type;
|
|
770
|
+
}
|
|
771
|
+
function escapeXml(text) {
|
|
772
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
773
|
+
}
|
|
774
|
+
function fitErDiagramToBox(width, height, maxWidth, maxHeight) {
|
|
775
|
+
const scale = Math.min(maxWidth / width, maxHeight / height, 1);
|
|
776
|
+
return {
|
|
777
|
+
width: Math.max(1, Math.round(width * scale)),
|
|
778
|
+
height: Math.max(1, Math.round(height * scale))
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
246
782
|
// src/exporters/excel/excel-exporter.ts
|
|
247
783
|
var COLOR = {
|
|
248
784
|
headerBg: "FF4472C4",
|
|
@@ -268,6 +804,9 @@ async function exportExcelDictionary(doc, options) {
|
|
|
268
804
|
sheetNames.set(table.name, buildSheetName(table.name, sheetNames));
|
|
269
805
|
}
|
|
270
806
|
addOverviewSheet(workbook, doc, labels, sheetNames);
|
|
807
|
+
if (doc.tables.length > 0) {
|
|
808
|
+
await addErDiagramSheet(workbook, doc, labels, options.outDir);
|
|
809
|
+
}
|
|
271
810
|
for (const table of doc.tables) {
|
|
272
811
|
const sheetName = sheetNames.get(table.name);
|
|
273
812
|
const sheet = workbook.addWorksheet(sheetName);
|
|
@@ -355,6 +894,51 @@ function addOverviewSheet(workbook, doc, labels, sheetNames) {
|
|
|
355
894
|
};
|
|
356
895
|
sheet.views = [{ state: "frozen", ySplit: headerRowNum }];
|
|
357
896
|
}
|
|
897
|
+
async function addErDiagramSheet(workbook, doc, labels, outDir) {
|
|
898
|
+
const sheet = workbook.addWorksheet(labels.erDiagramSheet);
|
|
899
|
+
sheet.mergeCells(1, 1, 1, 6);
|
|
900
|
+
const titleCell = sheet.getCell(1, 1);
|
|
901
|
+
titleCell.value = labels.erDiagramHeading;
|
|
902
|
+
titleCell.font = { bold: true, size: 14, color: { argb: COLOR.overviewFg } };
|
|
903
|
+
titleCell.fill = solidFill(COLOR.overviewBg);
|
|
904
|
+
titleCell.alignment = { horizontal: "center", vertical: "middle" };
|
|
905
|
+
sheet.getRow(1).height = 28;
|
|
906
|
+
let nextRow = 3;
|
|
907
|
+
try {
|
|
908
|
+
const { buffer: png, width, height } = await renderErDiagramPng(doc);
|
|
909
|
+
await (0, import_promises2.writeFile)((0, import_node_path2.join)(outDir, "er_diagram.png"), png);
|
|
910
|
+
const imageId = workbook.addImage({
|
|
911
|
+
base64: png.toString("base64"),
|
|
912
|
+
extension: "png"
|
|
913
|
+
});
|
|
914
|
+
const fitted = fitErDiagramToBox(width, height, 1100, 1200);
|
|
915
|
+
sheet.addImage(imageId, {
|
|
916
|
+
tl: { col: 0, row: 2 },
|
|
917
|
+
ext: fitted
|
|
918
|
+
});
|
|
919
|
+
nextRow = Math.max(28, Math.ceil(fitted.height / 18) + 4);
|
|
920
|
+
} catch {
|
|
921
|
+
sheet.getCell(3, 1).value = labels.viewErDiagram;
|
|
922
|
+
nextRow = 5;
|
|
923
|
+
}
|
|
924
|
+
const mermaid = getErDiagramMermaid(doc);
|
|
925
|
+
sheet.getCell(nextRow, 1).value = "Mermaid source";
|
|
926
|
+
sheet.getCell(nextRow, 1).font = { bold: true, color: { argb: COLOR.metaFg } };
|
|
927
|
+
nextRow += 1;
|
|
928
|
+
sheet.mergeCells(nextRow, 1, nextRow + 20, 6);
|
|
929
|
+
const sourceCell = sheet.getCell(nextRow, 1);
|
|
930
|
+
sourceCell.value = mermaid;
|
|
931
|
+
sourceCell.alignment = { wrapText: true, vertical: "top" };
|
|
932
|
+
sourceCell.font = { name: "Courier New", size: 9 };
|
|
933
|
+
sheet.columns = [
|
|
934
|
+
{ width: 24 },
|
|
935
|
+
{ width: 24 },
|
|
936
|
+
{ width: 24 },
|
|
937
|
+
{ width: 24 },
|
|
938
|
+
{ width: 24 },
|
|
939
|
+
{ width: 24 }
|
|
940
|
+
];
|
|
941
|
+
}
|
|
358
942
|
function populateTableSheet(sheet, table, doc, labels) {
|
|
359
943
|
const indexes = collectTableIndexes(table, doc);
|
|
360
944
|
sheet.mergeCells(1, 1, 1, 6);
|
|
@@ -395,48 +979,39 @@ function populateTableSheet(sheet, table, doc, labels) {
|
|
|
395
979
|
row.getCell(2).alignment = { wrapText: true, vertical: "top" };
|
|
396
980
|
}
|
|
397
981
|
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
|
-
]);
|
|
982
|
+
const headerRow = sheet.addRow(columnDefinitionHeaders(labels));
|
|
406
983
|
styleColorRow(headerRow, COLOR.headerBg, COLOR.headerFg);
|
|
407
|
-
applyBorderToRow(headerRow,
|
|
984
|
+
applyBorderToRow(headerRow, A5_COLUMN_COUNT);
|
|
408
985
|
const headerRowNum = headerRow.number;
|
|
409
986
|
for (const [i, column] of table.columns.entries()) {
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
column.name,
|
|
416
|
-
displayValue(column.comment, labels),
|
|
417
|
-
column.type,
|
|
418
|
-
column.nullable ? labels.no : labels.yes,
|
|
419
|
-
column.defaultValue ?? "-",
|
|
420
|
-
notes || "-"
|
|
421
|
-
]);
|
|
987
|
+
const row = sheet.addRow(
|
|
988
|
+
columnDefinitionRow(column, labels).map(
|
|
989
|
+
(value, index) => index === 1 ? displayValue(value, labels) : value
|
|
990
|
+
)
|
|
991
|
+
);
|
|
422
992
|
if (column.isPrimaryKey) {
|
|
423
|
-
shadeRow(row,
|
|
993
|
+
shadeRow(row, A5_COLUMN_COUNT, COLOR.pkBg);
|
|
424
994
|
row.getCell(1).font = { bold: true };
|
|
425
995
|
} else if (column.isForeignKey) {
|
|
426
|
-
shadeRow(row,
|
|
996
|
+
shadeRow(row, A5_COLUMN_COUNT, COLOR.fkBg);
|
|
427
997
|
} else if (i % 2 === 1) {
|
|
428
|
-
shadeRow(row,
|
|
998
|
+
shadeRow(row, A5_COLUMN_COUNT, COLOR.altRow);
|
|
429
999
|
}
|
|
430
|
-
row.getCell(
|
|
431
|
-
|
|
1000
|
+
row.getCell(5).alignment = { horizontal: "center" };
|
|
1001
|
+
row.getCell(9).alignment = { horizontal: "center" };
|
|
1002
|
+
applyBorderToRow(row, A5_COLUMN_COUNT);
|
|
432
1003
|
}
|
|
433
1004
|
sheet.columns = [
|
|
1005
|
+
{ width: 22 },
|
|
434
1006
|
{ width: 24 },
|
|
435
|
-
{ width:
|
|
436
|
-
{ width: 18 },
|
|
1007
|
+
{ width: 16 },
|
|
437
1008
|
{ width: 10 },
|
|
438
|
-
{ width:
|
|
439
|
-
{ width:
|
|
1009
|
+
{ width: 8 },
|
|
1010
|
+
{ width: 14 },
|
|
1011
|
+
{ width: 8 },
|
|
1012
|
+
{ width: 8 },
|
|
1013
|
+
{ width: 8 },
|
|
1014
|
+
{ width: 28 }
|
|
440
1015
|
];
|
|
441
1016
|
sheet.views = [{ state: "frozen", ySplit: headerRowNum }];
|
|
442
1017
|
}
|
|
@@ -521,6 +1096,14 @@ async function exportMarkdownDocs(doc, options) {
|
|
|
521
1096
|
const tablesDir = (0, import_node_path3.join)(options.outDir, "tables");
|
|
522
1097
|
await (0, import_promises3.mkdir)(tablesDir, { recursive: true });
|
|
523
1098
|
const labels = getOutputLabels(options.language);
|
|
1099
|
+
if (doc.tables.length > 0) {
|
|
1100
|
+
const mermaid = getErDiagramMermaid(doc);
|
|
1101
|
+
await (0, import_promises3.writeFile)(
|
|
1102
|
+
(0, import_node_path3.join)(options.outDir, "ER_DIAGRAM.md"),
|
|
1103
|
+
renderErDiagramMarkdown(mermaid, labels),
|
|
1104
|
+
"utf8"
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
524
1107
|
for (const table of doc.tables) {
|
|
525
1108
|
await (0, import_promises3.writeFile)(
|
|
526
1109
|
(0, import_node_path3.join)(tablesDir, `${sanitizeFilename(table.name)}.md`),
|
|
@@ -569,12 +1152,12 @@ function renderTableDoc(table, doc, labels) {
|
|
|
569
1152
|
lines.push(`## ${labels.columnsHeading}`);
|
|
570
1153
|
lines.push("");
|
|
571
1154
|
lines.push(
|
|
572
|
-
`| ${labels.
|
|
1155
|
+
`| ${columnDefinitionHeaders(labels).join(" | ")} |`
|
|
573
1156
|
);
|
|
574
|
-
lines.push("
|
|
1157
|
+
lines.push(`|${columnDefinitionHeaders(labels).map(() => "--------").join("|")}|`);
|
|
575
1158
|
for (const col of table.columns) {
|
|
576
1159
|
lines.push(
|
|
577
|
-
`| ${
|
|
1160
|
+
`| ${columnDefinitionRow(col, labels).map((value) => escapeMd(value)).join(" | ")} |`
|
|
578
1161
|
);
|
|
579
1162
|
}
|
|
580
1163
|
lines.push("");
|
|
@@ -606,6 +1189,14 @@ async function exportHtmlDocs(doc, options) {
|
|
|
606
1189
|
renderIndexPage(doc, labels),
|
|
607
1190
|
"utf8"
|
|
608
1191
|
);
|
|
1192
|
+
if (doc.tables.length > 0) {
|
|
1193
|
+
const mermaid = getErDiagramMermaid(doc);
|
|
1194
|
+
await (0, import_promises4.writeFile)(
|
|
1195
|
+
(0, import_node_path4.join)(htmlDir, "er-diagram.html"),
|
|
1196
|
+
renderErDiagramHtmlPage(mermaid, labels),
|
|
1197
|
+
"utf8"
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
609
1200
|
for (const table of doc.tables) {
|
|
610
1201
|
await (0, import_promises4.writeFile)(
|
|
611
1202
|
(0, import_node_path4.join)(tablesDir, `${sanitizeFilename(table.name)}.html`),
|
|
@@ -689,7 +1280,7 @@ function pageShell(title, body, fromSubdir = false) {
|
|
|
689
1280
|
<head>
|
|
690
1281
|
<meta charset="UTF-8">
|
|
691
1282
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
692
|
-
<title>${
|
|
1283
|
+
<title>${esc2(title)}</title>
|
|
693
1284
|
<style>${CSS} </style>
|
|
694
1285
|
</head>
|
|
695
1286
|
<body>
|
|
@@ -706,34 +1297,35 @@ function renderIndexPage(doc, labels) {
|
|
|
706
1297
|
const fkCount = table.foreignKeys.length;
|
|
707
1298
|
const fileName = sanitizeFilename(table.name);
|
|
708
1299
|
tableRows += ` <tr>
|
|
709
|
-
<td><a href="tables/${fileName}.html">${
|
|
710
|
-
<td>${
|
|
1300
|
+
<td><a href="tables/${fileName}.html">${esc2(table.name)}</a></td>
|
|
1301
|
+
<td>${esc2(table.comment ?? "")}</td>
|
|
711
1302
|
<td style="text-align:center">${table.columns.length}</td>
|
|
712
|
-
<td>${
|
|
1303
|
+
<td>${esc2(pkCols)}</td>
|
|
713
1304
|
<td style="text-align:center">${fkCount}</td>
|
|
714
1305
|
</tr>
|
|
715
1306
|
`;
|
|
716
1307
|
}
|
|
717
1308
|
const body = `
|
|
718
|
-
<h1>${
|
|
1309
|
+
<h1>${esc2(labels.docTitle)}</h1>
|
|
719
1310
|
<div class="summary">
|
|
720
|
-
<div class="summary-item"><div class="num">${doc.tables.length}</div><div class="lbl">${
|
|
721
|
-
<div class="summary-item"><div class="num">${doc.relationships.length}</div><div class="lbl">${
|
|
722
|
-
<div class="summary-item"><div class="num">${doc.dialect}</div><div class="lbl">${
|
|
1311
|
+
<div class="summary-item"><div class="num">${doc.tables.length}</div><div class="lbl">${esc2(labels.tablesLabel)}</div></div>
|
|
1312
|
+
<div class="summary-item"><div class="num">${doc.relationships.length}</div><div class="lbl">${esc2(labels.relationshipsLabel)}</div></div>
|
|
1313
|
+
<div class="summary-item"><div class="num">${doc.dialect}</div><div class="lbl">${esc2(labels.dialectLabel)}</div></div>
|
|
723
1314
|
</div>
|
|
724
|
-
<
|
|
1315
|
+
<p class="back"><a href="er-diagram.html">${esc2(labels.erDiagramHeading)} \u2192</a></p>
|
|
1316
|
+
<h2>${esc2(labels.tableListHeading)}</h2>
|
|
725
1317
|
<table class="table-list">
|
|
726
1318
|
<thead><tr>
|
|
727
|
-
<th>${
|
|
728
|
-
<th>${
|
|
1319
|
+
<th>${esc2(labels.tableLabel)}</th>
|
|
1320
|
+
<th>${esc2(labels.tableLogicalName)}</th>
|
|
729
1321
|
<th style="width:70px;text-align:center">Cols</th>
|
|
730
|
-
<th>${
|
|
1322
|
+
<th>${esc2(labels.primaryKey)}</th>
|
|
731
1323
|
<th style="width:50px;text-align:center">FK</th>
|
|
732
1324
|
</tr></thead>
|
|
733
1325
|
<tbody>
|
|
734
1326
|
${tableRows} </tbody>
|
|
735
1327
|
</table>
|
|
736
|
-
<p class="note">${
|
|
1328
|
+
<p class="note">${esc2(labels.generatedNote)}</p>
|
|
737
1329
|
`;
|
|
738
1330
|
return pageShell(labels.docTitle, body);
|
|
739
1331
|
}
|
|
@@ -751,39 +1343,34 @@ function renderTablePage(table, doc, labels) {
|
|
|
751
1343
|
const pkBadge = col.isPrimaryKey ? `<span class="badge badge-pk">PK</span>` : "";
|
|
752
1344
|
const fkBadge = col.isForeignKey ? `<span class="badge badge-fk">FK</span>` : "";
|
|
753
1345
|
const rowClass = col.isPrimaryKey ? "pk" : col.isForeignKey ? "fk" : "";
|
|
754
|
-
const
|
|
755
|
-
colRows += ` <tr${rowClass ? ` class="${rowClass}"` : ""}><td>${
|
|
1346
|
+
const cells = columnDefinitionRow(col, labels);
|
|
1347
|
+
colRows += ` <tr${rowClass ? ` class="${rowClass}"` : ""}><td>${esc2(cells[0] ?? "")}${pkBadge}${fkBadge}</td>` + cells.slice(1).map((cell) => `<td>${esc2(cell)}</td>`).join("") + `</tr>
|
|
756
1348
|
`;
|
|
757
1349
|
}
|
|
758
1350
|
const body = `
|
|
759
|
-
<p class="back"><a href="../index.html">\u2190 ${
|
|
760
|
-
<h1>${
|
|
761
|
-
<h2>${
|
|
1351
|
+
<p class="back"><a href="../index.html">\u2190 ${esc2(labels.tableListHeading)}</a></p>
|
|
1352
|
+
<h1>${esc2(table.name)}</h1>
|
|
1353
|
+
<h2>${esc2(labels.tableInfoHeading)}</h2>
|
|
762
1354
|
<table class="meta">
|
|
763
1355
|
<tbody>
|
|
764
|
-
<tr><th>${
|
|
765
|
-
<tr><th>${
|
|
766
|
-
<tr><th>${
|
|
767
|
-
<tr><th>${
|
|
768
|
-
<tr><th>${
|
|
769
|
-
<tr><th>${
|
|
1356
|
+
<tr><th>${esc2(labels.tablePhysicalName)}</th><td>${esc2(table.name)}</td></tr>
|
|
1357
|
+
<tr><th>${esc2(labels.tableLogicalName)}</th><td>${esc2(table.comment ?? "")}</td></tr>
|
|
1358
|
+
<tr><th>${esc2(labels.schema)}</th><td>${esc2(table.schema ?? "")}</td></tr>
|
|
1359
|
+
<tr><th>${esc2(labels.primaryKey)}</th><td>${esc2(table.primaryKeys.join(", ") || labels.none)}</td></tr>
|
|
1360
|
+
<tr><th>${esc2(labels.foreignKeys)}</th><td>${foreignKeys}</td></tr>
|
|
1361
|
+
<tr><th>${esc2(labels.indexes)}</th><td>${indexText}</td></tr>
|
|
770
1362
|
</tbody>
|
|
771
1363
|
</table>
|
|
772
1364
|
|
|
773
|
-
<h2>${
|
|
1365
|
+
<h2>${esc2(labels.columnsHeading)}</h2>
|
|
774
1366
|
<table class="columns">
|
|
775
1367
|
<thead><tr>
|
|
776
|
-
|
|
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>
|
|
1368
|
+
${columnDefinitionHeaders(labels).map((header) => `<th>${esc2(header)}</th>`).join("\n ")}
|
|
782
1369
|
</tr></thead>
|
|
783
1370
|
<tbody>
|
|
784
1371
|
${colRows} </tbody>
|
|
785
1372
|
</table>
|
|
786
|
-
<p class="note">${
|
|
1373
|
+
<p class="note">${esc2(labels.generatedNote)}</p>
|
|
787
1374
|
`;
|
|
788
1375
|
return pageShell(table.name, body);
|
|
789
1376
|
}
|
|
@@ -795,7 +1382,7 @@ function collectTableIndexes3(table, doc) {
|
|
|
795
1382
|
)
|
|
796
1383
|
];
|
|
797
1384
|
}
|
|
798
|
-
function
|
|
1385
|
+
function esc2(text) {
|
|
799
1386
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
800
1387
|
}
|
|
801
1388
|
|
|
@@ -835,6 +1422,53 @@ async function exportWordDocument(doc, options) {
|
|
|
835
1422
|
children: [new import_docx.TextRun(`${labels.relationshipsLabel}: ${doc.relationships.length}`)]
|
|
836
1423
|
})
|
|
837
1424
|
);
|
|
1425
|
+
if (doc.tables.length > 0) {
|
|
1426
|
+
children.push(
|
|
1427
|
+
new import_docx.Paragraph({
|
|
1428
|
+
heading: import_docx.HeadingLevel.HEADING_2,
|
|
1429
|
+
children: [new import_docx.TextRun(labels.erDiagramHeading)]
|
|
1430
|
+
})
|
|
1431
|
+
);
|
|
1432
|
+
try {
|
|
1433
|
+
const { buffer: png, width, height } = await renderErDiagramPng(doc);
|
|
1434
|
+
await (0, import_promises5.writeFile)((0, import_node_path5.join)(options.outDir, "er_diagram.png"), png);
|
|
1435
|
+
const fitted = fitErDiagramToBox(width, height, 620, 900);
|
|
1436
|
+
children.push(
|
|
1437
|
+
new import_docx.Paragraph({
|
|
1438
|
+
children: [
|
|
1439
|
+
new import_docx.ImageRun({
|
|
1440
|
+
data: png,
|
|
1441
|
+
transformation: fitted,
|
|
1442
|
+
type: "png"
|
|
1443
|
+
})
|
|
1444
|
+
]
|
|
1445
|
+
})
|
|
1446
|
+
);
|
|
1447
|
+
} catch {
|
|
1448
|
+
children.push(
|
|
1449
|
+
new import_docx.Paragraph({
|
|
1450
|
+
children: [new import_docx.TextRun(labels.viewErDiagram)]
|
|
1451
|
+
})
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
const mermaid = getErDiagramMermaid(doc);
|
|
1455
|
+
children.push(
|
|
1456
|
+
new import_docx.Paragraph({
|
|
1457
|
+
children: [new import_docx.TextRun({ text: "Mermaid source", bold: true })]
|
|
1458
|
+
})
|
|
1459
|
+
);
|
|
1460
|
+
children.push(
|
|
1461
|
+
new import_docx.Paragraph({
|
|
1462
|
+
children: [
|
|
1463
|
+
new import_docx.TextRun({
|
|
1464
|
+
text: mermaid,
|
|
1465
|
+
font: "Courier New",
|
|
1466
|
+
size: 18
|
|
1467
|
+
})
|
|
1468
|
+
]
|
|
1469
|
+
})
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
838
1472
|
children.push(
|
|
839
1473
|
new import_docx.Paragraph({
|
|
840
1474
|
heading: import_docx.HeadingLevel.HEADING_2,
|
|
@@ -1094,14 +1728,7 @@ function renderTableDetail(table, doc, labels) {
|
|
|
1094
1728
|
return items;
|
|
1095
1729
|
}
|
|
1096
1730
|
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(
|
|
1731
|
+
const headerCells = columnDefinitionHeaders(labels).map(
|
|
1105
1732
|
(h) => new import_docx.TableCell({
|
|
1106
1733
|
children: [
|
|
1107
1734
|
new import_docx.Paragraph({ children: [new import_docx.TextRun({ text: h, bold: true })] })
|
|
@@ -1112,40 +1739,11 @@ function renderColumnsTable(table, labels) {
|
|
|
1112
1739
|
for (const col of table.columns) {
|
|
1113
1740
|
colRows.push(
|
|
1114
1741
|
new import_docx.TableRow({
|
|
1115
|
-
children:
|
|
1116
|
-
new import_docx.TableCell({
|
|
1117
|
-
children: [new import_docx.Paragraph({ children: [new import_docx.TextRun(
|
|
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
|
-
]
|
|
1742
|
+
children: columnDefinitionRow(col, labels).map(
|
|
1743
|
+
(value) => new import_docx.TableCell({
|
|
1744
|
+
children: [new import_docx.Paragraph({ children: [new import_docx.TextRun(value)] })]
|
|
1147
1745
|
})
|
|
1148
|
-
|
|
1746
|
+
)
|
|
1149
1747
|
})
|
|
1150
1748
|
);
|
|
1151
1749
|
}
|
|
@@ -1193,6 +1791,221 @@ function createWarning(code, message, target) {
|
|
|
1193
1791
|
};
|
|
1194
1792
|
}
|
|
1195
1793
|
|
|
1794
|
+
// src/parsers/sql/column-meta.ts
|
|
1795
|
+
function normalizeColumnType(definition) {
|
|
1796
|
+
if (typeof definition !== "object" || definition === null) {
|
|
1797
|
+
return { type: String(definition ?? "unknown").toLowerCase() };
|
|
1798
|
+
}
|
|
1799
|
+
const def = definition;
|
|
1800
|
+
const base = String(def.dataType ?? def.type ?? def.name ?? "unknown").toLowerCase();
|
|
1801
|
+
if (String(def.dataType ?? "").toUpperCase() === "ENUM") {
|
|
1802
|
+
const values = extractEnumValues(def.expr);
|
|
1803
|
+
if (values.length > 0) {
|
|
1804
|
+
const joined = values.join(", ");
|
|
1805
|
+
return {
|
|
1806
|
+
type: `${base}(${values.map((v) => `'${v}'`).join(",")})`,
|
|
1807
|
+
size: String(values.length)
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
if (def.length !== void 0 && def.length !== null) {
|
|
1812
|
+
const length = formatAstValue(def.length);
|
|
1813
|
+
if (def.scale !== void 0 && def.scale !== null) {
|
|
1814
|
+
const scale = formatAstValue(def.scale);
|
|
1815
|
+
return {
|
|
1816
|
+
type: `${base}(${length},${scale})`,
|
|
1817
|
+
size: `${length},${scale}`
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
return {
|
|
1821
|
+
type: `${base}(${length})`,
|
|
1822
|
+
size: length
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
const suffix = Array.isArray(def.suffix) ? def.suffix.map((item) => formatAstValue(item)).filter(Boolean).join(" ") : def.suffix ? formatAstValue(def.suffix) : "";
|
|
1826
|
+
return {
|
|
1827
|
+
type: suffix ? `${base} ${suffix}`.trim() : base
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
function extractColumnComment(definition) {
|
|
1831
|
+
const comment = definition.comment;
|
|
1832
|
+
if (!comment) return void 0;
|
|
1833
|
+
const value = comment.value;
|
|
1834
|
+
if (value?.value !== void 0) return String(value.value);
|
|
1835
|
+
if (comment.value !== void 0 && typeof comment.value === "string") {
|
|
1836
|
+
return comment.value;
|
|
1837
|
+
}
|
|
1838
|
+
return void 0;
|
|
1839
|
+
}
|
|
1840
|
+
function hasColumnUnique(definition) {
|
|
1841
|
+
return definition.unique === "unique" || definition.unique === true;
|
|
1842
|
+
}
|
|
1843
|
+
function extractColumnConstraintNotes(definition) {
|
|
1844
|
+
const notes = [];
|
|
1845
|
+
if (definition.auto_increment === "auto_increment" || definition.auto_increment === true) {
|
|
1846
|
+
notes.push("AUTO_INCREMENT");
|
|
1847
|
+
}
|
|
1848
|
+
if (definition.on_update) {
|
|
1849
|
+
notes.push(`ON UPDATE ${formatOnUpdate(definition.on_update)}`);
|
|
1850
|
+
}
|
|
1851
|
+
const generated = definition.generated;
|
|
1852
|
+
if (generated) {
|
|
1853
|
+
const storage = String(generated.storage_type ?? "virtual").toUpperCase();
|
|
1854
|
+
const expression = stringifyGeneratedExpression(generated.expr);
|
|
1855
|
+
notes.push(
|
|
1856
|
+
expression ? `GENERATED ALWAYS ${storage}: ${expression}` : `GENERATED ALWAYS ${storage}`
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
const enumValues = extractEnumValues(definition.definition?.expr);
|
|
1860
|
+
if (enumValues.length > 0) {
|
|
1861
|
+
notes.push(`ENUM: ${enumValues.join(", ")}`);
|
|
1862
|
+
}
|
|
1863
|
+
return notes;
|
|
1864
|
+
}
|
|
1865
|
+
function extractEnumValues(expr) {
|
|
1866
|
+
if (!expr || typeof expr !== "object") return [];
|
|
1867
|
+
const object = expr;
|
|
1868
|
+
if (object.type !== "expr_list" || !Array.isArray(object.value)) return [];
|
|
1869
|
+
return object.value.map((item) => {
|
|
1870
|
+
if (!item || typeof item !== "object") return "";
|
|
1871
|
+
const entry = item;
|
|
1872
|
+
return entry.value !== void 0 ? String(entry.value) : "";
|
|
1873
|
+
}).filter(Boolean);
|
|
1874
|
+
}
|
|
1875
|
+
function formatOnUpdate(value) {
|
|
1876
|
+
if (!value || typeof value !== "object") return String(value ?? "");
|
|
1877
|
+
const object = value;
|
|
1878
|
+
if (object.type === "function" && object.name) {
|
|
1879
|
+
const name = object.name;
|
|
1880
|
+
const parts = Array.isArray(name.name) ? name.name : [];
|
|
1881
|
+
return parts.map((part) => String(part.value ?? "")).join("") || "CURRENT_TIMESTAMP";
|
|
1882
|
+
}
|
|
1883
|
+
return formatAstValue(value);
|
|
1884
|
+
}
|
|
1885
|
+
function stringifyGeneratedExpression(expr) {
|
|
1886
|
+
if (!expr || typeof expr !== "object") return void 0;
|
|
1887
|
+
const text = stringifyExpression(expr);
|
|
1888
|
+
return text === "check" ? void 0 : text;
|
|
1889
|
+
}
|
|
1890
|
+
function extractCheckBounds(expression, columnName) {
|
|
1891
|
+
const result = {};
|
|
1892
|
+
walkCheckExpression(expression, columnName, result);
|
|
1893
|
+
return result;
|
|
1894
|
+
}
|
|
1895
|
+
function walkCheckExpression(expression, columnName, result) {
|
|
1896
|
+
if (!expression || typeof expression !== "object") return;
|
|
1897
|
+
const expr = expression;
|
|
1898
|
+
if (expr.type === "binary_expr") {
|
|
1899
|
+
const operator = String(expr.operator ?? "").toUpperCase();
|
|
1900
|
+
if (operator === "AND" || operator === "OR") {
|
|
1901
|
+
walkCheckExpression(expr.left, columnName, result);
|
|
1902
|
+
walkCheckExpression(expr.right, columnName, result);
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
const columnRef = findColumnRef(expr.left) ?? findColumnRef(expr.right);
|
|
1906
|
+
if (columnRef !== columnName) return;
|
|
1907
|
+
const bound = readBound(expr, columnName);
|
|
1908
|
+
if (!bound) return;
|
|
1909
|
+
if (bound.kind === "min") {
|
|
1910
|
+
result.minValue = mergeBound(result.minValue, bound.value, "max");
|
|
1911
|
+
} else {
|
|
1912
|
+
result.maxValue = mergeBound(result.maxValue, bound.value, "min");
|
|
1913
|
+
}
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
result.expression ??= stringifyExpression(expr);
|
|
1917
|
+
}
|
|
1918
|
+
function readBound(expr, columnName) {
|
|
1919
|
+
const operator = String(expr.operator ?? "");
|
|
1920
|
+
const left = expr.left;
|
|
1921
|
+
const right = expr.right;
|
|
1922
|
+
if (findColumnRef(left) === columnName) {
|
|
1923
|
+
if (operator === ">=" || operator === ">") {
|
|
1924
|
+
return { kind: "min", value: formatAstValue(right) };
|
|
1925
|
+
}
|
|
1926
|
+
if (operator === "<=" || operator === "<") {
|
|
1927
|
+
return { kind: "max", value: formatAstValue(right) };
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
if (findColumnRef(right) === columnName) {
|
|
1931
|
+
if (operator === ">=" || operator === ">") {
|
|
1932
|
+
return { kind: "max", value: formatAstValue(left) };
|
|
1933
|
+
}
|
|
1934
|
+
if (operator === "<=" || operator === "<") {
|
|
1935
|
+
return { kind: "min", value: formatAstValue(left) };
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
return void 0;
|
|
1939
|
+
}
|
|
1940
|
+
function mergeBound(current, next, pick) {
|
|
1941
|
+
if (!current) return next;
|
|
1942
|
+
const currentNum = Number(current);
|
|
1943
|
+
const nextNum = Number(next);
|
|
1944
|
+
if (!Number.isNaN(currentNum) && !Number.isNaN(nextNum)) {
|
|
1945
|
+
return pick === "min" ? String(Math.max(currentNum, nextNum)) : String(Math.min(currentNum, nextNum));
|
|
1946
|
+
}
|
|
1947
|
+
return next;
|
|
1948
|
+
}
|
|
1949
|
+
function findColumnRef(value) {
|
|
1950
|
+
if (!value || typeof value !== "object") return void 0;
|
|
1951
|
+
const expr = value;
|
|
1952
|
+
if (expr.type === "column_ref" && expr.column) {
|
|
1953
|
+
return String(expr.column);
|
|
1954
|
+
}
|
|
1955
|
+
return void 0;
|
|
1956
|
+
}
|
|
1957
|
+
function formatAstValue(value) {
|
|
1958
|
+
if (value === null || value === void 0) return "";
|
|
1959
|
+
if (typeof value === "object") {
|
|
1960
|
+
const object = value;
|
|
1961
|
+
if (object.value !== void 0) return String(object.value);
|
|
1962
|
+
if (object.dataType) return normalizeColumnType(object).type;
|
|
1963
|
+
}
|
|
1964
|
+
return String(value);
|
|
1965
|
+
}
|
|
1966
|
+
function stringifyExpression(expr) {
|
|
1967
|
+
if (expr.type === "binary_expr") {
|
|
1968
|
+
const left = stringifyExpression(expr.left ?? {});
|
|
1969
|
+
const right = stringifyExpression(expr.right ?? {});
|
|
1970
|
+
return `${left} ${expr.operator} ${right}`.trim();
|
|
1971
|
+
}
|
|
1972
|
+
if (expr.type === "column_ref") return String(expr.column ?? "");
|
|
1973
|
+
if (expr.value !== void 0) return formatAstValue(expr);
|
|
1974
|
+
return "check";
|
|
1975
|
+
}
|
|
1976
|
+
function extractConstraintColumnNames(definition) {
|
|
1977
|
+
return extractDeepColumnNames(definition);
|
|
1978
|
+
}
|
|
1979
|
+
function extractDeepColumnNames(value) {
|
|
1980
|
+
if (!Array.isArray(value)) return [];
|
|
1981
|
+
return value.map((item) => {
|
|
1982
|
+
if (typeof item !== "object" || item === null) return String(item ?? "unknown");
|
|
1983
|
+
const object = item;
|
|
1984
|
+
if (object.column !== void 0) return String(object.column);
|
|
1985
|
+
if (object.expr) return extractDeepColumnName(object.expr);
|
|
1986
|
+
return String(object.name ?? "unknown");
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
function extractDeepColumnName(value) {
|
|
1990
|
+
if (typeof value !== "object" || value === null) {
|
|
1991
|
+
return String(value ?? "unknown");
|
|
1992
|
+
}
|
|
1993
|
+
const object = value;
|
|
1994
|
+
if (object.expr) return extractDeepColumnName(object.expr);
|
|
1995
|
+
if (object.column && typeof object.column === "object") {
|
|
1996
|
+
return extractDeepColumnName(object.column);
|
|
1997
|
+
}
|
|
1998
|
+
if (object.column !== void 0) return String(object.column);
|
|
1999
|
+
return String(object.name ?? "unknown");
|
|
2000
|
+
}
|
|
2001
|
+
function stringifyCheckDefinition(definition) {
|
|
2002
|
+
if (!Array.isArray(definition) || definition.length === 0) return void 0;
|
|
2003
|
+
if (definition.length === 1) {
|
|
2004
|
+
return stringifyExpression(definition[0]);
|
|
2005
|
+
}
|
|
2006
|
+
return definition.map((item) => stringifyExpression(item)).filter(Boolean).join("; ");
|
|
2007
|
+
}
|
|
2008
|
+
|
|
1196
2009
|
// src/parsers/sql/sql-normalizer.ts
|
|
1197
2010
|
function normalizeSqlAst(ast, dialect) {
|
|
1198
2011
|
const statements = Array.isArray(ast) ? ast : [ast];
|
|
@@ -1214,7 +2027,11 @@ function normalizeSqlAst(ast, dialect) {
|
|
|
1214
2027
|
}
|
|
1215
2028
|
for (const index of indexes) {
|
|
1216
2029
|
const table = tables.find((candidate) => candidate.name === index.table);
|
|
1217
|
-
table
|
|
2030
|
+
if (!table) continue;
|
|
2031
|
+
table.indexes.push(index);
|
|
2032
|
+
if (index.unique) {
|
|
2033
|
+
markColumnsUnique(table, index.columns, `INDEX ${index.name}`);
|
|
2034
|
+
}
|
|
1218
2035
|
}
|
|
1219
2036
|
return {
|
|
1220
2037
|
dialect,
|
|
@@ -1236,31 +2053,49 @@ function normalizeCreateTable(statement) {
|
|
|
1236
2053
|
reviewTodos: []
|
|
1237
2054
|
};
|
|
1238
2055
|
for (const definition of createDefinitions) {
|
|
1239
|
-
if (definition.resource
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
2056
|
+
if (definition.resource !== "column") continue;
|
|
2057
|
+
const columnName = extractDeepColumnName2(definition.column);
|
|
2058
|
+
const isPrimaryKey = hasPrimaryKey(definition);
|
|
2059
|
+
const isNotNull = hasNotNull(definition);
|
|
2060
|
+
const { type, size } = normalizeColumnType(definition.definition);
|
|
2061
|
+
const check = definition.check;
|
|
2062
|
+
const bounds = check?.definition ? extractCheckBounds(check.definition, columnName) : {};
|
|
2063
|
+
const constraintNotes = extractColumnConstraintNotes(definition);
|
|
2064
|
+
if (check?.definition) {
|
|
2065
|
+
const expression = stringifyCheckDefinition(check.definition);
|
|
2066
|
+
if (expression && (!bounds.minValue || !bounds.maxValue)) {
|
|
2067
|
+
constraintNotes.push(`CHECK: ${expression}`);
|
|
2068
|
+
}
|
|
1252
2069
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
2070
|
+
table.columns.push({
|
|
2071
|
+
name: columnName,
|
|
2072
|
+
type,
|
|
2073
|
+
size,
|
|
2074
|
+
nullable: !isNotNull && !isPrimaryKey,
|
|
2075
|
+
defaultValue: extractDefaultFromDef(definition),
|
|
2076
|
+
minValue: bounds.minValue,
|
|
2077
|
+
maxValue: bounds.maxValue,
|
|
2078
|
+
isUnique: hasColumnUnique(definition),
|
|
2079
|
+
isPrimaryKey,
|
|
2080
|
+
isForeignKey: false,
|
|
2081
|
+
comment: extractColumnComment(definition),
|
|
2082
|
+
constraintNotes: constraintNotes.length > 0 ? constraintNotes : void 0
|
|
2083
|
+
});
|
|
2084
|
+
if (isPrimaryKey) table.primaryKeys.push(columnName);
|
|
2085
|
+
}
|
|
2086
|
+
for (const definition of createDefinitions) {
|
|
2087
|
+
if (definition.resource !== "constraint") continue;
|
|
2088
|
+
if (isConstraintType(definition.constraint_type, "PRIMARY KEY")) {
|
|
2089
|
+
table.primaryKeys = extractDeepColumnNames2(definition.definition);
|
|
1255
2090
|
for (const column of table.columns) {
|
|
1256
2091
|
if (table.primaryKeys.includes(column.name)) column.isPrimaryKey = true;
|
|
1257
2092
|
}
|
|
1258
2093
|
}
|
|
1259
|
-
if (
|
|
1260
|
-
const columns =
|
|
2094
|
+
if (isConstraintType(definition.constraint_type, "FOREIGN KEY")) {
|
|
2095
|
+
const columns = extractDeepColumnNames2(definition.definition);
|
|
1261
2096
|
const refDef = definition.reference_definition;
|
|
1262
2097
|
const referencedTable = extractTableName(refDef?.table);
|
|
1263
|
-
const referencedColumns =
|
|
2098
|
+
const referencedColumns = extractDeepColumnNames2(refDef?.definition);
|
|
1264
2099
|
table.foreignKeys.push({
|
|
1265
2100
|
name: typeof definition.constraint === "string" ? definition.constraint : void 0,
|
|
1266
2101
|
columns,
|
|
@@ -1271,14 +2106,71 @@ function normalizeCreateTable(statement) {
|
|
|
1271
2106
|
if (columns.includes(column.name)) column.isForeignKey = true;
|
|
1272
2107
|
}
|
|
1273
2108
|
}
|
|
2109
|
+
if (isConstraintType(definition.constraint_type, "UNIQUE")) {
|
|
2110
|
+
const columns = extractConstraintColumnNames(definition.definition);
|
|
2111
|
+
const label = typeof definition.constraint === "string" ? definition.constraint : "UNIQUE";
|
|
2112
|
+
markColumnsUnique(table, columns, label);
|
|
2113
|
+
}
|
|
2114
|
+
if (isConstraintType(definition.constraint_type, "CHECK")) {
|
|
2115
|
+
applyTableCheckConstraint(table, definition);
|
|
2116
|
+
}
|
|
1274
2117
|
}
|
|
1275
2118
|
return table;
|
|
1276
2119
|
}
|
|
2120
|
+
function markColumnsUnique(table, columns, label) {
|
|
2121
|
+
const composite = columns.length > 1;
|
|
2122
|
+
for (const columnName of columns) {
|
|
2123
|
+
const column = table.columns.find((item) => item.name === columnName);
|
|
2124
|
+
if (!column) continue;
|
|
2125
|
+
column.isUnique = true;
|
|
2126
|
+
if (composite) {
|
|
2127
|
+
addConstraintNote(
|
|
2128
|
+
column,
|
|
2129
|
+
`UNIQUE (${label}: ${columns.join(", ")})`
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
function applyTableCheckConstraint(table, definition) {
|
|
2135
|
+
const expression = stringifyCheckDefinition(definition.definition);
|
|
2136
|
+
if (!expression) return;
|
|
2137
|
+
const referencedColumns = /* @__PURE__ */ new Set();
|
|
2138
|
+
for (const column of table.columns) {
|
|
2139
|
+
const bounds = extractCheckBounds(definition.definition, column.name);
|
|
2140
|
+
if (bounds.minValue) column.minValue = bounds.minValue;
|
|
2141
|
+
if (bounds.maxValue) column.maxValue = bounds.maxValue;
|
|
2142
|
+
if (bounds.minValue || bounds.maxValue) {
|
|
2143
|
+
referencedColumns.add(column.name);
|
|
2144
|
+
continue;
|
|
2145
|
+
}
|
|
2146
|
+
if (expression.includes(column.name)) {
|
|
2147
|
+
referencedColumns.add(column.name);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
if (referencedColumns.size === 0) {
|
|
2151
|
+
for (const column of table.columns) {
|
|
2152
|
+
addConstraintNote(column, `CHECK: ${expression}`);
|
|
2153
|
+
}
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
for (const columnName of referencedColumns) {
|
|
2157
|
+
const column = table.columns.find((item) => item.name === columnName);
|
|
2158
|
+
if (!column) continue;
|
|
2159
|
+
if (!column.minValue && !column.maxValue) {
|
|
2160
|
+
addConstraintNote(column, `CHECK: ${expression}`);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
function addConstraintNote(column, note) {
|
|
2165
|
+
const notes = column.constraintNotes ?? [];
|
|
2166
|
+
if (!notes.includes(note)) notes.push(note);
|
|
2167
|
+
column.constraintNotes = notes;
|
|
2168
|
+
}
|
|
1277
2169
|
function normalizeCreateIndex(statement) {
|
|
1278
2170
|
return {
|
|
1279
2171
|
name: String(statement.index ?? statement.index_name ?? "unnamed_index"),
|
|
1280
2172
|
table: extractTableName(statement.table),
|
|
1281
|
-
columns:
|
|
2173
|
+
columns: extractDeepColumnNames2(
|
|
1282
2174
|
statement.index_columns ?? statement.columns
|
|
1283
2175
|
),
|
|
1284
2176
|
unique: Boolean(statement.unique)
|
|
@@ -1332,15 +2224,15 @@ function extractTableName(value) {
|
|
|
1332
2224
|
}
|
|
1333
2225
|
return String(value ?? "unknown");
|
|
1334
2226
|
}
|
|
1335
|
-
function
|
|
2227
|
+
function extractDeepColumnName2(value) {
|
|
1336
2228
|
if (typeof value !== "object" || value === null)
|
|
1337
2229
|
return String(value ?? "unknown");
|
|
1338
2230
|
const object = value;
|
|
1339
2231
|
if (object.expr && typeof object.expr === "object") {
|
|
1340
|
-
return
|
|
2232
|
+
return extractDeepColumnName2(object.expr);
|
|
1341
2233
|
}
|
|
1342
2234
|
if (object.column && typeof object.column === "object") {
|
|
1343
|
-
return
|
|
2235
|
+
return extractDeepColumnName2(object.column);
|
|
1344
2236
|
}
|
|
1345
2237
|
if (object.value !== void 0) {
|
|
1346
2238
|
return String(object.value);
|
|
@@ -1350,18 +2242,9 @@ function extractDeepColumnName(value) {
|
|
|
1350
2242
|
}
|
|
1351
2243
|
return String(object.name ?? object.tableName ?? "unknown");
|
|
1352
2244
|
}
|
|
1353
|
-
function
|
|
2245
|
+
function extractDeepColumnNames2(value) {
|
|
1354
2246
|
if (!Array.isArray(value)) return [];
|
|
1355
|
-
return value.map((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();
|
|
2247
|
+
return value.map((item) => extractDeepColumnName2(item));
|
|
1365
2248
|
}
|
|
1366
2249
|
function hasPrimaryKey(def) {
|
|
1367
2250
|
if (def.primary_key) return true;
|
|
@@ -1641,18 +2524,40 @@ async function generateDbDocs(options) {
|
|
|
1641
2524
|
return doc;
|
|
1642
2525
|
}
|
|
1643
2526
|
|
|
2527
|
+
// src/core/output-path.ts
|
|
2528
|
+
var import_node_path6 = require("path");
|
|
2529
|
+
var OUTPUT_RUN_DIR_PREFIX = "db_doc_gen_";
|
|
2530
|
+
function createTimestampedRunName(date = /* @__PURE__ */ new Date()) {
|
|
2531
|
+
const year = String(date.getFullYear()).slice(-2);
|
|
2532
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
2533
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
2534
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
2535
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
2536
|
+
return `${OUTPUT_RUN_DIR_PREFIX}${year}${month}${day}${hours}${minutes}`;
|
|
2537
|
+
}
|
|
2538
|
+
function resolveGenerateOutDir(parentDir, date = /* @__PURE__ */ new Date()) {
|
|
2539
|
+
return (0, import_node_path6.join)(parentDir, createTimestampedRunName(date));
|
|
2540
|
+
}
|
|
2541
|
+
function isOutputRunDir(path) {
|
|
2542
|
+
return (0, import_node_path6.basename)(path).startsWith(OUTPUT_RUN_DIR_PREFIX);
|
|
2543
|
+
}
|
|
2544
|
+
function isOutputRunDirName(name) {
|
|
2545
|
+
return name.startsWith(OUTPUT_RUN_DIR_PREFIX);
|
|
2546
|
+
}
|
|
2547
|
+
|
|
1644
2548
|
// src/cli/index.ts
|
|
1645
2549
|
var DEFAULT_CONFIG_PATH = "dbdocgen.config.json";
|
|
1646
2550
|
var program = new import_commander.Command();
|
|
1647
2551
|
program.name("dbdocgen").description("Generate database documentation from SQL schema files.").version("0.1.0");
|
|
1648
2552
|
program.command("init").description("Create a default config file").option("-f, --force", "Overwrite existing config file").action(async (rawOptions) => {
|
|
1649
|
-
const configPath = (0,
|
|
2553
|
+
const configPath = (0, import_node_path7.resolve)(process.cwd(), DEFAULT_CONFIG_PATH);
|
|
1650
2554
|
if ((0, import_node_fs.existsSync)(configPath) && !rawOptions.force) {
|
|
1651
2555
|
console.log(`Config already exists at ${configPath}. Use --force to overwrite.`);
|
|
1652
2556
|
return;
|
|
1653
2557
|
}
|
|
1654
2558
|
const defaultConfig = {
|
|
1655
2559
|
schema: "./database/schema.sql",
|
|
2560
|
+
outDir: "./output",
|
|
1656
2561
|
output: {
|
|
1657
2562
|
formats: ["excel", "markdown", "html", "diagram", "word"],
|
|
1658
2563
|
language: "en"
|
|
@@ -1660,7 +2565,9 @@ program.command("init").description("Create a default config file").option("-f,
|
|
|
1660
2565
|
};
|
|
1661
2566
|
await (0, import_promises7.writeFile)(configPath, JSON.stringify(defaultConfig, null, 2), "utf8");
|
|
1662
2567
|
console.log(`Created config at ${configPath}`);
|
|
1663
|
-
console.log(
|
|
2568
|
+
console.log(
|
|
2569
|
+
"Each generate run writes to {outDir}/db_doc_gen_{yymmddhhmm} (outDir defaults to ./output)."
|
|
2570
|
+
);
|
|
1664
2571
|
console.log("Edit the file to configure your database schema path and output formats.");
|
|
1665
2572
|
});
|
|
1666
2573
|
var configCommand = program.command("config").description("Manage configuration");
|
|
@@ -1690,7 +2597,7 @@ configCommand.command("validate").description("Validate config file").option("--
|
|
|
1690
2597
|
process.exitCode = 1;
|
|
1691
2598
|
}
|
|
1692
2599
|
});
|
|
1693
|
-
program.command("generate").description("Generate database documentation").option("--schema <path>", "Path to schema.sql").option("--out <path>", "
|
|
2600
|
+
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) => {
|
|
1694
2601
|
console.log("[dbdocgen] Loading configuration...");
|
|
1695
2602
|
const config = await loadConfig({
|
|
1696
2603
|
cwd: process.cwd(),
|
|
@@ -1701,9 +2608,11 @@ program.command("generate").description("Generate database documentation").optio
|
|
|
1701
2608
|
configPath: rawOptions.config
|
|
1702
2609
|
}
|
|
1703
2610
|
});
|
|
1704
|
-
const
|
|
2611
|
+
const parentOutDir = config.outDir;
|
|
2612
|
+
const outDir = resolveGenerateOutDir(parentOutDir);
|
|
1705
2613
|
console.log("[dbdocgen] Configuration loaded");
|
|
1706
2614
|
console.log(` schema: ${config.schema}`);
|
|
2615
|
+
console.log(` outputParent: ${parentOutDir}`);
|
|
1707
2616
|
console.log(` outDir: ${outDir}`);
|
|
1708
2617
|
console.log(` formats: ${config.output.formats.join(", ")}`);
|
|
1709
2618
|
console.log(` language: ${config.output.language}`);
|
|
@@ -1773,7 +2682,7 @@ Found ${doc.tables.length} table(s):
|
|
|
1773
2682
|
}
|
|
1774
2683
|
console.log("Schema validation passed.");
|
|
1775
2684
|
});
|
|
1776
|
-
program.command("clean").description("Clean output directory").option("--out <path>", "
|
|
2685
|
+
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) => {
|
|
1777
2686
|
const config = await loadConfig({
|
|
1778
2687
|
cwd: process.cwd(),
|
|
1779
2688
|
cliOptions: {
|
|
@@ -1781,13 +2690,29 @@ program.command("clean").description("Clean output directory").option("--out <pa
|
|
|
1781
2690
|
configPath: rawOptions.config
|
|
1782
2691
|
}
|
|
1783
2692
|
});
|
|
1784
|
-
const
|
|
1785
|
-
if (!(0, import_node_fs.existsSync)(
|
|
1786
|
-
console.log(`Output
|
|
2693
|
+
const target = (0, import_node_path7.resolve)(config.outDir);
|
|
2694
|
+
if (!(0, import_node_fs.existsSync)(target)) {
|
|
2695
|
+
console.log(`Output path ${target} does not exist. Nothing to clean.`);
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
if (isOutputRunDir(target)) {
|
|
2699
|
+
console.log(`Cleaning ${target}...`);
|
|
2700
|
+
await (0, import_promises7.rm)(target, { recursive: true, force: true });
|
|
2701
|
+
console.log("Done.");
|
|
1787
2702
|
return;
|
|
1788
2703
|
}
|
|
1789
|
-
|
|
1790
|
-
|
|
2704
|
+
const entries = await (0, import_promises7.readdir)(target, { withFileTypes: true });
|
|
2705
|
+
const runDirs = entries.filter(
|
|
2706
|
+
(entry) => entry.isDirectory() && isOutputRunDirName(entry.name)
|
|
2707
|
+
);
|
|
2708
|
+
if (runDirs.length === 0) {
|
|
2709
|
+
console.log(`No db_doc_gen_* folders found under ${target}.`);
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
console.log(`Cleaning ${runDirs.length} run folder(s) under ${target}...`);
|
|
2713
|
+
for (const entry of runDirs) {
|
|
2714
|
+
await (0, import_promises7.rm)((0, import_node_path7.resolve)(target, entry.name), { recursive: true, force: true });
|
|
2715
|
+
}
|
|
1791
2716
|
console.log("Done.");
|
|
1792
2717
|
});
|
|
1793
2718
|
program.command("info").description("Show project info and supported features").action(() => {
|
|
@@ -1833,12 +2758,4 @@ function parseFormats(value) {
|
|
|
1833
2758
|
}
|
|
1834
2759
|
return valid.length > 0 ? valid : void 0;
|
|
1835
2760
|
}
|
|
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
2761
|
//# sourceMappingURL=index.cjs.map
|