ts-prorm-orm 1.2.2 → 2.0.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/CHANGELOG.md +209 -0
- package/README.md +32 -24
- package/dist/compliance/index.d.ts +2 -0
- package/dist/compliance/index.js +13 -1
- package/dist/connection-manager.js +13 -1
- package/dist/core/expressions.d.ts +58 -0
- package/dist/core/expressions.js +83 -0
- package/dist/core/hooks-manager.d.ts +179 -0
- package/dist/core/hooks-manager.js +291 -0
- package/dist/core/types.d.ts +235 -0
- package/dist/core/types.js +9 -0
- package/dist/decorators/index.d.ts +2 -0
- package/dist/decorators/index.js +11 -1
- package/dist/diagrams/chen-diagram.d.ts +15 -12
- package/dist/diagrams/chen-diagram.js +76 -85
- package/dist/diagrams/class-diagram.d.ts +9 -8
- package/dist/diagrams/class-diagram.js +60 -82
- package/dist/diagrams/core/index.d.ts +13 -0
- package/dist/diagrams/core/index.js +33 -0
- package/dist/diagrams/core/layout.d.ts +104 -0
- package/dist/diagrams/core/layout.js +223 -0
- package/dist/diagrams/{svg-dom.d.ts → core/svg.d.ts} +8 -20
- package/dist/diagrams/{svg-dom.js → core/svg.js} +21 -29
- package/dist/diagrams/core/theme.d.ts +84 -0
- package/dist/diagrams/core/theme.js +62 -0
- package/dist/diagrams/dependency-diagram.d.ts +15 -14
- package/dist/diagrams/dependency-diagram.js +71 -105
- package/dist/diagrams/er-diagram.d.ts +13 -26
- package/dist/diagrams/er-diagram.js +180 -259
- package/dist/diagrams/flow-diagram.d.ts +16 -11
- package/dist/diagrams/flow-diagram.js +96 -91
- package/dist/diagrams/gantt-diagram.d.ts +10 -5
- package/dist/diagrams/gantt-diagram.js +77 -103
- package/dist/diagrams/index-diagram.d.ts +10 -10
- package/dist/diagrams/index-diagram.js +106 -116
- package/dist/diagrams/index.d.ts +6 -9
- package/dist/diagrams/index.js +9 -24
- package/dist/diagrams/migration-diagram.d.ts +8 -4
- package/dist/diagrams/migration-diagram.js +189 -161
- package/dist/diagrams/model-diagram.d.ts +34 -34
- package/dist/diagrams/model-diagram.js +106 -485
- package/dist/diagrams/package-diagram.d.ts +9 -6
- package/dist/diagrams/package-diagram.js +64 -101
- package/dist/diagrams/relational-diagram.d.ts +12 -10
- package/dist/diagrams/relational-diagram.js +87 -114
- package/dist/diagrams/schemadoc-diagram.d.ts +8 -8
- package/dist/diagrams/schemadoc-diagram.js +71 -93
- package/dist/diagrams/sequence-diagram.d.ts +10 -10
- package/dist/diagrams/sequence-diagram.js +64 -89
- package/dist/diagrams/state-diagram.d.ts +10 -9
- package/dist/diagrams/state-diagram.js +69 -92
- package/dist/diagrams/tree-diagram.d.ts +11 -5
- package/dist/diagrams/tree-diagram.js +41 -65
- package/dist/dialects/clickhouse/index.js +15 -0
- package/dist/dialects/cockroachdb/index.d.ts +1 -1
- package/dist/dialects/cockroachdb/index.js +32 -63
- package/dist/dialects/db2/index.js +23 -8
- package/dist/dialects/dialect.d.ts +29 -5
- package/dist/dialects/duckdb/index.js +9 -2
- package/dist/dialects/hana/index.js +22 -5
- package/dist/dialects/lock-clause-helper.d.ts +167 -0
- package/dist/dialects/lock-clause-helper.js +316 -0
- package/dist/dialects/mariadb/index.d.ts +7 -4
- package/dist/dialects/mariadb/index.js +36 -60
- package/dist/dialects/mssql/index.js +34 -3
- package/dist/dialects/mysql/index.d.ts +1 -1
- package/dist/dialects/mysql/index.js +17 -45
- package/dist/dialects/oracle/index.d.ts +14 -3
- package/dist/dialects/oracle/index.js +66 -36
- package/dist/dialects/order-expression-helper.d.ts +72 -0
- package/dist/dialects/order-expression-helper.js +154 -0
- package/dist/dialects/partial-index.d.ts +52 -0
- package/dist/dialects/partial-index.js +251 -0
- package/dist/dialects/postgres/index.d.ts +1 -1
- package/dist/dialects/postgres/index.js +43 -68
- package/dist/dialects/redshift/index.js +9 -2
- package/dist/dialects/snowflake/index.js +17 -0
- package/dist/dialects/spanner/index.js +7 -2
- package/dist/dialects/sqlite/driver.d.ts +55 -0
- package/dist/dialects/sqlite/driver.js +102 -0
- package/dist/dialects/sqlite/index.d.ts +1 -1
- package/dist/dialects/sqlite/index.js +49 -27
- package/dist/graph/base-graph-dialect.d.ts +24 -3
- package/dist/graph/base-graph-dialect.js +43 -2
- package/dist/graph/dgraph/index.js +6 -0
- package/dist/graph/gremlin/index.d.ts +6 -0
- package/dist/graph/gremlin/index.js +12 -1
- package/dist/graph/index.d.ts +1 -1
- package/dist/graph/neo4j/index.d.ts +2 -0
- package/dist/graph/neo4j/index.js +6 -1
- package/dist/graph/types.d.ts +12 -1
- package/dist/index.d.ts +13 -4
- package/dist/index.js +54 -7
- package/dist/logging/index.d.ts +10 -0
- package/dist/logging/index.js +23 -0
- package/dist/logging/query-logging.d.ts +82 -0
- package/dist/logging/query-logging.js +102 -0
- package/dist/models/decorators.d.ts +39 -3
- package/dist/models/decorators.js +74 -0
- package/dist/models/eager-load.d.ts +99 -0
- package/dist/models/eager-load.js +348 -5
- package/dist/models/indexes.d.ts +3 -2
- package/dist/models/indexes.js +7 -1
- package/dist/models/model.js +69 -43
- package/dist/prorm.d.ts +175 -409
- package/dist/prorm.js +1142 -557
- package/dist/query-builders/order-limit-builder.js +12 -10
- package/dist/query-builders/sql-compiler.d.ts +10 -3
- package/dist/query-builders/sql-compiler.js +14 -43
- package/dist/query-interface.d.ts +2 -1
- package/dist/sql-constants.d.ts +44 -0
- package/dist/sql-constants.js +138 -1
- package/dist/types/index.d.ts +53 -17
- package/dist/types/index.js +10 -15
- package/dist/types/query-types.d.ts +26 -0
- package/dist/types/query-types.js +30 -0
- package/package.json +32 -5
- package/dist/decorators/belongs-to-many.js +0 -115
- package/dist/decorators/belongs-to.js +0 -115
- package/dist/decorators/has-many.js +0 -127
- package/dist/decorators/has-one.js +0 -116
- package/dist/diagrams/palette.d.ts +0 -138
- package/dist/diagrams/palette.js +0 -194
- package/dist/hooks/hooks-manager.d.ts +0 -189
- package/dist/hooks/hooks-manager.js +0 -350
- package/dist/hooks/index.d.ts +0 -8
- package/dist/hooks/index.js +0 -37
- /package/dist/{logging.d.ts → logging/logger.d.ts} +0 -0
- /package/dist/{logging.js → logging/logger.js} +0 -0
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* ERDiagram
|
|
4
|
-
* a collection of ORM models and their associations
|
|
5
|
-
* blueprint design system.
|
|
3
|
+
* ERDiagram - generates a fully-rendered Entity-Relationship diagram SVG for
|
|
4
|
+
* a collection of ORM models and their associations.
|
|
6
5
|
*
|
|
7
6
|
* Look:
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
11
|
-
* accent / accent-2 (two-tone) header bar — no vibrant gradients or shadows
|
|
12
|
-
* - Compact rows: name (mono) + type (mono, neutral) + PK/FK marks
|
|
7
|
+
* - White entity cards on a grid: square corners, 1px hairline borders, a
|
|
8
|
+
* solid header bar - no gradients or shadows
|
|
9
|
+
* - Compact rows: name (mono) + type (mono, muted) + PK/FK marks
|
|
13
10
|
* - Hairline crow's-foot relationship connectors with mono cardinality marks
|
|
14
|
-
* - Legend bottom-right
|
|
15
|
-
* - Fully self-contained SVG (
|
|
11
|
+
* - Legend bottom-right
|
|
12
|
+
* - Fully self-contained SVG (system font stack, no web-font import)
|
|
16
13
|
*
|
|
17
14
|
* Usage:
|
|
18
15
|
* const er = new ERDiagram({
|
|
@@ -58,15 +55,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
58
55
|
exports.ERDiagram = void 0;
|
|
59
56
|
const fs = __importStar(require("fs"));
|
|
60
57
|
const path = __importStar(require("path"));
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return String(s ?? '')
|
|
65
|
-
.replace(/&/g, '&')
|
|
66
|
-
.replace(/</g, '<')
|
|
67
|
-
.replace(/>/g, '>')
|
|
68
|
-
.replace(/"/g, '"');
|
|
69
|
-
}
|
|
58
|
+
const svg_1 = require("./core/svg");
|
|
59
|
+
const theme_1 = require("./core/theme");
|
|
60
|
+
const layout_1 = require("./core/layout");
|
|
70
61
|
function resolveType(field) {
|
|
71
62
|
const t = field.type;
|
|
72
63
|
if (typeof t === 'string')
|
|
@@ -79,16 +70,8 @@ function resolveType(field) {
|
|
|
79
70
|
}
|
|
80
71
|
return 'UNKNOWN';
|
|
81
72
|
}
|
|
82
|
-
// ─── ERDiagram class ────────────────────────────────────────────────────────
|
|
83
73
|
class ERDiagram {
|
|
84
74
|
constructor(options) {
|
|
85
|
-
// Resolved Industry colors (populated per-generate()).
|
|
86
|
-
this.accentMid = palette_1.INDUSTRY.accent;
|
|
87
|
-
this.accentText = palette_1.INDUSTRY.accentText;
|
|
88
|
-
this.accentLight = palette_1.INDUSTRY.accentLight;
|
|
89
|
-
this.accent2Mid = palette_1.INDUSTRY.accent2;
|
|
90
|
-
this.accent2Text = palette_1.INDUSTRY.accent2Text;
|
|
91
|
-
this.accent2Light = palette_1.INDUSTRY.accent2Light;
|
|
92
75
|
this.opts = {
|
|
93
76
|
title: options.title ?? 'Entity-Relationship Diagram',
|
|
94
77
|
columns: options.columns ?? 3,
|
|
@@ -99,19 +82,13 @@ class ERDiagram {
|
|
|
99
82
|
color: options.color ?? '',
|
|
100
83
|
};
|
|
101
84
|
}
|
|
102
|
-
|
|
103
|
-
entityStyle(idx) {
|
|
104
|
-
return idx % 2 === 0
|
|
105
|
-
? { header: this.accentMid, light: this.accentLight, text: this.accentText }
|
|
106
|
-
: { header: this.accent2Mid, light: this.accent2Light, text: this.accent2Text };
|
|
107
|
-
}
|
|
108
|
-
// ── Compute box height for a model ──────────────────────────────────────
|
|
85
|
+
// -- Compute box height for a model -----------------------------------------
|
|
109
86
|
boxHeight(model) {
|
|
110
87
|
const fieldCount = Object.keys(model.fields ?? {}).length;
|
|
111
88
|
const h = ERDiagram.BOX_HDR + fieldCount * ERDiagram.BOX_ROW + ERDiagram.BOX_FOOT;
|
|
112
89
|
return Math.max(h, ERDiagram.MIN_H);
|
|
113
90
|
}
|
|
114
|
-
//
|
|
91
|
+
// -- Compute grid layout -----------------------------------------------------
|
|
115
92
|
computeLayout() {
|
|
116
93
|
const { models, columns, hGap, vGap } = this.opts;
|
|
117
94
|
const W = ERDiagram.BOX_W;
|
|
@@ -120,7 +97,7 @@ class ERDiagram {
|
|
|
120
97
|
const layout = [];
|
|
121
98
|
let col = 0;
|
|
122
99
|
let row = 0;
|
|
123
|
-
// Track row max-heights for vertical positioning
|
|
100
|
+
// Track row max-heights for vertical positioning.
|
|
124
101
|
const rowHeights = [];
|
|
125
102
|
let curRowMaxH = 0;
|
|
126
103
|
models.forEach((m, i) => {
|
|
@@ -134,10 +111,10 @@ class ERDiagram {
|
|
|
134
111
|
col = 0;
|
|
135
112
|
row = 0;
|
|
136
113
|
let rowStart = BANNER + MARGIN;
|
|
137
|
-
models.forEach((m
|
|
114
|
+
models.forEach((m) => {
|
|
138
115
|
const bh = this.boxHeight(m);
|
|
139
116
|
const x = MARGIN + col * (W + hGap);
|
|
140
|
-
const y = rowStart + (rowHeights[row] - bh) / 2;
|
|
117
|
+
const y = rowStart + (rowHeights[row] - bh) / 2;
|
|
141
118
|
layout.push({
|
|
142
119
|
model: m,
|
|
143
120
|
x,
|
|
@@ -156,83 +133,70 @@ class ERDiagram {
|
|
|
156
133
|
});
|
|
157
134
|
return layout;
|
|
158
135
|
}
|
|
159
|
-
//
|
|
160
|
-
renderBox(
|
|
136
|
+
// -- Draw a single entity box -------------------------------------------------
|
|
137
|
+
renderBox(svg, box) {
|
|
161
138
|
const { model, x, y, w, h } = box;
|
|
162
|
-
const style = this.entityStyle(idx);
|
|
163
139
|
const HDR = ERDiagram.BOX_HDR;
|
|
164
140
|
const ROW = ERDiagram.BOX_ROW;
|
|
165
141
|
const FOOT = ERDiagram.BOX_FOOT;
|
|
166
|
-
const div = palette_1.INDUSTRY.divider;
|
|
167
142
|
const fieldEntries = Object.entries(model.fields ?? {});
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
font-family='${palette_1.FONTS.heading}'
|
|
176
|
-
font-size="17" font-weight="600" fill="#FFFFFF">${xmlEsc(model.name)}</text>`;
|
|
143
|
+
// Surface + hairline frame.
|
|
144
|
+
svg.child('rect', { x, y, width: w, height: h, fill: theme_1.INK.ground, stroke: theme_1.INK.line, 'stroke-width': theme_1.STROKE.hair });
|
|
145
|
+
// Header.
|
|
146
|
+
svg.child('rect', { x, y, width: w, height: HDR, fill: theme_1.INK.wash, stroke: theme_1.INK.line, 'stroke-width': theme_1.STROKE.hair });
|
|
147
|
+
svg
|
|
148
|
+
.child('text', { x: x + 12, y: y + 20, 'font-family': theme_1.FONT.family, 'font-size': theme_1.FONT.body + 6, 'font-weight': 'bold', fill: theme_1.INK.line })
|
|
149
|
+
.text(model.name);
|
|
177
150
|
const tbl = model.tableName ?? model.name.toLowerCase() + 's';
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
//
|
|
151
|
+
svg
|
|
152
|
+
.child('text', { x: x + 12, y: y + 35, 'font-family': theme_1.FONT.mono, 'font-size': theme_1.FONT.small - 1, fill: theme_1.INK.muted })
|
|
153
|
+
.text(tbl);
|
|
154
|
+
// Field rows.
|
|
182
155
|
fieldEntries.forEach(([colName, field], fi) => {
|
|
183
156
|
const fy = y + HDR + fi * ROW;
|
|
184
157
|
const isPK = !!field.primaryKey;
|
|
185
158
|
const isFK = !!(field.foreignKey || field.references);
|
|
186
159
|
const typeStr = resolveType(field);
|
|
187
|
-
const rowBg = fi % 2 === 0 ?
|
|
188
|
-
|
|
189
|
-
//
|
|
190
|
-
if (isPK)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
out += `\n <rect x="${x}" y="${fy}" width="3" height="${ROW}" fill="${this.accent2Mid}"/>`;
|
|
160
|
+
const rowBg = fi % 2 === 0 ? theme_1.INK.ground : theme_1.INK.wash;
|
|
161
|
+
svg.child('rect', { x, y: fy, width: w, height: ROW, fill: rowBg });
|
|
162
|
+
// Marker stripe: PK/FK get a heavier stripe than plain columns.
|
|
163
|
+
if (isPK || isFK) {
|
|
164
|
+
svg.child('rect', { x, y: fy, width: 3, height: ROW, fill: theme_1.INK.line });
|
|
165
|
+
}
|
|
194
166
|
const ity = fy + ROW / 2 + 4;
|
|
195
|
-
// PK/FK mark chip.
|
|
196
167
|
if (isPK || isFK) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
168
|
+
svg
|
|
169
|
+
.child('text', { x: x + 12, y: ity, 'font-family': theme_1.FONT.family, 'font-size': theme_1.FONT.small - 1, 'font-weight': 'bold', fill: theme_1.INK.line })
|
|
170
|
+
.text(isPK ? 'PK' : 'FK');
|
|
200
171
|
}
|
|
201
172
|
else {
|
|
202
|
-
|
|
173
|
+
svg.child('circle', { cx: x + 15, cy: fy + ROW / 2, r: 2, fill: theme_1.INK.muted });
|
|
203
174
|
}
|
|
204
175
|
// Column name.
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
fill="${palette_1.INDUSTRY.neutral[700]}" text-anchor="end">${xmlEsc(typTxt)}</text>`;
|
|
216
|
-
// Row separator (hairline).
|
|
217
|
-
out += `\n <line x1="${x}" y1="${fy + ROW}" x2="${x + w}" y2="${fy + ROW}"
|
|
218
|
-
stroke="${div}" stroke-width="1"/>`;
|
|
176
|
+
const nameWeight = isPK || isFK ? 'bold' : 'normal';
|
|
177
|
+
svg
|
|
178
|
+
.child('text', { x: x + 30, y: ity, 'font-family': theme_1.FONT.mono, 'font-size': theme_1.FONT.small + 2, 'font-weight': nameWeight, fill: theme_1.INK.line })
|
|
179
|
+
.text((0, layout_1.truncate)(colName, x + w - 8 - 30 - 70, theme_1.FONT.small + 2, true));
|
|
180
|
+
// Type (right-aligned, mono muted).
|
|
181
|
+
svg
|
|
182
|
+
.child('text', { x: x + w - 8, y: ity, 'font-family': theme_1.FONT.mono, 'font-size': theme_1.FONT.small - 1, fill: theme_1.INK.muted, 'text-anchor': 'end' })
|
|
183
|
+
.text((0, layout_1.truncate)(typeStr, 60, theme_1.FONT.small - 1, true));
|
|
184
|
+
// Row separator.
|
|
185
|
+
svg.child('line', { x1: x, y1: fy + ROW, x2: x + w, y2: fy + ROW, stroke: theme_1.INK.line, 'stroke-width': theme_1.STROKE.hair });
|
|
219
186
|
});
|
|
220
|
-
//
|
|
187
|
+
// Footer.
|
|
221
188
|
const footY = y + HDR + fieldEntries.length * ROW;
|
|
222
|
-
|
|
223
|
-
|
|
189
|
+
svg.child('rect', { x, y: footY, width: w, height: FOOT, fill: theme_1.INK.wash });
|
|
190
|
+
svg.child('line', { x1: x, y1: footY, x2: x + w, y2: footY, stroke: theme_1.INK.line, 'stroke-width': theme_1.STROKE.hair });
|
|
224
191
|
const assocCount = (model.associations ?? []).length;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
192
|
+
svg
|
|
193
|
+
.child('text', { x: x + 12, y: footY + 17, 'font-family': theme_1.FONT.family, 'font-size': theme_1.FONT.small - 1, fill: theme_1.INK.muted })
|
|
194
|
+
.text(`${Object.keys(model.fields ?? {}).length} fields${assocCount > 0 ? `, ${assocCount} associations` : ''}`);
|
|
228
195
|
}
|
|
229
|
-
//
|
|
230
|
-
renderRelationships(layout) {
|
|
196
|
+
// -- Draw relationship lines (crow's-foot) -------------------------------------
|
|
197
|
+
renderRelationships(svg, layout) {
|
|
231
198
|
const byName = new Map();
|
|
232
199
|
layout.forEach((b) => byName.set(b.model.name, b));
|
|
233
|
-
let out = '\n <!-- ═══ RELATIONSHIPS ═══ -->';
|
|
234
|
-
// Single-hue (accent) connectors — blueprint monochrome.
|
|
235
|
-
const color = this.accentMid;
|
|
236
200
|
// Cardinality marks per association type.
|
|
237
201
|
const REL_MARK = {
|
|
238
202
|
hasOne: '1:1',
|
|
@@ -240,7 +204,6 @@ class ERDiagram {
|
|
|
240
204
|
belongsTo: 'N:1',
|
|
241
205
|
belongsToMany: 'N:N',
|
|
242
206
|
};
|
|
243
|
-
// Track drawn pairs to avoid duplicates
|
|
244
207
|
const drawn = new Set();
|
|
245
208
|
layout.forEach((srcBox) => {
|
|
246
209
|
const assocs = srcBox.model.associations ?? [];
|
|
@@ -248,13 +211,11 @@ class ERDiagram {
|
|
|
248
211
|
const tgtBox = byName.get(assoc.target);
|
|
249
212
|
if (!tgtBox)
|
|
250
213
|
return;
|
|
251
|
-
// De-duplicate: only draw once per pair+type
|
|
252
214
|
const pairKey = [srcBox.model.name, assoc.target, assoc.type].sort().join('::');
|
|
253
215
|
if (drawn.has(pairKey))
|
|
254
216
|
return;
|
|
255
217
|
drawn.add(pairKey);
|
|
256
218
|
const label = REL_MARK[assoc.type] ?? assoc.type;
|
|
257
|
-
// Choose connection points (pick nearest horizontal sides)
|
|
258
219
|
let sx, sy, tx, ty;
|
|
259
220
|
let srcSide, tgtSide;
|
|
260
221
|
const srcMidX = srcBox.x + srcBox.w / 2;
|
|
@@ -275,7 +236,7 @@ class ERDiagram {
|
|
|
275
236
|
srcSide = 'left';
|
|
276
237
|
tgtSide = 'right';
|
|
277
238
|
}
|
|
278
|
-
// If stacked vertically (same column), use top/bottom exits
|
|
239
|
+
// If stacked vertically (same column), use top/bottom exits.
|
|
279
240
|
if (Math.abs(srcMidX - tgtMidX) < 40) {
|
|
280
241
|
if (tgtBox.y > srcBox.y) {
|
|
281
242
|
sx = srcBox.x + srcBox.w / 2;
|
|
@@ -289,103 +250,100 @@ class ERDiagram {
|
|
|
289
250
|
tx = tgtBox.x + tgtBox.w / 2;
|
|
290
251
|
ty = tgtBox.y + tgtBox.h;
|
|
291
252
|
}
|
|
292
|
-
srcSide = 'right'; //
|
|
293
|
-
tgtSide = 'left'; //
|
|
253
|
+
srcSide = 'right'; // repurposed to mean 'bottom'
|
|
254
|
+
tgtSide = 'left'; // repurposed to mean 'top'
|
|
294
255
|
}
|
|
295
|
-
// Bezier control points
|
|
296
256
|
const dx = Math.abs(tx - sx);
|
|
297
257
|
const cpOffset = Math.max(dx * 0.5, 60);
|
|
298
258
|
const cpsx = srcSide === 'right' ? sx + cpOffset : sx - cpOffset;
|
|
299
259
|
const cptx = tgtSide === 'left' ? tx - cpOffset : tx + cpOffset;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
260
|
+
svg.child('path', {
|
|
261
|
+
d: `M ${sx} ${sy} C ${cpsx} ${sy}, ${cptx} ${ty}, ${tx} ${ty}`,
|
|
262
|
+
fill: 'none',
|
|
263
|
+
stroke: theme_1.INK.line,
|
|
264
|
+
'stroke-width': theme_1.STROKE.hair,
|
|
265
|
+
});
|
|
304
266
|
const manyAtTarget = assoc.type === 'hasMany' || assoc.type === 'belongsToMany';
|
|
305
267
|
const manyAtSource = assoc.type === 'belongsTo' || assoc.type === 'belongsToMany';
|
|
306
|
-
if (manyAtTarget)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
out += this.singleBar(sx, sy, srcSide === 'right' ? 'left' : 'right', color);
|
|
317
|
-
}
|
|
268
|
+
if (manyAtTarget)
|
|
269
|
+
this.crowFoot(svg, tx, ty, tgtSide === 'left' ? 'right' : 'left');
|
|
270
|
+
else
|
|
271
|
+
this.singleBar(svg, tx, ty, tgtSide === 'left' ? 'right' : 'left');
|
|
272
|
+
if (manyAtSource)
|
|
273
|
+
this.crowFoot(svg, sx, sy, srcSide === 'right' ? 'left' : 'right');
|
|
274
|
+
else
|
|
275
|
+
this.singleBar(svg, sx, sy, srcSide === 'right' ? 'left' : 'right');
|
|
318
276
|
// Cardinality label at midpoint (mono, hairline chip).
|
|
319
277
|
const mx = (sx + tx) / 2;
|
|
320
278
|
const my = (sy + ty) / 2;
|
|
321
279
|
const labelW = label.length * 8 + 14;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
280
|
+
svg.child('rect', {
|
|
281
|
+
x: mx - labelW / 2,
|
|
282
|
+
y: my - 10,
|
|
283
|
+
width: labelW,
|
|
284
|
+
height: 18,
|
|
285
|
+
fill: theme_1.INK.ground,
|
|
286
|
+
stroke: theme_1.INK.line,
|
|
287
|
+
'stroke-width': theme_1.STROKE.hair,
|
|
288
|
+
});
|
|
289
|
+
svg
|
|
290
|
+
.child('text', { x: mx, y: my + 3, 'font-family': theme_1.FONT.mono, 'font-size': theme_1.FONT.small + 1, 'font-weight': 'bold', fill: theme_1.INK.line, 'text-anchor': 'middle' })
|
|
291
|
+
.text(label);
|
|
328
292
|
if (assoc.as) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
293
|
+
svg
|
|
294
|
+
.child('text', { x: mx, y: my + 20, 'font-family': theme_1.FONT.mono, 'font-size': theme_1.FONT.small - 1, fill: theme_1.INK.muted, 'text-anchor': 'middle' })
|
|
295
|
+
.text(`as: ${assoc.as}`);
|
|
332
296
|
}
|
|
333
297
|
});
|
|
334
298
|
});
|
|
335
|
-
return out;
|
|
336
299
|
}
|
|
337
|
-
//
|
|
338
|
-
crowFoot(x, y, dir
|
|
300
|
+
// -- Crow's foot symbol ---------------------------------------------------------
|
|
301
|
+
crowFoot(svg, x, y, dir) {
|
|
339
302
|
const sign = dir === 'right' ? -1 : 1;
|
|
340
303
|
const reach = 12;
|
|
341
304
|
const spread = 7;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
305
|
+
svg.child('line', { x1: x, y1: y, x2: x + sign * reach, y2: y - spread, stroke: theme_1.INK.line, 'stroke-width': theme_1.STROKE.hair });
|
|
306
|
+
svg.child('line', { x1: x, y1: y, x2: x + sign * reach, y2: y, stroke: theme_1.INK.line, 'stroke-width': theme_1.STROKE.hair });
|
|
307
|
+
svg.child('line', { x1: x, y1: y, x2: x + sign * reach, y2: y + spread, stroke: theme_1.INK.line, 'stroke-width': theme_1.STROKE.hair });
|
|
308
|
+
svg.child('line', {
|
|
309
|
+
x1: x + sign * reach,
|
|
310
|
+
y1: y - spread,
|
|
311
|
+
x2: x + sign * reach,
|
|
312
|
+
y2: y + spread,
|
|
313
|
+
stroke: theme_1.INK.line,
|
|
314
|
+
'stroke-width': theme_1.STROKE.hair,
|
|
315
|
+
});
|
|
352
316
|
}
|
|
353
|
-
//
|
|
354
|
-
singleBar(x, y, dir
|
|
317
|
+
// -- Single bar (one) symbol -----------------------------------------------------
|
|
318
|
+
singleBar(svg, x, y, dir) {
|
|
355
319
|
const sign = dir === 'right' ? -1 : 1;
|
|
356
320
|
const gap = 6;
|
|
357
321
|
const reach = 12;
|
|
358
322
|
const ht = 7;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
<line x1="${x + sign * gap}" y1="${y - ht}" x2="${x + sign * gap}" y2="${y + ht}"
|
|
362
|
-
stroke="${color}" stroke-width="1.6"/>
|
|
363
|
-
<line x1="${x + sign * reach}" y1="${y - ht}" x2="${x + sign * reach}" y2="${y + ht}"
|
|
364
|
-
stroke="${color}" stroke-width="1.6"/>`;
|
|
323
|
+
svg.child('line', { x1: x + sign * gap, y1: y - ht, x2: x + sign * gap, y2: y + ht, stroke: theme_1.INK.line, 'stroke-width': theme_1.STROKE.heavy });
|
|
324
|
+
svg.child('line', { x1: x + sign * reach, y1: y - ht, x2: x + sign * reach, y2: y + ht, stroke: theme_1.INK.line, 'stroke-width': theme_1.STROKE.heavy });
|
|
365
325
|
}
|
|
366
|
-
//
|
|
367
|
-
renderBanner(totalW) {
|
|
326
|
+
// -- Banner ------------------------------------------------------------------------
|
|
327
|
+
renderBanner(svg, totalW) {
|
|
368
328
|
const H = ERDiagram.BANNER_H;
|
|
369
329
|
const { title, models } = this.opts;
|
|
370
330
|
const now = new Date().toISOString().slice(0, 10);
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
letter-spacing="1.4" fill="rgba(255,255,255,0.85)" text-anchor="end">ER DIAGRAM</text>`;
|
|
331
|
+
svg.child('rect', { width: totalW, height: H, fill: theme_1.INK.wash, stroke: theme_1.INK.line, 'stroke-width': theme_1.STROKE.hair });
|
|
332
|
+
svg
|
|
333
|
+
.child('text', { x: 24, y: 30, 'font-family': theme_1.FONT.family, 'font-size': theme_1.FONT.title + 13, 'font-weight': 'bold', fill: theme_1.INK.line })
|
|
334
|
+
.text(title);
|
|
335
|
+
svg
|
|
336
|
+
.child('text', { x: 24, y: 48, 'font-family': theme_1.FONT.family, 'font-size': theme_1.FONT.small + 3, fill: theme_1.INK.muted })
|
|
337
|
+
.text(`${models.length} entities, generated ${now}`);
|
|
338
|
+
svg
|
|
339
|
+
.child('text', { x: totalW - 20, y: 30, 'font-family': theme_1.FONT.family, 'font-size': theme_1.FONT.small + 3, 'font-weight': 'bold', 'letter-spacing': 1.4, fill: theme_1.INK.muted, 'text-anchor': 'end' })
|
|
340
|
+
.text('ER DIAGRAM');
|
|
382
341
|
}
|
|
383
|
-
//
|
|
384
|
-
renderLegend(lx, ly) {
|
|
342
|
+
// -- Legend ----------------------------------------------------------------------
|
|
343
|
+
renderLegend(svg, lx, ly) {
|
|
385
344
|
const LW = ERDiagram.LEGEND_W;
|
|
386
345
|
const LP = ERDiagram.LEGEND_PAD;
|
|
387
346
|
const lineH = 28;
|
|
388
|
-
const div = palette_1.INDUSTRY.divider;
|
|
389
347
|
const items = [
|
|
390
348
|
{ mark: '1:1', label: 'Has One' },
|
|
391
349
|
{ mark: '1:N', label: 'Has Many' },
|
|
@@ -393,71 +351,52 @@ class ERDiagram {
|
|
|
393
351
|
{ mark: 'N:N', label: 'Belongs To Many' },
|
|
394
352
|
];
|
|
395
353
|
const legendH = LP * 2 + 24 + items.length * lineH + 44;
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
font-family='${palette_1.FONTS.heading}' font-size="13" font-weight="600"
|
|
402
|
-
letter-spacing="1" fill="#FFFFFF">LEGEND</text>`;
|
|
354
|
+
svg.child('rect', { x: lx, y: ly, width: LW, height: legendH, fill: theme_1.INK.ground, stroke: theme_1.INK.line, 'stroke-width': theme_1.STROKE.hair });
|
|
355
|
+
svg.child('rect', { x: lx, y: ly, width: LW, height: 30, fill: theme_1.INK.wash, stroke: theme_1.INK.line, 'stroke-width': theme_1.STROKE.hair });
|
|
356
|
+
svg
|
|
357
|
+
.child('text', { x: lx + LP, y: ly + 20, 'font-family': theme_1.FONT.family, 'font-size': theme_1.FONT.body + 2, 'font-weight': 'bold', 'letter-spacing': 1, fill: theme_1.INK.line })
|
|
358
|
+
.text('LEGEND');
|
|
403
359
|
items.forEach((item, i) => {
|
|
404
360
|
const iy = ly + 30 + LP + i * lineH;
|
|
405
361
|
const badgeW = 34;
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
362
|
+
svg.child('rect', { x: lx + LP, y: iy + 4, width: badgeW, height: 18, fill: theme_1.INK.wash, stroke: theme_1.INK.line, 'stroke-width': theme_1.STROKE.hair });
|
|
363
|
+
svg
|
|
364
|
+
.child('text', { x: lx + LP + badgeW / 2, y: iy + 17, 'font-family': theme_1.FONT.mono, 'font-size': theme_1.FONT.small - 1, 'font-weight': 'bold', fill: theme_1.INK.line, 'text-anchor': 'middle' })
|
|
365
|
+
.text(item.mark);
|
|
366
|
+
svg.child('line', {
|
|
367
|
+
x1: lx + LP + badgeW + 8,
|
|
368
|
+
y1: iy + 13,
|
|
369
|
+
x2: lx + LP + badgeW + 30,
|
|
370
|
+
y2: iy + 13,
|
|
371
|
+
stroke: theme_1.INK.line,
|
|
372
|
+
'stroke-width': theme_1.STROKE.hair,
|
|
373
|
+
});
|
|
374
|
+
svg
|
|
375
|
+
.child('text', { x: lx + LP + badgeW + 38, y: iy + 17, 'font-family': theme_1.FONT.family, 'font-size': theme_1.FONT.body + 1, fill: theme_1.INK.line })
|
|
376
|
+
.text(item.label);
|
|
418
377
|
});
|
|
419
378
|
// PK/FK key.
|
|
420
379
|
const ky = ly + 30 + LP + items.length * lineH + 6;
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* Embedded font @import so browsers resolve Barlow / JetBrains Mono. Wrapped
|
|
436
|
-
* in CDATA because the Google-Fonts URL contains raw `&` separators that
|
|
437
|
-
* would otherwise be invalid XML in this string-built document.
|
|
438
|
-
*/
|
|
439
|
-
fontDefs() {
|
|
440
|
-
return `\n <defs><style type="text/css"><![CDATA[${palette_1.FONT_IMPORT}]]></style></defs>`;
|
|
380
|
+
svg.child('line', { x1: lx + LP, y1: ky + 4, x2: lx + LP + 44, y2: ky + 4, stroke: theme_1.INK.line, 'stroke-width': theme_1.STROKE.hair });
|
|
381
|
+
svg
|
|
382
|
+
.child('text', { x: lx + LP, y: ky + 20, 'font-family': theme_1.FONT.family, 'font-size': theme_1.FONT.small - 1, 'font-weight': 'bold', fill: theme_1.INK.line })
|
|
383
|
+
.text('PK');
|
|
384
|
+
svg
|
|
385
|
+
.child('text', { x: lx + LP + 22, y: ky + 20, 'font-family': theme_1.FONT.family, 'font-size': theme_1.FONT.small + 2, fill: theme_1.INK.muted })
|
|
386
|
+
.text('Primary Key');
|
|
387
|
+
svg
|
|
388
|
+
.child('text', { x: lx + LP, y: ky + 36, 'font-family': theme_1.FONT.family, 'font-size': theme_1.FONT.small - 1, 'font-weight': 'bold', fill: theme_1.INK.line })
|
|
389
|
+
.text('FK');
|
|
390
|
+
svg
|
|
391
|
+
.child('text', { x: lx + LP + 22, y: ky + 36, 'font-family': theme_1.FONT.family, 'font-size': theme_1.FONT.small + 2, fill: theme_1.INK.muted })
|
|
392
|
+
.text('Foreign Key');
|
|
441
393
|
}
|
|
442
|
-
//
|
|
443
|
-
|
|
444
|
-
const { models, columns } = this.opts;
|
|
445
|
-
void columns;
|
|
394
|
+
// -- Build the full SVG document ---------------------------------------------------
|
|
395
|
+
build() {
|
|
446
396
|
const MARGIN = ERDiagram.MARGIN;
|
|
447
397
|
const BANNER = ERDiagram.BANNER_H;
|
|
448
398
|
const LEGEND_W = ERDiagram.LEGEND_W;
|
|
449
399
|
const BOX_W = ERDiagram.BOX_W;
|
|
450
|
-
// Resolve accent / accent-2 from --color (Industry defaults otherwise).
|
|
451
|
-
const accent = (0, palette_1.resolveAccent)(this.opts.color);
|
|
452
|
-
const a2 = (0, palette_1.accent2Of)(accent);
|
|
453
|
-
this.accentMid = accent.mid;
|
|
454
|
-
this.accentText = accent.text;
|
|
455
|
-
this.accentLight = accent.light;
|
|
456
|
-
this.accent2Mid = a2.color;
|
|
457
|
-
this.accent2Text = a2.text;
|
|
458
|
-
this.accent2Light = a2.light;
|
|
459
|
-
// Theme reserved for future dark-mode support of ER (kept API-compatible).
|
|
460
|
-
void (0, palette_1.resolveTheme)(this.opts.theme);
|
|
461
400
|
const layout = this.computeLayout();
|
|
462
401
|
// Guard: with no models there are no boxes, so `Math.max(...[])` would be
|
|
463
402
|
// -Infinity and produce invalid width/height/viewBox. Emit a minimal, valid
|
|
@@ -465,61 +404,43 @@ class ERDiagram {
|
|
|
465
404
|
if (layout.length === 0) {
|
|
466
405
|
const emptyW = BOX_W + 2 * MARGIN;
|
|
467
406
|
const emptyH = BANNER + MARGIN * 2;
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
this.renderBanner(emptyW),
|
|
477
|
-
` <text x="${emptyW / 2}" y="${BANNER + MARGIN + 20}"`,
|
|
478
|
-
` font-family='${palette_1.FONTS.body}' font-size="14" fill="${palette_1.INDUSTRY.neutral[600]}"`,
|
|
479
|
-
` text-anchor="middle">No entities to display</text>`,
|
|
480
|
-
`</svg>`,
|
|
481
|
-
].join('\n');
|
|
407
|
+
const doc = new svg_1.SvgDocument(emptyW, emptyH);
|
|
408
|
+
const svg = doc.root;
|
|
409
|
+
svg.child('rect', { width: emptyW, height: emptyH, fill: theme_1.INK.ground });
|
|
410
|
+
this.renderBanner(svg, emptyW);
|
|
411
|
+
svg
|
|
412
|
+
.child('text', { x: emptyW / 2, y: BANNER + MARGIN + 20, 'font-family': theme_1.FONT.family, 'font-size': theme_1.FONT.body + 3, fill: theme_1.INK.muted, 'text-anchor': 'middle' })
|
|
413
|
+
.text('No entities to display');
|
|
414
|
+
return doc;
|
|
482
415
|
}
|
|
483
|
-
// Compute total canvas size
|
|
416
|
+
// Compute total canvas size.
|
|
484
417
|
const maxX = Math.max(...layout.map((b) => b.x + b.w)) + MARGIN;
|
|
485
418
|
const maxY = Math.max(...layout.map((b) => b.y + b.h)) + MARGIN;
|
|
486
|
-
// Add space for legend on right
|
|
419
|
+
// Add space for legend on the right.
|
|
487
420
|
const legendX = maxX + 20;
|
|
488
421
|
const legendY = BANNER + MARGIN;
|
|
489
422
|
const totalW = legendX + LEGEND_W + MARGIN;
|
|
490
423
|
const totalH = Math.max(maxY, legendY + 250);
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
` viewBox="0 0 ${totalW} ${totalH}">`,
|
|
497
|
-
].join('\n');
|
|
498
|
-
svg += this.fontDefs();
|
|
499
|
-
// Ground
|
|
500
|
-
svg += `\n <rect width="${totalW}" height="${totalH}" fill="${palette_1.INDUSTRY.ground}"/>`;
|
|
501
|
-
// Faint hairline blueprint grid.
|
|
502
|
-
svg += `\n <!-- blueprint grid -->`;
|
|
424
|
+
const doc = new svg_1.SvgDocument(totalW, totalH);
|
|
425
|
+
const svg = doc.root;
|
|
426
|
+
svg.child('rect', { width: totalW, height: totalH, fill: theme_1.INK.ground });
|
|
427
|
+
// Faint hairline grid.
|
|
428
|
+
const grid = svg.child('g', { opacity: 0.5 });
|
|
503
429
|
for (let gx = 0; gx < totalW; gx += 40) {
|
|
504
|
-
|
|
505
|
-
stroke="${palette_1.INDUSTRY.neutral[200]}" stroke-width="0.5" opacity="0.6"/>`;
|
|
430
|
+
grid.child('line', { x1: gx, y1: BANNER, x2: gx, y2: totalH, stroke: theme_1.INK.wash, 'stroke-width': theme_1.STROKE.hair });
|
|
506
431
|
}
|
|
507
432
|
for (let gy = BANNER; gy < totalH; gy += 40) {
|
|
508
|
-
|
|
509
|
-
stroke="${palette_1.INDUSTRY.neutral[200]}" stroke-width="0.5" opacity="0.6"/>`;
|
|
433
|
+
grid.child('line', { x1: 0, y1: gy, x2: totalW, y2: gy, stroke: theme_1.INK.wash, 'stroke-width': theme_1.STROKE.hair });
|
|
510
434
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
svg += this.renderLegend(legendX, legendY);
|
|
521
|
-
svg += `\n</svg>`;
|
|
522
|
-
return svg;
|
|
435
|
+
this.renderBanner(svg, totalW);
|
|
436
|
+
// Relationship lines, behind the entity boxes.
|
|
437
|
+
this.renderRelationships(svg, layout);
|
|
438
|
+
layout.forEach((box) => this.renderBox(svg, box));
|
|
439
|
+
this.renderLegend(svg, legendX, legendY);
|
|
440
|
+
return doc;
|
|
441
|
+
}
|
|
442
|
+
generate() {
|
|
443
|
+
return this.build().toString('Generated by ts-prorm-orm ERDiagram');
|
|
523
444
|
}
|
|
524
445
|
/**
|
|
525
446
|
* Write the SVG to a file. Creates parent directories if needed.
|