duckerd 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # DuckERD CLI
2
+
3
+ A CLI tool for generating ERD diagrams from DuckDB databases.
4
+
5
+ ## Prerequisites
6
+
7
+ - Node.js (v18.19.0 or later)
8
+
9
+ ## Installation
10
+
11
+ To install the CLI tool, run:
12
+
13
+ ```
14
+ npm install -g duckerd
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```
20
+ duckerd [options]
21
+ ```
22
+
23
+ Generate an ERD diagram of the database schemas.
24
+
25
+ #### Options:
26
+
27
+ - `-d, --database <path>`: Path to the database file
28
+ - `-t, --theme [theme]`: Theme of the chart (choices: `default`, `forest`, `dark`, `neutral`, default: `default`)
29
+ - `-o, --output [output]`: Path to the output file
30
+ - `-w, --width [width]`: Width of the page (default: `1024`)
31
+ - `-H, --height [height]`: Height of the page (default: `768`)
32
+ - `-f, --outputFormat [format]`: Output format for the generated image (choices: `svg`, `png`, `pdf`, default: `png`)
33
+
34
+ #### Example:
35
+
36
+ ```bash
37
+ duckerd -d ./mydb.duckdb -o ./erd.png -f png -t neutral -w 1600
38
+ ```
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createERD = void 0;
4
+ const duckdb_async_1 = require("duckdb-async");
5
+ const metadata_1 = require("../lib/metadata");
6
+ const createERD = async (databasePath) => {
7
+ const db = await duckdb_async_1.Database.create(databasePath);
8
+ const conn = await db.connect();
9
+ const metadata = await (0, metadata_1.getMetadata)(conn);
10
+ await db.close();
11
+ const mermaidCode = (0, metadata_1.generateMermaidCodeForAllDBs)(metadata);
12
+ return mermaidCode;
13
+ };
14
+ exports.createERD = createERD;
package/dist/index.js ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || function (mod) {
20
+ if (mod && mod.__esModule) return mod;
21
+ var result = {};
22
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
23
+ __setModuleDefault(result, mod);
24
+ return result;
25
+ };
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ const commander_1 = require("commander");
28
+ const path = __importStar(require("path"));
29
+ const child_process_1 = require("child_process");
30
+ const fs = __importStar(require("fs"));
31
+ const erd_1 = require("./commands/erd");
32
+ const program = new commander_1.Command();
33
+ program
34
+ .version('0.1.0')
35
+ .description('A CLI tool for generating ERD diagrams from DuckDB databases')
36
+ .option('-d, --database <path>', 'Path to the database file')
37
+ .option('-t, --theme [theme]', 'Theme of the chart (choices: "default", "forest", "dark", "neutral", default: "default")')
38
+ .option('-o, --output <path>', 'Path to the output file')
39
+ .option('-w, --width [width]', 'Width of the page (default: 1024)')
40
+ .option('-H, --height [height]', 'Height of the page (default: 768)')
41
+ .option('-f, --outputFormat [format]', 'Output format for the generated image. (choices: "svg", "png", "pdf")')
42
+ .action((options) => {
43
+ const dbPath = options.database ? path.resolve(options.database) : ':memory:';
44
+ console.log('Generating ERD diagram...');
45
+ // Check if the database file exists
46
+ if (dbPath !== ':memory:' && !fs.existsSync(dbPath)) {
47
+ console.error(`Error: Database file not found at ${dbPath}`);
48
+ process.exit(1);
49
+ }
50
+ runErd(dbPath, options);
51
+ });
52
+ const runErd = async (dbPath, options) => {
53
+ const mermaidDiagram = await (0, erd_1.createERD)(dbPath);
54
+ const mermaidFile = path.resolve('schema_erd.mmd');
55
+ if (mermaidDiagram) {
56
+ fs.writeFileSync(mermaidFile, mermaidDiagram);
57
+ // Extract the database name from the dbPath
58
+ const dbName = dbPath === ':memory:' ? 'memory_db' : path.basename(dbPath, path.extname(dbPath));
59
+ const outputFile = options.output || `${dbName}_erd.${options.outputFormat || 'png'}`;
60
+ const width = options.width || 1024;
61
+ const height = options.height || 768;
62
+ const theme = options.theme || 'default';
63
+ const command = `npx mmdc -i ${mermaidFile} -o ${outputFile} -t ${theme} -w ${width} -H ${height}`;
64
+ (0, child_process_1.exec)(command, (error, stdout, stderr) => {
65
+ if (error) {
66
+ console.error(`Error generating ERD: ${error.message}`);
67
+ return;
68
+ }
69
+ if (stderr) {
70
+ console.error(`ERD generation stderr: ${stderr}`);
71
+ return;
72
+ }
73
+ console.log(`ERD diagram generated: ${outputFile}`);
74
+ fs.unlinkSync(mermaidFile);
75
+ });
76
+ }
77
+ };
78
+ program.parse(process.argv);
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanitizeDataType = exports.generateMermaidCodeForAllDBs = exports.getMetadata = void 0;
4
+ // Database metadata query
5
+ const databaseMetadataQuery = `SELECT database_name as databaseName FROM duckdb_databases() WHERE database_name NOT IN ('system', 'temp') ORDER BY database_name`;
6
+ // Schema metadata query
7
+ const schemaMetadataQuery = `SELECT database_name as databaseName, schema_name as schemaName FROM duckdb_schemas() WHERE database_name NOT IN ('system', 'temp') and schema_name NOT IN ('information_schema', 'pg_catalog') ORDER BY database_name, schema_name`;
8
+ // Table metadata query
9
+ const tablesMetadataQuery = `SELECT database_name as databaseName, schema_name as schemaName, table_name as name, has_primary_key as hasPrimaryKey, estimated_size as estimatedRowCount, column_count as columnCount, index_count as indexCount, check_constraint_count as checkConstraintCount, sql FROM duckdb_tables() WHERE internal = false ORDER BY database_name, schema_name, table_name`;
10
+ // Column metadata query
11
+ const columnsMetadataQuery = `SELECT database_name as databaseName, schema_name as schemaName, table_name as tableName, column_name as name, data_type as dataType, numeric_precision as precision, numeric_scale as scale, is_nullable as isNullable FROM duckdb_columns() WHERE internal = false ORDER BY database_name, schema_name, table_name, column_index`;
12
+ // Index metadata query
13
+ const indexesMetadataQuery = `SELECT database_name as databaseName, schema_name as schemaName, table_name as tableName, index_name as name, is_unique as isUnique, sql FROM duckdb_indexes() ORDER BY database_name, schema_name, table_name, index_name`;
14
+ // Constraints metadata query
15
+ const contraintsMetadataQuery = `SELECT database_name as databaseName, schema_name as schemaName, table_name as tableName, unnest(constraint_column_names) as columnName, constraint_type as constraintType, constraint_text as sql FROM duckdb_constraints() ORDER BY database_name, schema_name, table_name`;
16
+ // Constraints metadata query
17
+ const sequencesMetadataQuery = `SELECT database_name as databaseName, schema_name as schemaName, sequence_name as name, temporary as isTemporary, start_value as startValue, last_value as lastValue, min_value as minValue, max_value as maxValue, increment_by as incrementBy, sql FROM duckdb_sequences() ORDER BY database_name, schema_name, sequence_name`;
18
+ const getMetadata = async (conn) => {
19
+ const databaseRaw = await conn.all(databaseMetadataQuery);
20
+ const schemas = await conn.all(schemaMetadataQuery);
21
+ const tables = await conn.all(tablesMetadataQuery);
22
+ const columns = await conn.all(columnsMetadataQuery);
23
+ const indexes = await conn.all(indexesMetadataQuery);
24
+ const constraints = await conn.all(contraintsMetadataQuery);
25
+ const sequences = await conn.all(sequencesMetadataQuery);
26
+ const databases = databaseRaw.map(database => ({
27
+ name: database.databaseName,
28
+ schemas: [...new Set(schemas.filter(schema => database.databaseName === schema.databaseName).map(schema => ({
29
+ name: schema.schemaName,
30
+ tables: [...new Set(tables.filter(table => schema.databaseName === table.databaseName && schema.schemaName === table.schemaName).map(table => ({
31
+ databaseName: database.databaseName,
32
+ schemaName: schema.schemaName,
33
+ name: table.name,
34
+ hasPrimaryKey: table.hasPrimaryKey,
35
+ estimatedRowCount: table.estimatedRowCount,
36
+ columnCount: table.columnCount,
37
+ indexCount: table.indexCount,
38
+ checkConstraintCount: table.checkConstraintCount,
39
+ sql: table.sql,
40
+ columns: [...new Set(columns.filter(column => column.databaseName === table.databaseName && column.schemaName === table.schemaName && column.tableName === table.name).map(column => ({
41
+ name: column.name,
42
+ dataType: column.dataType,
43
+ precision: column.precision,
44
+ scale: column.scale,
45
+ isNullable: column.isNullable,
46
+ })))],
47
+ indexes: [...new Set(indexes.filter(index => index.databaseName === table.databaseName && index.schemaName === table.schemaName && index.tableName === table.name).map(index => ({
48
+ name: index.name,
49
+ isUnique: index.isUnique,
50
+ sql: index.sql,
51
+ })))],
52
+ constraints: [...new Set(constraints.filter((constraint) => constraint.databaseName === table.databaseName && constraint.schemaName === table.schemaName && constraint.tableName === table.name).map(constraint => ({
53
+ columnName: constraint.columnName,
54
+ constraintType: constraint.constraintType,
55
+ sql: constraint.sql,
56
+ })))],
57
+ })))],
58
+ sequences: [...new Set(sequences.filter(sequence => sequence.databaseName === schema.databaseName && sequence.schemaName === schema.schemaName).map(sequence => ({
59
+ name: sequence.name,
60
+ isTemporary: sequence.isTemporary,
61
+ startValue: sequence.startValue,
62
+ lastValue: sequence.lastValue,
63
+ minValue: sequence.minValue,
64
+ maxValue: sequence.maxValue,
65
+ incrementBy: sequence.incrementBy,
66
+ sql: sequence.sql,
67
+ })))],
68
+ })))],
69
+ }));
70
+ const metadata = {
71
+ databases,
72
+ };
73
+ return metadata;
74
+ };
75
+ exports.getMetadata = getMetadata;
76
+ const generateMermaidCodeForAllDBs = (metadata) => {
77
+ let mermaidCode = `erDiagram
78
+
79
+ `;
80
+ if (metadata.databases && metadata.databases.length > 0 && metadata.databases[0].schemas && metadata.databases[0].schemas?.length > 0) {
81
+ // Get tables
82
+ const tables = metadata.databases.map((db) => db.schemas.map((s) => s.tables).flat()).flat();
83
+ if (tables && tables.length > 0) {
84
+ // Add tables, columns and constraints
85
+ mermaidCode += tables.map((table) => {
86
+ return `"${table.databaseName}.${table.name}" {
87
+ ${table.columns.map((column) => ` ${(0, exports.sanitizeDataType)(column.dataType.toUpperCase())} ${column.name} ${table.constraints?.filter((constraint) => constraint.columnName === column.name).map((constraint) => ((constraint.constraintType === "PRIMARY KEY" ? "PK" : "") || (constraint.constraintType === "FOREIGN KEY" ? "FK" : "") || "")).filter(str => str)}`).join("\n")}
88
+ }\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")}`;
89
+ }).join("\n ");
90
+ return mermaidCode;
91
+ }
92
+ else {
93
+ return undefined;
94
+ }
95
+ }
96
+ else {
97
+ return undefined;
98
+ }
99
+ };
100
+ exports.generateMermaidCodeForAllDBs = generateMermaidCodeForAllDBs;
101
+ const sanitizeDataType = (dataType) => {
102
+ return dataType.startsWith("STRUCT(") ? "STRUCT" : dataType;
103
+ };
104
+ exports.sanitizeDataType = sanitizeDataType;
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "duckerd",
3
+ "version": "0.1.0",
4
+ "description": "A CLI tool for generating ERD diagrams from DuckDB databases",
5
+ "author": "TobiLG <tobilg@gmail.com>",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/tobilg/duckerd.git"
9
+ },
10
+ "license": "MIT",
11
+ "bugs": {
12
+ "url": "https://github.com/tobilg/duckerd/issues"
13
+ },
14
+ "homepage": "https://github.com/tobilg/duckerd#readme",
15
+ "bin": {
16
+ "duckerd": "dist/index.js"
17
+ },
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "start": "ts-node src/index.ts"
21
+ },
22
+ "engines": {
23
+ "node": "^18.19 || >=20.0"
24
+ },
25
+ "keywords": ["duckdb", "erd", "diagram", "cli"],
26
+ "dependencies": {
27
+ "@mermaid-js/mermaid-cli": "^11.1.1",
28
+ "commander": "^12.1.0",
29
+ "duckdb-async": "^1.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^18.11.9",
33
+ "ts-node": "^10.9.1",
34
+ "typescript": "^5.6.2"
35
+ }
36
+ }