@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/index.js
CHANGED
|
@@ -16,12 +16,17 @@ var reviewTodoSchema = z.object({
|
|
|
16
16
|
var columnDocSchema = z.object({
|
|
17
17
|
name: z.string().min(1),
|
|
18
18
|
type: z.string().min(1),
|
|
19
|
+
size: z.string().optional(),
|
|
19
20
|
nullable: z.boolean(),
|
|
20
21
|
defaultValue: z.string().optional(),
|
|
22
|
+
minValue: z.string().optional(),
|
|
23
|
+
maxValue: z.string().optional(),
|
|
24
|
+
isUnique: z.boolean(),
|
|
21
25
|
isPrimaryKey: z.boolean(),
|
|
22
26
|
isForeignKey: z.boolean(),
|
|
23
27
|
comment: z.string().optional(),
|
|
24
|
-
description: enrichedTextSchema.optional()
|
|
28
|
+
description: enrichedTextSchema.optional(),
|
|
29
|
+
constraintNotes: z.array(z.string()).optional()
|
|
25
30
|
});
|
|
26
31
|
var foreignKeyDocSchema = z.object({
|
|
27
32
|
name: z.string().optional(),
|
|
@@ -100,7 +105,7 @@ var outputLanguageSchema = z2.enum(["en", "jp"]);
|
|
|
100
105
|
var dbdocgenConfigSchema = z2.object({
|
|
101
106
|
schema: z2.string().default("./schema.sql"),
|
|
102
107
|
dialect: dialectSchema.optional(),
|
|
103
|
-
outDir: z2.string().default("./
|
|
108
|
+
outDir: z2.string().default("./output"),
|
|
104
109
|
output: z2.object({
|
|
105
110
|
formats: z2.array(outputFormatSchema).default(["excel", "markdown", "html", "diagram", "word"]),
|
|
106
111
|
language: outputLanguageSchema.default("en")
|
|
@@ -143,6 +148,221 @@ function createWarning(code, message, target) {
|
|
|
143
148
|
};
|
|
144
149
|
}
|
|
145
150
|
|
|
151
|
+
// src/parsers/sql/column-meta.ts
|
|
152
|
+
function normalizeColumnType(definition) {
|
|
153
|
+
if (typeof definition !== "object" || definition === null) {
|
|
154
|
+
return { type: String(definition ?? "unknown").toLowerCase() };
|
|
155
|
+
}
|
|
156
|
+
const def = definition;
|
|
157
|
+
const base = String(def.dataType ?? def.type ?? def.name ?? "unknown").toLowerCase();
|
|
158
|
+
if (String(def.dataType ?? "").toUpperCase() === "ENUM") {
|
|
159
|
+
const values = extractEnumValues(def.expr);
|
|
160
|
+
if (values.length > 0) {
|
|
161
|
+
const joined = values.join(", ");
|
|
162
|
+
return {
|
|
163
|
+
type: `${base}(${values.map((v) => `'${v}'`).join(",")})`,
|
|
164
|
+
size: String(values.length)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (def.length !== void 0 && def.length !== null) {
|
|
169
|
+
const length = formatAstValue(def.length);
|
|
170
|
+
if (def.scale !== void 0 && def.scale !== null) {
|
|
171
|
+
const scale = formatAstValue(def.scale);
|
|
172
|
+
return {
|
|
173
|
+
type: `${base}(${length},${scale})`,
|
|
174
|
+
size: `${length},${scale}`
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
type: `${base}(${length})`,
|
|
179
|
+
size: length
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const suffix = Array.isArray(def.suffix) ? def.suffix.map((item) => formatAstValue(item)).filter(Boolean).join(" ") : def.suffix ? formatAstValue(def.suffix) : "";
|
|
183
|
+
return {
|
|
184
|
+
type: suffix ? `${base} ${suffix}`.trim() : base
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function extractColumnComment(definition) {
|
|
188
|
+
const comment = definition.comment;
|
|
189
|
+
if (!comment) return void 0;
|
|
190
|
+
const value = comment.value;
|
|
191
|
+
if (value?.value !== void 0) return String(value.value);
|
|
192
|
+
if (comment.value !== void 0 && typeof comment.value === "string") {
|
|
193
|
+
return comment.value;
|
|
194
|
+
}
|
|
195
|
+
return void 0;
|
|
196
|
+
}
|
|
197
|
+
function hasColumnUnique(definition) {
|
|
198
|
+
return definition.unique === "unique" || definition.unique === true;
|
|
199
|
+
}
|
|
200
|
+
function extractColumnConstraintNotes(definition) {
|
|
201
|
+
const notes = [];
|
|
202
|
+
if (definition.auto_increment === "auto_increment" || definition.auto_increment === true) {
|
|
203
|
+
notes.push("AUTO_INCREMENT");
|
|
204
|
+
}
|
|
205
|
+
if (definition.on_update) {
|
|
206
|
+
notes.push(`ON UPDATE ${formatOnUpdate(definition.on_update)}`);
|
|
207
|
+
}
|
|
208
|
+
const generated = definition.generated;
|
|
209
|
+
if (generated) {
|
|
210
|
+
const storage = String(generated.storage_type ?? "virtual").toUpperCase();
|
|
211
|
+
const expression = stringifyGeneratedExpression(generated.expr);
|
|
212
|
+
notes.push(
|
|
213
|
+
expression ? `GENERATED ALWAYS ${storage}: ${expression}` : `GENERATED ALWAYS ${storage}`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const enumValues = extractEnumValues(definition.definition?.expr);
|
|
217
|
+
if (enumValues.length > 0) {
|
|
218
|
+
notes.push(`ENUM: ${enumValues.join(", ")}`);
|
|
219
|
+
}
|
|
220
|
+
return notes;
|
|
221
|
+
}
|
|
222
|
+
function extractEnumValues(expr) {
|
|
223
|
+
if (!expr || typeof expr !== "object") return [];
|
|
224
|
+
const object = expr;
|
|
225
|
+
if (object.type !== "expr_list" || !Array.isArray(object.value)) return [];
|
|
226
|
+
return object.value.map((item) => {
|
|
227
|
+
if (!item || typeof item !== "object") return "";
|
|
228
|
+
const entry = item;
|
|
229
|
+
return entry.value !== void 0 ? String(entry.value) : "";
|
|
230
|
+
}).filter(Boolean);
|
|
231
|
+
}
|
|
232
|
+
function formatOnUpdate(value) {
|
|
233
|
+
if (!value || typeof value !== "object") return String(value ?? "");
|
|
234
|
+
const object = value;
|
|
235
|
+
if (object.type === "function" && object.name) {
|
|
236
|
+
const name = object.name;
|
|
237
|
+
const parts = Array.isArray(name.name) ? name.name : [];
|
|
238
|
+
return parts.map((part) => String(part.value ?? "")).join("") || "CURRENT_TIMESTAMP";
|
|
239
|
+
}
|
|
240
|
+
return formatAstValue(value);
|
|
241
|
+
}
|
|
242
|
+
function stringifyGeneratedExpression(expr) {
|
|
243
|
+
if (!expr || typeof expr !== "object") return void 0;
|
|
244
|
+
const text = stringifyExpression(expr);
|
|
245
|
+
return text === "check" ? void 0 : text;
|
|
246
|
+
}
|
|
247
|
+
function extractCheckBounds(expression, columnName) {
|
|
248
|
+
const result = {};
|
|
249
|
+
walkCheckExpression(expression, columnName, result);
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
function walkCheckExpression(expression, columnName, result) {
|
|
253
|
+
if (!expression || typeof expression !== "object") return;
|
|
254
|
+
const expr = expression;
|
|
255
|
+
if (expr.type === "binary_expr") {
|
|
256
|
+
const operator = String(expr.operator ?? "").toUpperCase();
|
|
257
|
+
if (operator === "AND" || operator === "OR") {
|
|
258
|
+
walkCheckExpression(expr.left, columnName, result);
|
|
259
|
+
walkCheckExpression(expr.right, columnName, result);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const columnRef = findColumnRef(expr.left) ?? findColumnRef(expr.right);
|
|
263
|
+
if (columnRef !== columnName) return;
|
|
264
|
+
const bound = readBound(expr, columnName);
|
|
265
|
+
if (!bound) return;
|
|
266
|
+
if (bound.kind === "min") {
|
|
267
|
+
result.minValue = mergeBound(result.minValue, bound.value, "max");
|
|
268
|
+
} else {
|
|
269
|
+
result.maxValue = mergeBound(result.maxValue, bound.value, "min");
|
|
270
|
+
}
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
result.expression ??= stringifyExpression(expr);
|
|
274
|
+
}
|
|
275
|
+
function readBound(expr, columnName) {
|
|
276
|
+
const operator = String(expr.operator ?? "");
|
|
277
|
+
const left = expr.left;
|
|
278
|
+
const right = expr.right;
|
|
279
|
+
if (findColumnRef(left) === columnName) {
|
|
280
|
+
if (operator === ">=" || operator === ">") {
|
|
281
|
+
return { kind: "min", value: formatAstValue(right) };
|
|
282
|
+
}
|
|
283
|
+
if (operator === "<=" || operator === "<") {
|
|
284
|
+
return { kind: "max", value: formatAstValue(right) };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (findColumnRef(right) === columnName) {
|
|
288
|
+
if (operator === ">=" || operator === ">") {
|
|
289
|
+
return { kind: "max", value: formatAstValue(left) };
|
|
290
|
+
}
|
|
291
|
+
if (operator === "<=" || operator === "<") {
|
|
292
|
+
return { kind: "min", value: formatAstValue(left) };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return void 0;
|
|
296
|
+
}
|
|
297
|
+
function mergeBound(current, next, pick) {
|
|
298
|
+
if (!current) return next;
|
|
299
|
+
const currentNum = Number(current);
|
|
300
|
+
const nextNum = Number(next);
|
|
301
|
+
if (!Number.isNaN(currentNum) && !Number.isNaN(nextNum)) {
|
|
302
|
+
return pick === "min" ? String(Math.max(currentNum, nextNum)) : String(Math.min(currentNum, nextNum));
|
|
303
|
+
}
|
|
304
|
+
return next;
|
|
305
|
+
}
|
|
306
|
+
function findColumnRef(value) {
|
|
307
|
+
if (!value || typeof value !== "object") return void 0;
|
|
308
|
+
const expr = value;
|
|
309
|
+
if (expr.type === "column_ref" && expr.column) {
|
|
310
|
+
return String(expr.column);
|
|
311
|
+
}
|
|
312
|
+
return void 0;
|
|
313
|
+
}
|
|
314
|
+
function formatAstValue(value) {
|
|
315
|
+
if (value === null || value === void 0) return "";
|
|
316
|
+
if (typeof value === "object") {
|
|
317
|
+
const object = value;
|
|
318
|
+
if (object.value !== void 0) return String(object.value);
|
|
319
|
+
if (object.dataType) return normalizeColumnType(object).type;
|
|
320
|
+
}
|
|
321
|
+
return String(value);
|
|
322
|
+
}
|
|
323
|
+
function stringifyExpression(expr) {
|
|
324
|
+
if (expr.type === "binary_expr") {
|
|
325
|
+
const left = stringifyExpression(expr.left ?? {});
|
|
326
|
+
const right = stringifyExpression(expr.right ?? {});
|
|
327
|
+
return `${left} ${expr.operator} ${right}`.trim();
|
|
328
|
+
}
|
|
329
|
+
if (expr.type === "column_ref") return String(expr.column ?? "");
|
|
330
|
+
if (expr.value !== void 0) return formatAstValue(expr);
|
|
331
|
+
return "check";
|
|
332
|
+
}
|
|
333
|
+
function extractConstraintColumnNames(definition) {
|
|
334
|
+
return extractDeepColumnNames(definition);
|
|
335
|
+
}
|
|
336
|
+
function extractDeepColumnNames(value) {
|
|
337
|
+
if (!Array.isArray(value)) return [];
|
|
338
|
+
return value.map((item) => {
|
|
339
|
+
if (typeof item !== "object" || item === null) return String(item ?? "unknown");
|
|
340
|
+
const object = item;
|
|
341
|
+
if (object.column !== void 0) return String(object.column);
|
|
342
|
+
if (object.expr) return extractDeepColumnName(object.expr);
|
|
343
|
+
return String(object.name ?? "unknown");
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function extractDeepColumnName(value) {
|
|
347
|
+
if (typeof value !== "object" || value === null) {
|
|
348
|
+
return String(value ?? "unknown");
|
|
349
|
+
}
|
|
350
|
+
const object = value;
|
|
351
|
+
if (object.expr) return extractDeepColumnName(object.expr);
|
|
352
|
+
if (object.column && typeof object.column === "object") {
|
|
353
|
+
return extractDeepColumnName(object.column);
|
|
354
|
+
}
|
|
355
|
+
if (object.column !== void 0) return String(object.column);
|
|
356
|
+
return String(object.name ?? "unknown");
|
|
357
|
+
}
|
|
358
|
+
function stringifyCheckDefinition(definition) {
|
|
359
|
+
if (!Array.isArray(definition) || definition.length === 0) return void 0;
|
|
360
|
+
if (definition.length === 1) {
|
|
361
|
+
return stringifyExpression(definition[0]);
|
|
362
|
+
}
|
|
363
|
+
return definition.map((item) => stringifyExpression(item)).filter(Boolean).join("; ");
|
|
364
|
+
}
|
|
365
|
+
|
|
146
366
|
// src/parsers/sql/sql-normalizer.ts
|
|
147
367
|
function normalizeSqlAst(ast, dialect) {
|
|
148
368
|
const statements = Array.isArray(ast) ? ast : [ast];
|
|
@@ -164,7 +384,11 @@ function normalizeSqlAst(ast, dialect) {
|
|
|
164
384
|
}
|
|
165
385
|
for (const index of indexes) {
|
|
166
386
|
const table = tables.find((candidate) => candidate.name === index.table);
|
|
167
|
-
table
|
|
387
|
+
if (!table) continue;
|
|
388
|
+
table.indexes.push(index);
|
|
389
|
+
if (index.unique) {
|
|
390
|
+
markColumnsUnique(table, index.columns, `INDEX ${index.name}`);
|
|
391
|
+
}
|
|
168
392
|
}
|
|
169
393
|
return {
|
|
170
394
|
dialect,
|
|
@@ -186,31 +410,49 @@ function normalizeCreateTable(statement) {
|
|
|
186
410
|
reviewTodos: []
|
|
187
411
|
};
|
|
188
412
|
for (const definition of createDefinitions) {
|
|
189
|
-
if (definition.resource
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
413
|
+
if (definition.resource !== "column") continue;
|
|
414
|
+
const columnName = extractDeepColumnName2(definition.column);
|
|
415
|
+
const isPrimaryKey = hasPrimaryKey(definition);
|
|
416
|
+
const isNotNull = hasNotNull(definition);
|
|
417
|
+
const { type, size } = normalizeColumnType(definition.definition);
|
|
418
|
+
const check = definition.check;
|
|
419
|
+
const bounds = check?.definition ? extractCheckBounds(check.definition, columnName) : {};
|
|
420
|
+
const constraintNotes = extractColumnConstraintNotes(definition);
|
|
421
|
+
if (check?.definition) {
|
|
422
|
+
const expression = stringifyCheckDefinition(check.definition);
|
|
423
|
+
if (expression && (!bounds.minValue || !bounds.maxValue)) {
|
|
424
|
+
constraintNotes.push(`CHECK: ${expression}`);
|
|
425
|
+
}
|
|
202
426
|
}
|
|
203
|
-
|
|
204
|
-
|
|
427
|
+
table.columns.push({
|
|
428
|
+
name: columnName,
|
|
429
|
+
type,
|
|
430
|
+
size,
|
|
431
|
+
nullable: !isNotNull && !isPrimaryKey,
|
|
432
|
+
defaultValue: extractDefaultFromDef(definition),
|
|
433
|
+
minValue: bounds.minValue,
|
|
434
|
+
maxValue: bounds.maxValue,
|
|
435
|
+
isUnique: hasColumnUnique(definition),
|
|
436
|
+
isPrimaryKey,
|
|
437
|
+
isForeignKey: false,
|
|
438
|
+
comment: extractColumnComment(definition),
|
|
439
|
+
constraintNotes: constraintNotes.length > 0 ? constraintNotes : void 0
|
|
440
|
+
});
|
|
441
|
+
if (isPrimaryKey) table.primaryKeys.push(columnName);
|
|
442
|
+
}
|
|
443
|
+
for (const definition of createDefinitions) {
|
|
444
|
+
if (definition.resource !== "constraint") continue;
|
|
445
|
+
if (isConstraintType(definition.constraint_type, "PRIMARY KEY")) {
|
|
446
|
+
table.primaryKeys = extractDeepColumnNames2(definition.definition);
|
|
205
447
|
for (const column of table.columns) {
|
|
206
448
|
if (table.primaryKeys.includes(column.name)) column.isPrimaryKey = true;
|
|
207
449
|
}
|
|
208
450
|
}
|
|
209
|
-
if (
|
|
210
|
-
const columns =
|
|
451
|
+
if (isConstraintType(definition.constraint_type, "FOREIGN KEY")) {
|
|
452
|
+
const columns = extractDeepColumnNames2(definition.definition);
|
|
211
453
|
const refDef = definition.reference_definition;
|
|
212
454
|
const referencedTable = extractTableName(refDef?.table);
|
|
213
|
-
const referencedColumns =
|
|
455
|
+
const referencedColumns = extractDeepColumnNames2(refDef?.definition);
|
|
214
456
|
table.foreignKeys.push({
|
|
215
457
|
name: typeof definition.constraint === "string" ? definition.constraint : void 0,
|
|
216
458
|
columns,
|
|
@@ -221,14 +463,71 @@ function normalizeCreateTable(statement) {
|
|
|
221
463
|
if (columns.includes(column.name)) column.isForeignKey = true;
|
|
222
464
|
}
|
|
223
465
|
}
|
|
466
|
+
if (isConstraintType(definition.constraint_type, "UNIQUE")) {
|
|
467
|
+
const columns = extractConstraintColumnNames(definition.definition);
|
|
468
|
+
const label = typeof definition.constraint === "string" ? definition.constraint : "UNIQUE";
|
|
469
|
+
markColumnsUnique(table, columns, label);
|
|
470
|
+
}
|
|
471
|
+
if (isConstraintType(definition.constraint_type, "CHECK")) {
|
|
472
|
+
applyTableCheckConstraint(table, definition);
|
|
473
|
+
}
|
|
224
474
|
}
|
|
225
475
|
return table;
|
|
226
476
|
}
|
|
477
|
+
function markColumnsUnique(table, columns, label) {
|
|
478
|
+
const composite = columns.length > 1;
|
|
479
|
+
for (const columnName of columns) {
|
|
480
|
+
const column = table.columns.find((item) => item.name === columnName);
|
|
481
|
+
if (!column) continue;
|
|
482
|
+
column.isUnique = true;
|
|
483
|
+
if (composite) {
|
|
484
|
+
addConstraintNote(
|
|
485
|
+
column,
|
|
486
|
+
`UNIQUE (${label}: ${columns.join(", ")})`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function applyTableCheckConstraint(table, definition) {
|
|
492
|
+
const expression = stringifyCheckDefinition(definition.definition);
|
|
493
|
+
if (!expression) return;
|
|
494
|
+
const referencedColumns = /* @__PURE__ */ new Set();
|
|
495
|
+
for (const column of table.columns) {
|
|
496
|
+
const bounds = extractCheckBounds(definition.definition, column.name);
|
|
497
|
+
if (bounds.minValue) column.minValue = bounds.minValue;
|
|
498
|
+
if (bounds.maxValue) column.maxValue = bounds.maxValue;
|
|
499
|
+
if (bounds.minValue || bounds.maxValue) {
|
|
500
|
+
referencedColumns.add(column.name);
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (expression.includes(column.name)) {
|
|
504
|
+
referencedColumns.add(column.name);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (referencedColumns.size === 0) {
|
|
508
|
+
for (const column of table.columns) {
|
|
509
|
+
addConstraintNote(column, `CHECK: ${expression}`);
|
|
510
|
+
}
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
for (const columnName of referencedColumns) {
|
|
514
|
+
const column = table.columns.find((item) => item.name === columnName);
|
|
515
|
+
if (!column) continue;
|
|
516
|
+
if (!column.minValue && !column.maxValue) {
|
|
517
|
+
addConstraintNote(column, `CHECK: ${expression}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function addConstraintNote(column, note) {
|
|
522
|
+
const notes = column.constraintNotes ?? [];
|
|
523
|
+
if (!notes.includes(note)) notes.push(note);
|
|
524
|
+
column.constraintNotes = notes;
|
|
525
|
+
}
|
|
227
526
|
function normalizeCreateIndex(statement) {
|
|
228
527
|
return {
|
|
229
528
|
name: String(statement.index ?? statement.index_name ?? "unnamed_index"),
|
|
230
529
|
table: extractTableName(statement.table),
|
|
231
|
-
columns:
|
|
530
|
+
columns: extractDeepColumnNames2(
|
|
232
531
|
statement.index_columns ?? statement.columns
|
|
233
532
|
),
|
|
234
533
|
unique: Boolean(statement.unique)
|
|
@@ -282,15 +581,15 @@ function extractTableName(value) {
|
|
|
282
581
|
}
|
|
283
582
|
return String(value ?? "unknown");
|
|
284
583
|
}
|
|
285
|
-
function
|
|
584
|
+
function extractDeepColumnName2(value) {
|
|
286
585
|
if (typeof value !== "object" || value === null)
|
|
287
586
|
return String(value ?? "unknown");
|
|
288
587
|
const object = value;
|
|
289
588
|
if (object.expr && typeof object.expr === "object") {
|
|
290
|
-
return
|
|
589
|
+
return extractDeepColumnName2(object.expr);
|
|
291
590
|
}
|
|
292
591
|
if (object.column && typeof object.column === "object") {
|
|
293
|
-
return
|
|
592
|
+
return extractDeepColumnName2(object.column);
|
|
294
593
|
}
|
|
295
594
|
if (object.value !== void 0) {
|
|
296
595
|
return String(object.value);
|
|
@@ -300,18 +599,9 @@ function extractDeepColumnName(value) {
|
|
|
300
599
|
}
|
|
301
600
|
return String(object.name ?? object.tableName ?? "unknown");
|
|
302
601
|
}
|
|
303
|
-
function
|
|
602
|
+
function extractDeepColumnNames2(value) {
|
|
304
603
|
if (!Array.isArray(value)) return [];
|
|
305
|
-
return value.map((item) =>
|
|
306
|
-
}
|
|
307
|
-
function normalizeType(value) {
|
|
308
|
-
if (typeof value === "object" && value !== null) {
|
|
309
|
-
const object = value;
|
|
310
|
-
return String(
|
|
311
|
-
object.dataType ?? object.type ?? object.name ?? "unknown"
|
|
312
|
-
).toLowerCase();
|
|
313
|
-
}
|
|
314
|
-
return String(value ?? "unknown").toLowerCase();
|
|
604
|
+
return value.map((item) => extractDeepColumnName2(item));
|
|
315
605
|
}
|
|
316
606
|
function hasPrimaryKey(def) {
|
|
317
607
|
if (def.primary_key) return true;
|
|
@@ -506,8 +796,8 @@ function dialectBias(dialect, requestedDialect, detectedDialect) {
|
|
|
506
796
|
}
|
|
507
797
|
|
|
508
798
|
// src/exporters/excel/excel-exporter.ts
|
|
509
|
-
import { mkdir } from "fs/promises";
|
|
510
|
-
import { join } from "path";
|
|
799
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
800
|
+
import { join as join2 } from "path";
|
|
511
801
|
import ExcelJS from "exceljs";
|
|
512
802
|
|
|
513
803
|
// src/exporters/shared/output-labels.ts
|
|
@@ -530,6 +820,10 @@ var LABELS = {
|
|
|
530
820
|
type: "Type",
|
|
531
821
|
required: "Required",
|
|
532
822
|
defaultValue: "Default Value",
|
|
823
|
+
size: "Size",
|
|
824
|
+
minValue: "Min",
|
|
825
|
+
maxValue: "Max",
|
|
826
|
+
unique: "Unique",
|
|
533
827
|
notes: "Notes",
|
|
534
828
|
yes: "Yes",
|
|
535
829
|
no: "No",
|
|
@@ -558,7 +852,15 @@ var LABELS = {
|
|
|
558
852
|
rowNo: "#",
|
|
559
853
|
backToOverview: "\u2190 Overview",
|
|
560
854
|
pkMarker: "PK",
|
|
561
|
-
fkMarker: "FK"
|
|
855
|
+
fkMarker: "FK",
|
|
856
|
+
erDiagramHeading: "ER Diagram",
|
|
857
|
+
erDiagramSheet: "ER Diagram",
|
|
858
|
+
viewErDiagram: "View interactive ER diagram (html/er-diagram.html)",
|
|
859
|
+
zoomIn: "Zoom in",
|
|
860
|
+
zoomOut: "Zoom out",
|
|
861
|
+
zoomReset: "Reset",
|
|
862
|
+
zoomFit: "Fit",
|
|
863
|
+
panZoomHint: "Drag to pan \xB7 Scroll to zoom"
|
|
562
864
|
},
|
|
563
865
|
jp: {
|
|
564
866
|
docTitle: "Database Documentation",
|
|
@@ -578,6 +880,10 @@ var LABELS = {
|
|
|
578
880
|
type: "\u578B",
|
|
579
881
|
required: "\u5FC5\u9808",
|
|
580
882
|
defaultValue: "\u30C7\u30D5\u30A9\u30EB\u30C8\u5024",
|
|
883
|
+
size: "\u6841\u6570",
|
|
884
|
+
minValue: "\u6700\u5C0F\u5024",
|
|
885
|
+
maxValue: "\u6700\u5927\u5024",
|
|
886
|
+
unique: "\u4E00\u610F",
|
|
581
887
|
notes: "\u5099\u8003",
|
|
582
888
|
yes: "Yes",
|
|
583
889
|
no: "No",
|
|
@@ -606,13 +912,582 @@ var LABELS = {
|
|
|
606
912
|
rowNo: "No.",
|
|
607
913
|
backToOverview: "\u2190 \u4E00\u89A7",
|
|
608
914
|
pkMarker: "PK",
|
|
609
|
-
fkMarker: "FK"
|
|
915
|
+
fkMarker: "FK",
|
|
916
|
+
erDiagramHeading: "ER Diagram",
|
|
917
|
+
erDiagramSheet: "ER Diagram",
|
|
918
|
+
viewErDiagram: "\u30A4\u30F3\u30BF\u30E9\u30AF\u30C6\u30A3\u30D6ER\u56F3 (html/er-diagram.html)",
|
|
919
|
+
zoomIn: "\u62E1\u5927",
|
|
920
|
+
zoomOut: "\u7E2E\u5C0F",
|
|
921
|
+
zoomReset: "\u30EA\u30BB\u30C3\u30C8",
|
|
922
|
+
zoomFit: "\u5168\u4F53\u8868\u793A",
|
|
923
|
+
panZoomHint: "\u30C9\u30E9\u30C3\u30B0\u3067\u79FB\u52D5 \xB7 \u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u62E1\u5927\u7E2E\u5C0F"
|
|
610
924
|
}
|
|
611
925
|
};
|
|
612
926
|
function getOutputLabels(language = "en") {
|
|
613
927
|
return LABELS[language];
|
|
614
928
|
}
|
|
615
929
|
|
|
930
|
+
// src/exporters/shared/column-definition.ts
|
|
931
|
+
var A5_COLUMN_COUNT = 10;
|
|
932
|
+
function columnDefinitionHeaders(labels) {
|
|
933
|
+
return [
|
|
934
|
+
labels.physicalName,
|
|
935
|
+
labels.logicalName,
|
|
936
|
+
labels.type,
|
|
937
|
+
labels.size,
|
|
938
|
+
labels.required,
|
|
939
|
+
labels.defaultValue,
|
|
940
|
+
labels.minValue,
|
|
941
|
+
labels.maxValue,
|
|
942
|
+
labels.unique,
|
|
943
|
+
labels.notes
|
|
944
|
+
];
|
|
945
|
+
}
|
|
946
|
+
function formatColumnNotes(column, labels) {
|
|
947
|
+
const parts = [];
|
|
948
|
+
if (column.isPrimaryKey) parts.push(labels.pkMarker);
|
|
949
|
+
if (column.isForeignKey) parts.push(labels.fkMarker);
|
|
950
|
+
if (column.constraintNotes?.length) parts.push(...column.constraintNotes);
|
|
951
|
+
if (column.description?.value) parts.push(column.description.value);
|
|
952
|
+
return parts.join(", ") || labels.none;
|
|
953
|
+
}
|
|
954
|
+
function columnDefinitionRow(column, labels) {
|
|
955
|
+
return [
|
|
956
|
+
column.name,
|
|
957
|
+
column.comment ?? "",
|
|
958
|
+
column.type,
|
|
959
|
+
column.size ?? labels.none,
|
|
960
|
+
column.nullable ? labels.no : labels.yes,
|
|
961
|
+
column.defaultValue ?? labels.none,
|
|
962
|
+
column.minValue ?? labels.none,
|
|
963
|
+
column.maxValue ?? labels.none,
|
|
964
|
+
column.isUnique ? labels.yes : labels.no,
|
|
965
|
+
formatColumnNotes(column, labels)
|
|
966
|
+
];
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/exporters/diagram/mermaid-exporter.ts
|
|
970
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
971
|
+
import { join } from "path";
|
|
972
|
+
async function exportMermaidDiagram(doc, options) {
|
|
973
|
+
await mkdir(options.outDir, { recursive: true });
|
|
974
|
+
await writeFile(
|
|
975
|
+
join(options.outDir, "er_diagram.mmd"),
|
|
976
|
+
renderMermaid(doc),
|
|
977
|
+
"utf8"
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
function renderMermaid(doc) {
|
|
981
|
+
const lines = ["erDiagram"];
|
|
982
|
+
for (const warning of doc.warnings) {
|
|
983
|
+
const target = warning.target ? ` (${warning.target})` : "";
|
|
984
|
+
lines.push(
|
|
985
|
+
` %% WARNING [${warning.severity}] ${warning.code}${target}: ${warning.message}`
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
for (const table of doc.tables) {
|
|
989
|
+
for (const todo of table.reviewTodos) {
|
|
990
|
+
lines.push(` %% TODO [${todo.type}] ${todo.target}: ${todo.issue}`);
|
|
991
|
+
}
|
|
992
|
+
lines.push(` ${table.name} {`);
|
|
993
|
+
for (const column of table.columns) {
|
|
994
|
+
const markers = [
|
|
995
|
+
column.isPrimaryKey ? "PK" : "",
|
|
996
|
+
column.isForeignKey ? "FK" : ""
|
|
997
|
+
].filter(Boolean).join(" ");
|
|
998
|
+
lines.push(
|
|
999
|
+
` ${sanitizeType(column.type)} ${column.name}${markers ? ` "${markers}"` : ""}`
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
lines.push(" }");
|
|
1003
|
+
}
|
|
1004
|
+
for (const relationship of doc.relationships.filter(
|
|
1005
|
+
(item) => item.source === "schema"
|
|
1006
|
+
)) {
|
|
1007
|
+
lines.push(
|
|
1008
|
+
` ${relationship.toTable} ||--o{ ${relationship.fromTable} : "${relationship.constraintName ?? relationship.fromColumn}"`
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
return `${lines.join("\n")}
|
|
1012
|
+
`;
|
|
1013
|
+
}
|
|
1014
|
+
function sanitizeType(type) {
|
|
1015
|
+
return type.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// src/exporters/diagram/er-diagram-embed.ts
|
|
1019
|
+
function getErDiagramMermaid(doc) {
|
|
1020
|
+
return renderMermaid(doc);
|
|
1021
|
+
}
|
|
1022
|
+
function renderErDiagramHtmlPage(mermaidSource, labels) {
|
|
1023
|
+
const escaped = mermaidSource.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1024
|
+
return `<!DOCTYPE html>
|
|
1025
|
+
<html lang="en">
|
|
1026
|
+
<head>
|
|
1027
|
+
<meta charset="UTF-8">
|
|
1028
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1029
|
+
<title>${esc(labels.erDiagramHeading)}</title>
|
|
1030
|
+
<style>
|
|
1031
|
+
body { margin: 0; font-family: "Yu Gothic UI", "Meiryo", Arial, sans-serif; background: #f3f4f6; }
|
|
1032
|
+
.toolbar {
|
|
1033
|
+
background: #4472c4; color: #fff; padding: 10px 16px;
|
|
1034
|
+
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
|
1035
|
+
}
|
|
1036
|
+
.toolbar a { color: #fff; text-decoration: underline; }
|
|
1037
|
+
.toolbar .spacer { flex: 1; }
|
|
1038
|
+
.toolbar .hint { opacity: 0.9; font-size: 13px; }
|
|
1039
|
+
.toolbar button {
|
|
1040
|
+
background: #fff; color: #2f5597; border: none; border-radius: 4px;
|
|
1041
|
+
padding: 6px 12px; font-size: 13px; cursor: pointer; font-weight: 600;
|
|
1042
|
+
}
|
|
1043
|
+
.toolbar button:hover { background: #e8eef8; }
|
|
1044
|
+
.viewport {
|
|
1045
|
+
position: relative; height: calc(100vh - 52px); margin: 12px;
|
|
1046
|
+
background: #fff; border: 1px solid #bfc7d4; border-radius: 4px;
|
|
1047
|
+
overflow: hidden; cursor: grab; touch-action: none;
|
|
1048
|
+
}
|
|
1049
|
+
.viewport.dragging { cursor: grabbing; }
|
|
1050
|
+
.canvas {
|
|
1051
|
+
position: absolute; left: 0; top: 0; transform-origin: 0 0;
|
|
1052
|
+
padding: 24px;
|
|
1053
|
+
}
|
|
1054
|
+
.mermaid { min-width: 320px; }
|
|
1055
|
+
.mermaid svg { max-width: none !important; height: auto !important; }
|
|
1056
|
+
</style>
|
|
1057
|
+
</head>
|
|
1058
|
+
<body>
|
|
1059
|
+
<div class="toolbar">
|
|
1060
|
+
<strong>${esc(labels.erDiagramHeading)}</strong>
|
|
1061
|
+
<a href="index.html">\u2190 ${esc(labels.tableListHeading)}</a>
|
|
1062
|
+
<span class="spacer"></span>
|
|
1063
|
+
<span class="hint">${esc(labels.panZoomHint)}</span>
|
|
1064
|
+
<button type="button" id="zoom-out" title="${esc(labels.zoomOut)}">\u2212</button>
|
|
1065
|
+
<button type="button" id="zoom-reset" title="${esc(labels.zoomReset)}">${esc(labels.zoomReset)}</button>
|
|
1066
|
+
<button type="button" id="zoom-in" title="${esc(labels.zoomIn)}">+</button>
|
|
1067
|
+
<button type="button" id="zoom-fit" title="${esc(labels.zoomFit)}">${esc(labels.zoomFit)}</button>
|
|
1068
|
+
</div>
|
|
1069
|
+
<div class="viewport" id="viewport">
|
|
1070
|
+
<div class="canvas" id="canvas">
|
|
1071
|
+
<pre class="mermaid">${escaped}</pre>
|
|
1072
|
+
</div>
|
|
1073
|
+
</div>
|
|
1074
|
+
<script type="module">
|
|
1075
|
+
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
|
|
1076
|
+
|
|
1077
|
+
mermaid.initialize({
|
|
1078
|
+
startOnLoad: false,
|
|
1079
|
+
theme: "default",
|
|
1080
|
+
er: { useMaxWidth: false }
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
await mermaid.run({ querySelector: ".mermaid" });
|
|
1084
|
+
setupPanZoom(
|
|
1085
|
+
document.getElementById("viewport"),
|
|
1086
|
+
document.getElementById("canvas")
|
|
1087
|
+
);
|
|
1088
|
+
|
|
1089
|
+
function setupPanZoom(viewport, canvas) {
|
|
1090
|
+
let scale = 1;
|
|
1091
|
+
let tx = 40;
|
|
1092
|
+
let ty = 40;
|
|
1093
|
+
let dragging = false;
|
|
1094
|
+
let lastX = 0;
|
|
1095
|
+
let lastY = 0;
|
|
1096
|
+
|
|
1097
|
+
function apply() {
|
|
1098
|
+
canvas.style.transform = "translate(" + tx + "px," + ty + "px) scale(" + scale + ")";
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function zoomAt(factor, cx, cy) {
|
|
1102
|
+
const next = Math.min(4, Math.max(0.15, scale * factor));
|
|
1103
|
+
const ratio = next / scale;
|
|
1104
|
+
tx = cx - (cx - tx) * ratio;
|
|
1105
|
+
ty = cy - (cy - ty) * ratio;
|
|
1106
|
+
scale = next;
|
|
1107
|
+
apply();
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function fitToView() {
|
|
1111
|
+
const svg = canvas.querySelector("svg");
|
|
1112
|
+
if (!svg) return;
|
|
1113
|
+
const box = svg.getBBox();
|
|
1114
|
+
const pad = 32;
|
|
1115
|
+
const vw = viewport.clientWidth;
|
|
1116
|
+
const vh = viewport.clientHeight;
|
|
1117
|
+
scale = Math.min(
|
|
1118
|
+
(vw - pad * 2) / Math.max(box.width, 1),
|
|
1119
|
+
(vh - pad * 2) / Math.max(box.height, 1),
|
|
1120
|
+
1.5
|
|
1121
|
+
);
|
|
1122
|
+
tx = (vw - box.width * scale) / 2 - box.x * scale;
|
|
1123
|
+
ty = (vh - box.height * scale) / 2 - box.y * scale;
|
|
1124
|
+
apply();
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
viewport.addEventListener("wheel", (e) => {
|
|
1128
|
+
e.preventDefault();
|
|
1129
|
+
const rect = viewport.getBoundingClientRect();
|
|
1130
|
+
const cx = e.clientX - rect.left;
|
|
1131
|
+
const cy = e.clientY - rect.top;
|
|
1132
|
+
zoomAt(e.deltaY < 0 ? 1.12 : 0.89, cx, cy);
|
|
1133
|
+
}, { passive: false });
|
|
1134
|
+
|
|
1135
|
+
viewport.addEventListener("mousedown", (e) => {
|
|
1136
|
+
if (e.button !== 0) return;
|
|
1137
|
+
dragging = true;
|
|
1138
|
+
lastX = e.clientX;
|
|
1139
|
+
lastY = e.clientY;
|
|
1140
|
+
viewport.classList.add("dragging");
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
window.addEventListener("mousemove", (e) => {
|
|
1144
|
+
if (!dragging) return;
|
|
1145
|
+
tx += e.clientX - lastX;
|
|
1146
|
+
ty += e.clientY - lastY;
|
|
1147
|
+
lastX = e.clientX;
|
|
1148
|
+
lastY = e.clientY;
|
|
1149
|
+
apply();
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
window.addEventListener("mouseup", () => {
|
|
1153
|
+
dragging = false;
|
|
1154
|
+
viewport.classList.remove("dragging");
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
document.getElementById("zoom-in").addEventListener("click", () => {
|
|
1158
|
+
zoomAt(1.2, viewport.clientWidth / 2, viewport.clientHeight / 2);
|
|
1159
|
+
});
|
|
1160
|
+
document.getElementById("zoom-out").addEventListener("click", () => {
|
|
1161
|
+
zoomAt(1 / 1.2, viewport.clientWidth / 2, viewport.clientHeight / 2);
|
|
1162
|
+
});
|
|
1163
|
+
document.getElementById("zoom-reset").addEventListener("click", () => {
|
|
1164
|
+
scale = 1;
|
|
1165
|
+
tx = 40;
|
|
1166
|
+
ty = 40;
|
|
1167
|
+
apply();
|
|
1168
|
+
});
|
|
1169
|
+
document.getElementById("zoom-fit").addEventListener("click", fitToView);
|
|
1170
|
+
|
|
1171
|
+
fitToView();
|
|
1172
|
+
}
|
|
1173
|
+
</script>
|
|
1174
|
+
</body>
|
|
1175
|
+
</html>`;
|
|
1176
|
+
}
|
|
1177
|
+
function renderErDiagramMarkdown(mermaidSource, labels) {
|
|
1178
|
+
return [
|
|
1179
|
+
`# ${labels.erDiagramHeading}`,
|
|
1180
|
+
"",
|
|
1181
|
+
"```mermaid",
|
|
1182
|
+
mermaidSource.trimEnd(),
|
|
1183
|
+
"```",
|
|
1184
|
+
""
|
|
1185
|
+
].join("\n");
|
|
1186
|
+
}
|
|
1187
|
+
function esc(text) {
|
|
1188
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// src/exporters/diagram/er-diagram-layout.ts
|
|
1192
|
+
import ELK from "elkjs/lib/elk.bundled.js";
|
|
1193
|
+
var BOX_W = 200;
|
|
1194
|
+
var HEADER_H = 28;
|
|
1195
|
+
var LINE_H = 14;
|
|
1196
|
+
var MAX_COLS_SHOWN = 6;
|
|
1197
|
+
var COMPACT_THRESHOLD = 18;
|
|
1198
|
+
var PAD = 24;
|
|
1199
|
+
var CLUSTER_GAP = 56;
|
|
1200
|
+
function isCompactLayout(tableCount) {
|
|
1201
|
+
return tableCount >= COMPACT_THRESHOLD;
|
|
1202
|
+
}
|
|
1203
|
+
function getVisibleErColumns(table) {
|
|
1204
|
+
const prioritized = [
|
|
1205
|
+
...table.columns.filter((column) => column.isPrimaryKey),
|
|
1206
|
+
...table.columns.filter(
|
|
1207
|
+
(column) => column.isForeignKey && !column.isPrimaryKey
|
|
1208
|
+
),
|
|
1209
|
+
...table.columns.filter(
|
|
1210
|
+
(column) => !column.isPrimaryKey && !column.isForeignKey
|
|
1211
|
+
)
|
|
1212
|
+
];
|
|
1213
|
+
const unique = prioritized.filter(
|
|
1214
|
+
(column, index, columns) => columns.findIndex((item) => item.name === column.name) === index
|
|
1215
|
+
);
|
|
1216
|
+
return unique.slice(0, MAX_COLS_SHOWN);
|
|
1217
|
+
}
|
|
1218
|
+
function measureTableBox(table, _compact = false) {
|
|
1219
|
+
const visible = getVisibleErColumns(table);
|
|
1220
|
+
const extra = table.columns.length > visible.length ? 1 : 0;
|
|
1221
|
+
return {
|
|
1222
|
+
w: BOX_W,
|
|
1223
|
+
h: HEADER_H + (visible.length + extra) * LINE_H + 8
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
function buildAdjacency(doc) {
|
|
1227
|
+
const names = new Set(doc.tables.map((t) => t.name));
|
|
1228
|
+
const adj = /* @__PURE__ */ new Map();
|
|
1229
|
+
for (const name of names) adj.set(name, /* @__PURE__ */ new Set());
|
|
1230
|
+
for (const rel of doc.relationships.filter((r) => r.source === "schema")) {
|
|
1231
|
+
if (!names.has(rel.fromTable) || !names.has(rel.toTable)) continue;
|
|
1232
|
+
adj.get(rel.fromTable).add(rel.toTable);
|
|
1233
|
+
adj.get(rel.toTable).add(rel.fromTable);
|
|
1234
|
+
}
|
|
1235
|
+
return adj;
|
|
1236
|
+
}
|
|
1237
|
+
function connectedComponents(tableNames, adj) {
|
|
1238
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1239
|
+
const components = [];
|
|
1240
|
+
for (const name of tableNames) {
|
|
1241
|
+
if (visited.has(name)) continue;
|
|
1242
|
+
const stack = [name];
|
|
1243
|
+
const component = [];
|
|
1244
|
+
visited.add(name);
|
|
1245
|
+
while (stack.length > 0) {
|
|
1246
|
+
const current = stack.pop();
|
|
1247
|
+
component.push(current);
|
|
1248
|
+
for (const next of adj.get(current) ?? []) {
|
|
1249
|
+
if (!visited.has(next)) {
|
|
1250
|
+
visited.add(next);
|
|
1251
|
+
stack.push(next);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
component.sort();
|
|
1256
|
+
components.push(component);
|
|
1257
|
+
}
|
|
1258
|
+
return components.sort((a, b) => b.length - a.length);
|
|
1259
|
+
}
|
|
1260
|
+
function sectionToPoints(section) {
|
|
1261
|
+
return [section.startPoint, ...section.bendPoints ?? [], section.endPoint];
|
|
1262
|
+
}
|
|
1263
|
+
function extractEdges(layouted) {
|
|
1264
|
+
const edges = [];
|
|
1265
|
+
for (const edge of layouted.edges ?? []) {
|
|
1266
|
+
for (const section of edge.sections ?? []) {
|
|
1267
|
+
edges.push({
|
|
1268
|
+
id: edge.id,
|
|
1269
|
+
points: sectionToPoints(section)
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
return edges;
|
|
1274
|
+
}
|
|
1275
|
+
async function layoutComponent(doc, tableNames, compact) {
|
|
1276
|
+
const elk = new ELK();
|
|
1277
|
+
const nameSet = new Set(tableNames);
|
|
1278
|
+
const tables = doc.tables.filter((t) => nameSet.has(t.name));
|
|
1279
|
+
const direction = tables.length >= 8 ? "DOWN" : "RIGHT";
|
|
1280
|
+
const children = tables.map((table) => {
|
|
1281
|
+
const { w, h } = measureTableBox(table, compact);
|
|
1282
|
+
return { id: table.name, width: w, height: h };
|
|
1283
|
+
});
|
|
1284
|
+
const edges = [];
|
|
1285
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1286
|
+
for (const rel of doc.relationships.filter((r) => r.source === "schema")) {
|
|
1287
|
+
if (!nameSet.has(rel.fromTable) || !nameSet.has(rel.toTable)) continue;
|
|
1288
|
+
const key = `${rel.fromTable}->${rel.toTable}`;
|
|
1289
|
+
if (seen.has(key)) continue;
|
|
1290
|
+
seen.add(key);
|
|
1291
|
+
edges.push({
|
|
1292
|
+
id: key,
|
|
1293
|
+
sources: [rel.fromTable],
|
|
1294
|
+
targets: [rel.toTable]
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
const graph = {
|
|
1298
|
+
id: "root",
|
|
1299
|
+
layoutOptions: {
|
|
1300
|
+
"elk.algorithm": "layered",
|
|
1301
|
+
"elk.direction": direction,
|
|
1302
|
+
"elk.edgeRouting": "ORTHOGONAL",
|
|
1303
|
+
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
|
|
1304
|
+
"elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
|
|
1305
|
+
"elk.layered.spacing.nodeNodeBetweenLayers": compact ? "56" : "80",
|
|
1306
|
+
"elk.spacing.nodeNode": compact ? "32" : "48",
|
|
1307
|
+
"elk.spacing.edgeNode": "24",
|
|
1308
|
+
"elk.padding": `[top=${PAD},left=${PAD},bottom=${PAD},right=${PAD}]`
|
|
1309
|
+
},
|
|
1310
|
+
children,
|
|
1311
|
+
edges
|
|
1312
|
+
};
|
|
1313
|
+
const layouted = await elk.layout(graph);
|
|
1314
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
1315
|
+
let minX = Infinity;
|
|
1316
|
+
let minY = Infinity;
|
|
1317
|
+
let maxX = -Infinity;
|
|
1318
|
+
let maxY = -Infinity;
|
|
1319
|
+
for (const child of layouted.children ?? []) {
|
|
1320
|
+
const table = tables.find((t) => t.name === child.id);
|
|
1321
|
+
if (!table) continue;
|
|
1322
|
+
const { w, h } = measureTableBox(table, compact);
|
|
1323
|
+
const x = child.x ?? 0;
|
|
1324
|
+
const y = child.y ?? 0;
|
|
1325
|
+
const box = { x, y, w, h };
|
|
1326
|
+
boxes.set(child.id, box);
|
|
1327
|
+
minX = Math.min(minX, x);
|
|
1328
|
+
minY = Math.min(minY, y);
|
|
1329
|
+
maxX = Math.max(maxX, x + w);
|
|
1330
|
+
maxY = Math.max(maxY, y + h);
|
|
1331
|
+
}
|
|
1332
|
+
const width = Math.ceil(maxX - minX + PAD * 2);
|
|
1333
|
+
const height = Math.ceil(maxY - minY + PAD * 2);
|
|
1334
|
+
const dx = PAD - minX;
|
|
1335
|
+
const dy = PAD - minY;
|
|
1336
|
+
for (const [name, box] of boxes) {
|
|
1337
|
+
boxes.set(name, { x: box.x + dx, y: box.y + dy, w: box.w, h: box.h });
|
|
1338
|
+
}
|
|
1339
|
+
const shiftedEdges = extractEdges(layouted).map((edge) => ({
|
|
1340
|
+
...edge,
|
|
1341
|
+
points: edge.points.map((p) => ({ x: p.x + dx, y: p.y + dy }))
|
|
1342
|
+
}));
|
|
1343
|
+
return { boxes, edges: shiftedEdges, width, height };
|
|
1344
|
+
}
|
|
1345
|
+
function shiftLayout(boxes, edges, offsetX, offsetY) {
|
|
1346
|
+
for (const [name, box] of boxes) {
|
|
1347
|
+
boxes.set(name, { ...box, x: box.x + offsetX, y: box.y + offsetY });
|
|
1348
|
+
}
|
|
1349
|
+
for (const edge of edges) {
|
|
1350
|
+
for (const p of edge.points) {
|
|
1351
|
+
p.x += offsetX;
|
|
1352
|
+
p.y += offsetY;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
async function layoutErDiagram(doc) {
|
|
1357
|
+
const tables = doc.tables;
|
|
1358
|
+
if (tables.length === 0) {
|
|
1359
|
+
return {
|
|
1360
|
+
boxes: /* @__PURE__ */ new Map(),
|
|
1361
|
+
edges: [],
|
|
1362
|
+
compact: false,
|
|
1363
|
+
width: 400,
|
|
1364
|
+
height: 80
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
const compact = isCompactLayout(tables.length);
|
|
1368
|
+
const adj = buildAdjacency(doc);
|
|
1369
|
+
const components = connectedComponents(
|
|
1370
|
+
tables.map((t) => t.name),
|
|
1371
|
+
adj
|
|
1372
|
+
);
|
|
1373
|
+
const mergedBoxes = /* @__PURE__ */ new Map();
|
|
1374
|
+
const mergedEdges = [];
|
|
1375
|
+
const clusterCols = components.length <= 1 ? 1 : components.length <= 4 ? 2 : 3;
|
|
1376
|
+
let tileX = 0;
|
|
1377
|
+
let tileY = 0;
|
|
1378
|
+
let rowHeight = 0;
|
|
1379
|
+
let maxWidth = PAD;
|
|
1380
|
+
let maxHeight = PAD;
|
|
1381
|
+
for (const [i, component] of components.entries()) {
|
|
1382
|
+
const laid = await layoutComponent(doc, component, compact);
|
|
1383
|
+
shiftLayout(laid.boxes, laid.edges, tileX, tileY);
|
|
1384
|
+
for (const [name, box] of laid.boxes) mergedBoxes.set(name, box);
|
|
1385
|
+
mergedEdges.push(...laid.edges);
|
|
1386
|
+
rowHeight = Math.max(rowHeight, laid.height);
|
|
1387
|
+
tileX += laid.width + CLUSTER_GAP;
|
|
1388
|
+
maxWidth = Math.max(maxWidth, tileX);
|
|
1389
|
+
maxHeight = Math.max(maxHeight, tileY + laid.height);
|
|
1390
|
+
if ((i + 1) % clusterCols === 0) {
|
|
1391
|
+
tileX = 0;
|
|
1392
|
+
tileY += rowHeight + CLUSTER_GAP;
|
|
1393
|
+
rowHeight = 0;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return {
|
|
1397
|
+
boxes: mergedBoxes,
|
|
1398
|
+
edges: mergedEdges,
|
|
1399
|
+
compact,
|
|
1400
|
+
width: Math.ceil(maxWidth),
|
|
1401
|
+
height: Math.ceil(maxHeight + PAD)
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// src/exporters/diagram/er-diagram-svg.ts
|
|
1406
|
+
async function renderErDiagramSvg(doc) {
|
|
1407
|
+
const tables = doc.tables;
|
|
1408
|
+
if (tables.length === 0) {
|
|
1409
|
+
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>`;
|
|
1410
|
+
}
|
|
1411
|
+
const layout = await layoutErDiagram(doc);
|
|
1412
|
+
return buildErDiagramSvg(doc, layout);
|
|
1413
|
+
}
|
|
1414
|
+
async function renderErDiagramPng(doc) {
|
|
1415
|
+
const tables = doc.tables;
|
|
1416
|
+
if (tables.length === 0) {
|
|
1417
|
+
const svg2 = await renderErDiagramSvg(doc);
|
|
1418
|
+
const sharp2 = (await import("sharp")).default;
|
|
1419
|
+
const buffer2 = await sharp2(Buffer.from(svg2)).png().toBuffer();
|
|
1420
|
+
return { buffer: buffer2, width: 400, height: 80 };
|
|
1421
|
+
}
|
|
1422
|
+
const layout = await layoutErDiagram(doc);
|
|
1423
|
+
const svg = buildErDiagramSvg(doc, layout);
|
|
1424
|
+
const sharp = (await import("sharp")).default;
|
|
1425
|
+
const buffer = await sharp(Buffer.from(svg)).png().toBuffer();
|
|
1426
|
+
return { buffer, width: layout.width, height: layout.height };
|
|
1427
|
+
}
|
|
1428
|
+
function buildErDiagramSvg(doc, layout) {
|
|
1429
|
+
const { boxes, edges, width, height } = layout;
|
|
1430
|
+
const parts = [
|
|
1431
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" font-family="Arial,sans-serif" font-size="11">`,
|
|
1432
|
+
`<rect width="100%" height="100%" fill="#ffffff"/>`,
|
|
1433
|
+
`<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>`,
|
|
1434
|
+
`<g class="edges">`
|
|
1435
|
+
];
|
|
1436
|
+
for (const edge of edges) {
|
|
1437
|
+
if (edge.points.length < 2) continue;
|
|
1438
|
+
const d = pointsToPath(edge.points);
|
|
1439
|
+
parts.push(
|
|
1440
|
+
`<path d="${d}" fill="none" stroke="#7d96b8" stroke-width="1.25" marker-end="url(#arrow)"/>`
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
parts.push(`</g><g class="nodes">`);
|
|
1444
|
+
for (const table of doc.tables) {
|
|
1445
|
+
const box = boxes.get(table.name);
|
|
1446
|
+
parts.push(...renderTableBox(table, box));
|
|
1447
|
+
}
|
|
1448
|
+
parts.push("</g></svg>");
|
|
1449
|
+
return parts.join("");
|
|
1450
|
+
}
|
|
1451
|
+
function pointsToPath(points) {
|
|
1452
|
+
return points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ");
|
|
1453
|
+
}
|
|
1454
|
+
function renderTableBox(table, box) {
|
|
1455
|
+
const parts = [
|
|
1456
|
+
`<rect x="${box.x}" y="${box.y}" width="${box.w}" height="${box.h}" fill="#f8fafc" stroke="#4472c4" stroke-width="1.5" rx="4"/>`,
|
|
1457
|
+
`<rect x="${box.x}" y="${box.y}" width="${box.w}" height="${HEADER_H}" fill="#4472c4" rx="4"/>`,
|
|
1458
|
+
`<rect x="${box.x}" y="${box.y + HEADER_H - 4}" width="${box.w}" height="4" fill="#4472c4"/>`,
|
|
1459
|
+
`<text x="${box.x + 8}" y="${box.y + 18}" fill="#ffffff" font-weight="bold">${escapeXml(table.name)}</text>`
|
|
1460
|
+
];
|
|
1461
|
+
let cy = box.y + HEADER_H + 14;
|
|
1462
|
+
const visible = getVisibleErColumns(table);
|
|
1463
|
+
for (const col of visible) {
|
|
1464
|
+
const marker = col.isPrimaryKey ? " PK" : col.isForeignKey ? " FK" : "";
|
|
1465
|
+
parts.push(
|
|
1466
|
+
`<text x="${box.x + 8}" y="${cy}" fill="#333333">${escapeXml(col.name)} : ${escapeXml(shortType(col.type))}${marker}</text>`
|
|
1467
|
+
);
|
|
1468
|
+
cy += LINE_H;
|
|
1469
|
+
}
|
|
1470
|
+
if (table.columns.length > visible.length) {
|
|
1471
|
+
parts.push(
|
|
1472
|
+
`<text x="${box.x + 8}" y="${cy}" fill="#666666">... +${table.columns.length - visible.length} more</text>`
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
return parts;
|
|
1476
|
+
}
|
|
1477
|
+
function shortType(type) {
|
|
1478
|
+
return type.length > 18 ? `${type.slice(0, 15)}...` : type;
|
|
1479
|
+
}
|
|
1480
|
+
function escapeXml(text) {
|
|
1481
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1482
|
+
}
|
|
1483
|
+
function fitErDiagramToBox(width, height, maxWidth, maxHeight) {
|
|
1484
|
+
const scale = Math.min(maxWidth / width, maxHeight / height, 1);
|
|
1485
|
+
return {
|
|
1486
|
+
width: Math.max(1, Math.round(width * scale)),
|
|
1487
|
+
height: Math.max(1, Math.round(height * scale))
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
|
|
616
1491
|
// src/exporters/excel/excel-exporter.ts
|
|
617
1492
|
var COLOR = {
|
|
618
1493
|
headerBg: "FF4472C4",
|
|
@@ -630,7 +1505,7 @@ var COLOR = {
|
|
|
630
1505
|
};
|
|
631
1506
|
var COL_COUNT = 7;
|
|
632
1507
|
async function exportExcelDictionary(doc, options) {
|
|
633
|
-
await
|
|
1508
|
+
await mkdir2(options.outDir, { recursive: true });
|
|
634
1509
|
const workbook = new ExcelJS.Workbook();
|
|
635
1510
|
const labels = getOutputLabels(options.language);
|
|
636
1511
|
const sheetNames = /* @__PURE__ */ new Map();
|
|
@@ -638,13 +1513,16 @@ async function exportExcelDictionary(doc, options) {
|
|
|
638
1513
|
sheetNames.set(table.name, buildSheetName(table.name, sheetNames));
|
|
639
1514
|
}
|
|
640
1515
|
addOverviewSheet(workbook, doc, labels, sheetNames);
|
|
1516
|
+
if (doc.tables.length > 0) {
|
|
1517
|
+
await addErDiagramSheet(workbook, doc, labels, options.outDir);
|
|
1518
|
+
}
|
|
641
1519
|
for (const table of doc.tables) {
|
|
642
1520
|
const sheetName = sheetNames.get(table.name);
|
|
643
1521
|
const sheet = workbook.addWorksheet(sheetName);
|
|
644
1522
|
populateTableSheet(sheet, table, doc, labels);
|
|
645
1523
|
}
|
|
646
1524
|
await workbook.xlsx.writeFile(
|
|
647
|
-
|
|
1525
|
+
join2(options.outDir, "database_dictionary.xlsx")
|
|
648
1526
|
);
|
|
649
1527
|
}
|
|
650
1528
|
function addOverviewSheet(workbook, doc, labels, sheetNames) {
|
|
@@ -725,6 +1603,51 @@ function addOverviewSheet(workbook, doc, labels, sheetNames) {
|
|
|
725
1603
|
};
|
|
726
1604
|
sheet.views = [{ state: "frozen", ySplit: headerRowNum }];
|
|
727
1605
|
}
|
|
1606
|
+
async function addErDiagramSheet(workbook, doc, labels, outDir) {
|
|
1607
|
+
const sheet = workbook.addWorksheet(labels.erDiagramSheet);
|
|
1608
|
+
sheet.mergeCells(1, 1, 1, 6);
|
|
1609
|
+
const titleCell = sheet.getCell(1, 1);
|
|
1610
|
+
titleCell.value = labels.erDiagramHeading;
|
|
1611
|
+
titleCell.font = { bold: true, size: 14, color: { argb: COLOR.overviewFg } };
|
|
1612
|
+
titleCell.fill = solidFill(COLOR.overviewBg);
|
|
1613
|
+
titleCell.alignment = { horizontal: "center", vertical: "middle" };
|
|
1614
|
+
sheet.getRow(1).height = 28;
|
|
1615
|
+
let nextRow = 3;
|
|
1616
|
+
try {
|
|
1617
|
+
const { buffer: png, width, height } = await renderErDiagramPng(doc);
|
|
1618
|
+
await writeFile2(join2(outDir, "er_diagram.png"), png);
|
|
1619
|
+
const imageId = workbook.addImage({
|
|
1620
|
+
base64: png.toString("base64"),
|
|
1621
|
+
extension: "png"
|
|
1622
|
+
});
|
|
1623
|
+
const fitted = fitErDiagramToBox(width, height, 1100, 1200);
|
|
1624
|
+
sheet.addImage(imageId, {
|
|
1625
|
+
tl: { col: 0, row: 2 },
|
|
1626
|
+
ext: fitted
|
|
1627
|
+
});
|
|
1628
|
+
nextRow = Math.max(28, Math.ceil(fitted.height / 18) + 4);
|
|
1629
|
+
} catch {
|
|
1630
|
+
sheet.getCell(3, 1).value = labels.viewErDiagram;
|
|
1631
|
+
nextRow = 5;
|
|
1632
|
+
}
|
|
1633
|
+
const mermaid = getErDiagramMermaid(doc);
|
|
1634
|
+
sheet.getCell(nextRow, 1).value = "Mermaid source";
|
|
1635
|
+
sheet.getCell(nextRow, 1).font = { bold: true, color: { argb: COLOR.metaFg } };
|
|
1636
|
+
nextRow += 1;
|
|
1637
|
+
sheet.mergeCells(nextRow, 1, nextRow + 20, 6);
|
|
1638
|
+
const sourceCell = sheet.getCell(nextRow, 1);
|
|
1639
|
+
sourceCell.value = mermaid;
|
|
1640
|
+
sourceCell.alignment = { wrapText: true, vertical: "top" };
|
|
1641
|
+
sourceCell.font = { name: "Courier New", size: 9 };
|
|
1642
|
+
sheet.columns = [
|
|
1643
|
+
{ width: 24 },
|
|
1644
|
+
{ width: 24 },
|
|
1645
|
+
{ width: 24 },
|
|
1646
|
+
{ width: 24 },
|
|
1647
|
+
{ width: 24 },
|
|
1648
|
+
{ width: 24 }
|
|
1649
|
+
];
|
|
1650
|
+
}
|
|
728
1651
|
function populateTableSheet(sheet, table, doc, labels) {
|
|
729
1652
|
const indexes = collectTableIndexes(table, doc);
|
|
730
1653
|
sheet.mergeCells(1, 1, 1, 6);
|
|
@@ -765,48 +1688,39 @@ function populateTableSheet(sheet, table, doc, labels) {
|
|
|
765
1688
|
row.getCell(2).alignment = { wrapText: true, vertical: "top" };
|
|
766
1689
|
}
|
|
767
1690
|
sheet.addRow([]);
|
|
768
|
-
const headerRow = sheet.addRow(
|
|
769
|
-
labels.physicalName,
|
|
770
|
-
labels.logicalName,
|
|
771
|
-
labels.type,
|
|
772
|
-
labels.required,
|
|
773
|
-
labels.defaultValue,
|
|
774
|
-
labels.notes
|
|
775
|
-
]);
|
|
1691
|
+
const headerRow = sheet.addRow(columnDefinitionHeaders(labels));
|
|
776
1692
|
styleColorRow(headerRow, COLOR.headerBg, COLOR.headerFg);
|
|
777
|
-
applyBorderToRow(headerRow,
|
|
1693
|
+
applyBorderToRow(headerRow, A5_COLUMN_COUNT);
|
|
778
1694
|
const headerRowNum = headerRow.number;
|
|
779
1695
|
for (const [i, column] of table.columns.entries()) {
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
column.name,
|
|
786
|
-
displayValue(column.comment, labels),
|
|
787
|
-
column.type,
|
|
788
|
-
column.nullable ? labels.no : labels.yes,
|
|
789
|
-
column.defaultValue ?? "-",
|
|
790
|
-
notes || "-"
|
|
791
|
-
]);
|
|
1696
|
+
const row = sheet.addRow(
|
|
1697
|
+
columnDefinitionRow(column, labels).map(
|
|
1698
|
+
(value, index) => index === 1 ? displayValue(value, labels) : value
|
|
1699
|
+
)
|
|
1700
|
+
);
|
|
792
1701
|
if (column.isPrimaryKey) {
|
|
793
|
-
shadeRow(row,
|
|
1702
|
+
shadeRow(row, A5_COLUMN_COUNT, COLOR.pkBg);
|
|
794
1703
|
row.getCell(1).font = { bold: true };
|
|
795
1704
|
} else if (column.isForeignKey) {
|
|
796
|
-
shadeRow(row,
|
|
1705
|
+
shadeRow(row, A5_COLUMN_COUNT, COLOR.fkBg);
|
|
797
1706
|
} else if (i % 2 === 1) {
|
|
798
|
-
shadeRow(row,
|
|
1707
|
+
shadeRow(row, A5_COLUMN_COUNT, COLOR.altRow);
|
|
799
1708
|
}
|
|
800
|
-
row.getCell(
|
|
801
|
-
|
|
1709
|
+
row.getCell(5).alignment = { horizontal: "center" };
|
|
1710
|
+
row.getCell(9).alignment = { horizontal: "center" };
|
|
1711
|
+
applyBorderToRow(row, A5_COLUMN_COUNT);
|
|
802
1712
|
}
|
|
803
1713
|
sheet.columns = [
|
|
1714
|
+
{ width: 22 },
|
|
804
1715
|
{ width: 24 },
|
|
805
|
-
{ width:
|
|
806
|
-
{ width: 18 },
|
|
1716
|
+
{ width: 16 },
|
|
807
1717
|
{ width: 10 },
|
|
808
|
-
{ width:
|
|
809
|
-
{ width:
|
|
1718
|
+
{ width: 8 },
|
|
1719
|
+
{ width: 14 },
|
|
1720
|
+
{ width: 8 },
|
|
1721
|
+
{ width: 8 },
|
|
1722
|
+
{ width: 8 },
|
|
1723
|
+
{ width: 28 }
|
|
810
1724
|
];
|
|
811
1725
|
sheet.views = [{ state: "frozen", ySplit: headerRowNum }];
|
|
812
1726
|
}
|
|
@@ -876,57 +1790,8 @@ function collectTableIndexes(table, doc) {
|
|
|
876
1790
|
];
|
|
877
1791
|
}
|
|
878
1792
|
|
|
879
|
-
// src/exporters/diagram/mermaid-exporter.ts
|
|
880
|
-
import { mkdir as mkdir2, writeFile } from "fs/promises";
|
|
881
|
-
import { join as join2 } from "path";
|
|
882
|
-
async function exportMermaidDiagram(doc, options) {
|
|
883
|
-
await mkdir2(options.outDir, { recursive: true });
|
|
884
|
-
await writeFile(
|
|
885
|
-
join2(options.outDir, "er_diagram.mmd"),
|
|
886
|
-
renderMermaid(doc),
|
|
887
|
-
"utf8"
|
|
888
|
-
);
|
|
889
|
-
}
|
|
890
|
-
function renderMermaid(doc) {
|
|
891
|
-
const lines = ["erDiagram"];
|
|
892
|
-
for (const warning of doc.warnings) {
|
|
893
|
-
const target = warning.target ? ` (${warning.target})` : "";
|
|
894
|
-
lines.push(
|
|
895
|
-
` %% WARNING [${warning.severity}] ${warning.code}${target}: ${warning.message}`
|
|
896
|
-
);
|
|
897
|
-
}
|
|
898
|
-
for (const table of doc.tables) {
|
|
899
|
-
for (const todo of table.reviewTodos) {
|
|
900
|
-
lines.push(` %% TODO [${todo.type}] ${todo.target}: ${todo.issue}`);
|
|
901
|
-
}
|
|
902
|
-
lines.push(` ${table.name} {`);
|
|
903
|
-
for (const column of table.columns) {
|
|
904
|
-
const markers = [
|
|
905
|
-
column.isPrimaryKey ? "PK" : "",
|
|
906
|
-
column.isForeignKey ? "FK" : ""
|
|
907
|
-
].filter(Boolean).join(" ");
|
|
908
|
-
lines.push(
|
|
909
|
-
` ${sanitizeType(column.type)} ${column.name}${markers ? ` "${markers}"` : ""}`
|
|
910
|
-
);
|
|
911
|
-
}
|
|
912
|
-
lines.push(" }");
|
|
913
|
-
}
|
|
914
|
-
for (const relationship of doc.relationships.filter(
|
|
915
|
-
(item) => item.source === "schema"
|
|
916
|
-
)) {
|
|
917
|
-
lines.push(
|
|
918
|
-
` ${relationship.toTable} ||--o{ ${relationship.fromTable} : "${relationship.constraintName ?? relationship.fromColumn}"`
|
|
919
|
-
);
|
|
920
|
-
}
|
|
921
|
-
return `${lines.join("\n")}
|
|
922
|
-
`;
|
|
923
|
-
}
|
|
924
|
-
function sanitizeType(type) {
|
|
925
|
-
return type.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
926
|
-
}
|
|
927
|
-
|
|
928
1793
|
// src/exporters/markdown/markdown-exporter.ts
|
|
929
|
-
import { mkdir as mkdir3, writeFile as
|
|
1794
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
930
1795
|
import { join as join3 } from "path";
|
|
931
1796
|
|
|
932
1797
|
// src/core/sanitize.ts
|
|
@@ -940,8 +1805,16 @@ async function exportMarkdownDocs(doc, options) {
|
|
|
940
1805
|
const tablesDir = join3(options.outDir, "tables");
|
|
941
1806
|
await mkdir3(tablesDir, { recursive: true });
|
|
942
1807
|
const labels = getOutputLabels(options.language);
|
|
1808
|
+
if (doc.tables.length > 0) {
|
|
1809
|
+
const mermaid = getErDiagramMermaid(doc);
|
|
1810
|
+
await writeFile3(
|
|
1811
|
+
join3(options.outDir, "ER_DIAGRAM.md"),
|
|
1812
|
+
renderErDiagramMarkdown(mermaid, labels),
|
|
1813
|
+
"utf8"
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
943
1816
|
for (const table of doc.tables) {
|
|
944
|
-
await
|
|
1817
|
+
await writeFile3(
|
|
945
1818
|
join3(tablesDir, `${sanitizeFilename(table.name)}.md`),
|
|
946
1819
|
renderTableDoc(table, doc, labels),
|
|
947
1820
|
"utf8"
|
|
@@ -988,12 +1861,12 @@ function renderTableDoc(table, doc, labels) {
|
|
|
988
1861
|
lines.push(`## ${labels.columnsHeading}`);
|
|
989
1862
|
lines.push("");
|
|
990
1863
|
lines.push(
|
|
991
|
-
`| ${labels.
|
|
1864
|
+
`| ${columnDefinitionHeaders(labels).join(" | ")} |`
|
|
992
1865
|
);
|
|
993
|
-
lines.push("
|
|
1866
|
+
lines.push(`|${columnDefinitionHeaders(labels).map(() => "--------").join("|")}|`);
|
|
994
1867
|
for (const col of table.columns) {
|
|
995
1868
|
lines.push(
|
|
996
|
-
`| ${
|
|
1869
|
+
`| ${columnDefinitionRow(col, labels).map((value) => escapeMd(value)).join(" | ")} |`
|
|
997
1870
|
);
|
|
998
1871
|
}
|
|
999
1872
|
lines.push("");
|
|
@@ -1012,7 +1885,7 @@ function escapeMd(text) {
|
|
|
1012
1885
|
}
|
|
1013
1886
|
|
|
1014
1887
|
// src/exporters/html/html-exporter.ts
|
|
1015
|
-
import { mkdir as mkdir4, writeFile as
|
|
1888
|
+
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
1016
1889
|
import { join as join4 } from "path";
|
|
1017
1890
|
async function exportHtmlDocs(doc, options) {
|
|
1018
1891
|
try {
|
|
@@ -1020,13 +1893,21 @@ async function exportHtmlDocs(doc, options) {
|
|
|
1020
1893
|
const tablesDir = join4(htmlDir, "tables");
|
|
1021
1894
|
await mkdir4(tablesDir, { recursive: true });
|
|
1022
1895
|
const labels = getOutputLabels(options.language);
|
|
1023
|
-
await
|
|
1896
|
+
await writeFile4(
|
|
1024
1897
|
join4(htmlDir, "index.html"),
|
|
1025
1898
|
renderIndexPage(doc, labels),
|
|
1026
1899
|
"utf8"
|
|
1027
1900
|
);
|
|
1901
|
+
if (doc.tables.length > 0) {
|
|
1902
|
+
const mermaid = getErDiagramMermaid(doc);
|
|
1903
|
+
await writeFile4(
|
|
1904
|
+
join4(htmlDir, "er-diagram.html"),
|
|
1905
|
+
renderErDiagramHtmlPage(mermaid, labels),
|
|
1906
|
+
"utf8"
|
|
1907
|
+
);
|
|
1908
|
+
}
|
|
1028
1909
|
for (const table of doc.tables) {
|
|
1029
|
-
await
|
|
1910
|
+
await writeFile4(
|
|
1030
1911
|
join4(tablesDir, `${sanitizeFilename(table.name)}.html`),
|
|
1031
1912
|
renderTablePage(table, doc, labels),
|
|
1032
1913
|
"utf8"
|
|
@@ -1108,7 +1989,7 @@ function pageShell(title, body, fromSubdir = false) {
|
|
|
1108
1989
|
<head>
|
|
1109
1990
|
<meta charset="UTF-8">
|
|
1110
1991
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1111
|
-
<title>${
|
|
1992
|
+
<title>${esc2(title)}</title>
|
|
1112
1993
|
<style>${CSS} </style>
|
|
1113
1994
|
</head>
|
|
1114
1995
|
<body>
|
|
@@ -1125,34 +2006,35 @@ function renderIndexPage(doc, labels) {
|
|
|
1125
2006
|
const fkCount = table.foreignKeys.length;
|
|
1126
2007
|
const fileName = sanitizeFilename(table.name);
|
|
1127
2008
|
tableRows += ` <tr>
|
|
1128
|
-
<td><a href="tables/${fileName}.html">${
|
|
1129
|
-
<td>${
|
|
2009
|
+
<td><a href="tables/${fileName}.html">${esc2(table.name)}</a></td>
|
|
2010
|
+
<td>${esc2(table.comment ?? "")}</td>
|
|
1130
2011
|
<td style="text-align:center">${table.columns.length}</td>
|
|
1131
|
-
<td>${
|
|
2012
|
+
<td>${esc2(pkCols)}</td>
|
|
1132
2013
|
<td style="text-align:center">${fkCount}</td>
|
|
1133
2014
|
</tr>
|
|
1134
2015
|
`;
|
|
1135
2016
|
}
|
|
1136
2017
|
const body = `
|
|
1137
|
-
<h1>${
|
|
2018
|
+
<h1>${esc2(labels.docTitle)}</h1>
|
|
1138
2019
|
<div class="summary">
|
|
1139
|
-
<div class="summary-item"><div class="num">${doc.tables.length}</div><div class="lbl">${
|
|
1140
|
-
<div class="summary-item"><div class="num">${doc.relationships.length}</div><div class="lbl">${
|
|
1141
|
-
<div class="summary-item"><div class="num">${doc.dialect}</div><div class="lbl">${
|
|
2020
|
+
<div class="summary-item"><div class="num">${doc.tables.length}</div><div class="lbl">${esc2(labels.tablesLabel)}</div></div>
|
|
2021
|
+
<div class="summary-item"><div class="num">${doc.relationships.length}</div><div class="lbl">${esc2(labels.relationshipsLabel)}</div></div>
|
|
2022
|
+
<div class="summary-item"><div class="num">${doc.dialect}</div><div class="lbl">${esc2(labels.dialectLabel)}</div></div>
|
|
1142
2023
|
</div>
|
|
1143
|
-
<
|
|
2024
|
+
<p class="back"><a href="er-diagram.html">${esc2(labels.erDiagramHeading)} \u2192</a></p>
|
|
2025
|
+
<h2>${esc2(labels.tableListHeading)}</h2>
|
|
1144
2026
|
<table class="table-list">
|
|
1145
2027
|
<thead><tr>
|
|
1146
|
-
<th>${
|
|
1147
|
-
<th>${
|
|
2028
|
+
<th>${esc2(labels.tableLabel)}</th>
|
|
2029
|
+
<th>${esc2(labels.tableLogicalName)}</th>
|
|
1148
2030
|
<th style="width:70px;text-align:center">Cols</th>
|
|
1149
|
-
<th>${
|
|
2031
|
+
<th>${esc2(labels.primaryKey)}</th>
|
|
1150
2032
|
<th style="width:50px;text-align:center">FK</th>
|
|
1151
2033
|
</tr></thead>
|
|
1152
2034
|
<tbody>
|
|
1153
2035
|
${tableRows} </tbody>
|
|
1154
2036
|
</table>
|
|
1155
|
-
<p class="note">${
|
|
2037
|
+
<p class="note">${esc2(labels.generatedNote)}</p>
|
|
1156
2038
|
`;
|
|
1157
2039
|
return pageShell(labels.docTitle, body);
|
|
1158
2040
|
}
|
|
@@ -1170,39 +2052,34 @@ function renderTablePage(table, doc, labels) {
|
|
|
1170
2052
|
const pkBadge = col.isPrimaryKey ? `<span class="badge badge-pk">PK</span>` : "";
|
|
1171
2053
|
const fkBadge = col.isForeignKey ? `<span class="badge badge-fk">FK</span>` : "";
|
|
1172
2054
|
const rowClass = col.isPrimaryKey ? "pk" : col.isForeignKey ? "fk" : "";
|
|
1173
|
-
const
|
|
1174
|
-
colRows += ` <tr${rowClass ? ` class="${rowClass}"` : ""}><td>${
|
|
2055
|
+
const cells = columnDefinitionRow(col, labels);
|
|
2056
|
+
colRows += ` <tr${rowClass ? ` class="${rowClass}"` : ""}><td>${esc2(cells[0] ?? "")}${pkBadge}${fkBadge}</td>` + cells.slice(1).map((cell) => `<td>${esc2(cell)}</td>`).join("") + `</tr>
|
|
1175
2057
|
`;
|
|
1176
2058
|
}
|
|
1177
2059
|
const body = `
|
|
1178
|
-
<p class="back"><a href="../index.html">\u2190 ${
|
|
1179
|
-
<h1>${
|
|
1180
|
-
<h2>${
|
|
2060
|
+
<p class="back"><a href="../index.html">\u2190 ${esc2(labels.tableListHeading)}</a></p>
|
|
2061
|
+
<h1>${esc2(table.name)}</h1>
|
|
2062
|
+
<h2>${esc2(labels.tableInfoHeading)}</h2>
|
|
1181
2063
|
<table class="meta">
|
|
1182
2064
|
<tbody>
|
|
1183
|
-
<tr><th>${
|
|
1184
|
-
<tr><th>${
|
|
1185
|
-
<tr><th>${
|
|
1186
|
-
<tr><th>${
|
|
1187
|
-
<tr><th>${
|
|
1188
|
-
<tr><th>${
|
|
2065
|
+
<tr><th>${esc2(labels.tablePhysicalName)}</th><td>${esc2(table.name)}</td></tr>
|
|
2066
|
+
<tr><th>${esc2(labels.tableLogicalName)}</th><td>${esc2(table.comment ?? "")}</td></tr>
|
|
2067
|
+
<tr><th>${esc2(labels.schema)}</th><td>${esc2(table.schema ?? "")}</td></tr>
|
|
2068
|
+
<tr><th>${esc2(labels.primaryKey)}</th><td>${esc2(table.primaryKeys.join(", ") || labels.none)}</td></tr>
|
|
2069
|
+
<tr><th>${esc2(labels.foreignKeys)}</th><td>${foreignKeys}</td></tr>
|
|
2070
|
+
<tr><th>${esc2(labels.indexes)}</th><td>${indexText}</td></tr>
|
|
1189
2071
|
</tbody>
|
|
1190
2072
|
</table>
|
|
1191
2073
|
|
|
1192
|
-
<h2>${
|
|
2074
|
+
<h2>${esc2(labels.columnsHeading)}</h2>
|
|
1193
2075
|
<table class="columns">
|
|
1194
2076
|
<thead><tr>
|
|
1195
|
-
|
|
1196
|
-
<th>${esc(labels.logicalName)}</th>
|
|
1197
|
-
<th>${esc(labels.type)}</th>
|
|
1198
|
-
<th>${esc(labels.required)}</th>
|
|
1199
|
-
<th>${esc(labels.defaultValue)}</th>
|
|
1200
|
-
<th>${esc(labels.notes)}</th>
|
|
2077
|
+
${columnDefinitionHeaders(labels).map((header) => `<th>${esc2(header)}</th>`).join("\n ")}
|
|
1201
2078
|
</tr></thead>
|
|
1202
2079
|
<tbody>
|
|
1203
2080
|
${colRows} </tbody>
|
|
1204
2081
|
</table>
|
|
1205
|
-
<p class="note">${
|
|
2082
|
+
<p class="note">${esc2(labels.generatedNote)}</p>
|
|
1206
2083
|
`;
|
|
1207
2084
|
return pageShell(table.name, body);
|
|
1208
2085
|
}
|
|
@@ -1214,12 +2091,12 @@ function collectTableIndexes3(table, doc) {
|
|
|
1214
2091
|
)
|
|
1215
2092
|
];
|
|
1216
2093
|
}
|
|
1217
|
-
function
|
|
2094
|
+
function esc2(text) {
|
|
1218
2095
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1219
2096
|
}
|
|
1220
2097
|
|
|
1221
2098
|
// src/exporters/word/word-exporter.ts
|
|
1222
|
-
import { mkdir as mkdir5, writeFile as
|
|
2099
|
+
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
1223
2100
|
import { join as join5 } from "path";
|
|
1224
2101
|
import {
|
|
1225
2102
|
Document,
|
|
@@ -1229,7 +2106,8 @@ import {
|
|
|
1229
2106
|
TableCell,
|
|
1230
2107
|
TableRow,
|
|
1231
2108
|
TextRun,
|
|
1232
|
-
HeadingLevel
|
|
2109
|
+
HeadingLevel,
|
|
2110
|
+
ImageRun
|
|
1233
2111
|
} from "docx";
|
|
1234
2112
|
async function exportWordDocument(doc, options) {
|
|
1235
2113
|
try {
|
|
@@ -1263,6 +2141,53 @@ async function exportWordDocument(doc, options) {
|
|
|
1263
2141
|
children: [new TextRun(`${labels.relationshipsLabel}: ${doc.relationships.length}`)]
|
|
1264
2142
|
})
|
|
1265
2143
|
);
|
|
2144
|
+
if (doc.tables.length > 0) {
|
|
2145
|
+
children.push(
|
|
2146
|
+
new Paragraph({
|
|
2147
|
+
heading: HeadingLevel.HEADING_2,
|
|
2148
|
+
children: [new TextRun(labels.erDiagramHeading)]
|
|
2149
|
+
})
|
|
2150
|
+
);
|
|
2151
|
+
try {
|
|
2152
|
+
const { buffer: png, width, height } = await renderErDiagramPng(doc);
|
|
2153
|
+
await writeFile5(join5(options.outDir, "er_diagram.png"), png);
|
|
2154
|
+
const fitted = fitErDiagramToBox(width, height, 620, 900);
|
|
2155
|
+
children.push(
|
|
2156
|
+
new Paragraph({
|
|
2157
|
+
children: [
|
|
2158
|
+
new ImageRun({
|
|
2159
|
+
data: png,
|
|
2160
|
+
transformation: fitted,
|
|
2161
|
+
type: "png"
|
|
2162
|
+
})
|
|
2163
|
+
]
|
|
2164
|
+
})
|
|
2165
|
+
);
|
|
2166
|
+
} catch {
|
|
2167
|
+
children.push(
|
|
2168
|
+
new Paragraph({
|
|
2169
|
+
children: [new TextRun(labels.viewErDiagram)]
|
|
2170
|
+
})
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
2173
|
+
const mermaid = getErDiagramMermaid(doc);
|
|
2174
|
+
children.push(
|
|
2175
|
+
new Paragraph({
|
|
2176
|
+
children: [new TextRun({ text: "Mermaid source", bold: true })]
|
|
2177
|
+
})
|
|
2178
|
+
);
|
|
2179
|
+
children.push(
|
|
2180
|
+
new Paragraph({
|
|
2181
|
+
children: [
|
|
2182
|
+
new TextRun({
|
|
2183
|
+
text: mermaid,
|
|
2184
|
+
font: "Courier New",
|
|
2185
|
+
size: 18
|
|
2186
|
+
})
|
|
2187
|
+
]
|
|
2188
|
+
})
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
1266
2191
|
children.push(
|
|
1267
2192
|
new Paragraph({
|
|
1268
2193
|
heading: HeadingLevel.HEADING_2,
|
|
@@ -1463,7 +2388,7 @@ async function exportWordDocument(doc, options) {
|
|
|
1463
2388
|
sections: [{ children }]
|
|
1464
2389
|
});
|
|
1465
2390
|
const buffer = await Packer.toBuffer(wordDoc);
|
|
1466
|
-
await
|
|
2391
|
+
await writeFile5(join5(options.outDir, "database_document.docx"), buffer);
|
|
1467
2392
|
} catch (err) {
|
|
1468
2393
|
throw new Error(
|
|
1469
2394
|
`Failed to export Word document: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -1522,14 +2447,7 @@ function renderTableDetail(table, doc, labels) {
|
|
|
1522
2447
|
return items;
|
|
1523
2448
|
}
|
|
1524
2449
|
function renderColumnsTable(table, labels) {
|
|
1525
|
-
const headerCells =
|
|
1526
|
-
labels.physicalName,
|
|
1527
|
-
labels.logicalName,
|
|
1528
|
-
labels.type,
|
|
1529
|
-
labels.required,
|
|
1530
|
-
labels.defaultValue,
|
|
1531
|
-
labels.notes
|
|
1532
|
-
].map(
|
|
2450
|
+
const headerCells = columnDefinitionHeaders(labels).map(
|
|
1533
2451
|
(h) => new TableCell({
|
|
1534
2452
|
children: [
|
|
1535
2453
|
new Paragraph({ children: [new TextRun({ text: h, bold: true })] })
|
|
@@ -1540,40 +2458,11 @@ function renderColumnsTable(table, labels) {
|
|
|
1540
2458
|
for (const col of table.columns) {
|
|
1541
2459
|
colRows.push(
|
|
1542
2460
|
new TableRow({
|
|
1543
|
-
children:
|
|
1544
|
-
new TableCell({
|
|
1545
|
-
children: [new Paragraph({ children: [new TextRun(
|
|
1546
|
-
}),
|
|
1547
|
-
new TableCell({
|
|
1548
|
-
children: [
|
|
1549
|
-
new Paragraph({ children: [new TextRun(col.comment ?? "")] })
|
|
1550
|
-
]
|
|
1551
|
-
}),
|
|
1552
|
-
new TableCell({
|
|
1553
|
-
children: [new Paragraph({ children: [new TextRun(col.type)] })]
|
|
1554
|
-
}),
|
|
1555
|
-
new TableCell({
|
|
1556
|
-
children: [
|
|
1557
|
-
new Paragraph({
|
|
1558
|
-
children: [new TextRun(col.nullable ? labels.no : labels.yes)]
|
|
1559
|
-
})
|
|
1560
|
-
]
|
|
1561
|
-
}),
|
|
1562
|
-
new TableCell({
|
|
1563
|
-
children: [
|
|
1564
|
-
new Paragraph({
|
|
1565
|
-
children: [new TextRun(col.defaultValue ?? "-")]
|
|
1566
|
-
})
|
|
1567
|
-
]
|
|
1568
|
-
}),
|
|
1569
|
-
new TableCell({
|
|
1570
|
-
children: [
|
|
1571
|
-
new Paragraph({
|
|
1572
|
-
children: [new TextRun(col.description?.value ?? "")]
|
|
1573
|
-
})
|
|
1574
|
-
]
|
|
2461
|
+
children: columnDefinitionRow(col, labels).map(
|
|
2462
|
+
(value) => new TableCell({
|
|
2463
|
+
children: [new Paragraph({ children: [new TextRun(value)] })]
|
|
1575
2464
|
})
|
|
1576
|
-
|
|
2465
|
+
)
|
|
1577
2466
|
})
|
|
1578
2467
|
);
|
|
1579
2468
|
}
|