dbtour 0.2.0 → 0.2.4
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/bin/dbtour.mjs +3 -2
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +156 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.js +75 -0
- package/dist/index.js +6 -0
- package/dist/model.d.ts +121 -0
- package/dist/model.js +210 -0
- package/dist/notes.d.ts +21 -0
- package/dist/notes.js +115 -0
- package/dist/plugins.d.ts +18 -0
- package/dist/plugins.js +50 -0
- package/dist/render/markdown.d.ts +8 -0
- package/dist/render/markdown.js +142 -0
- package/dist/render/mermaid.d.ts +6 -0
- package/dist/render/mermaid.js +72 -0
- package/package.json +17 -5
- package/src/cli.ts +0 -156
- package/src/config.ts +0 -93
- package/src/model.ts +0 -318
- package/src/notes.ts +0 -130
- package/src/plugins.ts +0 -63
- package/src/render/markdown.ts +0 -147
- package/src/render/mermaid.ts +0 -75
- /package/{src/index.ts → dist/index.d.ts} +0 -0
package/src/render/markdown.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
// Markdown GitHub shows: an index with every table, and one page per table with its columns, keys,
|
|
2
|
-
// relations both ways, indexes, constraints and the diagram of its neighbourhood.
|
|
3
|
-
import { findTable, isUniqueColumn, relationsOf, tableRef, type Database, type Model, type Table } from '../model.ts';
|
|
4
|
-
import { erDiagram, neighbourhood } from './mermaid.ts';
|
|
5
|
-
|
|
6
|
-
// Mermaid lays out a whole database only while it is small; above this many tables the index shows the
|
|
7
|
-
// groups and each table page shows its own neighbourhood instead.
|
|
8
|
-
export const wholeDiagramLimit = 40;
|
|
9
|
-
|
|
10
|
-
export function renderMarkdown(model: Model): Record<string, string> {
|
|
11
|
-
const files: Record<string, string> = { 'README.md': renderIndex(model) };
|
|
12
|
-
for (const database of model.databases) {
|
|
13
|
-
for (const schema of database.schemas) {
|
|
14
|
-
for (const table of schema.tables) {
|
|
15
|
-
const ref = tableRef(schema.name, table.name);
|
|
16
|
-
files[`tables/${ref}.md`] = renderTable(model, ref, table);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return files;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function renderIndex(model: Model): string {
|
|
24
|
-
const parts: string[] = [];
|
|
25
|
-
const single = model.databases.length === 1;
|
|
26
|
-
if (!single) parts.push('# Databases\n');
|
|
27
|
-
for (const database of model.databases) {
|
|
28
|
-
parts.push(`${single ? '#' : '##'} ${database.name}\n`);
|
|
29
|
-
if (database.description) parts.push(`${database.description}\n`);
|
|
30
|
-
parts.push(`${driverLine(database)}\n`);
|
|
31
|
-
parts.push('| Table | Kind | Columns | Description |\n|---|---|---|---|');
|
|
32
|
-
for (const schema of database.schemas) {
|
|
33
|
-
for (const table of schema.tables) {
|
|
34
|
-
const ref = tableRef(schema.name, table.name);
|
|
35
|
-
parts.push(`| [${ref}](tables/${ref}.md) | ${table.kind} | ${table.columns.length} | ${cell(table.description)} |`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
parts.push('');
|
|
39
|
-
}
|
|
40
|
-
for (const group of model.groups) {
|
|
41
|
-
parts.push(`## ${group.name}\n`);
|
|
42
|
-
if (group.description) parts.push(`${group.description}\n`);
|
|
43
|
-
parts.push(group.tables.map((ref) => `- [${ref}](tables/${ref}.md)`).join('\n') + '\n');
|
|
44
|
-
parts.push(mermaid(erDiagram(model, group.tables)));
|
|
45
|
-
}
|
|
46
|
-
const refs = model.databases.flatMap((d) => d.schemas.flatMap((s) => s.tables.map((t) => tableRef(s.name, t.name))));
|
|
47
|
-
if (refs.length <= wholeDiagramLimit) {
|
|
48
|
-
parts.push('## Diagram\n');
|
|
49
|
-
parts.push(mermaid(erDiagram(model, refs)));
|
|
50
|
-
} else {
|
|
51
|
-
parts.push(`## Diagram\n\n${refs.length} tables is too many for one diagram: each table's page draws its own neighbourhood.\n`);
|
|
52
|
-
}
|
|
53
|
-
return parts.join('\n');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** The product's name for a driver: mssql is SQL Server. */
|
|
57
|
-
export function driverName(driver: string): string {
|
|
58
|
-
const names: Record<string, string> = { mssql: 'SQL Server', postgres: 'PostgreSQL', mysql: 'MySQL', sqlite: 'SQLite' };
|
|
59
|
-
return names[driver] ?? driver;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function driverLine(database: Database): string {
|
|
63
|
-
const name = driverName(database.driver);
|
|
64
|
-
return database.version ? `${name} ${database.version}.` : `${name}.`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** One table's page on its own: what the page hands to an agent. */
|
|
68
|
-
export function renderTableMarkdown(model: Model, ref: string): string {
|
|
69
|
-
const found = findTable(model, ref);
|
|
70
|
-
if (!found) throw new Error(`${ref} is not a table in the model`);
|
|
71
|
-
return renderTable(model, ref, found.table);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function renderTable(model: Model, ref: string, table: Table): string {
|
|
75
|
-
const parts: string[] = [`# ${ref}\n`];
|
|
76
|
-
if (table.description) parts.push(`${table.description}\n`);
|
|
77
|
-
const relations = relationsOf(model, ref);
|
|
78
|
-
|
|
79
|
-
parts.push('| Column | Type | Nullable | Default | Key | Description |\n|---|---|---|---|---|---|');
|
|
80
|
-
for (const column of table.columns) {
|
|
81
|
-
const keys: string[] = [];
|
|
82
|
-
if (table.primaryKey?.includes(column.name)) keys.push('PK');
|
|
83
|
-
for (const relation of relations.out) {
|
|
84
|
-
const i = relation.from.columns.indexOf(column.name);
|
|
85
|
-
if (i === -1) continue;
|
|
86
|
-
const { table: toTable, columns } = relation.to;
|
|
87
|
-
keys.push(`FK → [${toTable}](${toTable}.md).${columns[i] ?? columns.join(',')}${relation.virtual ? ' (notes)' : ''}`);
|
|
88
|
-
}
|
|
89
|
-
if (isUniqueColumn(table, column.name)) keys.push('unique');
|
|
90
|
-
if (column.identity) keys.push('identity');
|
|
91
|
-
if (column.computed !== undefined) keys.push(`computed \`${column.computed}\``);
|
|
92
|
-
const dflt = column.default === undefined ? '' : `\`${column.default}\``;
|
|
93
|
-
parts.push(`| ${cell(column.name)} | ${cell(column.type)} | ${column.nullable ? 'yes' : 'no'} | ${cell(dflt)} | ${keys.join(', ')} | ${cell(column.description)} |`);
|
|
94
|
-
}
|
|
95
|
-
parts.push('');
|
|
96
|
-
|
|
97
|
-
if (relations.in.length > 0) {
|
|
98
|
-
parts.push('## Referenced by\n');
|
|
99
|
-
const lines = relations.in
|
|
100
|
-
.map((r) => `- [${r.from.table}](${r.from.table}.md).${r.from.columns.join(', ')}${r.virtual ? ' (notes)' : ''}`)
|
|
101
|
-
.sort();
|
|
102
|
-
parts.push(lines.join('\n') + '\n');
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (table.indexes.length > 0) {
|
|
106
|
-
parts.push('## Indexes\n');
|
|
107
|
-
parts.push('| Name | Columns | Unique |\n|---|---|---|');
|
|
108
|
-
for (const index of table.indexes) {
|
|
109
|
-
const columns = index.columns.join(', ') + (index.included?.length ? ` (includes ${index.included.join(', ')})` : '');
|
|
110
|
-
parts.push(`| ${cell(index.name)} | ${cell(columns)} | ${index.unique ? 'yes' : 'no'} |`);
|
|
111
|
-
}
|
|
112
|
-
parts.push('');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (table.constraints.length > 0) {
|
|
116
|
-
parts.push('## Constraints\n');
|
|
117
|
-
parts.push('| Name | Kind | Columns | Definition |\n|---|---|---|---|');
|
|
118
|
-
for (const constraint of table.constraints) {
|
|
119
|
-
const definition = constraint.definition === undefined ? '' : `\`${constraint.definition}\``;
|
|
120
|
-
parts.push(`| ${cell(constraint.name)} | ${constraint.kind} | ${cell(constraint.columns.join(', '))} | ${cell(definition)} |`);
|
|
121
|
-
}
|
|
122
|
-
parts.push('');
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (table.definition !== undefined) {
|
|
126
|
-
parts.push('## Definition\n');
|
|
127
|
-
parts.push('```sql\n' + table.definition.trim() + '\n```\n');
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const around = neighbourhood(model, ref);
|
|
131
|
-
if (around.length > 1) {
|
|
132
|
-
parts.push('## Diagram\n');
|
|
133
|
-
parts.push(mermaid(erDiagram(model, around)));
|
|
134
|
-
}
|
|
135
|
-
return parts.join('\n');
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function mermaid(diagram: string): string {
|
|
139
|
-
return '```mermaid\n' + diagram + '```\n';
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** A table cell: pipes and line breaks would end it. */
|
|
143
|
-
function cell(text: string | undefined): string {
|
|
144
|
-
return (text ?? '').replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export { findTable };
|
package/src/render/mermaid.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
// Mermaid erDiagram text: GitHub draws it in the Markdown, no image service needed.
|
|
2
|
-
import { findTable, relationsOf, type Model, type Relation, type Table } from '../model.ts';
|
|
3
|
-
|
|
4
|
-
/** Mermaid takes letters, digits and underscores in an entity name. */
|
|
5
|
-
export function entityName(ref: string): string {
|
|
6
|
-
return ref.replace(/[^A-Za-z0-9_]/g, '_');
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/** The table and every table one relation away, in the order the relations are listed. */
|
|
10
|
-
export function neighbourhood(model: Model, ref: string): string[] {
|
|
11
|
-
const refs = [ref];
|
|
12
|
-
for (const relation of model.databases.flatMap((d) => d.relations)) {
|
|
13
|
-
const other = relation.from.table === ref ? relation.to.table : relation.to.table === ref ? relation.from.table : undefined;
|
|
14
|
-
if (other !== undefined && !refs.includes(other)) refs.push(other);
|
|
15
|
-
}
|
|
16
|
-
return refs;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function erDiagram(model: Model, refs: string[]): string {
|
|
20
|
-
const lines = ['erDiagram'];
|
|
21
|
-
const shown = new Set(refs);
|
|
22
|
-
for (const ref of refs) {
|
|
23
|
-
const found = findTable(model, ref);
|
|
24
|
-
if (!found) continue;
|
|
25
|
-
const foreign = new Set(relationsOf(model, ref).out.flatMap((r) => r.from.columns));
|
|
26
|
-
lines.push(` ${entityName(ref)} {`);
|
|
27
|
-
for (const column of found.table.columns) {
|
|
28
|
-
const keys = keyMarks(found.table, column.name, foreign);
|
|
29
|
-
lines.push(` ${attributeType(column.type)} ${attributeName(column.name)}${keys ? ' ' + keys : ''}`);
|
|
30
|
-
}
|
|
31
|
-
lines.push(' }');
|
|
32
|
-
}
|
|
33
|
-
for (const relation of model.databases.flatMap((d) => d.relations)) {
|
|
34
|
-
if (!shown.has(relation.from.table) || !shown.has(relation.to.table)) continue;
|
|
35
|
-
lines.push(` ${entityName(relation.from.table)} ${cardinality(model, relation)} ${entityName(relation.to.table)} : ${label(relation)}`);
|
|
36
|
-
}
|
|
37
|
-
return lines.join('\n') + '\n';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function keyMarks(table: Table, column: string, foreign: Set<string>): string {
|
|
41
|
-
const marks: string[] = [];
|
|
42
|
-
if (table.primaryKey?.includes(column)) marks.push('PK');
|
|
43
|
-
if (foreign.has(column)) marks.push('FK');
|
|
44
|
-
if (!marks.includes('PK') && table.constraints.some((c) => c.kind === 'unique' && c.columns.length === 1 && c.columns[0] === column)) marks.push('UK');
|
|
45
|
-
return marks.join(',');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Mermaid 11 reads a type as one word of letters, digits, underscores, hyphens, brackets and the commas
|
|
49
|
-
// inside them (decimal(18,2) is fine), and a name as one word; it does not take quotes on either, so anything
|
|
50
|
-
// else becomes an underscore.
|
|
51
|
-
function attributeType(type: string): string {
|
|
52
|
-
return type.replace(/^[^A-Za-z_]|[^A-Za-z0-9_,()[\]-]/g, '_');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function attributeName(name: string): string {
|
|
56
|
-
return name.replace(/^[^A-Za-z_]|[^A-Za-z0-9_-]/g, '_');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** many-to-one unless the referencing columns are themselves a key, then one-to-one. Dotted when virtual. */
|
|
60
|
-
function cardinality(model: Model, relation: Relation): string {
|
|
61
|
-
const from = findTable(model, relation.from.table)?.table;
|
|
62
|
-
const columns = relation.from.columns.join(',');
|
|
63
|
-
const isKey =
|
|
64
|
-
from !== undefined &&
|
|
65
|
-
(from.primaryKey?.join(',') === columns ||
|
|
66
|
-
from.constraints.some((c) => c.kind === 'unique' && c.columns.join(',') === columns) ||
|
|
67
|
-
from.indexes.some((i) => i.unique && i.columns.join(',') === columns));
|
|
68
|
-
const line = relation.virtual ? '..' : '--';
|
|
69
|
-
return `${isKey ? '|o' : '}o'}${line}||`;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function label(relation: Relation): string {
|
|
73
|
-
const text = relation.from.columns.join(', ') + (relation.virtual ? ' (notes)' : '');
|
|
74
|
-
return /^[A-Za-z0-9_]+$/.test(text) ? text : `"${text.replace(/"/g, "'")}"`;
|
|
75
|
-
}
|
|
File without changes
|