ts-prorm-orm 1.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.
Files changed (310) hide show
  1. package/CHANGELOG.md +1111 -0
  2. package/LICENSE +21 -0
  3. package/README.md +573 -0
  4. package/dist/audit/history-query.js +180 -0
  5. package/dist/audit/index.js +23 -0
  6. package/dist/audit/logger.js +236 -0
  7. package/dist/cache/cache-manager.js +84 -0
  8. package/dist/cache/index.js +16 -0
  9. package/dist/cache/redis-cluster-cache.js +557 -0
  10. package/dist/cli.js +2200 -0
  11. package/dist/compliance/audit-trail.js +68 -0
  12. package/dist/compliance/backup-verification.js +685 -0
  13. package/dist/compliance/breach-detector.js +505 -0
  14. package/dist/compliance/consent-record.js +227 -0
  15. package/dist/compliance/consent-versioning.js +331 -0
  16. package/dist/compliance/cross-border-log.js +530 -0
  17. package/dist/compliance/data-classifier.js +258 -0
  18. package/dist/compliance/data-lineage.js +565 -0
  19. package/dist/compliance/data-masker.js +303 -0
  20. package/dist/compliance/data-portability.js +265 -0
  21. package/dist/compliance/data-retention.js +195 -0
  22. package/dist/compliance/dsar-workflow.js +342 -0
  23. package/dist/compliance/field-encryption.js +223 -0
  24. package/dist/compliance/immutable-record.js +148 -0
  25. package/dist/compliance/index.js +260 -0
  26. package/dist/compliance/privacy-impact-assessment.js +364 -0
  27. package/dist/compliance/pseudonymization.js +269 -0
  28. package/dist/compliance/query-firewall.js +1182 -0
  29. package/dist/compliance/rate-limiter.js +580 -0
  30. package/dist/compliance/right-to-erasure.js +117 -0
  31. package/dist/compliance/row-level-security.js +246 -0
  32. package/dist/compliance/security-decorator.js +574 -0
  33. package/dist/compliance/security-monitor.js +525 -0
  34. package/dist/compliance/sensitive-data-discovery.js +476 -0
  35. package/dist/compliance/session-isolation.js +472 -0
  36. package/dist/compliance/tls-enforcer.js +108 -0
  37. package/dist/compliance/worm-storage.js +712 -0
  38. package/dist/connection-manager.js +198 -0
  39. package/dist/connection-pool.js +519 -0
  40. package/dist/decorators/audit.js +136 -0
  41. package/dist/decorators/belongs-to-many.js +115 -0
  42. package/dist/decorators/belongs-to.js +115 -0
  43. package/dist/decorators/check.js +435 -0
  44. package/dist/decorators/collate.js +329 -0
  45. package/dist/decorators/comment.js +205 -0
  46. package/dist/decorators/database-settings.js +236 -0
  47. package/dist/decorators/default.js +244 -0
  48. package/dist/decorators/encryption.js +235 -0
  49. package/dist/decorators/engine.js +97 -0
  50. package/dist/decorators/fk-constraints.js +594 -0
  51. package/dist/decorators/foreign-table.js +136 -0
  52. package/dist/decorators/generated.js +274 -0
  53. package/dist/decorators/has-many.js +127 -0
  54. package/dist/decorators/has-one.js +116 -0
  55. package/dist/decorators/hstore.js +129 -0
  56. package/dist/decorators/index.js +355 -0
  57. package/dist/decorators/json-column.js +83 -0
  58. package/dist/decorators/jsonb.js +101 -0
  59. package/dist/decorators/orm-decorators.js +425 -0
  60. package/dist/decorators/permissions.js +294 -0
  61. package/dist/decorators/procedure.js +210 -0
  62. package/dist/decorators/query-options.js +556 -0
  63. package/dist/decorators/range.js +167 -0
  64. package/dist/decorators/set-column.js +83 -0
  65. package/dist/decorators/spatial.js +114 -0
  66. package/dist/decorators/storage.js +580 -0
  67. package/dist/decorators/timezone.js +512 -0
  68. package/dist/decorators/trigger.js +90 -0
  69. package/dist/decorators/uuid.js +135 -0
  70. package/dist/decorators/view.js +258 -0
  71. package/dist/diagrams/chen-diagram.js +354 -0
  72. package/dist/diagrams/class-diagram.js +384 -0
  73. package/dist/diagrams/dependency-diagram.js +432 -0
  74. package/dist/diagrams/er-diagram.js +605 -0
  75. package/dist/diagrams/flow-diagram.js +394 -0
  76. package/dist/diagrams/gantt-diagram.js +411 -0
  77. package/dist/diagrams/index-diagram.js +353 -0
  78. package/dist/diagrams/index.js +184 -0
  79. package/dist/diagrams/migration-diagram.js +316 -0
  80. package/dist/diagrams/model-diagram.js +616 -0
  81. package/dist/diagrams/package-diagram.js +376 -0
  82. package/dist/diagrams/palette.js +85 -0
  83. package/dist/diagrams/relational-diagram.js +455 -0
  84. package/dist/diagrams/schemadoc-diagram.js +309 -0
  85. package/dist/diagrams/sequence-diagram.js +307 -0
  86. package/dist/diagrams/state-diagram.js +344 -0
  87. package/dist/diagrams/svg-dom.js +111 -0
  88. package/dist/diagrams/tree-diagram.js +250 -0
  89. package/dist/dialects/clickhouse/index.js +1742 -0
  90. package/dist/dialects/cockroachdb/index.js +4677 -0
  91. package/dist/dialects/cratedb/index.js +441 -0
  92. package/dist/dialects/databricks/index.js +517 -0
  93. package/dist/dialects/db2/index.js +2225 -0
  94. package/dist/dialects/dialect.js +720 -0
  95. package/dist/dialects/duckdb/index.js +2014 -0
  96. package/dist/dialects/exasol/index.js +357 -0
  97. package/dist/dialects/firebird/index.js +495 -0
  98. package/dist/dialects/greenplum/index.js +457 -0
  99. package/dist/dialects/hana/index.js +1741 -0
  100. package/dist/dialects/mariadb/index.js +3590 -0
  101. package/dist/dialects/mssql/index.js +2296 -0
  102. package/dist/dialects/mysql/index.js +4058 -0
  103. package/dist/dialects/oracle/index.js +2999 -0
  104. package/dist/dialects/postgres/index.js +5319 -0
  105. package/dist/dialects/query-stream-helper.js +152 -0
  106. package/dist/dialects/questdb/index.js +433 -0
  107. package/dist/dialects/redshift/index.js +2277 -0
  108. package/dist/dialects/singlestore/index.js +354 -0
  109. package/dist/dialects/snowflake/index.js +2082 -0
  110. package/dist/dialects/spanner/index.js +1768 -0
  111. package/dist/dialects/sqlite/index.js +3382 -0
  112. package/dist/dialects/tidb/index.js +377 -0
  113. package/dist/dialects/timescaledb/index.js +164 -0
  114. package/dist/dialects/trino/index.js +420 -0
  115. package/dist/dialects/turso/index.js +227 -0
  116. package/dist/dialects/vertica/index.js +317 -0
  117. package/dist/dialects/yugabytedb/index.js +424 -0
  118. package/dist/errors/index.js +472 -0
  119. package/dist/errors/utils.js +340 -0
  120. package/dist/errors.js +21 -0
  121. package/dist/extensions/catalog/cloud-warehouse-features.js +207 -0
  122. package/dist/extensions/catalog/mssql-features.js +147 -0
  123. package/dist/extensions/catalog/mysql-mariadb-plugins.js +229 -0
  124. package/dist/extensions/catalog/oracle-db2-features.js +196 -0
  125. package/dist/extensions/catalog/postgres-extensions.js +516 -0
  126. package/dist/extensions/index.js +71 -0
  127. package/dist/extensions/types.js +13 -0
  128. package/dist/foreign-data.js +173 -0
  129. package/dist/hooks/hooks-manager.js +350 -0
  130. package/dist/hooks/index.js +37 -0
  131. package/dist/index.js +463 -0
  132. package/dist/logging.js +335 -0
  133. package/dist/migrations/index.js +40 -0
  134. package/dist/migrations/migration.js +196 -0
  135. package/dist/migrations/migrator.js +411 -0
  136. package/dist/migrations/prormmigration.js +275 -0
  137. package/dist/migrations/query-interface.js +435 -0
  138. package/dist/migrations/seeder.js +353 -0
  139. package/dist/models/associations.js +852 -0
  140. package/dist/models/constraints.js +288 -0
  141. package/dist/models/data-types.js +2518 -0
  142. package/dist/models/decorators.js +445 -0
  143. package/dist/models/index.js +33 -0
  144. package/dist/models/indexes.js +531 -0
  145. package/dist/models/methods.js +382 -0
  146. package/dist/models/model-manager.js +103 -0
  147. package/dist/models/model.js +5349 -0
  148. package/dist/models/operators.js +67 -0
  149. package/dist/models/scopes.js +189 -0
  150. package/dist/models/typescript-types.js +26 -0
  151. package/dist/nosql/aerospike/index.js +205 -0
  152. package/dist/nosql/allegrograph/index.js +169 -0
  153. package/dist/nosql/arangodb/index.js +364 -0
  154. package/dist/nosql/azure-blob/index.js +206 -0
  155. package/dist/nosql/beanstalkd/index.js +231 -0
  156. package/dist/nosql/beequeue/index.js +210 -0
  157. package/dist/nosql/bigchaindb/index.js +195 -0
  158. package/dist/nosql/bigtable/index.js +224 -0
  159. package/dist/nosql/blazegraph/index.js +173 -0
  160. package/dist/nosql/bullmq/index.js +191 -0
  161. package/dist/nosql/cassandra/index.js +174 -0
  162. package/dist/nosql/chroma/index.js +190 -0
  163. package/dist/nosql/cloudflare-kv/index.js +220 -0
  164. package/dist/nosql/coherence/index.js +200 -0
  165. package/dist/nosql/cosmosdb/index.js +157 -0
  166. package/dist/nosql/couchbase/index.js +213 -0
  167. package/dist/nosql/dax/index.js +212 -0
  168. package/dist/nosql/deno-kv/index.js +206 -0
  169. package/dist/nosql/dgraph/index.js +171 -0
  170. package/dist/nosql/doris/index.js +169 -0
  171. package/dist/nosql/druid/index.js +162 -0
  172. package/dist/nosql/dynamodb/index.js +937 -0
  173. package/dist/nosql/elasticsearch/index.js +377 -0
  174. package/dist/nosql/etcd/index.js +502 -0
  175. package/dist/nosql/eventhubs/index.js +213 -0
  176. package/dist/nosql/eventstore/index.js +254 -0
  177. package/dist/nosql/faunadb/index.js +188 -0
  178. package/dist/nosql/firestore/index.js +177 -0
  179. package/dist/nosql/fluree/index.js +148 -0
  180. package/dist/nosql/fuseki/index.js +170 -0
  181. package/dist/nosql/gcs/index.js +172 -0
  182. package/dist/nosql/gearman/index.js +160 -0
  183. package/dist/nosql/geode/index.js +196 -0
  184. package/dist/nosql/graphdb/index.js +169 -0
  185. package/dist/nosql/graylog/index.js +188 -0
  186. package/dist/nosql/gridgain/index.js +171 -0
  187. package/dist/nosql/hazelcast/index.js +162 -0
  188. package/dist/nosql/hbase/index.js +230 -0
  189. package/dist/nosql/ignite/index.js +173 -0
  190. package/dist/nosql/immudb/index.js +184 -0
  191. package/dist/nosql/index.js +232 -0
  192. package/dist/nosql/infinispan/index.js +200 -0
  193. package/dist/nosql/influxdb/index.js +0 -0
  194. package/dist/nosql/kafka/index.js +234 -0
  195. package/dist/nosql/keyspaces/index.js +189 -0
  196. package/dist/nosql/kinesis/index.js +253 -0
  197. package/dist/nosql/leveldb/index.js +153 -0
  198. package/dist/nosql/lmdb/index.js +160 -0
  199. package/dist/nosql/loki/index.js +201 -0
  200. package/dist/nosql/marklogic/index.js +204 -0
  201. package/dist/nosql/materialize/index.js +145 -0
  202. package/dist/nosql/meilisearch/index.js +154 -0
  203. package/dist/nosql/memcached/index.js +223 -0
  204. package/dist/nosql/milvus/index.js +410 -0
  205. package/dist/nosql/minio/index.js +265 -0
  206. package/dist/nosql/momento/index.js +179 -0
  207. package/dist/nosql/mongodb/index.js +461 -0
  208. package/dist/nosql/nats/index.js +247 -0
  209. package/dist/nosql/nedb/index.js +164 -0
  210. package/dist/nosql/neo4j/index.js +450 -0
  211. package/dist/nosql/neptune/index.js +470 -0
  212. package/dist/nosql/nsq/index.js +200 -0
  213. package/dist/nosql/opensearch/index.js +186 -0
  214. package/dist/nosql/orientdb/index.js +175 -0
  215. package/dist/nosql/papertrail/index.js +200 -0
  216. package/dist/nosql/pinecone/index.js +0 -0
  217. package/dist/nosql/pinot/index.js +133 -0
  218. package/dist/nosql/pouchdb/index.js +172 -0
  219. package/dist/nosql/prometheus/index.js +174 -0
  220. package/dist/nosql/provendb/index.js +147 -0
  221. package/dist/nosql/pubsub/index.js +187 -0
  222. package/dist/nosql/pulsar/index.js +232 -0
  223. package/dist/nosql/qdrant/index.js +295 -0
  224. package/dist/nosql/qldb/index.js +192 -0
  225. package/dist/nosql/r2/index.js +281 -0
  226. package/dist/nosql/rabbitmq/index.js +237 -0
  227. package/dist/nosql/ravendb/index.js +175 -0
  228. package/dist/nosql/redis/index.js +607 -0
  229. package/dist/nosql/redpanda/index.js +237 -0
  230. package/dist/nosql/resque/index.js +203 -0
  231. package/dist/nosql/rethinkdb/index.js +232 -0
  232. package/dist/nosql/rocksdb/index.js +152 -0
  233. package/dist/nosql/rockset/index.js +126 -0
  234. package/dist/nosql/s3/index.js +298 -0
  235. package/dist/nosql/scylladb/index.js +178 -0
  236. package/dist/nosql/signoz/index.js +227 -0
  237. package/dist/nosql/sns/index.js +201 -0
  238. package/dist/nosql/solr/index.js +231 -0
  239. package/dist/nosql/splunk/index.js +227 -0
  240. package/dist/nosql/sqs/index.js +246 -0
  241. package/dist/nosql/stardog/index.js +169 -0
  242. package/dist/nosql/starrocks/index.js +170 -0
  243. package/dist/nosql/store.js +2 -0
  244. package/dist/nosql/sumologic/index.js +213 -0
  245. package/dist/nosql/surrealdb/index.js +179 -0
  246. package/dist/nosql/terminusdb/index.js +151 -0
  247. package/dist/nosql/tigergraph/index.js +469 -0
  248. package/dist/nosql/typesense/index.js +148 -0
  249. package/dist/nosql/unqlite/index.js +157 -0
  250. package/dist/nosql/upstash/index.js +167 -0
  251. package/dist/nosql/vercel-kv/index.js +166 -0
  252. package/dist/nosql/victoriametrics/index.js +213 -0
  253. package/dist/nosql/virtuoso/index.js +169 -0
  254. package/dist/nosql/weaviate/index.js +276 -0
  255. package/dist/operators/index.js +108 -0
  256. package/dist/operators.js +2690 -0
  257. package/dist/prisma-migrate/index.js +38 -0
  258. package/dist/prisma-migrate/migration-generator.js +125 -0
  259. package/dist/prisma-migrate/model-generator.js +172 -0
  260. package/dist/prisma-migrate/relations.js +100 -0
  261. package/dist/prisma-migrate/schema-parser.js +167 -0
  262. package/dist/prisma-migrate/type-mapper.js +40 -0
  263. package/dist/prorm.js +6832 -0
  264. package/dist/query-builders/cte-builder.js +80 -0
  265. package/dist/query-builders/functions/aggregate.js +390 -0
  266. package/dist/query-builders/functions/conditional.js +503 -0
  267. package/dist/query-builders/functions/datetime.js +695 -0
  268. package/dist/query-builders/functions/fulltext.js +439 -0
  269. package/dist/query-builders/functions/index.js +93 -0
  270. package/dist/query-builders/functions/json.js +427 -0
  271. package/dist/query-builders/functions/math.js +399 -0
  272. package/dist/query-builders/functions/string.js +518 -0
  273. package/dist/query-builders/functions/window.js +328 -0
  274. package/dist/query-builders/include-builder.js +323 -0
  275. package/dist/query-builders/index-expression-builder.js +242 -0
  276. package/dist/query-builders/index.js +161 -0
  277. package/dist/query-builders/insert-builder.js +164 -0
  278. package/dist/query-builders/model-helpers.js +38 -0
  279. package/dist/query-builders/order-limit-builder.js +239 -0
  280. package/dist/query-builders/sql-compiler.js +2040 -0
  281. package/dist/query-builders/subquery-builder.js +152 -0
  282. package/dist/query-builders/update-builder.js +182 -0
  283. package/dist/query-builders/view-builder.js +218 -0
  284. package/dist/query-builders/where-builder.js +1532 -0
  285. package/dist/query-interface.js +622 -0
  286. package/dist/query-optimizers/batch-optimizer.js +426 -0
  287. package/dist/query-optimizers/explain-plans.js +415 -0
  288. package/dist/query-optimizers/index.js +51 -0
  289. package/dist/query-optimizers/prepared-statement-cache.js +423 -0
  290. package/dist/query-optimizers/query-hints.js +438 -0
  291. package/dist/query-optimizers/query-optimizer.js +278 -0
  292. package/dist/query-optimizers/slow-query-logger.js +271 -0
  293. package/dist/replica-manager.js +507 -0
  294. package/dist/schema/index.js +15 -0
  295. package/dist/schema/migration-generator.js +374 -0
  296. package/dist/schema/schema-differ.js +549 -0
  297. package/dist/schema/types.js +6 -0
  298. package/dist/sql-constants.js +300 -0
  299. package/dist/sqlite-advanced.js +1047 -0
  300. package/dist/streams/index.js +12 -0
  301. package/dist/streams/transforms.js +178 -0
  302. package/dist/transaction.js +456 -0
  303. package/dist/types/index.js +180 -0
  304. package/dist/user-management.js +127 -0
  305. package/dist/utils/date.js +300 -0
  306. package/dist/utils/index.js +1079 -0
  307. package/dist/utils/string.js +161 -0
  308. package/dist/validators/index.js +19 -0
  309. package/dist/validators/validator.js +911 -0
  310. package/package.json +190 -0
@@ -0,0 +1,605 @@
1
+ "use strict";
2
+ /**
3
+ * ERDiagram — generates a fully-rendered Entity-Relationship diagram SVG for
4
+ * a collection of ORM models and their associations.
5
+ *
6
+ * Features:
7
+ * - Auto-layout (configurable columns, auto-height per entity)
8
+ * - Color-coded entity headers (unique hue per model)
9
+ * - Compact field rows: name + type badge + PK/FK icons
10
+ * - Crow's-foot relationship lines with cardinality labels
11
+ * - Bezier curves that avoid box overlap
12
+ * - Relationship legend bottom-right
13
+ * - Title bar & metadata strip
14
+ * - Fully self-contained SVG (no external resources)
15
+ *
16
+ * Usage:
17
+ * const er = new ERDiagram({
18
+ * title: 'E-Commerce Schema',
19
+ * models: [userDef, productDef, orderDef],
20
+ * });
21
+ * er.save('./diagrams/schema.svg');
22
+ */
23
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ var desc = Object.getOwnPropertyDescriptor(m, k);
26
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
27
+ desc = { enumerable: true, get: function() { return m[k]; } };
28
+ }
29
+ Object.defineProperty(o, k2, desc);
30
+ }) : (function(o, m, k, k2) {
31
+ if (k2 === undefined) k2 = k;
32
+ o[k2] = m[k];
33
+ }));
34
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
35
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
36
+ }) : function(o, v) {
37
+ o["default"] = v;
38
+ });
39
+ var __importStar = (this && this.__importStar) || (function () {
40
+ var ownKeys = function(o) {
41
+ ownKeys = Object.getOwnPropertyNames || function (o) {
42
+ var ar = [];
43
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
44
+ return ar;
45
+ };
46
+ return ownKeys(o);
47
+ };
48
+ return function (mod) {
49
+ if (mod && mod.__esModule) return mod;
50
+ var result = {};
51
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
52
+ __setModuleDefault(result, mod);
53
+ return result;
54
+ };
55
+ })();
56
+ Object.defineProperty(exports, "__esModule", { value: true });
57
+ exports.ERDiagram = void 0;
58
+ const fs = __importStar(require("fs"));
59
+ const path = __importStar(require("path"));
60
+ // ─── Color palette for entity headers (cycles for > 12 models) ─────────────
61
+ const ENTITY_COLORS = [
62
+ { header: '#1E3A8A', mid: '#2563EB', light: '#DBEAFE', text: '#1E3A8A' }, // blue
63
+ { header: '#5B21B6', mid: '#7C3AED', light: '#EDE9FE', text: '#5B21B6' }, // violet
64
+ { header: '#065F46', mid: '#059669', light: '#D1FAE5', text: '#065F46' }, // emerald
65
+ { header: '#92400E', mid: '#D97706', light: '#FEF3C7', text: '#92400E' }, // amber
66
+ { header: '#831843', mid: '#DB2777', light: '#FCE7F3', text: '#831843' }, // pink
67
+ { header: '#0C4A6E', mid: '#0284C7', light: '#E0F2FE', text: '#0C4A6E' }, // sky
68
+ { header: '#1E1B4B', mid: '#4338CA', light: '#E0E7FF', text: '#1E1B4B' }, // indigo
69
+ { header: '#4A1942', mid: '#9333EA', light: '#F3E8FF', text: '#4A1942' }, // purple
70
+ { header: '#7F1D1D', mid: '#DC2626', light: '#FEE2E2', text: '#7F1D1D' }, // red
71
+ { header: '#134E4A', mid: '#0D9488', light: '#CCFBF1', text: '#134E4A' }, // teal
72
+ { header: '#1C1917', mid: '#44403C', light: '#F5F5F4', text: '#1C1917' }, // stone
73
+ { header: '#14532D', mid: '#16A34A', light: '#DCFCE7', text: '#14532D' }, // green
74
+ ];
75
+ // ─── Type-color helper (compact, same palette as model-diagram) ─────────────
76
+ function typeCol(t) {
77
+ const s = t.toUpperCase();
78
+ if (/STRING|VARCHAR|CHAR|TEXT/.test(s))
79
+ return { text: '#059669', bg: '#D1FAE5' };
80
+ if (/INT|BIGINT|SMALLINT|FLOAT|DOUBLE|DECIMAL|NUMERIC/.test(s))
81
+ return { text: '#7C3AED', bg: '#EDE9FE' };
82
+ if (/DATE|TIME|TIMESTAMP/.test(s))
83
+ return { text: '#EA580C', bg: '#FFEDD5' };
84
+ if (/BOOL/.test(s))
85
+ return { text: '#DB2777', bg: '#FCE7F3' };
86
+ if (/UUID/.test(s))
87
+ return { text: '#0891B2', bg: '#CFFAFE' };
88
+ if (/JSONB/.test(s))
89
+ return { text: '#4338CA', bg: '#E0E7FF' };
90
+ if (/JSON/.test(s))
91
+ return { text: '#4F46E5', bg: '#E0E7FF' };
92
+ if (/BLOB|BINARY/.test(s))
93
+ return { text: '#92400E', bg: '#FEF3C7' };
94
+ if (/ENUM/.test(s))
95
+ return { text: '#B45309', bg: '#FEF3C7' };
96
+ if (/ARRAY/.test(s))
97
+ return { text: '#0369A1', bg: '#E0F2FE' };
98
+ return { text: '#475569', bg: '#F1F5F9' };
99
+ }
100
+ function xmlEsc(s) {
101
+ return String(s ?? '')
102
+ .replace(/&/g, '&amp;')
103
+ .replace(/</g, '&lt;')
104
+ .replace(/>/g, '&gt;')
105
+ .replace(/"/g, '&quot;');
106
+ }
107
+ function resolveType(field) {
108
+ const t = field.type;
109
+ if (typeof t === 'string')
110
+ return t;
111
+ if (t && typeof t === 'object') {
112
+ if (typeof t.toSqlString === 'function')
113
+ return t.toSqlString();
114
+ if (t.key)
115
+ return String(t.key);
116
+ }
117
+ return 'UNKNOWN';
118
+ }
119
+ // ─── ERDiagram class ────────────────────────────────────────────────────────
120
+ class ERDiagram {
121
+ constructor(options) {
122
+ this.opts = {
123
+ title: options.title ?? 'Entity-Relationship Diagram',
124
+ columns: options.columns ?? 3,
125
+ hGap: options.hGap ?? 90,
126
+ vGap: options.vGap ?? 70,
127
+ models: options.models ?? [],
128
+ theme: options.theme ?? 'light',
129
+ color: options.color ?? '',
130
+ };
131
+ }
132
+ // ── Compute box height for a model ──────────────────────────────────────
133
+ boxHeight(model) {
134
+ const fieldCount = Object.keys(model.fields ?? {}).length;
135
+ const h = ERDiagram.BOX_HDR + fieldCount * ERDiagram.BOX_ROW + ERDiagram.BOX_FOOT;
136
+ return Math.max(h, ERDiagram.MIN_H);
137
+ }
138
+ // ── Compute grid layout ─────────────────────────────────────────────────
139
+ computeLayout() {
140
+ const { models, columns, hGap, vGap } = this.opts;
141
+ const W = ERDiagram.BOX_W;
142
+ const MARGIN = ERDiagram.MARGIN;
143
+ const BANNER = ERDiagram.BANNER_H;
144
+ const layout = [];
145
+ let col = 0;
146
+ let row = 0;
147
+ // Track row max-heights for vertical positioning
148
+ const rowHeights = [];
149
+ let curRowMaxH = 0;
150
+ models.forEach((m, i) => {
151
+ const bh = this.boxHeight(m);
152
+ curRowMaxH = Math.max(curRowMaxH, bh);
153
+ if ((i + 1) % columns === 0 || i === models.length - 1) {
154
+ rowHeights.push(curRowMaxH);
155
+ curRowMaxH = 0;
156
+ }
157
+ });
158
+ col = 0;
159
+ row = 0;
160
+ let rowStart = BANNER + MARGIN;
161
+ models.forEach((m, i) => {
162
+ const bh = this.boxHeight(m);
163
+ const x = MARGIN + col * (W + hGap);
164
+ const y = rowStart + (rowHeights[row] - bh) / 2; // vertically center in row
165
+ layout.push({
166
+ model: m,
167
+ x,
168
+ y,
169
+ w: W,
170
+ h: bh,
171
+ cx: x + W / 2,
172
+ cy: y + bh / 2,
173
+ });
174
+ col++;
175
+ if (col >= columns) {
176
+ col = 0;
177
+ rowStart += rowHeights[row] + vGap;
178
+ row++;
179
+ }
180
+ });
181
+ return layout;
182
+ }
183
+ // ── Draw a single entity box ─────────────────────────────────────────────
184
+ renderBox(box, colorIdx) {
185
+ const { model, x, y, w, h } = box;
186
+ const colors = ENTITY_COLORS[colorIdx % ENTITY_COLORS.length];
187
+ const uid = model.name.replace(/\W/g, '') + colorIdx;
188
+ const HDR = ERDiagram.BOX_HDR;
189
+ const ROW = ERDiagram.BOX_ROW;
190
+ const FOOT = ERDiagram.BOX_FOOT;
191
+ const R = 10;
192
+ const fieldEntries = Object.entries(model.fields ?? {});
193
+ let out = `\n <!-- ═ Entity: ${xmlEsc(model.name)} ═ -->`;
194
+ // ── Shadow & shell ──
195
+ out += `\n <defs>
196
+ <linearGradient id="ehg${uid}" x1="0" y1="0" x2="1" y2="1" gradientUnits="objectBoundingBox">
197
+ <stop offset="0%" stop-color="${colors.header}"/>
198
+ <stop offset="100%" stop-color="${colors.mid}"/>
199
+ </linearGradient>
200
+ <filter id="esh${uid}" x="-8%" y="-8%" width="116%" height="130%">
201
+ <feDropShadow dx="0" dy="3" stdDeviation="6" flood-color="#00000020"/>
202
+ </filter>
203
+ </defs>`;
204
+ out += `\n <rect x="${x - 1}" y="${y - 1}" width="${w + 2}" height="${h + 2}"
205
+ rx="${R + 1}" fill="#CBD5E1" filter="url(#esh${uid})"/>`;
206
+ out += `\n <rect x="${x}" y="${y}" width="${w}" height="${h}"
207
+ rx="${R}" fill="#FFFFFF"/>`;
208
+ // ── Header ──
209
+ out += `\n <rect x="${x}" y="${y}" width="${w}" height="${HDR}"
210
+ rx="${R}" fill="url(#ehg${uid})"/>`;
211
+ // flatten bottom of header
212
+ out += `\n <rect x="${x}" y="${y + HDR - R}" width="${w}" height="${R}"
213
+ fill="url(#ehg${uid})"/>`;
214
+ // Entity name
215
+ out += `\n <text x="${x + w / 2}" y="${y + 28}"
216
+ font-family="system-ui,-apple-system,sans-serif"
217
+ font-size="16" font-weight="800" fill="#FFFFFF"
218
+ text-anchor="middle">${xmlEsc(model.name)}</text>`;
219
+ // Table name sub-label
220
+ const tbl = model.tableName ?? model.name.toLowerCase() + 's';
221
+ out += `\n <text x="${x + w / 2}" y="${y + 42}"
222
+ font-family="'SF Mono',monospace" font-size="10"
223
+ fill="rgba(255,255,255,0.65)" text-anchor="middle">${xmlEsc(tbl)}</text>`;
224
+ // ── Field rows ──
225
+ fieldEntries.forEach(([colName, field], fi) => {
226
+ const fy = y + HDR + fi * ROW;
227
+ const isPK = !!field.primaryKey;
228
+ const isFK = !!(field.foreignKey || field.references);
229
+ const typeStr = resolveType(field);
230
+ const tc = typeCol(typeStr);
231
+ const rowBg = fi % 2 === 0 ? '#FFFFFF' : '#F9FAFB';
232
+ out += `\n <rect x="${x}" y="${fy}" width="${w}" height="${ROW}" fill="${rowBg}"/>`;
233
+ // Accent stripe
234
+ if (isPK)
235
+ out += `\n <rect x="${x}" y="${fy}" width="4" height="${ROW}" fill="#F59E0B"/>`;
236
+ else if (isFK)
237
+ out += `\n <rect x="${x}" y="${fy}" width="4" height="${ROW}" fill="#3B82F6"/>`;
238
+ // Icon (SVG instead of emoji)
239
+ const ixc = x + 14;
240
+ const ity = fy + ROW / 2 + 4;
241
+ if (isPK) {
242
+ out += `\n <g transform="translate(${ixc - 5}, ${ity - 7})">
243
+ <path d="M9 1l3 3-3 3 2 2 3-3 3 3 2-2-3-3 3-3-2-2-3 3-3-3z" fill="#F59E0B"/>
244
+ </g>`;
245
+ }
246
+ else if (isFK) {
247
+ out += `\n <g transform="translate(${ixc - 5}, ${ity - 7})">
248
+ <path d="M6 4l4 4-4 4M10 4l4 4" stroke="#3B82F6" stroke-width="1.5" fill="none"/>
249
+ </g>`;
250
+ }
251
+ else {
252
+ out += `\n <circle cx="${ixc}" cy="${fy + ROW / 2}" r="2.5" fill="#CBD5E1"/>`;
253
+ }
254
+ // Column name
255
+ const nameC = isPK ? '#92400E' : isFK ? '#1E40AF' : '#1E293B';
256
+ const nameW = isPK ? '700' : isFK ? '600' : '400';
257
+ const dispN = colName.length > 18 ? colName.slice(0, 17) + '…' : colName;
258
+ out += `\n <text x="${x + 24}" y="${ity}"
259
+ font-family="'SF Mono',monospace" font-size="11"
260
+ font-weight="${nameW}" fill="${nameC}">${xmlEsc(dispN)}</text>`;
261
+ // Type badge (right-aligned)
262
+ const typTxt = typeStr.length > 12 ? typeStr.slice(0, 11) + '…' : typeStr;
263
+ const tbW = typTxt.length * 6.5 + 10;
264
+ const tbX = x + w - tbW - 6;
265
+ out += `\n <rect x="${tbX}" y="${fy + 6}" width="${tbW}" height="16" rx="4" fill="${tc.bg}"/>`;
266
+ out += `\n <text x="${tbX + tbW / 2}" y="${fy + 18}"
267
+ font-family="'SF Mono',monospace" font-size="9"
268
+ font-weight="600" fill="${tc.text}" text-anchor="middle">${xmlEsc(typTxt)}</text>`;
269
+ // Row separator
270
+ out += `\n <line x1="${x}" y1="${fy + ROW}" x2="${x + w}" y2="${fy + ROW}"
271
+ stroke="#E2E8F0" stroke-width="0.5"/>`;
272
+ });
273
+ // ── Footer ──
274
+ const footY = y + HDR + fieldEntries.length * ROW;
275
+ out += `\n <rect x="${x}" y="${footY}" width="${w}" height="${FOOT}" fill="${colors.light}"/>`;
276
+ // round bottom
277
+ out += `\n <rect x="${x}" y="${footY + FOOT - R}" width="${w}" height="${R}"
278
+ rx="${R}" fill="${colors.light}"/>`;
279
+ const assocCount = (model.associations ?? []).length;
280
+ out += `\n <text x="${x + 10}" y="${footY + 19}"
281
+ font-family="system-ui,sans-serif" font-size="10" fill="${colors.text}" font-weight="600">
282
+ ${Object.keys(model.fields ?? {}).length} fields${assocCount > 0 ? ` · ${assocCount} associations` : ''}
283
+ </text>`;
284
+ return out;
285
+ }
286
+ // ── Draw relationship lines (crow's-foot) ──────────────────────────────
287
+ renderRelationships(layout) {
288
+ const byName = new Map();
289
+ layout.forEach((b) => byName.set(b.model.name, b));
290
+ let out = '\n <!-- ═══ RELATIONSHIPS ═══ -->';
291
+ // Relationship line colors
292
+ const REL_COLORS = {
293
+ hasOne: '#0891B2',
294
+ hasMany: '#7C3AED',
295
+ belongsTo: '#059669',
296
+ belongsToMany: '#DC2626',
297
+ };
298
+ // Track drawn pairs to avoid duplicates
299
+ const drawn = new Set();
300
+ layout.forEach((srcBox) => {
301
+ const assocs = srcBox.model.associations ?? [];
302
+ assocs.forEach((assoc) => {
303
+ const tgtBox = byName.get(assoc.target);
304
+ if (!tgtBox)
305
+ return;
306
+ // De-duplicate: only draw once per pair+type
307
+ const pairKey = [srcBox.model.name, assoc.target, assoc.type].sort().join('::');
308
+ if (drawn.has(pairKey))
309
+ return;
310
+ drawn.add(pairKey);
311
+ const color = REL_COLORS[assoc.type] ?? '#94A3B8';
312
+ const label = assoc.type;
313
+ // Choose connection points (pick nearest horizontal sides)
314
+ let sx, sy, tx, ty;
315
+ let srcSide, tgtSide;
316
+ const srcMidX = srcBox.x + srcBox.w / 2;
317
+ const tgtMidX = tgtBox.x + tgtBox.w / 2;
318
+ if (tgtMidX >= srcMidX) {
319
+ // target is to the right
320
+ sx = srcBox.x + srcBox.w;
321
+ sy = srcBox.y + srcBox.h / 2;
322
+ tx = tgtBox.x;
323
+ ty = tgtBox.y + tgtBox.h / 2;
324
+ srcSide = 'right';
325
+ tgtSide = 'left';
326
+ }
327
+ else {
328
+ // target is to the left
329
+ sx = srcBox.x;
330
+ sy = srcBox.y + srcBox.h / 2;
331
+ tx = tgtBox.x + tgtBox.w;
332
+ ty = tgtBox.y + tgtBox.h / 2;
333
+ srcSide = 'left';
334
+ tgtSide = 'right';
335
+ }
336
+ // If stacked vertically (same column), use top/bottom exits
337
+ if (Math.abs(srcMidX - tgtMidX) < 40) {
338
+ if (tgtBox.y > srcBox.y) {
339
+ sx = srcBox.x + srcBox.w / 2;
340
+ sy = srcBox.y + srcBox.h;
341
+ tx = tgtBox.x + tgtBox.w / 2;
342
+ ty = tgtBox.y;
343
+ }
344
+ else {
345
+ sx = srcBox.x + srcBox.w / 2;
346
+ sy = srcBox.y;
347
+ tx = tgtBox.x + tgtBox.w / 2;
348
+ ty = tgtBox.y + tgtBox.h;
349
+ }
350
+ srcSide = 'right'; // repurpose to mean 'bottom'
351
+ tgtSide = 'left'; // repurpose to mean 'top'
352
+ }
353
+ // Bezier control points
354
+ const dx = Math.abs(tx - sx);
355
+ const cpOffset = Math.max(dx * 0.5, 60);
356
+ const cpsx = srcSide === 'right' ? sx + cpOffset : sx - cpOffset;
357
+ const cptx = tgtSide === 'left' ? tx - cpOffset : tx + cpOffset;
358
+ // Main line
359
+ out += `\n <path d="M ${sx} ${sy} C ${cpsx} ${sy}, ${cptx} ${ty}, ${tx} ${ty}"
360
+ fill="none" stroke="${color}" stroke-width="1.8"
361
+ stroke-dasharray="0" opacity="0.85"/>`;
362
+ // ── Crow's foot at TARGET (many side for hasMany/belongsToMany) ──
363
+ const manyAtTarget = assoc.type === 'hasMany' || assoc.type === 'belongsToMany';
364
+ const manyAtSource = assoc.type === 'belongsTo' || assoc.type === 'belongsToMany';
365
+ if (manyAtTarget) {
366
+ out += this.crowFoot(tx, ty, tgtSide === 'left' ? 'right' : 'left', color);
367
+ }
368
+ else {
369
+ out += this.singleBar(tx, ty, tgtSide === 'left' ? 'right' : 'left', color);
370
+ }
371
+ if (manyAtSource) {
372
+ out += this.crowFoot(sx, sy, srcSide === 'right' ? 'left' : 'right', color);
373
+ }
374
+ else {
375
+ out += this.singleBar(sx, sy, srcSide === 'right' ? 'left' : 'right', color);
376
+ }
377
+ // Relationship label at midpoint
378
+ const mx = (sx + tx) / 2;
379
+ const my = (sy + ty) / 2;
380
+ const labelW = label.length * 7 + 16;
381
+ out += `\n <rect x="${mx - labelW / 2}" y="${my - 11}" width="${labelW}" height="18"
382
+ rx="5" fill="white" stroke="${color}" stroke-width="1" opacity="0.95"/>`;
383
+ out += `\n <text x="${mx}" y="${my + 3}"
384
+ font-family="system-ui,sans-serif" font-size="10" font-weight="600"
385
+ fill="${color}" text-anchor="middle">${xmlEsc(label)}</text>`;
386
+ // Alias label (smaller, below)
387
+ if (assoc.as) {
388
+ out += `\n <text x="${mx}" y="${my + 18}"
389
+ font-family="'SF Mono',monospace" font-size="9" fill="#94A3B8"
390
+ text-anchor="middle">as: ${xmlEsc(assoc.as)}</text>`;
391
+ }
392
+ });
393
+ });
394
+ return out;
395
+ }
396
+ // ── Crow's foot symbol ──────────────────────────────────────────────────
397
+ // direction = which way the line is approaching the symbol
398
+ // (the symbol points AWAY from the entity)
399
+ crowFoot(x, y, dir, color) {
400
+ // 'right' means line comes from the right → symbol points left into the entity
401
+ const sign = dir === 'right' ? -1 : 1;
402
+ const reach = 12;
403
+ const spread = 7;
404
+ return `
405
+ <!-- crow foot at (${x},${y}) dir=${dir} -->
406
+ <line x1="${x}" y1="${y}" x2="${x + sign * reach}" y2="${y - spread}"
407
+ stroke="${color}" stroke-width="1.8"/>
408
+ <line x1="${x}" y1="${y}" x2="${x + sign * reach}" y2="${y}"
409
+ stroke="${color}" stroke-width="1.8"/>
410
+ <line x1="${x}" y1="${y}" x2="${x + sign * reach}" y2="${y + spread}"
411
+ stroke="${color}" stroke-width="1.8"/>
412
+ <line x1="${x + sign * reach}" y1="${y - spread}" x2="${x + sign * reach}" y2="${y + spread}"
413
+ stroke="${color}" stroke-width="1.5"/>`;
414
+ }
415
+ // ── Single bar (one) symbol ─────────────────────────────────────────────
416
+ singleBar(x, y, dir, color) {
417
+ const sign = dir === 'right' ? -1 : 1;
418
+ const gap = 6;
419
+ const reach = 12;
420
+ const ht = 7;
421
+ return `
422
+ <!-- single bar at (${x},${y}) dir=${dir} -->
423
+ <line x1="${x + sign * gap}" y1="${y - ht}" x2="${x + sign * gap}" y2="${y + ht}"
424
+ stroke="${color}" stroke-width="1.8"/>
425
+ <line x1="${x + sign * reach}" y1="${y - ht}" x2="${x + sign * reach}" y2="${y + ht}"
426
+ stroke="${color}" stroke-width="1.8"/>`;
427
+ }
428
+ // ── Banner ──────────────────────────────────────────────────────────────
429
+ renderBanner(totalW) {
430
+ const H = ERDiagram.BANNER_H;
431
+ const { title, models } = this.opts;
432
+ const now = new Date().toISOString().slice(0, 10);
433
+ return /* xml */ `
434
+ <!-- ═══ BANNER ═══ -->
435
+ <defs>
436
+ <linearGradient id="erBanner" x1="0" y1="0" x2="1" y2="0"
437
+ gradientUnits="objectBoundingBox">
438
+ <stop offset="0%" stop-color="#0F172A"/>
439
+ <stop offset="60%" stop-color="#1E3A8A"/>
440
+ <stop offset="100%" stop-color="#2563EB"/>
441
+ </linearGradient>
442
+ </defs>
443
+ <rect width="${totalW}" height="${H}" fill="url(#erBanner)"/>
444
+ <text x="24" y="36"
445
+ font-family="system-ui,-apple-system,sans-serif"
446
+ font-size="24" font-weight="800" fill="#FFFFFF">${xmlEsc(title)}</text>
447
+ <text x="24" y="54"
448
+ font-family="system-ui,sans-serif" font-size="12" fill="rgba(255,255,255,0.55)">
449
+ ${models.length} entities · generated ${now}
450
+ </text>
451
+ <!-- ER tag pill -->
452
+ <rect x="${totalW - 100}" y="18" width="80" height="28" rx="14"
453
+ fill="rgba(255,255,255,0.15)"/>
454
+ <text x="${totalW - 60}" y="37"
455
+ font-family="'SF Mono',monospace" font-size="12" font-weight="700"
456
+ fill="rgba(255,255,255,0.9)" text-anchor="middle">ER DIAGRAM</text>`;
457
+ }
458
+ // ── Legend ──────────────────────────────────────────────────────────────
459
+ renderLegend(lx, ly) {
460
+ const LW = ERDiagram.LEGEND_W;
461
+ const LP = ERDiagram.LEGEND_PAD;
462
+ const lineH = 28;
463
+ const items = [
464
+ { type: 'hasOne', icon: '1:1', color: '#0891B2', label: 'Has One' },
465
+ { type: 'hasMany', icon: '1:N', color: '#7C3AED', label: 'Has Many' },
466
+ { type: 'belongsTo', icon: 'N:1', color: '#059669', label: 'Belongs To' },
467
+ { type: 'belongsToMany', icon: 'N:N', color: '#DC2626', label: 'Belongs To Many' },
468
+ ];
469
+ const legendH = LP * 2 + 24 + items.length * lineH;
470
+ let out = `\n <!-- ═══ LEGEND ═══ -->`;
471
+ out += `\n <rect x="${lx}" y="${ly}" width="${LW}" height="${legendH}"
472
+ rx="10" fill="white" stroke="#E2E8F0" stroke-width="1.5"/>`;
473
+ out += `\n <rect x="${lx}" y="${ly}" width="${LW}" height="34"
474
+ rx="10" fill="#0F172A"/>`;
475
+ out += `\n <rect x="${lx}" y="${ly + 20}" width="${LW}" height="14" fill="#0F172A"/>`;
476
+ out += `\n <text x="${lx + LW / 2}" y="${ly + 23}"
477
+ font-family="system-ui,sans-serif" font-size="12" font-weight="700"
478
+ fill="#FFFFFF" text-anchor="middle">LEGEND</text>`;
479
+ items.forEach((item, i) => {
480
+ const iy = ly + 34 + LP + i * lineH;
481
+ const badgeW = 32;
482
+ out += `\n <rect x="${lx + LP}" y="${iy + 5}" width="${badgeW}" height="18"
483
+ rx="5" fill="${item.color}22"/>`;
484
+ out += `\n <text x="${lx + LP + badgeW / 2}" y="${iy + 18}"
485
+ font-family="monospace" font-size="10" font-weight="700"
486
+ fill="${item.color}" text-anchor="middle">${item.icon}</text>`;
487
+ // Sample line
488
+ out += `\n <line x1="${lx + LP + badgeW + 6}" y1="${iy + 14}"
489
+ x2="${lx + LP + badgeW + 28}" y2="${iy + 14}"
490
+ stroke="${item.color}" stroke-width="2"/>`;
491
+ out += `\n <text x="${lx + LP + badgeW + 34}" y="${iy + 18}"
492
+ font-family="system-ui,sans-serif" font-size="12"
493
+ fill="#374151">${item.label}</text>`;
494
+ // Key icon legend (SVG instead of emoji)
495
+ if (i === items.length - 1) {
496
+ const ky = iy + lineH + 4;
497
+ // Key icon
498
+ out += `\n <g transform="translate(${lx + LP}, ${ky + 2})">
499
+ <path d="M8 0l2 2-2 2 1.5 1.5 2-2 2 2 1.5-1.5-2-2 2-2-1.5-1.5-2 2-2-2z" fill="#F59E0B"/>
500
+ </g>`;
501
+ out += `\n <text x="${lx + LP + 22}" y="${ky + 14}"
502
+ font-family="system-ui,sans-serif" font-size="11" fill="#64748B">Primary Key</text>`;
503
+ // Link icon
504
+ out += `\n <g transform="translate(${lx + LP}, ${ky + 18})">
505
+ <path d="M4 2l4 4-4 4M8 2l4 4" stroke="#3B82F6" stroke-width="1.5" fill="none"/>
506
+ </g>`;
507
+ out += `\n <text x="${lx + LP + 22}" y="${ky + 30}"
508
+ font-family="system-ui,sans-serif" font-size="11" fill="#64748B">Foreign Key</text>`;
509
+ }
510
+ });
511
+ return out;
512
+ }
513
+ // ── Generate the full SVG ────────────────────────────────────────────────
514
+ generate() {
515
+ const { models, hGap, vGap, columns } = this.opts;
516
+ const MARGIN = ERDiagram.MARGIN;
517
+ const BANNER = ERDiagram.BANNER_H;
518
+ const LEGEND_W = ERDiagram.LEGEND_W;
519
+ const BOX_W = ERDiagram.BOX_W;
520
+ const layout = this.computeLayout();
521
+ // Guard: with no models there are no boxes, so `Math.max(...[])` would be
522
+ // -Infinity and produce invalid width/height/viewBox. Emit a minimal, valid
523
+ // empty SVG (banner only) with sane dimensions instead.
524
+ if (layout.length === 0) {
525
+ const emptyW = BOX_W + 2 * MARGIN;
526
+ const emptyH = BANNER + MARGIN * 2;
527
+ return [
528
+ `<?xml version="1.0" encoding="UTF-8"?>`,
529
+ `<!-- Generated by ts-prorm-orm ERDiagram -->`,
530
+ `<svg xmlns="http://www.w3.org/2000/svg"`,
531
+ ` width="${emptyW}" height="${emptyH}"`,
532
+ ` viewBox="0 0 ${emptyW} ${emptyH}">`,
533
+ ` <rect width="${emptyW}" height="${emptyH}" fill="#F0F4F8"/>`,
534
+ this.renderBanner(emptyW),
535
+ ` <text x="${emptyW / 2}" y="${BANNER + MARGIN + 20}"`,
536
+ ` font-family="system-ui,sans-serif" font-size="14" fill="#64748B"`,
537
+ ` text-anchor="middle">No entities to display</text>`,
538
+ `</svg>`,
539
+ ].join('\n');
540
+ }
541
+ // Compute total canvas size
542
+ const maxX = Math.max(...layout.map((b) => b.x + b.w)) + MARGIN;
543
+ const maxY = Math.max(...layout.map((b) => b.y + b.h)) + MARGIN;
544
+ // Add space for legend on right (only if fits neatly)
545
+ const legendX = maxX + 20;
546
+ const legendY = BANNER + MARGIN;
547
+ const totalW = legendX + LEGEND_W + MARGIN;
548
+ const totalH = Math.max(maxY, legendY + 250);
549
+ let svg = [
550
+ `<?xml version="1.0" encoding="UTF-8"?>`,
551
+ `<!-- Generated by ts-prorm-orm ERDiagram -->`,
552
+ `<svg xmlns="http://www.w3.org/2000/svg"`,
553
+ ` width="${totalW}" height="${totalH}"`,
554
+ ` viewBox="0 0 ${totalW} ${totalH}">`,
555
+ ].join('\n');
556
+ // Background
557
+ svg += `\n <rect width="${totalW}" height="${totalH}" fill="#F0F4F8"/>`;
558
+ // Grid guide lines (subtle)
559
+ svg += `\n <!-- background grid -->`;
560
+ for (let gx = 0; gx < totalW; gx += 40) {
561
+ svg += `\n <line x1="${gx}" y1="${BANNER}" x2="${gx}" y2="${totalH}"
562
+ stroke="#E2E8F0" stroke-width="0.4" opacity="0.6"/>`;
563
+ }
564
+ for (let gy = BANNER; gy < totalH; gy += 40) {
565
+ svg += `\n <line x1="0" y1="${gy}" x2="${totalW}" y2="${gy}"
566
+ stroke="#E2E8F0" stroke-width="0.4" opacity="0.6"/>`;
567
+ }
568
+ // Banner
569
+ svg += this.renderBanner(totalW);
570
+ // Relationship lines (behind boxes)
571
+ svg += this.renderRelationships(layout);
572
+ // Entity boxes
573
+ layout.forEach((box, i) => {
574
+ svg += this.renderBox(box, i);
575
+ });
576
+ // Legend
577
+ svg += this.renderLegend(legendX, legendY);
578
+ svg += `\n</svg>`;
579
+ return svg;
580
+ }
581
+ /**
582
+ * Write the SVG to a file. Creates parent directories if needed.
583
+ */
584
+ save(outputPath) {
585
+ const resolved = path.resolve(outputPath);
586
+ const dir = path.dirname(resolved);
587
+ if (!fs.existsSync(dir))
588
+ fs.mkdirSync(dir, { recursive: true });
589
+ const svg = this.generate();
590
+ fs.writeFileSync(resolved, svg, 'utf-8');
591
+ return resolved;
592
+ }
593
+ }
594
+ exports.ERDiagram = ERDiagram;
595
+ // Entity box constants (px)
596
+ ERDiagram.BOX_W = 270;
597
+ ERDiagram.BOX_HDR = 44;
598
+ ERDiagram.BOX_ROW = 26;
599
+ ERDiagram.BOX_FOOT = 28;
600
+ ERDiagram.MIN_H = 100;
601
+ // Diagram chrome constants
602
+ ERDiagram.BANNER_H = 64;
603
+ ERDiagram.LEGEND_W = 200;
604
+ ERDiagram.LEGEND_PAD = 16;
605
+ ERDiagram.MARGIN = 48;