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.
Files changed (129) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/README.md +32 -24
  3. package/dist/compliance/index.d.ts +2 -0
  4. package/dist/compliance/index.js +13 -1
  5. package/dist/connection-manager.js +13 -1
  6. package/dist/core/expressions.d.ts +58 -0
  7. package/dist/core/expressions.js +83 -0
  8. package/dist/core/hooks-manager.d.ts +179 -0
  9. package/dist/core/hooks-manager.js +291 -0
  10. package/dist/core/types.d.ts +235 -0
  11. package/dist/core/types.js +9 -0
  12. package/dist/decorators/index.d.ts +2 -0
  13. package/dist/decorators/index.js +11 -1
  14. package/dist/diagrams/chen-diagram.d.ts +15 -12
  15. package/dist/diagrams/chen-diagram.js +76 -85
  16. package/dist/diagrams/class-diagram.d.ts +9 -8
  17. package/dist/diagrams/class-diagram.js +60 -82
  18. package/dist/diagrams/core/index.d.ts +13 -0
  19. package/dist/diagrams/core/index.js +33 -0
  20. package/dist/diagrams/core/layout.d.ts +104 -0
  21. package/dist/diagrams/core/layout.js +223 -0
  22. package/dist/diagrams/{svg-dom.d.ts → core/svg.d.ts} +8 -20
  23. package/dist/diagrams/{svg-dom.js → core/svg.js} +21 -29
  24. package/dist/diagrams/core/theme.d.ts +84 -0
  25. package/dist/diagrams/core/theme.js +62 -0
  26. package/dist/diagrams/dependency-diagram.d.ts +15 -14
  27. package/dist/diagrams/dependency-diagram.js +71 -105
  28. package/dist/diagrams/er-diagram.d.ts +13 -26
  29. package/dist/diagrams/er-diagram.js +180 -259
  30. package/dist/diagrams/flow-diagram.d.ts +16 -11
  31. package/dist/diagrams/flow-diagram.js +96 -91
  32. package/dist/diagrams/gantt-diagram.d.ts +10 -5
  33. package/dist/diagrams/gantt-diagram.js +77 -103
  34. package/dist/diagrams/index-diagram.d.ts +10 -10
  35. package/dist/diagrams/index-diagram.js +106 -116
  36. package/dist/diagrams/index.d.ts +6 -9
  37. package/dist/diagrams/index.js +9 -24
  38. package/dist/diagrams/migration-diagram.d.ts +8 -4
  39. package/dist/diagrams/migration-diagram.js +189 -161
  40. package/dist/diagrams/model-diagram.d.ts +34 -34
  41. package/dist/diagrams/model-diagram.js +106 -485
  42. package/dist/diagrams/package-diagram.d.ts +9 -6
  43. package/dist/diagrams/package-diagram.js +64 -101
  44. package/dist/diagrams/relational-diagram.d.ts +12 -10
  45. package/dist/diagrams/relational-diagram.js +87 -114
  46. package/dist/diagrams/schemadoc-diagram.d.ts +8 -8
  47. package/dist/diagrams/schemadoc-diagram.js +71 -93
  48. package/dist/diagrams/sequence-diagram.d.ts +10 -10
  49. package/dist/diagrams/sequence-diagram.js +64 -89
  50. package/dist/diagrams/state-diagram.d.ts +10 -9
  51. package/dist/diagrams/state-diagram.js +69 -92
  52. package/dist/diagrams/tree-diagram.d.ts +11 -5
  53. package/dist/diagrams/tree-diagram.js +41 -65
  54. package/dist/dialects/clickhouse/index.js +15 -0
  55. package/dist/dialects/cockroachdb/index.d.ts +1 -1
  56. package/dist/dialects/cockroachdb/index.js +32 -63
  57. package/dist/dialects/db2/index.js +23 -8
  58. package/dist/dialects/dialect.d.ts +29 -5
  59. package/dist/dialects/duckdb/index.js +9 -2
  60. package/dist/dialects/hana/index.js +22 -5
  61. package/dist/dialects/lock-clause-helper.d.ts +167 -0
  62. package/dist/dialects/lock-clause-helper.js +316 -0
  63. package/dist/dialects/mariadb/index.d.ts +7 -4
  64. package/dist/dialects/mariadb/index.js +36 -60
  65. package/dist/dialects/mssql/index.js +34 -3
  66. package/dist/dialects/mysql/index.d.ts +1 -1
  67. package/dist/dialects/mysql/index.js +17 -45
  68. package/dist/dialects/oracle/index.d.ts +14 -3
  69. package/dist/dialects/oracle/index.js +66 -36
  70. package/dist/dialects/order-expression-helper.d.ts +72 -0
  71. package/dist/dialects/order-expression-helper.js +154 -0
  72. package/dist/dialects/partial-index.d.ts +52 -0
  73. package/dist/dialects/partial-index.js +251 -0
  74. package/dist/dialects/postgres/index.d.ts +1 -1
  75. package/dist/dialects/postgres/index.js +43 -68
  76. package/dist/dialects/redshift/index.js +9 -2
  77. package/dist/dialects/snowflake/index.js +17 -0
  78. package/dist/dialects/spanner/index.js +7 -2
  79. package/dist/dialects/sqlite/driver.d.ts +55 -0
  80. package/dist/dialects/sqlite/driver.js +102 -0
  81. package/dist/dialects/sqlite/index.d.ts +1 -1
  82. package/dist/dialects/sqlite/index.js +49 -27
  83. package/dist/graph/base-graph-dialect.d.ts +24 -3
  84. package/dist/graph/base-graph-dialect.js +43 -2
  85. package/dist/graph/dgraph/index.js +6 -0
  86. package/dist/graph/gremlin/index.d.ts +6 -0
  87. package/dist/graph/gremlin/index.js +12 -1
  88. package/dist/graph/index.d.ts +1 -1
  89. package/dist/graph/neo4j/index.d.ts +2 -0
  90. package/dist/graph/neo4j/index.js +6 -1
  91. package/dist/graph/types.d.ts +12 -1
  92. package/dist/index.d.ts +13 -4
  93. package/dist/index.js +54 -7
  94. package/dist/logging/index.d.ts +10 -0
  95. package/dist/logging/index.js +23 -0
  96. package/dist/logging/query-logging.d.ts +82 -0
  97. package/dist/logging/query-logging.js +102 -0
  98. package/dist/models/decorators.d.ts +39 -3
  99. package/dist/models/decorators.js +74 -0
  100. package/dist/models/eager-load.d.ts +99 -0
  101. package/dist/models/eager-load.js +348 -5
  102. package/dist/models/indexes.d.ts +3 -2
  103. package/dist/models/indexes.js +7 -1
  104. package/dist/models/model.js +69 -43
  105. package/dist/prorm.d.ts +175 -409
  106. package/dist/prorm.js +1142 -557
  107. package/dist/query-builders/order-limit-builder.js +12 -10
  108. package/dist/query-builders/sql-compiler.d.ts +10 -3
  109. package/dist/query-builders/sql-compiler.js +14 -43
  110. package/dist/query-interface.d.ts +2 -1
  111. package/dist/sql-constants.d.ts +44 -0
  112. package/dist/sql-constants.js +138 -1
  113. package/dist/types/index.d.ts +53 -17
  114. package/dist/types/index.js +10 -15
  115. package/dist/types/query-types.d.ts +26 -0
  116. package/dist/types/query-types.js +30 -0
  117. package/package.json +32 -5
  118. package/dist/decorators/belongs-to-many.js +0 -115
  119. package/dist/decorators/belongs-to.js +0 -115
  120. package/dist/decorators/has-many.js +0 -127
  121. package/dist/decorators/has-one.js +0 -116
  122. package/dist/diagrams/palette.d.ts +0 -138
  123. package/dist/diagrams/palette.js +0 -194
  124. package/dist/hooks/hooks-manager.d.ts +0 -189
  125. package/dist/hooks/hooks-manager.js +0 -350
  126. package/dist/hooks/index.d.ts +0 -8
  127. package/dist/hooks/index.js +0 -37
  128. /package/dist/{logging.d.ts → logging/logger.d.ts} +0 -0
  129. /package/dist/{logging.js → logging/logger.js} +0 -0
@@ -1,18 +1,15 @@
1
1
  "use strict";
2
2
  /**
3
- * ERDiagram generates a fully-rendered Entity-Relationship diagram SVG for
4
- * a collection of ORM models and their associations, styled to the "Industry"
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
- * - #f2f2f3 ground with a faint hairline blueprint grid
9
- * - Solid accent (#5980a6) banner, Barlow Condensed title
10
- * - White entity cards on a grid: SQUARE corners, 1px hairline borders, a solid
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, Barlow labels
15
- * - Fully self-contained SVG (fonts via an embedded @import + system fallbacks)
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 palette_1 = require("./palette");
62
- // ─── Helpers ─────────────────────────────────────────────────────────────────
63
- function xmlEsc(s) {
64
- return String(s ?? '')
65
- .replace(/&/g, '&')
66
- .replace(/</g, '&lt;')
67
- .replace(/>/g, '&gt;')
68
- .replace(/"/g, '&quot;');
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
- /** Two-tone entity styling even entities accent, odd accent-2. */
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
- // ── Compute grid layout ─────────────────────────────────────────────────
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, i) => {
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; // vertically center in row
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
- // ── Draw a single entity box ─────────────────────────────────────────────
160
- renderBox(box, idx) {
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
- let out = `\n <!-- ═ Entity: ${xmlEsc(model.name)} ═ -->`;
169
- // ── Surface + hairline frame (square) ──
170
- out += `\n <rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${palette_1.INDUSTRY.card}"/>`;
171
- out += `\n <rect x="${x}" y="${y}" width="${w}" height="${h}" fill="none" stroke="${div}" stroke-width="1"/>`;
172
- // ── Header (solid accent bar) ──
173
- out += `\n <rect x="${x}" y="${y}" width="${w}" height="${HDR}" fill="${style.header}"/>`;
174
- out += `\n <text x="${x + 12}" y="${y + 20}"
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
- out += `\n <text x="${x + 12}" y="${y + 35}"
179
- font-family='${palette_1.FONTS.mono}' font-size="10"
180
- fill="rgba(255,255,255,0.78)">${xmlEsc(tbl)}</text>`;
181
- // ── Field rows ──
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 ? palette_1.INDUSTRY.card : palette_1.INDUSTRY.neutral[100];
188
- out += `\n <rect x="${x}" y="${fy}" width="${w}" height="${ROW}" fill="${rowBg}"/>`;
189
- // Accent stripe: PK accent, FK accent-2.
190
- if (isPK)
191
- out += `\n <rect x="${x}" y="${fy}" width="3" height="${ROW}" fill="${this.accentMid}"/>`;
192
- else if (isFK)
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
- out += `\n <text x="${x + 12}" y="${ity}"
198
- font-family='${palette_1.FONTS.heading}' font-size="9" font-weight="600"
199
- fill="${isPK ? this.accentText : this.accent2Text}">${isPK ? 'PK' : 'FK'}</text>`;
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
- out += `\n <circle cx="${x + 15}" cy="${fy + ROW / 2}" r="2" fill="${palette_1.INDUSTRY.neutral[400]}"/>`;
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 nameC = isPK ? this.accentText : isFK ? this.accent2Text : palette_1.INDUSTRY.text;
206
- const nameW = isPK || isFK ? '600' : '400';
207
- const dispN = colName.length > 16 ? colName.slice(0, 15) + '' : colName;
208
- out += `\n <text x="${x + 30}" y="${ity}"
209
- font-family='${palette_1.FONTS.mono}' font-size="11"
210
- font-weight="${nameW}" fill="${nameC}">${xmlEsc(dispN)}</text>`;
211
- // Type (right-aligned, mono neutral).
212
- const typTxt = typeStr.length > 14 ? typeStr.slice(0, 13) + '…' : typeStr;
213
- out += `\n <text x="${x + w - 8}" y="${ity}"
214
- font-family='${palette_1.FONTS.mono}' font-size="10"
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
- // ── Footer ──
187
+ // Footer.
221
188
  const footY = y + HDR + fieldEntries.length * ROW;
222
- out += `\n <rect x="${x}" y="${footY}" width="${w}" height="${FOOT}" fill="${style.light}"/>`;
223
- out += `\n <line x1="${x}" y1="${footY}" x2="${x + w}" y2="${footY}" stroke="${div}" stroke-width="1"/>`;
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
- out += `\n <text x="${x + 12}" y="${footY + 17}"
226
- font-family='${palette_1.FONTS.body}' font-size="10" fill="${palette_1.INDUSTRY.neutral[700]}">${Object.keys(model.fields ?? {}).length} fields${assocCount > 0 ? ` · ${assocCount} associations` : ''}</text>`;
227
- return out;
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
- // ── Draw relationship lines (crow's-foot) ──────────────────────────────
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'; // repurpose to mean 'bottom'
293
- tgtSide = 'left'; // repurpose to mean 'top'
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
- // Main line (hairline).
301
- out += `\n <path d="M ${sx} ${sy} C ${cpsx} ${sy}, ${cptx} ${ty}, ${tx} ${ty}"
302
- fill="none" stroke="${color}" stroke-width="1.4"/>`;
303
- // ── Crow's foot at TARGET (many side for hasMany/belongsToMany) ──
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
- out += this.crowFoot(tx, ty, tgtSide === 'left' ? 'right' : 'left', color);
308
- }
309
- else {
310
- out += this.singleBar(tx, ty, tgtSide === 'left' ? 'right' : 'left', color);
311
- }
312
- if (manyAtSource) {
313
- out += this.crowFoot(sx, sy, srcSide === 'right' ? 'left' : 'right', color);
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
- out += `\n <rect x="${mx - labelW / 2}" y="${my - 10}" width="${labelW}" height="18"
323
- fill="${palette_1.INDUSTRY.card}" stroke="${color}" stroke-width="1"/>`;
324
- out += `\n <text x="${mx}" y="${my + 3}"
325
- font-family='${palette_1.FONTS.mono}' font-size="11" font-weight="600"
326
- fill="${this.accentText}" text-anchor="middle">${xmlEsc(label)}</text>`;
327
- // Alias label (smaller, below)
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
- out += `\n <text x="${mx}" y="${my + 20}"
330
- font-family='${palette_1.FONTS.mono}' font-size="9" fill="${palette_1.INDUSTRY.neutral[600]}"
331
- text-anchor="middle">as: ${xmlEsc(assoc.as)}</text>`;
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
- // ── Crow's foot symbol ──────────────────────────────────────────────────
338
- crowFoot(x, y, dir, color) {
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
- return `
343
- <!-- crow foot at (${x},${y}) dir=${dir} -->
344
- <line x1="${x}" y1="${y}" x2="${x + sign * reach}" y2="${y - spread}"
345
- stroke="${color}" stroke-width="1.4"/>
346
- <line x1="${x}" y1="${y}" x2="${x + sign * reach}" y2="${y}"
347
- stroke="${color}" stroke-width="1.4"/>
348
- <line x1="${x}" y1="${y}" x2="${x + sign * reach}" y2="${y + spread}"
349
- stroke="${color}" stroke-width="1.4"/>
350
- <line x1="${x + sign * reach}" y1="${y - spread}" x2="${x + sign * reach}" y2="${y + spread}"
351
- stroke="${color}" stroke-width="1.2"/>`;
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
- // ── Single bar (one) symbol ─────────────────────────────────────────────
354
- singleBar(x, y, dir, color) {
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
- return `
360
- <!-- single bar at (${x},${y}) dir=${dir} -->
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
- // ── Banner ──────────────────────────────────────────────────────────────
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
- return /* xml */ `
372
- <!-- ═══ BANNER ═══ -->
373
- <rect width="${totalW}" height="${H}" fill="${this.accentMid}"/>
374
- <text x="24" y="30"
375
- font-family='${palette_1.FONTS.heading}'
376
- font-size="24" font-weight="600" fill="#FFFFFF">${xmlEsc(title)}</text>
377
- <text x="24" y="48"
378
- font-family='${palette_1.FONTS.body}' font-size="12" fill="rgba(255,255,255,0.82)">${models.length} entities · generated ${now}</text>
379
- <text x="${totalW - 20}" y="30"
380
- font-family='${palette_1.FONTS.heading}' font-size="12" font-weight="600"
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
- // ── Legend ──────────────────────────────────────────────────────────────
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
- let out = `\n <!-- ═══ LEGEND ═══ -->`;
397
- out += `\n <rect x="${lx}" y="${ly}" width="${LW}" height="${legendH}"
398
- fill="${palette_1.INDUSTRY.card}" stroke="${div}" stroke-width="1"/>`;
399
- out += `\n <rect x="${lx}" y="${ly}" width="${LW}" height="30" fill="${this.accentMid}"/>`;
400
- out += `\n <text x="${lx + LP}" y="${ly + 20}"
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
- out += `\n <rect x="${lx + LP}" y="${iy + 4}" width="${badgeW}" height="18"
407
- fill="${this.accentLight}" stroke="${div}" stroke-width="1"/>`;
408
- out += `\n <text x="${lx + LP + badgeW / 2}" y="${iy + 17}"
409
- font-family='${palette_1.FONTS.mono}' font-size="10" font-weight="600"
410
- fill="${this.accentText}" text-anchor="middle">${item.mark}</text>`;
411
- // Sample connector line.
412
- out += `\n <line x1="${lx + LP + badgeW + 8}" y1="${iy + 13}"
413
- x2="${lx + LP + badgeW + 30}" y2="${iy + 13}"
414
- stroke="${this.accentMid}" stroke-width="1.4"/>`;
415
- out += `\n <text x="${lx + LP + badgeW + 38}" y="${iy + 17}"
416
- font-family='${palette_1.FONTS.body}' font-size="12"
417
- fill="${palette_1.INDUSTRY.text}">${item.label}</text>`;
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
- out += `\n <line x1="${lx + LP}" y1="${ky + 4}" x2="${lx + LP + 44}" y2="${ky + 4}" stroke="${div}" stroke-width="1"/>`;
422
- out += `\n <text x="${lx + LP}" y="${ky + 20}"
423
- font-family='${palette_1.FONTS.heading}' font-size="9" font-weight="600"
424
- fill="${this.accentText}">PK</text>`;
425
- out += `\n <text x="${lx + LP + 22}" y="${ky + 20}"
426
- font-family='${palette_1.FONTS.body}' font-size="11" fill="${palette_1.INDUSTRY.neutral[700]}">Primary Key</text>`;
427
- out += `\n <text x="${lx + LP}" y="${ky + 36}"
428
- font-family='${palette_1.FONTS.heading}' font-size="9" font-weight="600"
429
- fill="${this.accent2Text}">FK</text>`;
430
- out += `\n <text x="${lx + LP + 22}" y="${ky + 36}"
431
- font-family='${palette_1.FONTS.body}' font-size="11" fill="${palette_1.INDUSTRY.neutral[700]}">Foreign Key</text>`;
432
- return out;
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
- // ── Generate the full SVG ────────────────────────────────────────────────
443
- generate() {
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
- return [
469
- `<?xml version="1.0" encoding="UTF-8"?>`,
470
- `<!-- Generated by ts-prorm-orm ERDiagram -->`,
471
- `<svg xmlns="http://www.w3.org/2000/svg"`,
472
- ` width="${emptyW}" height="${emptyH}"`,
473
- ` viewBox="0 0 ${emptyW} ${emptyH}">`,
474
- this.fontDefs(),
475
- ` <rect width="${emptyW}" height="${emptyH}" fill="${palette_1.INDUSTRY.ground}"/>`,
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 (only if fits neatly)
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
- let svg = [
492
- `<?xml version="1.0" encoding="UTF-8"?>`,
493
- `<!-- Generated by ts-prorm-orm ERDiagram -->`,
494
- `<svg xmlns="http://www.w3.org/2000/svg"`,
495
- ` width="${totalW}" height="${totalH}"`,
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
- svg += `\n <line x1="${gx}" y1="${BANNER}" x2="${gx}" y2="${totalH}"
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
- svg += `\n <line x1="0" y1="${gy}" x2="${totalW}" y2="${gy}"
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
- // Banner
512
- svg += this.renderBanner(totalW);
513
- // Relationship lines (behind boxes)
514
- svg += this.renderRelationships(layout);
515
- // Entity boxes
516
- layout.forEach((box, i) => {
517
- svg += this.renderBox(box, i);
518
- });
519
- // Legend
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.