duckerd 0.2.0 → 0.2.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 +2 -0
- package/dist/commands/erd.js +2 -2
- package/dist/index.js +6 -1
- package/dist/lib/metadata.js +62 -9
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -27,9 +27,11 @@ Generate an ERD diagram of the database schemas.
|
|
|
27
27
|
- `-d, --database <path>`: Path to the database file
|
|
28
28
|
- `-t, --theme [theme]`: Theme of the chart (choices: `default`, `forest`, `dark`, `neutral`, default: `default`)
|
|
29
29
|
- `-o, --output <path>`: Path to the output file
|
|
30
|
+
- `-m, --mmd-output <path>`: Path to write the Mermaid source (`.mmd`) file (default: cleaned up after rendering)
|
|
30
31
|
- `-w, --width [width]`: Width of the page (default: `1024`)
|
|
31
32
|
- `-H, --height [height]`: Height of the page (default: `768`)
|
|
32
33
|
- `-f, --outputFormat [format]`: Output format for the generated image (choices: `svg`, `png`, `pdf`, default: `png`)
|
|
34
|
+
- `-e, --expand-structs`: Expand `STRUCT` columns into individual sub-field rows (e.g. `full_name STRUCT(given VARCHAR, family VARCHAR)` becomes `full_name__given` and `full_name__family`)
|
|
33
35
|
|
|
34
36
|
#### Example:
|
|
35
37
|
|
package/dist/commands/erd.js
CHANGED
|
@@ -3,12 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createERD = void 0;
|
|
4
4
|
const node_api_1 = require("@duckdb/node-api");
|
|
5
5
|
const metadata_1 = require("../lib/metadata");
|
|
6
|
-
const createERD = async (databasePath) => {
|
|
6
|
+
const createERD = async (databasePath, expandStructs = false) => {
|
|
7
7
|
const instance = await node_api_1.DuckDBInstance.create(databasePath);
|
|
8
8
|
const conn = await instance.connect();
|
|
9
9
|
try {
|
|
10
10
|
const metadata = await (0, metadata_1.getMetadata)(conn);
|
|
11
|
-
return (0, metadata_1.generateMermaidCodeForAllDBs)(metadata);
|
|
11
|
+
return (0, metadata_1.generateMermaidCodeForAllDBs)(metadata, expandStructs);
|
|
12
12
|
}
|
|
13
13
|
finally {
|
|
14
14
|
conn.closeSync();
|
package/dist/index.js
CHANGED
|
@@ -47,6 +47,8 @@ program
|
|
|
47
47
|
.option('-d, --database <path>', 'Path to the database file')
|
|
48
48
|
.option('-t, --theme [theme]', 'Theme of the chart (choices: "default", "forest", "dark", "neutral", default: "default")')
|
|
49
49
|
.option('-o, --output <path>', 'Path to the output file')
|
|
50
|
+
.option('-m, --mmd-output <path>', 'Path to write the Mermaid source (.mmd) file (default: cleaned up after rendering)')
|
|
51
|
+
.option('-e, --expand-structs', 'Expand STRUCT type columns into individual sub-field rows')
|
|
50
52
|
.option('-w, --width [width]', 'Width of the page (default: 1024)')
|
|
51
53
|
.option('-H, --height [height]', 'Height of the page (default: 768)')
|
|
52
54
|
.option('-f, --outputFormat [format]', 'Output format for the generated image. (choices: "svg", "png", "pdf")')
|
|
@@ -61,10 +63,13 @@ program
|
|
|
61
63
|
run(dbPath, options);
|
|
62
64
|
});
|
|
63
65
|
const run = async (dbPath, options) => {
|
|
64
|
-
const mermaidDiagram = await (0, erd_1.createERD)(dbPath);
|
|
66
|
+
const mermaidDiagram = await (0, erd_1.createERD)(dbPath, options.expandStructs ?? false);
|
|
65
67
|
const mermaidFile = path.resolve('schema_erd.mmd');
|
|
66
68
|
if (mermaidDiagram) {
|
|
67
69
|
fs.writeFileSync(mermaidFile, mermaidDiagram);
|
|
70
|
+
if (options.mmdOutput) {
|
|
71
|
+
fs.copyFileSync(mermaidFile, path.resolve(options.mmdOutput));
|
|
72
|
+
}
|
|
68
73
|
// Extract the database name from the dbPath
|
|
69
74
|
const dbName = dbPath === ':memory:' ? 'memory_db' : path.basename(dbPath, path.extname(dbPath));
|
|
70
75
|
const outputFile = options.output || `${dbName}_erd.${options.outputFormat || 'png'}`;
|
package/dist/lib/metadata.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.addCommentIfNecessary = exports.sanitizeDataType = exports.generateMermaidCodeForAllDBs = exports.getMetadata = void 0;
|
|
3
|
+
exports.addCommentIfNecessary = exports.sanitizeDataType = exports.expandStructFields = exports.parseStructFields = exports.generateMermaidCodeForAllDBs = exports.getMetadata = void 0;
|
|
4
4
|
// Database metadata query
|
|
5
5
|
const databaseMetadataQuery = `SELECT database_name as databaseName FROM duckdb_databases() WHERE database_name NOT IN ('system', 'temp') ORDER BY database_name`;
|
|
6
6
|
// Schema metadata query
|
|
@@ -77,7 +77,7 @@ const getMetadata = async (conn) => {
|
|
|
77
77
|
return metadata;
|
|
78
78
|
};
|
|
79
79
|
exports.getMetadata = getMetadata;
|
|
80
|
-
const generateMermaidCodeForAllDBs = (metadata) => {
|
|
80
|
+
const generateMermaidCodeForAllDBs = (metadata, expandStructs = false) => {
|
|
81
81
|
let mermaidCode = `erDiagram
|
|
82
82
|
|
|
83
83
|
`;
|
|
@@ -88,7 +88,14 @@ const generateMermaidCodeForAllDBs = (metadata) => {
|
|
|
88
88
|
// Add tables, columns and constraints
|
|
89
89
|
mermaidCode += tables.map((table) => {
|
|
90
90
|
return `"${table.databaseName}.${table.name}" {
|
|
91
|
-
${table.columns.
|
|
91
|
+
${table.columns.flatMap((column) => {
|
|
92
|
+
const constraintMarkers = table.constraints?.filter((constraint) => constraint.columnName === column.name).map((constraint) => ((constraint.constraintType === "PRIMARY KEY" ? "PK" : "") || (constraint.constraintType === "FOREIGN KEY" ? "FK" : "") || "")).filter(str => str) ?? [];
|
|
93
|
+
const structFields = expandStructs ? (0, exports.expandStructFields)(column.name, column.dataType) : [];
|
|
94
|
+
if (structFields.length > 0) {
|
|
95
|
+
return structFields.map((field) => ` ${(0, exports.sanitizeDataType)(field.type.toUpperCase())} ${field.name}`);
|
|
96
|
+
}
|
|
97
|
+
return [` ${(0, exports.sanitizeDataType)(column.dataType.toUpperCase())} ${column.name} ${constraintMarkers}${(0, exports.addCommentIfNecessary)(column.dataType)}`];
|
|
98
|
+
}).join("\n")}
|
|
92
99
|
}\n${table.constraints?.filter((constraint) => constraint.constraintType === "FOREIGN KEY").map((constraint) => ` "${table.databaseName}.${constraint.sql.match(/(?:^|)REFERENCES\s([^*]+?)\b\(/i)[1]}" ||--o{ "${table.databaseName}.${table.name}" : has`).join("\n")}`;
|
|
93
100
|
}).join("\n ");
|
|
94
101
|
return mermaidCode;
|
|
@@ -102,15 +109,61 @@ ${table.columns.map((column) => ` ${(0, exports.sanitizeDataType)(column.da
|
|
|
102
109
|
}
|
|
103
110
|
};
|
|
104
111
|
exports.generateMermaidCodeForAllDBs = generateMermaidCodeForAllDBs;
|
|
105
|
-
const
|
|
106
|
-
if (dataType.startsWith("STRUCT(")) {
|
|
107
|
-
return
|
|
112
|
+
const parseStructFields = (dataType) => {
|
|
113
|
+
if (!dataType.toUpperCase().startsWith("STRUCT(")) {
|
|
114
|
+
return [];
|
|
108
115
|
}
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
const inner = dataType.slice(dataType.indexOf("(") + 1, dataType.lastIndexOf(")"));
|
|
117
|
+
const fields = [];
|
|
118
|
+
let depth = 0;
|
|
119
|
+
let current = "";
|
|
120
|
+
for (const ch of inner) {
|
|
121
|
+
if (ch === "(" || ch === "<")
|
|
122
|
+
depth++;
|
|
123
|
+
else if (ch === ")" || ch === ">")
|
|
124
|
+
depth--;
|
|
125
|
+
else if (ch === "," && depth === 0) {
|
|
126
|
+
const parts = current.trim().split(/\s+/);
|
|
127
|
+
if (parts.length >= 2) {
|
|
128
|
+
fields.push({ name: parts[0].replace(/['"]/g, ""), type: parts.slice(1).join(" ") });
|
|
129
|
+
}
|
|
130
|
+
current = "";
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
current += ch;
|
|
134
|
+
}
|
|
135
|
+
if (current.trim()) {
|
|
136
|
+
const parts = current.trim().split(/\s+/);
|
|
137
|
+
if (parts.length >= 2) {
|
|
138
|
+
fields.push({ name: parts[0].replace(/['"]/g, ""), type: parts.slice(1).join(" ") });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return fields;
|
|
142
|
+
};
|
|
143
|
+
exports.parseStructFields = parseStructFields;
|
|
144
|
+
const expandStructFields = (prefix, dataType) => {
|
|
145
|
+
const fields = (0, exports.parseStructFields)(dataType);
|
|
146
|
+
if (fields.length === 0)
|
|
147
|
+
return [];
|
|
148
|
+
return fields.flatMap((field) => {
|
|
149
|
+
const nestedFields = (0, exports.parseStructFields)(field.type);
|
|
150
|
+
if (nestedFields.length > 0) {
|
|
151
|
+
return (0, exports.expandStructFields)(`${prefix}__${field.name}`, field.type);
|
|
152
|
+
}
|
|
153
|
+
return [{ name: `${prefix}__${field.name}`, type: field.type }];
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
exports.expandStructFields = expandStructFields;
|
|
157
|
+
const COLLAPSIBLE_TYPES = ["STRUCT", "MAP", "ENUM"];
|
|
158
|
+
const sanitizeDataType = (dataType) => {
|
|
159
|
+
const upper = dataType.toUpperCase();
|
|
160
|
+
for (const t of COLLAPSIBLE_TYPES) {
|
|
161
|
+
if (upper.startsWith(`${t}(`)) {
|
|
162
|
+
return t;
|
|
163
|
+
}
|
|
111
164
|
}
|
|
112
165
|
if (dataType.includes(",")) {
|
|
113
|
-
return dataType.replace(
|
|
166
|
+
return dataType.replace(/,/g, "_");
|
|
114
167
|
}
|
|
115
168
|
return dataType;
|
|
116
169
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "duckerd",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A CLI tool for generating ERD diagrams from DuckDB databases",
|
|
5
5
|
"author": "TobiLG <tobilg@gmail.com>",
|
|
6
6
|
"repository": {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "rm -rf dist && tsc",
|
|
20
|
-
"start": "ts-node src/index.ts"
|
|
20
|
+
"start": "ts-node src/index.ts",
|
|
21
|
+
"test": "ts-node src/lib/metadata.test.ts"
|
|
21
22
|
},
|
|
22
23
|
"engines": {
|
|
23
24
|
"node": ">=22.0"
|