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,852 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Association classes for the ORM
|
|
4
|
+
* Supports hasOne, hasMany, belongsTo, and belongsToMany relationships
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.BelongsToManyAssociation = exports.BelongsToAssociation = exports.HasManyAssociation = exports.HasOneAssociation = void 0;
|
|
8
|
+
exports.createAssociationMixins = createAssociationMixins;
|
|
9
|
+
exports.processNestedIncludes = processNestedIncludes;
|
|
10
|
+
// Helper to lower-case the first character of a model name so inferred foreign
|
|
11
|
+
// key column names follow the lowerCamelCase convention used everywhere else
|
|
12
|
+
// in this codebase (e.g. `userId`, not `UserId`). Every test that pre-defines
|
|
13
|
+
// a foreign key column and relies on an association's default inference
|
|
14
|
+
// (rather than passing an explicit `foreignKey` option) uses lowerCamelCase,
|
|
15
|
+
// and downstream lookups (e.g. eager-load default-key resolution in
|
|
16
|
+
// prorm.ts) already lowercase the first character - the association classes
|
|
17
|
+
// below were the one place still producing PascalCase defaults, so the
|
|
18
|
+
// inferred key never matched the actual (lowerCamelCase) column, silently
|
|
19
|
+
// breaking foreign-key constraint enforcement and default-association CRUD
|
|
20
|
+
// helpers (e.g. `createPost`) whenever no explicit `foreignKey` was given.
|
|
21
|
+
function lowerFirst(name) {
|
|
22
|
+
if (!name)
|
|
23
|
+
return name;
|
|
24
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
25
|
+
}
|
|
26
|
+
// Helper to extract foreign key name(s) from various formats
|
|
27
|
+
// Supports composite foreign keys: foreignKey: ['field1', 'field2']
|
|
28
|
+
function extractForeignKeyName(foreignKey, defaultKey) {
|
|
29
|
+
if (!foreignKey) {
|
|
30
|
+
return defaultKey;
|
|
31
|
+
}
|
|
32
|
+
// Handle array format for composite foreign keys
|
|
33
|
+
if (Array.isArray(foreignKey)) {
|
|
34
|
+
return foreignKey;
|
|
35
|
+
}
|
|
36
|
+
if (typeof foreignKey === 'string') {
|
|
37
|
+
return foreignKey;
|
|
38
|
+
}
|
|
39
|
+
// Handle object form: { name: 'profile_id' } or { allowNull: false }
|
|
40
|
+
return foreignKey.name || defaultKey;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Check if foreign key is a composite key (array)
|
|
44
|
+
*/
|
|
45
|
+
function isCompositeForeignKey(foreignKey) {
|
|
46
|
+
return Array.isArray(foreignKey) && foreignKey.length > 1;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Format foreign key for SQL (handles both single and composite).
|
|
50
|
+
* Always returns a parenthesized column list ready to embed directly after
|
|
51
|
+
* `FOREIGN KEY` / table name in a constraint clause, e.g. `(id)` or `(a, b)`.
|
|
52
|
+
*/
|
|
53
|
+
function formatForeignKeyForSql(foreignKey) {
|
|
54
|
+
if (Array.isArray(foreignKey)) {
|
|
55
|
+
return `(${foreignKey.join(', ')})`;
|
|
56
|
+
}
|
|
57
|
+
return `(${foreignKey})`;
|
|
58
|
+
}
|
|
59
|
+
// Helper to extract allowNull from foreignKey options
|
|
60
|
+
function extractAllowNull(foreignKey, defaultValue = true) {
|
|
61
|
+
if (!foreignKey || typeof foreignKey === 'string') {
|
|
62
|
+
return defaultValue;
|
|
63
|
+
}
|
|
64
|
+
return foreignKey.allowNull ?? defaultValue;
|
|
65
|
+
}
|
|
66
|
+
// Base Association class
|
|
67
|
+
class BaseAssociation {
|
|
68
|
+
constructor(source, target, options = {}) {
|
|
69
|
+
this.source = source;
|
|
70
|
+
this.target = target;
|
|
71
|
+
// Handle string, array, and object forms of foreignKey
|
|
72
|
+
// e.g., 'profile_id' or ['order_id', 'product_id'] or { name: 'profile_id', allowNull: false }
|
|
73
|
+
this.foreignKey = extractForeignKeyName(options.foreignKey, this.inferForeignKey());
|
|
74
|
+
this.as = options.as || this.target.name.toLowerCase();
|
|
75
|
+
this.options = options;
|
|
76
|
+
this.type = this.getType();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get the association config for queries
|
|
80
|
+
*/
|
|
81
|
+
getAssociationConfig() {
|
|
82
|
+
return {
|
|
83
|
+
model: this.target,
|
|
84
|
+
as: this.as,
|
|
85
|
+
foreignKey: this.foreignKey,
|
|
86
|
+
targetKey: this.options.targetKey,
|
|
87
|
+
sourceKey: this.options.sourceKey,
|
|
88
|
+
constraints: this.options.constraints,
|
|
89
|
+
onDelete: this.options.onDelete,
|
|
90
|
+
onUpdate: this.options.onUpdate,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Flatten nested includes to a flat array with path information
|
|
96
|
+
*/
|
|
97
|
+
class HasOneAssociation extends BaseAssociation {
|
|
98
|
+
constructor(source, target, options = {}) {
|
|
99
|
+
super(source, target, options);
|
|
100
|
+
}
|
|
101
|
+
inferForeignKey() {
|
|
102
|
+
// Default foreign key is target model name + Id (e.g., userId)
|
|
103
|
+
return `${lowerFirst(this.target.name)}Id`;
|
|
104
|
+
}
|
|
105
|
+
getType() {
|
|
106
|
+
return 'hasOne';
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Get the foreign key constraint SQL
|
|
110
|
+
*/
|
|
111
|
+
getConstraintSql(tableName) {
|
|
112
|
+
const targetKey = this.options.targetKey || 'id';
|
|
113
|
+
// Handle composite foreign keys (string | string[])
|
|
114
|
+
const fk = formatForeignKeyForSql(this.foreignKey);
|
|
115
|
+
const tk = formatForeignKeyForSql(targetKey);
|
|
116
|
+
let sql = `FOREIGN KEY ${fk} REFERENCES ${tableName}${tk}`;
|
|
117
|
+
if (this.options.onDelete) {
|
|
118
|
+
sql += ` ON DELETE ${this.options.onDelete}`;
|
|
119
|
+
}
|
|
120
|
+
if (this.options.onUpdate) {
|
|
121
|
+
sql += ` ON UPDATE ${this.options.onUpdate}`;
|
|
122
|
+
}
|
|
123
|
+
return sql;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.HasOneAssociation = HasOneAssociation;
|
|
127
|
+
// HasMany Association - one-to-many where source has many targets
|
|
128
|
+
class HasManyAssociation extends BaseAssociation {
|
|
129
|
+
constructor(source, target, options = {}) {
|
|
130
|
+
super(source, target, options);
|
|
131
|
+
// Override to use plural form for hasMany
|
|
132
|
+
if (!options.as) {
|
|
133
|
+
this.as = this.target.name.toLowerCase() + 's';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
inferForeignKey() {
|
|
137
|
+
return `${lowerFirst(this.source.name)}Id`;
|
|
138
|
+
}
|
|
139
|
+
getType() {
|
|
140
|
+
return 'hasMany';
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get the foreign key constraint SQL
|
|
144
|
+
*/
|
|
145
|
+
getConstraintSql(tableName) {
|
|
146
|
+
const targetKey = this.options.targetKey || 'id';
|
|
147
|
+
const fk = formatForeignKeyForSql(this.foreignKey);
|
|
148
|
+
const tk = formatForeignKeyForSql(targetKey);
|
|
149
|
+
let sql = `FOREIGN KEY ${fk} REFERENCES ${tableName}${tk}`;
|
|
150
|
+
if (this.options.onDelete) {
|
|
151
|
+
sql += ` ON DELETE ${this.options.onDelete}`;
|
|
152
|
+
}
|
|
153
|
+
if (this.options.onUpdate) {
|
|
154
|
+
sql += ` ON UPDATE ${this.options.onUpdate}`;
|
|
155
|
+
}
|
|
156
|
+
return sql;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
exports.HasManyAssociation = HasManyAssociation;
|
|
160
|
+
// BelongsTo Association - one-to-one where source belongs to target
|
|
161
|
+
class BelongsToAssociation extends BaseAssociation {
|
|
162
|
+
constructor(source, target, options = {}) {
|
|
163
|
+
super(source, target, options);
|
|
164
|
+
}
|
|
165
|
+
inferForeignKey() {
|
|
166
|
+
// Default foreign key is target model name + Id (e.g., userId)
|
|
167
|
+
return `${lowerFirst(this.target.name)}Id`;
|
|
168
|
+
}
|
|
169
|
+
getType() {
|
|
170
|
+
return 'belongsTo';
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Get the foreign key constraint SQL with onDelete and onUpdate support
|
|
174
|
+
*/
|
|
175
|
+
getConstraintSql(tableName) {
|
|
176
|
+
const targetKey = this.options.targetKey || 'id';
|
|
177
|
+
const fk = formatForeignKeyForSql(this.foreignKey);
|
|
178
|
+
const tk = formatForeignKeyForSql(targetKey);
|
|
179
|
+
let sql = `FOREIGN KEY ${fk} REFERENCES ${tableName}${tk}`;
|
|
180
|
+
if (this.options.onDelete) {
|
|
181
|
+
sql += ` ON DELETE ${this.options.onDelete}`;
|
|
182
|
+
}
|
|
183
|
+
if (this.options.onUpdate) {
|
|
184
|
+
sql += ` ON UPDATE ${this.options.onUpdate}`;
|
|
185
|
+
}
|
|
186
|
+
return sql;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Get the full foreignKey options including allowNull from object form
|
|
190
|
+
*/
|
|
191
|
+
getForeignKeyOptions() {
|
|
192
|
+
const fk = this.options.foreignKey;
|
|
193
|
+
if (!fk || typeof fk === 'string') {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
return fk;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
exports.BelongsToAssociation = BelongsToAssociation;
|
|
200
|
+
// BelongsToMany Association - many-to-many through a junction table
|
|
201
|
+
class BelongsToManyAssociation extends BaseAssociation {
|
|
202
|
+
constructor(source, target, options = {}) {
|
|
203
|
+
super(source, target, options);
|
|
204
|
+
this.throughModel = null;
|
|
205
|
+
// Override to use plural form for belongsToMany
|
|
206
|
+
if (!options.as) {
|
|
207
|
+
this.as = this.target.name.toLowerCase() + 's';
|
|
208
|
+
}
|
|
209
|
+
// Determine the through table/model
|
|
210
|
+
if (typeof options.through === 'string') {
|
|
211
|
+
this.through = options.through;
|
|
212
|
+
}
|
|
213
|
+
else if (options.through && typeof options.through === 'object') {
|
|
214
|
+
// `options.through` is typed as `string | ThroughOptions`, but at runtime callers may
|
|
215
|
+
// also pass a full ModelStatic directly (e.g. `through: UserProject`). Treat it as an
|
|
216
|
+
// untyped record here so we can duck-type which shape we actually received, then cast
|
|
217
|
+
// to the appropriate typed value for each branch below (avoids invalid casts to
|
|
218
|
+
// ModelStatic<any> from a non-overlapping ThroughOptions type).
|
|
219
|
+
const throughVal = options.through;
|
|
220
|
+
// Check if through is a model directly (not an options object)
|
|
221
|
+
if (throughVal.name && throughVal.rawAttributes) {
|
|
222
|
+
// It's a model
|
|
223
|
+
this.through = throughVal.tableName || `${source.name}${target.name}`;
|
|
224
|
+
this.throughModel = throughVal;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
// It's an options object (ThroughOptions)
|
|
228
|
+
const throughOptions = throughVal;
|
|
229
|
+
this.through =
|
|
230
|
+
typeof throughOptions.model === 'string'
|
|
231
|
+
? throughOptions.model
|
|
232
|
+
: throughOptions.model?.tableName ||
|
|
233
|
+
`${source.name}${target.name}`;
|
|
234
|
+
this.throughModel = throughOptions.model || null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else if (options.through) {
|
|
238
|
+
// Handle case where through is passed directly as a model
|
|
239
|
+
this.through = options.through.tableName || `${source.name}${target.name}`;
|
|
240
|
+
this.throughModel = options.through;
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
// Default through table name
|
|
244
|
+
this.through = `${source.name}${target.name}`;
|
|
245
|
+
}
|
|
246
|
+
// Other key in the junction table. Supports both string ('roleId') and object
|
|
247
|
+
// ({ name: 'roleId', allowNull: false }) forms, same as top-level foreignKey.
|
|
248
|
+
const throughOtherKey = options.through && typeof options.through === 'object'
|
|
249
|
+
? options.through.otherKey
|
|
250
|
+
: undefined;
|
|
251
|
+
// throughOtherKey can only be string | AssociationForeignKeyOptions (no composite array form),
|
|
252
|
+
// so the result of extractForeignKeyName is always a plain string here.
|
|
253
|
+
this._otherKey = extractForeignKeyName(throughOtherKey, `${lowerFirst(target.name)}Id`);
|
|
254
|
+
}
|
|
255
|
+
inferForeignKey() {
|
|
256
|
+
return `${lowerFirst(this.source.name)}Id`;
|
|
257
|
+
}
|
|
258
|
+
getType() {
|
|
259
|
+
return 'belongsToMany';
|
|
260
|
+
}
|
|
261
|
+
getThrough() {
|
|
262
|
+
return this.through;
|
|
263
|
+
}
|
|
264
|
+
getOtherKey() {
|
|
265
|
+
return this._otherKey;
|
|
266
|
+
}
|
|
267
|
+
setThroughModel(model) {
|
|
268
|
+
this.throughModel = model;
|
|
269
|
+
}
|
|
270
|
+
getThroughModel() {
|
|
271
|
+
return this.throughModel;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
exports.BelongsToManyAssociation = BelongsToManyAssociation;
|
|
275
|
+
// Factory function to create association mixin methods
|
|
276
|
+
function createAssociationMixins(source, sourceModelName, instancePrototype) {
|
|
277
|
+
// Use provided instance prototype, or fall back to source.prototype, or source itself
|
|
278
|
+
const prototypeTarget = instancePrototype || source.prototype || source;
|
|
279
|
+
return {
|
|
280
|
+
hasOne: (target, options = {}) => {
|
|
281
|
+
const association = new HasOneAssociation(source, target, options);
|
|
282
|
+
// Add to source associations
|
|
283
|
+
source.associations[association.as] = association;
|
|
284
|
+
// Get the prototype target - use source.prototype if it exists, otherwise use source
|
|
285
|
+
// Add the getter to the source model
|
|
286
|
+
prototypeTarget[`get${capitalize(association.as)}`] = function (options) {
|
|
287
|
+
const pk = getPrimaryKeyValue(this);
|
|
288
|
+
return target.findOne({
|
|
289
|
+
where: { [association.foreignKey]: pk },
|
|
290
|
+
...options,
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
// Add the setter - supports instance, null, or foreign key value
|
|
294
|
+
prototypeTarget[`set${capitalize(association.as)}`] = async function (value, options) {
|
|
295
|
+
const pk = getPrimaryKeyValue(this);
|
|
296
|
+
// Handle different value types
|
|
297
|
+
if (value === null) {
|
|
298
|
+
// Remove association by setting foreign key to null
|
|
299
|
+
await target.update({ [association.foreignKey]: null }, { where: { [association.options.targetKey || 'id']: pk }, ...options });
|
|
300
|
+
}
|
|
301
|
+
else if (typeof value === 'object' &&
|
|
302
|
+
value !== null &&
|
|
303
|
+
value[association.options.targetKey || 'id'] !== undefined) {
|
|
304
|
+
// It's an instance - set the foreign key to its primary key
|
|
305
|
+
const targetPk = value[association.options.targetKey || 'id'] || value.id;
|
|
306
|
+
await target.update({ [association.foreignKey]: pk }, { where: { [association.options.targetKey || 'id']: targetPk }, ...options });
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
// It's a raw foreign key value (number/string)
|
|
310
|
+
await target.update({ [association.foreignKey]: value }, { where: { [association.options.targetKey || 'id']: pk }, ...options });
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
// Add the create method - creates and associates in one step
|
|
314
|
+
prototypeTarget[`create${capitalize(association.as)}`] = async function (values, options) {
|
|
315
|
+
const pk = getPrimaryKeyValue(this);
|
|
316
|
+
return target.create({ ...values, [association.foreignKey]: pk }, options);
|
|
317
|
+
};
|
|
318
|
+
// Add the has method - check if associated record exists
|
|
319
|
+
prototypeTarget[`has${capitalize(association.as)}`] = async function (options) {
|
|
320
|
+
const pk = getPrimaryKeyValue(this);
|
|
321
|
+
const count = await target.count({
|
|
322
|
+
where: { [association.foreignKey]: pk },
|
|
323
|
+
...options,
|
|
324
|
+
});
|
|
325
|
+
return count > 0;
|
|
326
|
+
};
|
|
327
|
+
// Add the count method - count associated records (will be 0 or 1 for hasOne)
|
|
328
|
+
prototypeTarget[`count${capitalize(association.as)}`] = async function (options) {
|
|
329
|
+
const pk = getPrimaryKeyValue(this);
|
|
330
|
+
return target.count({ where: { [association.foreignKey]: pk }, ...options });
|
|
331
|
+
};
|
|
332
|
+
return association;
|
|
333
|
+
},
|
|
334
|
+
hasMany: (target, options = {}) => {
|
|
335
|
+
const association = new HasManyAssociation(source, target, options);
|
|
336
|
+
// Add to source associations
|
|
337
|
+
source.associations[association.as] = association;
|
|
338
|
+
// Get the prototype target - use source.prototype if it exists, otherwise use source
|
|
339
|
+
// Add the getter
|
|
340
|
+
prototypeTarget[`get${capitalize(association.as)}`] = function (options) {
|
|
341
|
+
const pk = getPrimaryKeyValue(this);
|
|
342
|
+
return target.findAll({
|
|
343
|
+
where: { [association.foreignKey]: pk },
|
|
344
|
+
...options,
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
// Add the setter
|
|
348
|
+
prototypeTarget[`set${capitalize(association.as)}`] = async function (values, options) {
|
|
349
|
+
const pk = getPrimaryKeyValue(this);
|
|
350
|
+
// Remove existing and add new
|
|
351
|
+
await target.destroy({ where: { [association.foreignKey]: pk }, ...options });
|
|
352
|
+
for (const value of values) {
|
|
353
|
+
await target.create({ ...value, [association.foreignKey]: pk }, options);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
// Add the add method
|
|
357
|
+
prototypeTarget[`add${capitalize(association.as)}`] = async function (values, options) {
|
|
358
|
+
const pk = getPrimaryKeyValue(this);
|
|
359
|
+
if (Array.isArray(values)) {
|
|
360
|
+
for (const value of values) {
|
|
361
|
+
await target.update({ [association.foreignKey]: pk }, { where: { [association.options.targetKey || 'id']: value }, ...options });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
await target.update({ [association.foreignKey]: pk }, { where: { [association.options.targetKey || 'id']: values }, ...options });
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
// Add the create method
|
|
369
|
+
prototypeTarget[`create${capitalize(association.as)}`] = async function (values, options) {
|
|
370
|
+
const pk = getPrimaryKeyValue(this);
|
|
371
|
+
return target.create({ ...values, [association.foreignKey]: pk }, options);
|
|
372
|
+
};
|
|
373
|
+
// Add the remove method
|
|
374
|
+
prototypeTarget[`remove${capitalize(association.as)}`] = async function (values, options) {
|
|
375
|
+
const pk = getPrimaryKeyValue(this);
|
|
376
|
+
if (Array.isArray(values)) {
|
|
377
|
+
for (const value of values) {
|
|
378
|
+
await target.update({ [association.foreignKey]: null }, { where: { [association.options.targetKey || 'id']: value }, ...options });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
await target.update({ [association.foreignKey]: null }, { where: { [association.options.targetKey || 'id']: values }, ...options });
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
// Add the count method
|
|
386
|
+
prototypeTarget[`count${capitalize(association.as)}`] = async function (options) {
|
|
387
|
+
const pk = getPrimaryKeyValue(this);
|
|
388
|
+
return target.count({ where: { [association.foreignKey]: pk }, ...options });
|
|
389
|
+
};
|
|
390
|
+
// Add the has method - check if instance has associated record(s)
|
|
391
|
+
prototypeTarget[`has${capitalize(association.as)}`] = async function (values, options) {
|
|
392
|
+
const pk = getPrimaryKeyValue(this);
|
|
393
|
+
if (values === undefined) {
|
|
394
|
+
// Check if any associated record exists
|
|
395
|
+
const count = await target.count({
|
|
396
|
+
where: { [association.foreignKey]: pk },
|
|
397
|
+
...options,
|
|
398
|
+
});
|
|
399
|
+
return count > 0;
|
|
400
|
+
}
|
|
401
|
+
// Check if specific value(s) are associated
|
|
402
|
+
if (Array.isArray(values)) {
|
|
403
|
+
for (const value of values) {
|
|
404
|
+
const targetPk = value[association.options.targetKey || 'id'] || value.id;
|
|
405
|
+
const exists = await target.findOne({
|
|
406
|
+
where: {
|
|
407
|
+
[association.options.targetKey || 'id']: targetPk,
|
|
408
|
+
[association.foreignKey]: pk,
|
|
409
|
+
},
|
|
410
|
+
...options,
|
|
411
|
+
});
|
|
412
|
+
if (!exists)
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
const targetPk = values[association.options.targetKey || 'id'] || values;
|
|
419
|
+
const exists = await target.findOne({
|
|
420
|
+
where: {
|
|
421
|
+
[association.options.targetKey || 'id']: targetPk,
|
|
422
|
+
[association.foreignKey]: pk,
|
|
423
|
+
},
|
|
424
|
+
...options,
|
|
425
|
+
});
|
|
426
|
+
return exists !== null;
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
// Add singular alias for add (e.g., addRole for addRoles)
|
|
430
|
+
const singularAs = association.as.replace(/s$/, '').replace(/ies$/, 'y');
|
|
431
|
+
prototypeTarget[`add${capitalize(singularAs)}`] = async function (value, options) {
|
|
432
|
+
const pk = getPrimaryKeyValue(this);
|
|
433
|
+
const targetPk = value[association.options.targetKey || 'id'] || value.id;
|
|
434
|
+
await target.update({ [association.foreignKey]: pk }, { where: { [association.options.targetKey || 'id']: targetPk }, ...options });
|
|
435
|
+
};
|
|
436
|
+
// Add singular alias for remove (e.g., removeRole for removeRoles)
|
|
437
|
+
prototypeTarget[`remove${capitalize(singularAs)}`] = async function (value, options) {
|
|
438
|
+
const pk = getPrimaryKeyValue(this);
|
|
439
|
+
const targetPk = value[association.options.targetKey || 'id'] || value.id;
|
|
440
|
+
await target.update({ [association.foreignKey]: null }, { where: { [association.options.targetKey || 'id']: targetPk }, ...options });
|
|
441
|
+
};
|
|
442
|
+
// Add singular alias for has (e.g., hasRole for hasRoles)
|
|
443
|
+
prototypeTarget[`has${capitalize(singularAs)}`] = async function (value, options) {
|
|
444
|
+
const pk = getPrimaryKeyValue(this);
|
|
445
|
+
const targetPk = value[association.options.targetKey || 'id'] || value;
|
|
446
|
+
const exists = await target.findOne({
|
|
447
|
+
where: {
|
|
448
|
+
[association.options.targetKey || 'id']: targetPk,
|
|
449
|
+
[association.foreignKey]: pk,
|
|
450
|
+
},
|
|
451
|
+
...options,
|
|
452
|
+
});
|
|
453
|
+
return exists !== null;
|
|
454
|
+
};
|
|
455
|
+
// Add singular alias for create (e.g., createRole for createRoles)
|
|
456
|
+
prototypeTarget[`create${capitalize(singularAs)}`] = async function (values, options) {
|
|
457
|
+
const pk = getPrimaryKeyValue(this);
|
|
458
|
+
return target.create({ ...values, [association.foreignKey]: pk }, options);
|
|
459
|
+
};
|
|
460
|
+
return association;
|
|
461
|
+
},
|
|
462
|
+
belongsTo: (target, options = {}) => {
|
|
463
|
+
const association = new BelongsToAssociation(source, target, options);
|
|
464
|
+
// Add to source associations
|
|
465
|
+
source.associations[association.as] = association;
|
|
466
|
+
// Get the prototype target - use source.prototype if it exists, otherwise use source
|
|
467
|
+
// Add the getter
|
|
468
|
+
prototypeTarget[`get${capitalize(association.as)}`] = function (options) {
|
|
469
|
+
const fkValue = this.getDataValue(association.foreignKey);
|
|
470
|
+
return target.findOne({
|
|
471
|
+
where: { [association.options.targetKey || 'id']: fkValue },
|
|
472
|
+
...options,
|
|
473
|
+
});
|
|
474
|
+
};
|
|
475
|
+
// Add the setter
|
|
476
|
+
prototypeTarget[`set${capitalize(association.as)}`] = async function (value, options) {
|
|
477
|
+
const pk = value ? value[association.options.targetKey || 'id'] || value.id : null;
|
|
478
|
+
this.setDataValue(association.foreignKey, pk);
|
|
479
|
+
if (options?.save !== false) {
|
|
480
|
+
await this.save(options);
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
// Add the create method
|
|
484
|
+
prototypeTarget[`create${capitalize(association.as)}`] = async function (values, options) {
|
|
485
|
+
const created = await target.create(values, options);
|
|
486
|
+
this.setDataValue(association.foreignKey, created[association.options.targetKey || 'id']);
|
|
487
|
+
if (options?.save !== false) {
|
|
488
|
+
await this.save(options);
|
|
489
|
+
}
|
|
490
|
+
return created;
|
|
491
|
+
};
|
|
492
|
+
// Add the count method - count associated records (will be 0 or 1 for belongsTo)
|
|
493
|
+
prototypeTarget[`count${capitalize(association.as)}`] = async function (options) {
|
|
494
|
+
const fkValue = this.getDataValue(association.foreignKey);
|
|
495
|
+
if (fkValue === null || fkValue === undefined) {
|
|
496
|
+
return 0;
|
|
497
|
+
}
|
|
498
|
+
return target.count({
|
|
499
|
+
where: { [association.options.targetKey || 'id']: fkValue },
|
|
500
|
+
...options,
|
|
501
|
+
});
|
|
502
|
+
};
|
|
503
|
+
// Add the has method - check if has associated record
|
|
504
|
+
prototypeTarget[`has${capitalize(association.as)}`] = async function (options) {
|
|
505
|
+
const fkValue = this.getDataValue(association.foreignKey);
|
|
506
|
+
if (fkValue === null || fkValue === undefined) {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
const count = await target.count({
|
|
510
|
+
where: { [association.options.targetKey || 'id']: fkValue },
|
|
511
|
+
...options,
|
|
512
|
+
});
|
|
513
|
+
return count > 0;
|
|
514
|
+
};
|
|
515
|
+
return association;
|
|
516
|
+
},
|
|
517
|
+
belongsToMany: (target, options = {}) => {
|
|
518
|
+
const association = new BelongsToManyAssociation(source, target, options);
|
|
519
|
+
// Add to source associations
|
|
520
|
+
source.associations[association.as] = association;
|
|
521
|
+
// Create the through model if needed (use user-provided model if available)
|
|
522
|
+
let throughModel = association.getThroughModel();
|
|
523
|
+
if (!throughModel) {
|
|
524
|
+
throughModel = createThroughModel(source, target, association);
|
|
525
|
+
association.setThroughModel(throughModel);
|
|
526
|
+
}
|
|
527
|
+
// Get the prototype target - use source.prototype if it exists, otherwise use source
|
|
528
|
+
// Add the getter
|
|
529
|
+
prototypeTarget[`get${capitalize(association.as)}`] = function (options) {
|
|
530
|
+
const pk = getPrimaryKeyValue(this);
|
|
531
|
+
return target.findAll({
|
|
532
|
+
include: [
|
|
533
|
+
{
|
|
534
|
+
model: throughModel,
|
|
535
|
+
where: { [association.foreignKey]: pk },
|
|
536
|
+
as: association.getThrough(),
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
...options,
|
|
540
|
+
});
|
|
541
|
+
};
|
|
542
|
+
// Add the setter
|
|
543
|
+
prototypeTarget[`set${capitalize(association.as)}`] = async function (values, options) {
|
|
544
|
+
const pk = getPrimaryKeyValue(this);
|
|
545
|
+
// Remove existing associations
|
|
546
|
+
await throughModel.destroy({
|
|
547
|
+
where: { [association.foreignKey]: pk },
|
|
548
|
+
...options,
|
|
549
|
+
});
|
|
550
|
+
// Add new associations
|
|
551
|
+
for (const value of values) {
|
|
552
|
+
const targetPk = value[association.options.targetKey || 'id'] || value.id;
|
|
553
|
+
await throughModel.create({
|
|
554
|
+
[association.foreignKey]: pk,
|
|
555
|
+
[association.getOtherKey()]: targetPk,
|
|
556
|
+
}, options);
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
// Add the add method
|
|
560
|
+
prototypeTarget[`add${capitalize(association.as)}`] = async function (values, options) {
|
|
561
|
+
const pk = getPrimaryKeyValue(this);
|
|
562
|
+
if (Array.isArray(values)) {
|
|
563
|
+
for (const value of values) {
|
|
564
|
+
const targetPk = value[association.options.targetKey || 'id'] || value.id;
|
|
565
|
+
await throughModel.findOrCreate({
|
|
566
|
+
where: {
|
|
567
|
+
[association.foreignKey]: pk,
|
|
568
|
+
[association.getOtherKey()]: targetPk,
|
|
569
|
+
},
|
|
570
|
+
...options,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
const targetPk = values[association.options.targetKey || 'id'] || values.id;
|
|
576
|
+
await throughModel.findOrCreate({
|
|
577
|
+
where: {
|
|
578
|
+
[association.foreignKey]: pk,
|
|
579
|
+
[association.getOtherKey()]: targetPk,
|
|
580
|
+
},
|
|
581
|
+
...options,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
// Add the remove method
|
|
586
|
+
prototypeTarget[`remove${capitalize(association.as)}`] = async function (values, options) {
|
|
587
|
+
const pk = getPrimaryKeyValue(this);
|
|
588
|
+
if (Array.isArray(values)) {
|
|
589
|
+
for (const value of values) {
|
|
590
|
+
const targetPk = value[association.options.targetKey || 'id'] || value.id;
|
|
591
|
+
await throughModel.destroy({
|
|
592
|
+
where: {
|
|
593
|
+
[association.foreignKey]: pk,
|
|
594
|
+
[association.getOtherKey()]: targetPk,
|
|
595
|
+
},
|
|
596
|
+
...options,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
const targetPk = values[association.options.targetKey || 'id'] || values.id;
|
|
602
|
+
await throughModel.destroy({
|
|
603
|
+
where: {
|
|
604
|
+
[association.foreignKey]: pk,
|
|
605
|
+
[association.getOtherKey()]: targetPk,
|
|
606
|
+
},
|
|
607
|
+
...options,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
// Add the create method
|
|
612
|
+
prototypeTarget[`create${capitalize(association.as)}`] = async function (values, options) {
|
|
613
|
+
const pk = getPrimaryKeyValue(this);
|
|
614
|
+
const created = await target.create(values, options);
|
|
615
|
+
await throughModel.create({
|
|
616
|
+
[association.foreignKey]: pk,
|
|
617
|
+
[association.getOtherKey()]: created[association.options.targetKey || 'id'],
|
|
618
|
+
}, options);
|
|
619
|
+
return created;
|
|
620
|
+
};
|
|
621
|
+
// Add the count method
|
|
622
|
+
prototypeTarget[`count${capitalize(association.as)}`] = async function (options) {
|
|
623
|
+
const pk = getPrimaryKeyValue(this);
|
|
624
|
+
return throughModel.count({
|
|
625
|
+
where: { [association.foreignKey]: pk },
|
|
626
|
+
...options,
|
|
627
|
+
});
|
|
628
|
+
};
|
|
629
|
+
// Add the has method - check if instance has associated record(s) through junction table
|
|
630
|
+
prototypeTarget[`has${capitalize(association.as)}`] = async function (values, options) {
|
|
631
|
+
const pk = getPrimaryKeyValue(this);
|
|
632
|
+
if (values === undefined) {
|
|
633
|
+
// Check if any associated record exists
|
|
634
|
+
const count = await throughModel.count({
|
|
635
|
+
where: { [association.foreignKey]: pk },
|
|
636
|
+
...options,
|
|
637
|
+
});
|
|
638
|
+
return count > 0;
|
|
639
|
+
}
|
|
640
|
+
// Check if specific value(s) are associated
|
|
641
|
+
if (Array.isArray(values)) {
|
|
642
|
+
for (const value of values) {
|
|
643
|
+
const targetPk = value[association.options.targetKey || 'id'] || value.id;
|
|
644
|
+
const exists = await throughModel.findOne({
|
|
645
|
+
where: { [association.foreignKey]: pk, [association.getOtherKey()]: targetPk },
|
|
646
|
+
...options,
|
|
647
|
+
});
|
|
648
|
+
if (!exists)
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
const targetPk = values[association.options.targetKey || 'id'] || values;
|
|
655
|
+
const exists = await throughModel.findOne({
|
|
656
|
+
where: { [association.foreignKey]: pk, [association.getOtherKey()]: targetPk },
|
|
657
|
+
...options,
|
|
658
|
+
});
|
|
659
|
+
return exists !== null;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
// Add singular alias for add (e.g., addRole for addRoles)
|
|
663
|
+
const btmSingularAs = association.as.replace(/s$/, '').replace(/ies$/, 'y');
|
|
664
|
+
prototypeTarget[`add${capitalize(btmSingularAs)}`] = async function (value, options) {
|
|
665
|
+
const pk = getPrimaryKeyValue(this);
|
|
666
|
+
const targetPk = value[association.options.targetKey || 'id'] || value.id;
|
|
667
|
+
await throughModel.findOrCreate({
|
|
668
|
+
where: {
|
|
669
|
+
[association.foreignKey]: pk,
|
|
670
|
+
[association.getOtherKey()]: targetPk,
|
|
671
|
+
},
|
|
672
|
+
...options,
|
|
673
|
+
});
|
|
674
|
+
};
|
|
675
|
+
// Add singular alias for remove (e.g., removeRole for removeRoles)
|
|
676
|
+
prototypeTarget[`remove${capitalize(btmSingularAs)}`] = async function (value, options) {
|
|
677
|
+
const pk = getPrimaryKeyValue(this);
|
|
678
|
+
const targetPk = value[association.options.targetKey || 'id'] || value.id;
|
|
679
|
+
await throughModel.destroy({
|
|
680
|
+
where: {
|
|
681
|
+
[association.foreignKey]: pk,
|
|
682
|
+
[association.getOtherKey()]: targetPk,
|
|
683
|
+
},
|
|
684
|
+
...options,
|
|
685
|
+
});
|
|
686
|
+
};
|
|
687
|
+
// Add singular alias for has (e.g., hasRole for hasRoles)
|
|
688
|
+
prototypeTarget[`has${capitalize(btmSingularAs)}`] = async function (value, options) {
|
|
689
|
+
const pk = getPrimaryKeyValue(this);
|
|
690
|
+
const targetPk = value[association.options.targetKey || 'id'] || value;
|
|
691
|
+
const exists = await throughModel.findOne({
|
|
692
|
+
where: { [association.foreignKey]: pk, [association.getOtherKey()]: targetPk },
|
|
693
|
+
...options,
|
|
694
|
+
});
|
|
695
|
+
return exists !== null;
|
|
696
|
+
};
|
|
697
|
+
// Add singular alias for create (e.g., createRole for createRoles)
|
|
698
|
+
prototypeTarget[`create${capitalize(btmSingularAs)}`] = async function (values, options) {
|
|
699
|
+
const pk = getPrimaryKeyValue(this);
|
|
700
|
+
const created = await target.create(values, options);
|
|
701
|
+
await throughModel.create({
|
|
702
|
+
[association.foreignKey]: pk,
|
|
703
|
+
[association.getOtherKey()]: created[association.options.targetKey || 'id'],
|
|
704
|
+
}, options);
|
|
705
|
+
return created;
|
|
706
|
+
};
|
|
707
|
+
// Add the addModel method - alias for add (adds associated records)
|
|
708
|
+
prototypeTarget[`add${capitalize(association.as)}Model`] = async function (values, options) {
|
|
709
|
+
const pk = getPrimaryKeyValue(this);
|
|
710
|
+
if (Array.isArray(values)) {
|
|
711
|
+
for (const value of values) {
|
|
712
|
+
const targetPk = value[association.options.targetKey || 'id'] || value.id;
|
|
713
|
+
await throughModel.findOrCreate({
|
|
714
|
+
where: {
|
|
715
|
+
[association.foreignKey]: pk,
|
|
716
|
+
[association.getOtherKey()]: targetPk,
|
|
717
|
+
},
|
|
718
|
+
...options,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
const targetPk = values[association.options.targetKey || 'id'] || values.id;
|
|
724
|
+
await throughModel.findOrCreate({
|
|
725
|
+
where: {
|
|
726
|
+
[association.foreignKey]: pk,
|
|
727
|
+
[association.getOtherKey()]: targetPk,
|
|
728
|
+
},
|
|
729
|
+
...options,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
// Add the removeModel method - alias for remove (removes associated records)
|
|
734
|
+
prototypeTarget[`remove${capitalize(association.as)}Model`] = async function (values, options) {
|
|
735
|
+
const pk = getPrimaryKeyValue(this);
|
|
736
|
+
if (Array.isArray(values)) {
|
|
737
|
+
for (const value of values) {
|
|
738
|
+
const targetPk = value[association.options.targetKey || 'id'] || value.id;
|
|
739
|
+
await throughModel.destroy({
|
|
740
|
+
where: {
|
|
741
|
+
[association.foreignKey]: pk,
|
|
742
|
+
[association.getOtherKey()]: targetPk,
|
|
743
|
+
},
|
|
744
|
+
...options,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
const targetPk = values[association.options.targetKey || 'id'] || values.id;
|
|
750
|
+
await throughModel.destroy({
|
|
751
|
+
where: {
|
|
752
|
+
[association.foreignKey]: pk,
|
|
753
|
+
[association.getOtherKey()]: targetPk,
|
|
754
|
+
},
|
|
755
|
+
...options,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
return association;
|
|
760
|
+
},
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
// Helper to create a through model for belongsToMany
|
|
764
|
+
function createThroughModel(source, target, association) {
|
|
765
|
+
const throughName = `${source.name}${target.name}`;
|
|
766
|
+
return {
|
|
767
|
+
name: throughName,
|
|
768
|
+
tableName: association.getThrough(),
|
|
769
|
+
rawAttributes: {
|
|
770
|
+
[association.foreignKey]: {
|
|
771
|
+
type: 'INTEGER',
|
|
772
|
+
allowNull: false,
|
|
773
|
+
references: { model: source.name, key: 'id' },
|
|
774
|
+
},
|
|
775
|
+
[association.getOtherKey()]: {
|
|
776
|
+
type: 'INTEGER',
|
|
777
|
+
allowNull: false,
|
|
778
|
+
references: { model: target.name, key: 'id' },
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
associations: {},
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
// Helper to get primary key value from an instance
|
|
785
|
+
function getPrimaryKeyValue(instance) {
|
|
786
|
+
// Prefer the model's declared primary key attribute (models can define a custom
|
|
787
|
+
// primary key, e.g. `sku`, not just `id`). Falls back to name-guessing below for
|
|
788
|
+
// instances that don't expose their model's rawAttributes.
|
|
789
|
+
const rawAttributes = instance?.model?.rawAttributes || instance?.constructor?.rawAttributes;
|
|
790
|
+
if (rawAttributes) {
|
|
791
|
+
for (const [attrName, attr] of Object.entries(rawAttributes)) {
|
|
792
|
+
if (attr?.primaryKey) {
|
|
793
|
+
if (instance.getDataValue && instance.getDataValue(attrName) !== undefined) {
|
|
794
|
+
return instance.getDataValue(attrName);
|
|
795
|
+
}
|
|
796
|
+
if (instance[attrName] !== undefined) {
|
|
797
|
+
return instance[attrName];
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
// Try common primary key names
|
|
803
|
+
for (const key of ['id', 'Id', 'ID', 'uuid', 'UUID']) {
|
|
804
|
+
if (instance.getDataValue && instance.getDataValue(key) !== undefined) {
|
|
805
|
+
return instance.getDataValue(key);
|
|
806
|
+
}
|
|
807
|
+
if (instance[key] !== undefined) {
|
|
808
|
+
return instance[key];
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return instance.id;
|
|
812
|
+
}
|
|
813
|
+
// Helper to capitalize first letter
|
|
814
|
+
function capitalize(str) {
|
|
815
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
816
|
+
}
|
|
817
|
+
// ==================== Polymorphic Associations ====================
|
|
818
|
+
/**
|
|
819
|
+
* Options for polymorphic associations
|
|
820
|
+
*/
|
|
821
|
+
function processNestedIncludes(includes, parentModel = null, parentAlias = '') {
|
|
822
|
+
const result = [];
|
|
823
|
+
function processInclude(inc, parent, path, level) {
|
|
824
|
+
const model = inc.model;
|
|
825
|
+
const tableName = model?.tableName || model?.name || '';
|
|
826
|
+
const alias = inc.as || tableName;
|
|
827
|
+
const fullPath = path ? `${path}->${alias}` : alias;
|
|
828
|
+
result.push({
|
|
829
|
+
include: inc,
|
|
830
|
+
parentAlias: parent,
|
|
831
|
+
path: fullPath,
|
|
832
|
+
nestedLevel: level,
|
|
833
|
+
});
|
|
834
|
+
// Process nested includes
|
|
835
|
+
if (inc.include && Array.isArray(inc.include)) {
|
|
836
|
+
for (const nested of inc.include) {
|
|
837
|
+
processInclude(nested, fullPath, fullPath, level + 1);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
for (const inc of includes) {
|
|
842
|
+
processInclude(inc, parentAlias, '', 0);
|
|
843
|
+
}
|
|
844
|
+
return result;
|
|
845
|
+
}
|
|
846
|
+
exports.default = {
|
|
847
|
+
HasOneAssociation,
|
|
848
|
+
HasManyAssociation,
|
|
849
|
+
BelongsToAssociation,
|
|
850
|
+
BelongsToManyAssociation,
|
|
851
|
+
createAssociationMixins,
|
|
852
|
+
};
|