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.
- package/CHANGELOG.md +1111 -0
- package/LICENSE +21 -0
- package/README.md +573 -0
- package/dist/audit/history-query.js +180 -0
- package/dist/audit/index.js +23 -0
- package/dist/audit/logger.js +236 -0
- package/dist/cache/cache-manager.js +84 -0
- package/dist/cache/index.js +16 -0
- package/dist/cache/redis-cluster-cache.js +557 -0
- package/dist/cli.js +2200 -0
- package/dist/compliance/audit-trail.js +68 -0
- package/dist/compliance/backup-verification.js +685 -0
- package/dist/compliance/breach-detector.js +505 -0
- package/dist/compliance/consent-record.js +227 -0
- package/dist/compliance/consent-versioning.js +331 -0
- package/dist/compliance/cross-border-log.js +530 -0
- package/dist/compliance/data-classifier.js +258 -0
- package/dist/compliance/data-lineage.js +565 -0
- package/dist/compliance/data-masker.js +303 -0
- package/dist/compliance/data-portability.js +265 -0
- package/dist/compliance/data-retention.js +195 -0
- package/dist/compliance/dsar-workflow.js +342 -0
- package/dist/compliance/field-encryption.js +223 -0
- package/dist/compliance/immutable-record.js +148 -0
- package/dist/compliance/index.js +260 -0
- package/dist/compliance/privacy-impact-assessment.js +364 -0
- package/dist/compliance/pseudonymization.js +269 -0
- package/dist/compliance/query-firewall.js +1182 -0
- package/dist/compliance/rate-limiter.js +580 -0
- package/dist/compliance/right-to-erasure.js +117 -0
- package/dist/compliance/row-level-security.js +246 -0
- package/dist/compliance/security-decorator.js +574 -0
- package/dist/compliance/security-monitor.js +525 -0
- package/dist/compliance/sensitive-data-discovery.js +476 -0
- package/dist/compliance/session-isolation.js +472 -0
- package/dist/compliance/tls-enforcer.js +108 -0
- package/dist/compliance/worm-storage.js +712 -0
- package/dist/connection-manager.js +198 -0
- package/dist/connection-pool.js +519 -0
- package/dist/decorators/audit.js +136 -0
- package/dist/decorators/belongs-to-many.js +115 -0
- package/dist/decorators/belongs-to.js +115 -0
- package/dist/decorators/check.js +435 -0
- package/dist/decorators/collate.js +329 -0
- package/dist/decorators/comment.js +205 -0
- package/dist/decorators/database-settings.js +236 -0
- package/dist/decorators/default.js +244 -0
- package/dist/decorators/encryption.js +235 -0
- package/dist/decorators/engine.js +97 -0
- package/dist/decorators/fk-constraints.js +594 -0
- package/dist/decorators/foreign-table.js +136 -0
- package/dist/decorators/generated.js +274 -0
- package/dist/decorators/has-many.js +127 -0
- package/dist/decorators/has-one.js +116 -0
- package/dist/decorators/hstore.js +129 -0
- package/dist/decorators/index.js +355 -0
- package/dist/decorators/json-column.js +83 -0
- package/dist/decorators/jsonb.js +101 -0
- package/dist/decorators/orm-decorators.js +425 -0
- package/dist/decorators/permissions.js +294 -0
- package/dist/decorators/procedure.js +210 -0
- package/dist/decorators/query-options.js +556 -0
- package/dist/decorators/range.js +167 -0
- package/dist/decorators/set-column.js +83 -0
- package/dist/decorators/spatial.js +114 -0
- package/dist/decorators/storage.js +580 -0
- package/dist/decorators/timezone.js +512 -0
- package/dist/decorators/trigger.js +90 -0
- package/dist/decorators/uuid.js +135 -0
- package/dist/decorators/view.js +258 -0
- package/dist/diagrams/chen-diagram.js +354 -0
- package/dist/diagrams/class-diagram.js +384 -0
- package/dist/diagrams/dependency-diagram.js +432 -0
- package/dist/diagrams/er-diagram.js +605 -0
- package/dist/diagrams/flow-diagram.js +394 -0
- package/dist/diagrams/gantt-diagram.js +411 -0
- package/dist/diagrams/index-diagram.js +353 -0
- package/dist/diagrams/index.js +184 -0
- package/dist/diagrams/migration-diagram.js +316 -0
- package/dist/diagrams/model-diagram.js +616 -0
- package/dist/diagrams/package-diagram.js +376 -0
- package/dist/diagrams/palette.js +85 -0
- package/dist/diagrams/relational-diagram.js +455 -0
- package/dist/diagrams/schemadoc-diagram.js +309 -0
- package/dist/diagrams/sequence-diagram.js +307 -0
- package/dist/diagrams/state-diagram.js +344 -0
- package/dist/diagrams/svg-dom.js +111 -0
- package/dist/diagrams/tree-diagram.js +250 -0
- package/dist/dialects/clickhouse/index.js +1742 -0
- package/dist/dialects/cockroachdb/index.js +4677 -0
- package/dist/dialects/cratedb/index.js +441 -0
- package/dist/dialects/databricks/index.js +517 -0
- package/dist/dialects/db2/index.js +2225 -0
- package/dist/dialects/dialect.js +720 -0
- package/dist/dialects/duckdb/index.js +2014 -0
- package/dist/dialects/exasol/index.js +357 -0
- package/dist/dialects/firebird/index.js +495 -0
- package/dist/dialects/greenplum/index.js +457 -0
- package/dist/dialects/hana/index.js +1741 -0
- package/dist/dialects/mariadb/index.js +3590 -0
- package/dist/dialects/mssql/index.js +2296 -0
- package/dist/dialects/mysql/index.js +4058 -0
- package/dist/dialects/oracle/index.js +2999 -0
- package/dist/dialects/postgres/index.js +5319 -0
- package/dist/dialects/query-stream-helper.js +152 -0
- package/dist/dialects/questdb/index.js +433 -0
- package/dist/dialects/redshift/index.js +2277 -0
- package/dist/dialects/singlestore/index.js +354 -0
- package/dist/dialects/snowflake/index.js +2082 -0
- package/dist/dialects/spanner/index.js +1768 -0
- package/dist/dialects/sqlite/index.js +3382 -0
- package/dist/dialects/tidb/index.js +377 -0
- package/dist/dialects/timescaledb/index.js +164 -0
- package/dist/dialects/trino/index.js +420 -0
- package/dist/dialects/turso/index.js +227 -0
- package/dist/dialects/vertica/index.js +317 -0
- package/dist/dialects/yugabytedb/index.js +424 -0
- package/dist/errors/index.js +472 -0
- package/dist/errors/utils.js +340 -0
- package/dist/errors.js +21 -0
- package/dist/extensions/catalog/cloud-warehouse-features.js +207 -0
- package/dist/extensions/catalog/mssql-features.js +147 -0
- package/dist/extensions/catalog/mysql-mariadb-plugins.js +229 -0
- package/dist/extensions/catalog/oracle-db2-features.js +196 -0
- package/dist/extensions/catalog/postgres-extensions.js +516 -0
- package/dist/extensions/index.js +71 -0
- package/dist/extensions/types.js +13 -0
- package/dist/foreign-data.js +173 -0
- package/dist/hooks/hooks-manager.js +350 -0
- package/dist/hooks/index.js +37 -0
- package/dist/index.js +463 -0
- package/dist/logging.js +335 -0
- package/dist/migrations/index.js +40 -0
- package/dist/migrations/migration.js +196 -0
- package/dist/migrations/migrator.js +411 -0
- package/dist/migrations/prormmigration.js +275 -0
- package/dist/migrations/query-interface.js +435 -0
- package/dist/migrations/seeder.js +353 -0
- package/dist/models/associations.js +852 -0
- package/dist/models/constraints.js +288 -0
- package/dist/models/data-types.js +2518 -0
- package/dist/models/decorators.js +445 -0
- package/dist/models/index.js +33 -0
- package/dist/models/indexes.js +531 -0
- package/dist/models/methods.js +382 -0
- package/dist/models/model-manager.js +103 -0
- package/dist/models/model.js +5349 -0
- package/dist/models/operators.js +67 -0
- package/dist/models/scopes.js +189 -0
- package/dist/models/typescript-types.js +26 -0
- package/dist/nosql/aerospike/index.js +205 -0
- package/dist/nosql/allegrograph/index.js +169 -0
- package/dist/nosql/arangodb/index.js +364 -0
- package/dist/nosql/azure-blob/index.js +206 -0
- package/dist/nosql/beanstalkd/index.js +231 -0
- package/dist/nosql/beequeue/index.js +210 -0
- package/dist/nosql/bigchaindb/index.js +195 -0
- package/dist/nosql/bigtable/index.js +224 -0
- package/dist/nosql/blazegraph/index.js +173 -0
- package/dist/nosql/bullmq/index.js +191 -0
- package/dist/nosql/cassandra/index.js +174 -0
- package/dist/nosql/chroma/index.js +190 -0
- package/dist/nosql/cloudflare-kv/index.js +220 -0
- package/dist/nosql/coherence/index.js +200 -0
- package/dist/nosql/cosmosdb/index.js +157 -0
- package/dist/nosql/couchbase/index.js +213 -0
- package/dist/nosql/dax/index.js +212 -0
- package/dist/nosql/deno-kv/index.js +206 -0
- package/dist/nosql/dgraph/index.js +171 -0
- package/dist/nosql/doris/index.js +169 -0
- package/dist/nosql/druid/index.js +162 -0
- package/dist/nosql/dynamodb/index.js +937 -0
- package/dist/nosql/elasticsearch/index.js +377 -0
- package/dist/nosql/etcd/index.js +502 -0
- package/dist/nosql/eventhubs/index.js +213 -0
- package/dist/nosql/eventstore/index.js +254 -0
- package/dist/nosql/faunadb/index.js +188 -0
- package/dist/nosql/firestore/index.js +177 -0
- package/dist/nosql/fluree/index.js +148 -0
- package/dist/nosql/fuseki/index.js +170 -0
- package/dist/nosql/gcs/index.js +172 -0
- package/dist/nosql/gearman/index.js +160 -0
- package/dist/nosql/geode/index.js +196 -0
- package/dist/nosql/graphdb/index.js +169 -0
- package/dist/nosql/graylog/index.js +188 -0
- package/dist/nosql/gridgain/index.js +171 -0
- package/dist/nosql/hazelcast/index.js +162 -0
- package/dist/nosql/hbase/index.js +230 -0
- package/dist/nosql/ignite/index.js +173 -0
- package/dist/nosql/immudb/index.js +184 -0
- package/dist/nosql/index.js +232 -0
- package/dist/nosql/infinispan/index.js +200 -0
- package/dist/nosql/influxdb/index.js +0 -0
- package/dist/nosql/kafka/index.js +234 -0
- package/dist/nosql/keyspaces/index.js +189 -0
- package/dist/nosql/kinesis/index.js +253 -0
- package/dist/nosql/leveldb/index.js +153 -0
- package/dist/nosql/lmdb/index.js +160 -0
- package/dist/nosql/loki/index.js +201 -0
- package/dist/nosql/marklogic/index.js +204 -0
- package/dist/nosql/materialize/index.js +145 -0
- package/dist/nosql/meilisearch/index.js +154 -0
- package/dist/nosql/memcached/index.js +223 -0
- package/dist/nosql/milvus/index.js +410 -0
- package/dist/nosql/minio/index.js +265 -0
- package/dist/nosql/momento/index.js +179 -0
- package/dist/nosql/mongodb/index.js +461 -0
- package/dist/nosql/nats/index.js +247 -0
- package/dist/nosql/nedb/index.js +164 -0
- package/dist/nosql/neo4j/index.js +450 -0
- package/dist/nosql/neptune/index.js +470 -0
- package/dist/nosql/nsq/index.js +200 -0
- package/dist/nosql/opensearch/index.js +186 -0
- package/dist/nosql/orientdb/index.js +175 -0
- package/dist/nosql/papertrail/index.js +200 -0
- package/dist/nosql/pinecone/index.js +0 -0
- package/dist/nosql/pinot/index.js +133 -0
- package/dist/nosql/pouchdb/index.js +172 -0
- package/dist/nosql/prometheus/index.js +174 -0
- package/dist/nosql/provendb/index.js +147 -0
- package/dist/nosql/pubsub/index.js +187 -0
- package/dist/nosql/pulsar/index.js +232 -0
- package/dist/nosql/qdrant/index.js +295 -0
- package/dist/nosql/qldb/index.js +192 -0
- package/dist/nosql/r2/index.js +281 -0
- package/dist/nosql/rabbitmq/index.js +237 -0
- package/dist/nosql/ravendb/index.js +175 -0
- package/dist/nosql/redis/index.js +607 -0
- package/dist/nosql/redpanda/index.js +237 -0
- package/dist/nosql/resque/index.js +203 -0
- package/dist/nosql/rethinkdb/index.js +232 -0
- package/dist/nosql/rocksdb/index.js +152 -0
- package/dist/nosql/rockset/index.js +126 -0
- package/dist/nosql/s3/index.js +298 -0
- package/dist/nosql/scylladb/index.js +178 -0
- package/dist/nosql/signoz/index.js +227 -0
- package/dist/nosql/sns/index.js +201 -0
- package/dist/nosql/solr/index.js +231 -0
- package/dist/nosql/splunk/index.js +227 -0
- package/dist/nosql/sqs/index.js +246 -0
- package/dist/nosql/stardog/index.js +169 -0
- package/dist/nosql/starrocks/index.js +170 -0
- package/dist/nosql/store.js +2 -0
- package/dist/nosql/sumologic/index.js +213 -0
- package/dist/nosql/surrealdb/index.js +179 -0
- package/dist/nosql/terminusdb/index.js +151 -0
- package/dist/nosql/tigergraph/index.js +469 -0
- package/dist/nosql/typesense/index.js +148 -0
- package/dist/nosql/unqlite/index.js +157 -0
- package/dist/nosql/upstash/index.js +167 -0
- package/dist/nosql/vercel-kv/index.js +166 -0
- package/dist/nosql/victoriametrics/index.js +213 -0
- package/dist/nosql/virtuoso/index.js +169 -0
- package/dist/nosql/weaviate/index.js +276 -0
- package/dist/operators/index.js +108 -0
- package/dist/operators.js +2690 -0
- package/dist/prisma-migrate/index.js +38 -0
- package/dist/prisma-migrate/migration-generator.js +125 -0
- package/dist/prisma-migrate/model-generator.js +172 -0
- package/dist/prisma-migrate/relations.js +100 -0
- package/dist/prisma-migrate/schema-parser.js +167 -0
- package/dist/prisma-migrate/type-mapper.js +40 -0
- package/dist/prorm.js +6832 -0
- package/dist/query-builders/cte-builder.js +80 -0
- package/dist/query-builders/functions/aggregate.js +390 -0
- package/dist/query-builders/functions/conditional.js +503 -0
- package/dist/query-builders/functions/datetime.js +695 -0
- package/dist/query-builders/functions/fulltext.js +439 -0
- package/dist/query-builders/functions/index.js +93 -0
- package/dist/query-builders/functions/json.js +427 -0
- package/dist/query-builders/functions/math.js +399 -0
- package/dist/query-builders/functions/string.js +518 -0
- package/dist/query-builders/functions/window.js +328 -0
- package/dist/query-builders/include-builder.js +323 -0
- package/dist/query-builders/index-expression-builder.js +242 -0
- package/dist/query-builders/index.js +161 -0
- package/dist/query-builders/insert-builder.js +164 -0
- package/dist/query-builders/model-helpers.js +38 -0
- package/dist/query-builders/order-limit-builder.js +239 -0
- package/dist/query-builders/sql-compiler.js +2040 -0
- package/dist/query-builders/subquery-builder.js +152 -0
- package/dist/query-builders/update-builder.js +182 -0
- package/dist/query-builders/view-builder.js +218 -0
- package/dist/query-builders/where-builder.js +1532 -0
- package/dist/query-interface.js +622 -0
- package/dist/query-optimizers/batch-optimizer.js +426 -0
- package/dist/query-optimizers/explain-plans.js +415 -0
- package/dist/query-optimizers/index.js +51 -0
- package/dist/query-optimizers/prepared-statement-cache.js +423 -0
- package/dist/query-optimizers/query-hints.js +438 -0
- package/dist/query-optimizers/query-optimizer.js +278 -0
- package/dist/query-optimizers/slow-query-logger.js +271 -0
- package/dist/replica-manager.js +507 -0
- package/dist/schema/index.js +15 -0
- package/dist/schema/migration-generator.js +374 -0
- package/dist/schema/schema-differ.js +549 -0
- package/dist/schema/types.js +6 -0
- package/dist/sql-constants.js +300 -0
- package/dist/sqlite-advanced.js +1047 -0
- package/dist/streams/index.js +12 -0
- package/dist/streams/transforms.js +178 -0
- package/dist/transaction.js +456 -0
- package/dist/types/index.js +180 -0
- package/dist/user-management.js +127 -0
- package/dist/utils/date.js +300 -0
- package/dist/utils/index.js +1079 -0
- package/dist/utils/string.js +161 -0
- package/dist/validators/index.js +19 -0
- package/dist/validators/validator.js +911 -0
- 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, '&')
|
|
103
|
+
.replace(/</g, '<')
|
|
104
|
+
.replace(/>/g, '>')
|
|
105
|
+
.replace(/"/g, '"');
|
|
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;
|