@rudderjs/orm 1.9.1 → 1.9.3
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/dist/aggregate.d.ts +16 -7
- package/dist/aggregate.d.ts.map +1 -1
- package/dist/aggregate.js +27 -29
- package/dist/aggregate.js.map +1 -1
- package/dist/index.d.ts +49 -127
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +149 -919
- package/dist/index.js.map +1 -1
- package/dist/relations/pivot-accessors.d.ts +127 -0
- package/dist/relations/pivot-accessors.d.ts.map +1 -0
- package/dist/relations/pivot-accessors.js +211 -0
- package/dist/relations/pivot-accessors.js.map +1 -0
- package/dist/relations/pivot-deferred.d.ts +13 -0
- package/dist/relations/pivot-deferred.d.ts.map +1 -0
- package/dist/relations/pivot-deferred.js +210 -0
- package/dist/relations/pivot-deferred.js.map +1 -0
- package/dist/relations/pivot-meta.d.ts +50 -0
- package/dist/relations/pivot-meta.d.ts.map +1 -0
- package/dist/relations/pivot-meta.js +34 -0
- package/dist/relations/pivot-meta.js.map +1 -0
- package/dist/relations/where-has.d.ts +53 -0
- package/dist/relations/where-has.d.ts.map +1 -0
- package/dist/relations/where-has.js +239 -0
- package/dist/relations/where-has.js.map +1 -0
- package/dist/utils.d.ts +35 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +63 -0
- package/dist/utils.js.map +1 -0
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { castGet, castSet } from './cast.js';
|
|
2
2
|
import { AGGREGATES_SYMBOL, aggregateKeysOf, loadCountOrExists, loadMissingRelations, loadNumericAggregate, normalizeWithCount, normalizeWithExists, normalizeWithNumericAggregate, } from './aggregate.js';
|
|
3
|
+
import { attrEqual, readField, writeField, deleteField } from './utils.js';
|
|
4
|
+
import { resolveBelongsToManyMeta, resolveMorphToManyMeta, resolveMorphedByManyMeta, } from './relations/pivot-meta.js';
|
|
5
|
+
import { attachWhereHas, attachWithWhereHas, attachWhereBelongsTo, } from './relations/where-has.js';
|
|
6
|
+
import { morphParentQuery, belongsToManyDeferredQb, morphToManyDeferredQb, morphedByManyDeferredQb, } from './relations/pivot-deferred.js';
|
|
7
|
+
import { makeBelongsToManyAccessor, makeMorphToManyAccessor, makeMorphedByManyAccessor, installBelongsToManyMethods, installMorphPivotMethods, } from './relations/pivot-accessors.js';
|
|
3
8
|
export { vector } from './cast.js';
|
|
4
9
|
export { VectorDimensionMismatchError, VectorStorageUnsupportedError, MissingEmbedderError, } from './vector-errors.js';
|
|
5
10
|
export { Attribute } from './attribute.js';
|
|
@@ -9,22 +14,27 @@ export { ModelFactory, sequence } from './factory.js';
|
|
|
9
14
|
export { Seeder } from './seeder.js';
|
|
10
15
|
export { AggregateConstraintBuilder, AGGREGATES_SYMBOL } from './aggregate.js';
|
|
11
16
|
export { pruneModels } from './prune.js';
|
|
12
|
-
|
|
17
|
+
const _g = globalThis;
|
|
18
|
+
if (!_g['__rudderjs_orm_registry__']) {
|
|
19
|
+
_g['__rudderjs_orm_registry__'] = {
|
|
20
|
+
adapter: null,
|
|
21
|
+
models: new Map(),
|
|
22
|
+
listeners: new Set(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const _store = _g['__rudderjs_orm_registry__'];
|
|
13
26
|
export class ModelRegistry {
|
|
14
|
-
static adapter = null;
|
|
15
|
-
static models = new Map();
|
|
16
|
-
static listeners = new Set();
|
|
17
27
|
static set(adapter) {
|
|
18
|
-
|
|
28
|
+
_store.adapter = adapter;
|
|
19
29
|
}
|
|
20
30
|
static get() {
|
|
21
|
-
return
|
|
31
|
+
return _store.adapter;
|
|
22
32
|
}
|
|
23
33
|
static getAdapter() {
|
|
24
|
-
if (!
|
|
34
|
+
if (!_store.adapter) {
|
|
25
35
|
throw new Error('[RudderJS ORM] No ORM adapter registered. Did you add a database provider to your providers list?');
|
|
26
36
|
}
|
|
27
|
-
return
|
|
37
|
+
return _store.adapter;
|
|
28
38
|
}
|
|
29
39
|
/**
|
|
30
40
|
* Register a Model class so consumers (e.g. Telescope's model collector)
|
|
@@ -39,12 +49,12 @@ export class ModelRegistry {
|
|
|
39
49
|
*/
|
|
40
50
|
static register(ModelClass) {
|
|
41
51
|
const name = ModelClass.name;
|
|
42
|
-
if (!name ||
|
|
52
|
+
if (!name || _store.models.has(name))
|
|
43
53
|
return;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
for (const listener of
|
|
54
|
+
_store.models.set(name, ModelClass);
|
|
55
|
+
installBelongsToManyMethods(ModelClass);
|
|
56
|
+
installMorphPivotMethods(ModelClass);
|
|
57
|
+
for (const listener of _store.listeners)
|
|
48
58
|
listener(name, ModelClass);
|
|
49
59
|
}
|
|
50
60
|
/**
|
|
@@ -52,20 +62,20 @@ export class ModelRegistry {
|
|
|
52
62
|
* model collector and any code that needs to iterate discovered models.
|
|
53
63
|
*/
|
|
54
64
|
static all() {
|
|
55
|
-
return
|
|
65
|
+
return _store.models;
|
|
56
66
|
}
|
|
57
67
|
/**
|
|
58
68
|
* Subscribe to model registrations. Fires once per newly registered
|
|
59
69
|
* class. Returns an unsubscribe function.
|
|
60
70
|
*/
|
|
61
71
|
static onRegister(listener) {
|
|
62
|
-
|
|
63
|
-
return () => {
|
|
72
|
+
_store.listeners.add(listener);
|
|
73
|
+
return () => { _store.listeners.delete(listener); };
|
|
64
74
|
}
|
|
65
75
|
static reset() {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
_store.adapter = null;
|
|
77
|
+
_store.models.clear();
|
|
78
|
+
_store.listeners.clear();
|
|
69
79
|
}
|
|
70
80
|
}
|
|
71
81
|
// ─── Errors ────────────────────────────────────────────────
|
|
@@ -356,8 +366,22 @@ export class Model {
|
|
|
356
366
|
// Used by isDirty / isClean / wasChanged / getOriginal / getChanges /
|
|
357
367
|
// getDirty. Captured by hydrate(), save(), refresh(), and increment/
|
|
358
368
|
// decrement so the baseline always matches the persisted state.
|
|
359
|
-
/**
|
|
360
|
-
|
|
369
|
+
/**
|
|
370
|
+
* @internal — materialized own-column baseline as of last load/save/refresh.
|
|
371
|
+
* Populated lazily: hydrate just stores the raw input record in
|
|
372
|
+
* {@link Model.#originalRaw} and defers the filter pass until the first
|
|
373
|
+
* dirty-tracking access. Save/refresh/etc. populate this field eagerly
|
|
374
|
+
* because they have current instance state in hand.
|
|
375
|
+
*/
|
|
376
|
+
#originalSnapshot = {};
|
|
377
|
+
/**
|
|
378
|
+
* @internal — reference to the raw record passed to `hydrate()`. While
|
|
379
|
+
* non-undefined, dirty-tracking reads route through {@link Model._original}
|
|
380
|
+
* which materializes {@link Model.#originalSnapshot} on first access. Reset
|
|
381
|
+
* to `undefined` once materialized or once an explicit `_syncOriginal()`
|
|
382
|
+
* captures the post-save / post-refresh state.
|
|
383
|
+
*/
|
|
384
|
+
#originalRaw = undefined;
|
|
361
385
|
/** @internal — diff of attributes that changed during the most recent save. */
|
|
362
386
|
#changes = {};
|
|
363
387
|
// ── Scopes ─────────────────────────────────────────────
|
|
@@ -387,12 +411,27 @@ export class Model {
|
|
|
387
411
|
this._listeners.set(event, list);
|
|
388
412
|
}
|
|
389
413
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
390
|
-
|
|
414
|
+
// Returns `unknown` (not `Promise<unknown>`) so the common fast path stays
|
|
415
|
+
// synchronous: when a class has no observers or event listeners — the typical
|
|
416
|
+
// case for read paths like `.all()` / `.find()` — the call returns the payload
|
|
417
|
+
// directly, and `await self._fireEvent(...)` becomes a no-op in V8 (no
|
|
418
|
+
// microtask scheduling). This recovers ~1ms on `.all()` over 5000 rows where
|
|
419
|
+
// the per-row `retrieved` event would otherwise schedule 5000 empty microtasks.
|
|
420
|
+
// Slow-path observers/listeners route through `_fireEventSlow` and still get
|
|
421
|
+
// their async semantics.
|
|
422
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
423
|
+
static _fireEvent(event, ...args) {
|
|
391
424
|
if (Object.prototype.hasOwnProperty.call(this, '_eventsMuted') && this._eventsMuted) {
|
|
392
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
393
425
|
return args[0];
|
|
394
426
|
}
|
|
395
|
-
|
|
427
|
+
const hasObservers = Object.prototype.hasOwnProperty.call(this, '_observers') && this._observers.length > 0;
|
|
428
|
+
const hasListeners = Object.prototype.hasOwnProperty.call(this, '_listeners') && this._listeners.size > 0;
|
|
429
|
+
if (!hasObservers && !hasListeners)
|
|
430
|
+
return args[0];
|
|
431
|
+
return this._fireEventSlow(event, ...args);
|
|
432
|
+
}
|
|
433
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
434
|
+
static async _fireEventSlow(event, ...args) {
|
|
396
435
|
let result = args[0];
|
|
397
436
|
const observers = Object.prototype.hasOwnProperty.call(this, '_observers') ? this._observers : [];
|
|
398
437
|
for (const obs of observers) {
|
|
@@ -409,7 +448,6 @@ export class Model {
|
|
|
409
448
|
? (this._listeners.get(event) ?? [])
|
|
410
449
|
: [];
|
|
411
450
|
for (const fn of listeners) {
|
|
412
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
413
451
|
const ret = await fn(...args);
|
|
414
452
|
if (ret === false)
|
|
415
453
|
return false;
|
|
@@ -450,7 +488,9 @@ export class Model {
|
|
|
450
488
|
const Ctor = this;
|
|
451
489
|
const instance = new Ctor();
|
|
452
490
|
Object.assign(instance, record);
|
|
453
|
-
|
|
491
|
+
// Defer the dirty-tracking baseline. _original() materializes the filtered
|
|
492
|
+
// snapshot on first access — typically never, for read-and-discard rows.
|
|
493
|
+
instance.#originalRaw = record;
|
|
454
494
|
return instance;
|
|
455
495
|
}
|
|
456
496
|
/** @internal — wrap a QueryBuilder so its read methods return Model instances. */
|
|
@@ -476,6 +516,10 @@ export class Model {
|
|
|
476
516
|
aggregateAliases.add(r.alias);
|
|
477
517
|
qb.withAggregate(reqs);
|
|
478
518
|
};
|
|
519
|
+
// The Proxy's `get` handler implements the extra `HydratingQueryBuilder`
|
|
520
|
+
// methods at runtime (whereHas / withCount / etc.). TS can't verify that
|
|
521
|
+
// through the Proxy constructor, so we assert here — the assertion is
|
|
522
|
+
// contained to this one site instead of leaking to every call site.
|
|
479
523
|
const proxy = new Proxy(qb, {
|
|
480
524
|
get(target, prop, receiver) {
|
|
481
525
|
// ORM-side chainables that don't exist on the adapter QB itself —
|
|
@@ -483,25 +527,25 @@ export class Model {
|
|
|
483
527
|
// are added by this proxy, not by the adapter.
|
|
484
528
|
if (prop === 'whereHas') {
|
|
485
529
|
return (relation, constrain) => {
|
|
486
|
-
|
|
530
|
+
attachWhereHas(ModelClass, target, relation, true, constrain);
|
|
487
531
|
return proxy;
|
|
488
532
|
};
|
|
489
533
|
}
|
|
490
534
|
if (prop === 'whereDoesntHave') {
|
|
491
535
|
return (relation, constrain) => {
|
|
492
|
-
|
|
536
|
+
attachWhereHas(ModelClass, target, relation, false, constrain);
|
|
493
537
|
return proxy;
|
|
494
538
|
};
|
|
495
539
|
}
|
|
496
540
|
if (prop === 'withWhereHas') {
|
|
497
541
|
return (relation, constrain) => {
|
|
498
|
-
|
|
542
|
+
attachWithWhereHas(ModelClass, target, relation, constrain);
|
|
499
543
|
return proxy;
|
|
500
544
|
};
|
|
501
545
|
}
|
|
502
546
|
if (prop === 'whereBelongsTo') {
|
|
503
547
|
return (parent, relation) => {
|
|
504
|
-
|
|
548
|
+
attachWhereBelongsTo(ModelClass, target, parent, relation);
|
|
505
549
|
return proxy;
|
|
506
550
|
};
|
|
507
551
|
}
|
|
@@ -565,7 +609,6 @@ export class Model {
|
|
|
565
609
|
// Chainable methods (where/orderBy/with/...) typically return `target` —
|
|
566
610
|
// re-wrap so `Model.where('a', 1).first()` keeps hydrating.
|
|
567
611
|
return (...args) => {
|
|
568
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
569
612
|
const result = value.apply(target, args);
|
|
570
613
|
return result === target ? proxy : result;
|
|
571
614
|
};
|
|
@@ -606,7 +649,6 @@ export class Model {
|
|
|
606
649
|
excludedScopes.add(name);
|
|
607
650
|
return enhance(buildScoped());
|
|
608
651
|
};
|
|
609
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
610
652
|
return enhanced;
|
|
611
653
|
};
|
|
612
654
|
return enhance(buildScoped());
|
|
@@ -691,7 +733,7 @@ export class Model {
|
|
|
691
733
|
* await Post.whereHas('comments').get() // morphMany — adds the {morph}Type filter automatically
|
|
692
734
|
*/
|
|
693
735
|
static whereHas(relation, constrain) {
|
|
694
|
-
return
|
|
736
|
+
return attachWhereHas(this, Model._q(this), relation, true, constrain);
|
|
695
737
|
}
|
|
696
738
|
/**
|
|
697
739
|
* Inverse of {@link Model.whereHas} — rows whose named relation has zero
|
|
@@ -700,7 +742,7 @@ export class Model {
|
|
|
700
742
|
* matching the constraint" rather than "no children at all".
|
|
701
743
|
*/
|
|
702
744
|
static whereDoesntHave(relation, constrain) {
|
|
703
|
-
return
|
|
745
|
+
return attachWhereHas(this, Model._q(this), relation, false, constrain);
|
|
704
746
|
}
|
|
705
747
|
/**
|
|
706
748
|
* `whereHas` + `with` — filter by the relation predicate AND eager-load the
|
|
@@ -710,7 +752,7 @@ export class Model {
|
|
|
710
752
|
* the parent was matched on a narrower predicate.
|
|
711
753
|
*/
|
|
712
754
|
static withWhereHas(relation, constrain) {
|
|
713
|
-
return
|
|
755
|
+
return attachWithWhereHas(this, Model._q(this), relation, constrain);
|
|
714
756
|
}
|
|
715
757
|
/**
|
|
716
758
|
* Filter rows whose `belongsTo` relation points at `parent`. Sugar for
|
|
@@ -724,7 +766,7 @@ export class Model {
|
|
|
724
766
|
* await Comment.whereBelongsTo(post, 'post').get() // explicit relation name when ambiguous
|
|
725
767
|
*/
|
|
726
768
|
static whereBelongsTo(parent, relation) {
|
|
727
|
-
return
|
|
769
|
+
return attachWhereBelongsTo(this, Model._q(this), parent, relation);
|
|
728
770
|
}
|
|
729
771
|
/**
|
|
730
772
|
* Aggregate eager-loading — count related rows alongside the parent in a
|
|
@@ -736,7 +778,7 @@ export class Model {
|
|
|
736
778
|
* - `withCount('posts')` — single relation, no constraint.
|
|
737
779
|
* - `withCount(['posts', 'comments'])` — multiple, no constraints.
|
|
738
780
|
* - `withCount({ posts: q => q.where('published', true).as('publishedPosts') })`
|
|
739
|
-
* — map form with `where
|
|
781
|
+
* — map form with `where` constraints + optional alias override.
|
|
740
782
|
*
|
|
741
783
|
* Closes the N+1 footgun for hot list pages. For a single instance use
|
|
742
784
|
* `instance.loadCount('posts')` instead.
|
|
@@ -969,7 +1011,7 @@ export class Model {
|
|
|
969
1011
|
/** @internal — pull the primary-key value from this instance, or `undefined` if unset. */
|
|
970
1012
|
_getKey() {
|
|
971
1013
|
const ctor = this.constructor;
|
|
972
|
-
const value = this
|
|
1014
|
+
const value = readField(this, ctor.primaryKey);
|
|
973
1015
|
if (value === undefined || value === null)
|
|
974
1016
|
return undefined;
|
|
975
1017
|
return value;
|
|
@@ -1004,9 +1046,34 @@ export class Model {
|
|
|
1004
1046
|
_toData() {
|
|
1005
1047
|
return this._currentAttrs();
|
|
1006
1048
|
}
|
|
1049
|
+
/**
|
|
1050
|
+
* @internal — return the dirty-tracking baseline, materializing the deferred
|
|
1051
|
+
* raw record from hydrate on first access. After materialization (or after
|
|
1052
|
+
* an explicit `_syncOriginal()` reset), subsequent calls return the cached
|
|
1053
|
+
* snapshot directly.
|
|
1054
|
+
*/
|
|
1055
|
+
_original() {
|
|
1056
|
+
if (this.#originalRaw === undefined)
|
|
1057
|
+
return this.#originalSnapshot;
|
|
1058
|
+
const aggregates = this[AGGREGATES_SYMBOL];
|
|
1059
|
+
const out = {};
|
|
1060
|
+
for (const [k, v] of Object.entries(this.#originalRaw)) {
|
|
1061
|
+
if (k.startsWith('_'))
|
|
1062
|
+
continue;
|
|
1063
|
+
if (v === undefined)
|
|
1064
|
+
continue;
|
|
1065
|
+
if (aggregates && aggregates.has(k))
|
|
1066
|
+
continue;
|
|
1067
|
+
out[k] = v;
|
|
1068
|
+
}
|
|
1069
|
+
this.#originalSnapshot = out;
|
|
1070
|
+
this.#originalRaw = undefined;
|
|
1071
|
+
return out;
|
|
1072
|
+
}
|
|
1007
1073
|
/** @internal — capture current attrs as the new dirty-tracking baseline. */
|
|
1008
1074
|
_syncOriginal() {
|
|
1009
|
-
this.#
|
|
1075
|
+
this.#originalSnapshot = this._currentAttrs();
|
|
1076
|
+
this.#originalRaw = undefined;
|
|
1010
1077
|
}
|
|
1011
1078
|
/**
|
|
1012
1079
|
* Persist this instance. Inserts when the primary key is unset; otherwise updates.
|
|
@@ -1026,13 +1093,15 @@ export class Model {
|
|
|
1026
1093
|
: await Model._doUpdate.call(ctor, id, data);
|
|
1027
1094
|
Object.assign(this, persisted);
|
|
1028
1095
|
const next = this._currentAttrs();
|
|
1096
|
+
const prev = this._original();
|
|
1029
1097
|
const diff = {};
|
|
1030
|
-
for (const k of new Set([...Object.keys(next), ...Object.keys(
|
|
1031
|
-
if (!
|
|
1098
|
+
for (const k of new Set([...Object.keys(next), ...Object.keys(prev)])) {
|
|
1099
|
+
if (!attrEqual(next[k], prev[k]))
|
|
1032
1100
|
diff[k] = next[k];
|
|
1033
1101
|
}
|
|
1034
1102
|
this.#changes = diff;
|
|
1035
|
-
this.#
|
|
1103
|
+
this.#originalSnapshot = next;
|
|
1104
|
+
this.#originalRaw = undefined;
|
|
1036
1105
|
return this;
|
|
1037
1106
|
}
|
|
1038
1107
|
/**
|
|
@@ -1073,7 +1142,7 @@ export class Model {
|
|
|
1073
1142
|
throw new ModelNotFoundError(ctor.name, id);
|
|
1074
1143
|
for (const k of Object.keys(this)) {
|
|
1075
1144
|
if (!k.startsWith('_'))
|
|
1076
|
-
|
|
1145
|
+
deleteField(this, k);
|
|
1077
1146
|
}
|
|
1078
1147
|
Object.assign(this, fresh);
|
|
1079
1148
|
this.#changes = {};
|
|
@@ -1096,8 +1165,7 @@ export class Model {
|
|
|
1096
1165
|
}
|
|
1097
1166
|
await ctor.delete(id);
|
|
1098
1167
|
if (ctor.softDeletes) {
|
|
1099
|
-
;
|
|
1100
|
-
this.deletedAt = new Date();
|
|
1168
|
+
writeField(this, 'deletedAt', new Date());
|
|
1101
1169
|
this._syncOriginal();
|
|
1102
1170
|
}
|
|
1103
1171
|
}
|
|
@@ -1258,7 +1326,7 @@ export class Model {
|
|
|
1258
1326
|
for (const [k, v] of Object.entries(this)) {
|
|
1259
1327
|
if (k.startsWith('_') || exclude.has(k) || v === undefined)
|
|
1260
1328
|
continue;
|
|
1261
|
-
clone
|
|
1329
|
+
writeField(clone, k, v);
|
|
1262
1330
|
}
|
|
1263
1331
|
return clone;
|
|
1264
1332
|
}
|
|
@@ -1285,9 +1353,10 @@ export class Model {
|
|
|
1285
1353
|
: key in this.#changes;
|
|
1286
1354
|
}
|
|
1287
1355
|
getOriginal(key) {
|
|
1356
|
+
const snap = this._original();
|
|
1288
1357
|
if (key === undefined)
|
|
1289
|
-
return { ...
|
|
1290
|
-
return
|
|
1358
|
+
return { ...snap };
|
|
1359
|
+
return snap[key];
|
|
1291
1360
|
}
|
|
1292
1361
|
/** Diff map of attributes that changed during the most recent {@link save}. */
|
|
1293
1362
|
getChanges() {
|
|
@@ -1297,8 +1366,9 @@ export class Model {
|
|
|
1297
1366
|
getDirty() {
|
|
1298
1367
|
const out = {};
|
|
1299
1368
|
const current = this._currentAttrs();
|
|
1300
|
-
|
|
1301
|
-
|
|
1369
|
+
const prev = this._original();
|
|
1370
|
+
for (const k of new Set([...Object.keys(current), ...Object.keys(prev)])) {
|
|
1371
|
+
if (!attrEqual(current[k], prev[k]))
|
|
1302
1372
|
out[k] = current[k];
|
|
1303
1373
|
}
|
|
1304
1374
|
return out;
|
|
@@ -1324,7 +1394,7 @@ export class Model {
|
|
|
1324
1394
|
}
|
|
1325
1395
|
/** True when this instance has been soft-deleted (its `deletedAt` is set). */
|
|
1326
1396
|
trashed() {
|
|
1327
|
-
const v = this
|
|
1397
|
+
const v = readField(this, 'deletedAt');
|
|
1328
1398
|
return v !== null && v !== undefined;
|
|
1329
1399
|
}
|
|
1330
1400
|
// ── Relations ──────────────────────────────────────────
|
|
@@ -1356,8 +1426,8 @@ export class Model {
|
|
|
1356
1426
|
if (def.type === 'morphTo') {
|
|
1357
1427
|
const idCol = `${def.morphName}Id`;
|
|
1358
1428
|
const typeCol = `${def.morphName}Type`;
|
|
1359
|
-
const idVal = this
|
|
1360
|
-
const typeVal = this
|
|
1429
|
+
const idVal = readField(this, idCol);
|
|
1430
|
+
const typeVal = readField(this, typeCol);
|
|
1361
1431
|
if (idVal === undefined || idVal === null || typeVal === undefined || typeVal === null) {
|
|
1362
1432
|
throw new Error(`[RudderJS ORM] Cannot resolve morphTo "${name}" on ${ctor.name} — ${idCol}/${typeCol} unset.`);
|
|
1363
1433
|
}
|
|
@@ -1386,42 +1456,42 @@ export class Model {
|
|
|
1386
1456
|
// expectation (`first()` vs `get()`). Split into two ifs so TS can narrow
|
|
1387
1457
|
// each tag literal out of the union for the fall-through branches below.
|
|
1388
1458
|
if (def.type === 'morphMany') {
|
|
1389
|
-
return
|
|
1459
|
+
return morphParentQuery(this, ctor, def, name);
|
|
1390
1460
|
}
|
|
1391
1461
|
if (def.type === 'morphOne') {
|
|
1392
|
-
return
|
|
1462
|
+
return morphParentQuery(this, ctor, def, name);
|
|
1393
1463
|
}
|
|
1394
1464
|
const Related = def.model();
|
|
1395
1465
|
const fkCamel = (s) => s.charAt(0).toLowerCase() + s.slice(1);
|
|
1396
1466
|
if (def.type === 'belongsToMany') {
|
|
1397
|
-
const meta =
|
|
1398
|
-
const parentVal = this
|
|
1467
|
+
const meta = resolveBelongsToManyMeta(ctor, Related, def);
|
|
1468
|
+
const parentVal = readField(this, meta.parentKey);
|
|
1399
1469
|
if (parentVal === undefined || parentVal === null) {
|
|
1400
1470
|
throw new Error(`[RudderJS ORM] Cannot resolve "${name}" on ${ctor.name} — ${meta.parentKey} is unset.`);
|
|
1401
1471
|
}
|
|
1402
|
-
return
|
|
1472
|
+
return belongsToManyDeferredQb(Related, def, meta, parentVal);
|
|
1403
1473
|
}
|
|
1404
1474
|
if (def.type === 'morphToMany') {
|
|
1405
|
-
const meta =
|
|
1406
|
-
const parentVal = this
|
|
1475
|
+
const meta = resolveMorphToManyMeta(ctor, Related, def);
|
|
1476
|
+
const parentVal = readField(this, meta.parentKey);
|
|
1407
1477
|
if (parentVal === undefined || parentVal === null) {
|
|
1408
1478
|
throw new Error(`[RudderJS ORM] Cannot resolve "${name}" on ${ctor.name} — ${meta.parentKey} is unset.`);
|
|
1409
1479
|
}
|
|
1410
|
-
return
|
|
1480
|
+
return morphToManyDeferredQb(Related, def, meta, parentVal);
|
|
1411
1481
|
}
|
|
1412
1482
|
if (def.type === 'morphedByMany') {
|
|
1413
|
-
const meta =
|
|
1414
|
-
const parentVal = this
|
|
1483
|
+
const meta = resolveMorphedByManyMeta(ctor, Related, def);
|
|
1484
|
+
const parentVal = readField(this, meta.parentKey);
|
|
1415
1485
|
if (parentVal === undefined || parentVal === null) {
|
|
1416
1486
|
throw new Error(`[RudderJS ORM] Cannot resolve "${name}" on ${ctor.name} — ${meta.parentKey} is unset.`);
|
|
1417
1487
|
}
|
|
1418
|
-
return
|
|
1488
|
+
return morphedByManyDeferredQb(Related, def, meta, parentVal);
|
|
1419
1489
|
}
|
|
1420
1490
|
if (def.type === 'belongsTo') {
|
|
1421
1491
|
// This model holds the FK; query the related model's PK.
|
|
1422
1492
|
const fk = def.foreignKey ?? `${fkCamel(Related.name)}Id`;
|
|
1423
1493
|
const localCol = def.localKey ?? fk;
|
|
1424
|
-
const localVal = this
|
|
1494
|
+
const localVal = readField(this, localCol);
|
|
1425
1495
|
if (localVal === undefined || localVal === null) {
|
|
1426
1496
|
throw new Error(`[RudderJS ORM] Cannot resolve belongsTo "${name}" — ${ctor.name}.${localCol} is unset.`);
|
|
1427
1497
|
}
|
|
@@ -1430,7 +1500,7 @@ export class Model {
|
|
|
1430
1500
|
// hasOne / hasMany — related model holds the FK pointing back to us.
|
|
1431
1501
|
const fk = def.foreignKey ?? `${fkCamel(ctor.name)}Id`;
|
|
1432
1502
|
const localCol = def.localKey ?? ctor.primaryKey;
|
|
1433
|
-
const localVal = this
|
|
1503
|
+
const localVal = readField(this, localCol);
|
|
1434
1504
|
if (localVal === undefined || localVal === null) {
|
|
1435
1505
|
throw new Error(`[RudderJS ORM] Cannot resolve "${name}" on ${ctor.name} — ${localCol} is unset.`);
|
|
1436
1506
|
}
|
|
@@ -1465,15 +1535,15 @@ export class Model {
|
|
|
1465
1535
|
throw new Error(`[RudderJS ORM] Relation "${name}" on ${ctor.name} is "${def.type}", not "belongsToMany".`);
|
|
1466
1536
|
}
|
|
1467
1537
|
const Related = def.model();
|
|
1468
|
-
const meta =
|
|
1469
|
-
const parentVal = parent
|
|
1538
|
+
const meta = resolveBelongsToManyMeta(ctor, Related, def);
|
|
1539
|
+
const parentVal = readField(parent, meta.parentKey);
|
|
1470
1540
|
if (parentVal === undefined || parentVal === null) {
|
|
1471
1541
|
throw new Error(`[RudderJS ORM] Cannot use belongsToMany "${name}" on ${ctor.name} — ${meta.parentKey} is unset.`);
|
|
1472
1542
|
}
|
|
1473
1543
|
// Belt-and-suspenders: make sure the auto-method is installed even
|
|
1474
1544
|
// for instances constructed before any query against this class.
|
|
1475
|
-
|
|
1476
|
-
return
|
|
1545
|
+
installBelongsToManyMethods(ctor);
|
|
1546
|
+
return makeBelongsToManyAccessor(ctor, Related, def, parentVal);
|
|
1477
1547
|
}
|
|
1478
1548
|
/**
|
|
1479
1549
|
* Pivot-mutation accessor for a `morphToMany` relation (the polymorphic
|
|
@@ -1499,13 +1569,13 @@ export class Model {
|
|
|
1499
1569
|
throw new Error(`[RudderJS ORM] Relation "${name}" on ${ctor.name} is "${def.type}", not "morphToMany".`);
|
|
1500
1570
|
}
|
|
1501
1571
|
const Related = def.model();
|
|
1502
|
-
const meta =
|
|
1503
|
-
const parentVal = parent
|
|
1572
|
+
const meta = resolveMorphToManyMeta(ctor, Related, def);
|
|
1573
|
+
const parentVal = readField(parent, meta.parentKey);
|
|
1504
1574
|
if (parentVal === undefined || parentVal === null) {
|
|
1505
1575
|
throw new Error(`[RudderJS ORM] Cannot use morphToMany "${name}" on ${ctor.name} — ${meta.parentKey} is unset.`);
|
|
1506
1576
|
}
|
|
1507
|
-
|
|
1508
|
-
return
|
|
1577
|
+
installMorphPivotMethods(ctor);
|
|
1578
|
+
return makeMorphToManyAccessor(ctor, Related, def, parentVal);
|
|
1509
1579
|
}
|
|
1510
1580
|
/**
|
|
1511
1581
|
* Pivot-mutation accessor for a `morphedByMany` relation (the inverse
|
|
@@ -1532,13 +1602,13 @@ export class Model {
|
|
|
1532
1602
|
throw new Error(`[RudderJS ORM] Relation "${name}" on ${ctor.name} is "${def.type}", not "morphedByMany".`);
|
|
1533
1603
|
}
|
|
1534
1604
|
const Related = def.model();
|
|
1535
|
-
const meta =
|
|
1536
|
-
const parentVal = parent
|
|
1605
|
+
const meta = resolveMorphedByManyMeta(ctor, Related, def);
|
|
1606
|
+
const parentVal = readField(parent, meta.parentKey);
|
|
1537
1607
|
if (parentVal === undefined || parentVal === null) {
|
|
1538
1608
|
throw new Error(`[RudderJS ORM] Cannot use morphedByMany "${name}" on ${ctor.name} — ${meta.parentKey} is unset.`);
|
|
1539
1609
|
}
|
|
1540
|
-
|
|
1541
|
-
return
|
|
1610
|
+
installMorphPivotMethods(ctor);
|
|
1611
|
+
return makeMorphedByManyAccessor(ctor, Related, def, parentVal);
|
|
1542
1612
|
}
|
|
1543
1613
|
/**
|
|
1544
1614
|
* Build the `{name}Id + {name}Type` payload for a polymorphic write.
|
|
@@ -1555,7 +1625,7 @@ export class Model {
|
|
|
1555
1625
|
*/
|
|
1556
1626
|
static morph(name, parent) {
|
|
1557
1627
|
const ctor = parent.constructor;
|
|
1558
|
-
const pk = parent
|
|
1628
|
+
const pk = readField(parent, ctor.primaryKey);
|
|
1559
1629
|
if (pk === undefined || pk === null) {
|
|
1560
1630
|
throw new Error(`[RudderJS ORM] Model.morph("${name}", parent): parent.${ctor.primaryKey} is unset — save the parent first.`);
|
|
1561
1631
|
}
|
|
@@ -1679,845 +1749,6 @@ export class Model {
|
|
|
1679
1749
|
return Object.fromEntries(Object.entries(result).filter(([k]) => !effectiveHidden.includes(k)));
|
|
1680
1750
|
}
|
|
1681
1751
|
}
|
|
1682
|
-
// ─── Dirty tracking equality ───────────────────────────────
|
|
1683
|
-
/**
|
|
1684
|
-
* @internal — value equality used by dirty tracking. Mirrors Eloquent's
|
|
1685
|
-
* `originalIsEquivalent`: strict for primitives, `getTime()` for Date,
|
|
1686
|
-
* structural-by-JSON for arrays / plain objects (covers `json` / `array`
|
|
1687
|
-
* casts), null/undefined collapsed.
|
|
1688
|
-
*
|
|
1689
|
-
* Caveat: JSON.stringify is key-order sensitive, so `{a:1,b:2}` vs
|
|
1690
|
-
* `{b:2,a:1}` compares unequal. Same posture Laravel takes — documented
|
|
1691
|
-
* in the Dirty Tracking section of the orm README.
|
|
1692
|
-
*/
|
|
1693
|
-
function _attrEqual(a, b) {
|
|
1694
|
-
if (a === b)
|
|
1695
|
-
return true;
|
|
1696
|
-
if (a == null && b == null)
|
|
1697
|
-
return true;
|
|
1698
|
-
if (a instanceof Date && b instanceof Date)
|
|
1699
|
-
return a.getTime() === b.getTime();
|
|
1700
|
-
if (typeof a === 'object' && a !== null && typeof b === 'object' && b !== null) {
|
|
1701
|
-
try {
|
|
1702
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
1703
|
-
}
|
|
1704
|
-
catch {
|
|
1705
|
-
return false;
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
return false;
|
|
1709
|
-
}
|
|
1710
|
-
function _camelHead(s) {
|
|
1711
|
-
return s.charAt(0).toLowerCase() + s.slice(1);
|
|
1712
|
-
}
|
|
1713
|
-
function _morphParentQuery(self, ctor, def, name) {
|
|
1714
|
-
const Related = def.model();
|
|
1715
|
-
const idCol = `${def.morphName}Id`;
|
|
1716
|
-
const typeCol = `${def.morphName}Type`;
|
|
1717
|
-
const localCol = def.localKey ?? ctor.primaryKey;
|
|
1718
|
-
const localVal = self[localCol];
|
|
1719
|
-
const typeVal = def.morphType ?? ctor.morphAlias ?? ctor.name;
|
|
1720
|
-
if (localVal === undefined || localVal === null) {
|
|
1721
|
-
throw new Error(`[RudderJS ORM] Cannot resolve "${name}" on ${ctor.name} — ${localCol} is unset.`);
|
|
1722
|
-
}
|
|
1723
|
-
return Related.where(idCol, localVal).where(typeCol, typeVal);
|
|
1724
|
-
}
|
|
1725
|
-
function _resolveBelongsToManyMeta(Parent, Related, def) {
|
|
1726
|
-
return {
|
|
1727
|
-
pivotTable: def.pivotTable,
|
|
1728
|
-
foreignPivotKey: def.foreignPivotKey ?? `${_camelHead(Parent.name)}Id`,
|
|
1729
|
-
relatedPivotKey: def.relatedPivotKey ?? `${_camelHead(Related.name)}Id`,
|
|
1730
|
-
parentKey: def.parentKey ?? Parent.primaryKey,
|
|
1731
|
-
relatedKey: def.relatedKey ?? Related.primaryKey,
|
|
1732
|
-
};
|
|
1733
|
-
}
|
|
1734
|
-
function _resolveMorphToManyMeta(Parent, Related, def) {
|
|
1735
|
-
return {
|
|
1736
|
-
pivotTable: def.pivotTable,
|
|
1737
|
-
foreignPivotKey: `${def.morphName}Id`,
|
|
1738
|
-
morphTypeKey: `${def.morphName}Type`,
|
|
1739
|
-
morphTypeValue: def.morphType ?? Parent.morphAlias ?? Parent.name,
|
|
1740
|
-
relatedPivotKey: def.relatedPivotKey ?? `${_camelHead(Related.name)}Id`,
|
|
1741
|
-
parentKey: def.parentKey ?? Parent.primaryKey,
|
|
1742
|
-
relatedKey: def.relatedKey ?? Related.primaryKey,
|
|
1743
|
-
};
|
|
1744
|
-
}
|
|
1745
|
-
function _resolveMorphedByManyMeta(Parent, Related, def) {
|
|
1746
|
-
return {
|
|
1747
|
-
pivotTable: def.pivotTable,
|
|
1748
|
-
relatedPivotKey: `${def.morphName}Id`,
|
|
1749
|
-
morphTypeKey: `${def.morphName}Type`,
|
|
1750
|
-
morphTypeValue: def.morphType ?? Related.morphAlias ?? Related.name,
|
|
1751
|
-
foreignPivotKey: def.foreignPivotKey ?? `${_camelHead(Parent.name)}Id`,
|
|
1752
|
-
parentKey: def.parentKey ?? Parent.primaryKey,
|
|
1753
|
-
relatedKey: def.relatedKey ?? Related.primaryKey,
|
|
1754
|
-
};
|
|
1755
|
-
}
|
|
1756
|
-
// ─── whereHas internals ────────────────────────────────────
|
|
1757
|
-
/**
|
|
1758
|
-
* Run the constrain callback against a recording-only QueryBuilder that
|
|
1759
|
-
* captures `.where()` calls into a flat `WhereClause[]` and treats every
|
|
1760
|
-
* other chainable method as a no-op. Nested `whereHas` inside the callback
|
|
1761
|
-
* throws — recursive predicates are deferred to v2.
|
|
1762
|
-
*/
|
|
1763
|
-
function _captureConstraintWheres(constrain) {
|
|
1764
|
-
const wheres = [];
|
|
1765
|
-
const recorder = new Proxy({}, {
|
|
1766
|
-
get(_t, prop) {
|
|
1767
|
-
const name = String(prop);
|
|
1768
|
-
if (name === 'where') {
|
|
1769
|
-
return (col, opOrVal, maybeVal) => {
|
|
1770
|
-
if (maybeVal === undefined) {
|
|
1771
|
-
wheres.push({ column: col, operator: '=', value: opOrVal });
|
|
1772
|
-
}
|
|
1773
|
-
else {
|
|
1774
|
-
wheres.push({ column: col, operator: opOrVal, value: maybeVal });
|
|
1775
|
-
}
|
|
1776
|
-
return recorder;
|
|
1777
|
-
};
|
|
1778
|
-
}
|
|
1779
|
-
if (name === 'whereHas' || name === 'whereDoesntHave' || name === 'withWhereHas') {
|
|
1780
|
-
return () => {
|
|
1781
|
-
throw new Error(`[RudderJS ORM] Nested ${name} inside a whereHas constrain callback is deferred to v2. ` +
|
|
1782
|
-
`Filter on flat columns inside the callback for now.`);
|
|
1783
|
-
};
|
|
1784
|
-
}
|
|
1785
|
-
if (name === 'orWhere') {
|
|
1786
|
-
return () => {
|
|
1787
|
-
throw new Error(`[RudderJS ORM] orWhere inside a whereHas constrain callback is not supported in v1 — ` +
|
|
1788
|
-
`the WhereClause contract has no boolean flag, so the OR semantic can't round-trip to the adapter. ` +
|
|
1789
|
-
`Compose the predicate with where() (AND), or run two queries and merge in app code.`);
|
|
1790
|
-
};
|
|
1791
|
-
}
|
|
1792
|
-
// All other chainable methods record nothing and return the recorder so
|
|
1793
|
-
// `q.orderBy('x').limit(1)` chains through silently. Terminal methods
|
|
1794
|
-
// (find/get/etc.) don't make sense in a constrain callback — they'd
|
|
1795
|
-
// execute mid-build — but we don't intercept them here; they'd just
|
|
1796
|
-
// return the recorder which then fails downstream. Keep the contract
|
|
1797
|
-
// simple.
|
|
1798
|
-
return () => recorder;
|
|
1799
|
-
},
|
|
1800
|
-
});
|
|
1801
|
-
constrain(recorder);
|
|
1802
|
-
return wheres;
|
|
1803
|
-
}
|
|
1804
|
-
/**
|
|
1805
|
-
* Resolve the `belongsTo` relation declaration on `Self` that points at
|
|
1806
|
-
* `ParentCtor`. When `relation` is given, looks it up directly. Otherwise
|
|
1807
|
-
* scans `Self.relations` for a single `belongsTo` whose `model()` resolves
|
|
1808
|
-
* to `ParentCtor` — throws on zero or multiple candidates.
|
|
1809
|
-
*/
|
|
1810
|
-
function _resolveBelongsToFor(Self, ParentCtor, relation) {
|
|
1811
|
-
if (relation !== undefined) {
|
|
1812
|
-
const def = Self.relations[relation];
|
|
1813
|
-
if (!def)
|
|
1814
|
-
throw new Error(`[RudderJS ORM] Relation "${relation}" is not defined on ${Self.name}.`);
|
|
1815
|
-
if (def.type !== 'belongsTo') {
|
|
1816
|
-
throw new Error(`[RudderJS ORM] Relation "${relation}" on ${Self.name} is "${def.type}", not "belongsTo".`);
|
|
1817
|
-
}
|
|
1818
|
-
return def;
|
|
1819
|
-
}
|
|
1820
|
-
const candidates = [];
|
|
1821
|
-
for (const [name, def] of Object.entries(Self.relations)) {
|
|
1822
|
-
if (def.type !== 'belongsTo')
|
|
1823
|
-
continue;
|
|
1824
|
-
const btDef = def;
|
|
1825
|
-
if (btDef.model() === ParentCtor)
|
|
1826
|
-
candidates.push([name, btDef]);
|
|
1827
|
-
}
|
|
1828
|
-
if (candidates.length === 0) {
|
|
1829
|
-
throw new Error(`[RudderJS ORM] whereBelongsTo: ${Self.name} has no belongsTo relation pointing at ${ParentCtor.name}. ` +
|
|
1830
|
-
`Pass a relation name explicitly.`);
|
|
1831
|
-
}
|
|
1832
|
-
if (candidates.length > 1) {
|
|
1833
|
-
const names = candidates.map(([n]) => n).join(', ');
|
|
1834
|
-
throw new Error(`[RudderJS ORM] whereBelongsTo: ${Self.name} has multiple belongsTo relations pointing at ${ParentCtor.name} (${names}). ` +
|
|
1835
|
-
`Pass the relation name explicitly.`);
|
|
1836
|
-
}
|
|
1837
|
-
return candidates[0][1];
|
|
1838
|
-
}
|
|
1839
|
-
/**
|
|
1840
|
-
* Build the {@link RelationExistencePredicate} for a relation declared on
|
|
1841
|
-
* `Parent` by `relation` and dispatch it to the adapter via
|
|
1842
|
-
* `q.whereRelationExists(predicate)`. Returns the same QueryBuilder for
|
|
1843
|
-
* chaining (the adapter mutates in place).
|
|
1844
|
-
*
|
|
1845
|
-
* `morphTo` throws — the related table isn't statically known so a single
|
|
1846
|
-
* EXISTS subquery can't represent it. Filter on the discriminator + id
|
|
1847
|
-
* columns directly when you need that semantic.
|
|
1848
|
-
*/
|
|
1849
|
-
function _attachWhereHas(Parent, q, relation, exists, constrain) {
|
|
1850
|
-
const def = Parent.relations[relation];
|
|
1851
|
-
if (!def) {
|
|
1852
|
-
throw new Error(`[RudderJS ORM] Relation "${relation}" is not defined on ${Parent.name}.`);
|
|
1853
|
-
}
|
|
1854
|
-
if (def.type === 'morphTo') {
|
|
1855
|
-
throw new Error(`[RudderJS ORM] morphTo "${relation}" cannot be used with whereHas — the related table is dynamic. ` +
|
|
1856
|
-
`Filter on ${def.morphName}Id / ${def.morphName}Type directly instead.`);
|
|
1857
|
-
}
|
|
1858
|
-
const constraintWheres = constrain ? _captureConstraintWheres(constrain) : [];
|
|
1859
|
-
const predicate = _buildRelationPredicate(Parent, relation, def, exists, constraintWheres);
|
|
1860
|
-
return q.whereRelationExists(predicate);
|
|
1861
|
-
}
|
|
1862
|
-
function _buildRelationPredicate(Parent, relation, def, exists, constraintWheres) {
|
|
1863
|
-
const Related = def.model();
|
|
1864
|
-
if (def.type === 'belongsToMany') {
|
|
1865
|
-
const meta = _resolveBelongsToManyMeta(Parent, Related, def);
|
|
1866
|
-
return {
|
|
1867
|
-
relation,
|
|
1868
|
-
exists,
|
|
1869
|
-
relatedTable: Related.getTable(),
|
|
1870
|
-
parentColumn: meta.parentKey,
|
|
1871
|
-
relatedColumn: meta.relatedKey,
|
|
1872
|
-
constraintWheres,
|
|
1873
|
-
through: {
|
|
1874
|
-
pivotTable: meta.pivotTable,
|
|
1875
|
-
foreignPivotKey: meta.foreignPivotKey,
|
|
1876
|
-
relatedPivotKey: meta.relatedPivotKey,
|
|
1877
|
-
},
|
|
1878
|
-
};
|
|
1879
|
-
}
|
|
1880
|
-
if (def.type === 'morphToMany') {
|
|
1881
|
-
const meta = _resolveMorphToManyMeta(Parent, Related, def);
|
|
1882
|
-
return {
|
|
1883
|
-
relation,
|
|
1884
|
-
exists,
|
|
1885
|
-
relatedTable: Related.getTable(),
|
|
1886
|
-
parentColumn: meta.parentKey,
|
|
1887
|
-
relatedColumn: meta.relatedKey,
|
|
1888
|
-
constraintWheres,
|
|
1889
|
-
extraEquals: { [meta.morphTypeKey]: meta.morphTypeValue },
|
|
1890
|
-
through: {
|
|
1891
|
-
pivotTable: meta.pivotTable,
|
|
1892
|
-
foreignPivotKey: meta.foreignPivotKey,
|
|
1893
|
-
relatedPivotKey: meta.relatedPivotKey,
|
|
1894
|
-
},
|
|
1895
|
-
};
|
|
1896
|
-
}
|
|
1897
|
-
if (def.type === 'morphedByMany') {
|
|
1898
|
-
const meta = _resolveMorphedByManyMeta(Parent, Related, def);
|
|
1899
|
-
return {
|
|
1900
|
-
relation,
|
|
1901
|
-
exists,
|
|
1902
|
-
relatedTable: Related.getTable(),
|
|
1903
|
-
parentColumn: meta.parentKey,
|
|
1904
|
-
relatedColumn: meta.relatedKey,
|
|
1905
|
-
constraintWheres,
|
|
1906
|
-
extraEquals: { [meta.morphTypeKey]: meta.morphTypeValue },
|
|
1907
|
-
through: {
|
|
1908
|
-
pivotTable: meta.pivotTable,
|
|
1909
|
-
foreignPivotKey: meta.foreignPivotKey,
|
|
1910
|
-
relatedPivotKey: meta.relatedPivotKey,
|
|
1911
|
-
},
|
|
1912
|
-
};
|
|
1913
|
-
}
|
|
1914
|
-
if (def.type === 'morphMany' || def.type === 'morphOne') {
|
|
1915
|
-
const idCol = `${def.morphName}Id`;
|
|
1916
|
-
const typeCol = `${def.morphName}Type`;
|
|
1917
|
-
const localCol = def.localKey ?? Parent.primaryKey;
|
|
1918
|
-
const typeVal = def.morphType ?? Parent.morphAlias ?? Parent.name;
|
|
1919
|
-
return {
|
|
1920
|
-
relation,
|
|
1921
|
-
exists,
|
|
1922
|
-
relatedTable: Related.getTable(),
|
|
1923
|
-
parentColumn: localCol,
|
|
1924
|
-
relatedColumn: idCol,
|
|
1925
|
-
constraintWheres,
|
|
1926
|
-
extraEquals: { [typeCol]: typeVal },
|
|
1927
|
-
};
|
|
1928
|
-
}
|
|
1929
|
-
if (def.type === 'belongsTo') {
|
|
1930
|
-
const fk = def.foreignKey ?? `${_camelHead(Related.name)}Id`;
|
|
1931
|
-
const localCol = def.localKey ?? fk;
|
|
1932
|
-
return {
|
|
1933
|
-
relation,
|
|
1934
|
-
exists,
|
|
1935
|
-
relatedTable: Related.getTable(),
|
|
1936
|
-
parentColumn: localCol,
|
|
1937
|
-
relatedColumn: Related.primaryKey,
|
|
1938
|
-
constraintWheres,
|
|
1939
|
-
};
|
|
1940
|
-
}
|
|
1941
|
-
// hasOne / hasMany — related table holds the FK pointing back to Parent.
|
|
1942
|
-
const fk = def.foreignKey ?? `${_camelHead(Parent.name)}Id`;
|
|
1943
|
-
const localCol = def.localKey ?? Parent.primaryKey;
|
|
1944
|
-
return {
|
|
1945
|
-
relation,
|
|
1946
|
-
exists,
|
|
1947
|
-
relatedTable: Related.getTable(),
|
|
1948
|
-
parentColumn: localCol,
|
|
1949
|
-
relatedColumn: fk,
|
|
1950
|
-
constraintWheres,
|
|
1951
|
-
};
|
|
1952
|
-
}
|
|
1953
|
-
/**
|
|
1954
|
-
* `withWhereHas` — run `whereHas` AND eager-load the relation under the
|
|
1955
|
-
* same constraint when the adapter implements `withConstrained`. Adapters
|
|
1956
|
-
* without it fall back to plain `with(relation)` (constraint applies only
|
|
1957
|
-
* to the parent filter, not the eagerly loaded children).
|
|
1958
|
-
*/
|
|
1959
|
-
function _attachWithWhereHas(Parent, q, relation, constrain) {
|
|
1960
|
-
const constraintWheres = constrain ? _captureConstraintWheres(constrain) : [];
|
|
1961
|
-
// Reuse _attachWhereHas for the parent-side filter — it re-runs the
|
|
1962
|
-
// constrain callback against a fresh recorder, so the WhereClause[] we
|
|
1963
|
-
// capture above and the one captured inside _attachWhereHas come from
|
|
1964
|
-
// distinct recorder instances. That's intentional: each captures the
|
|
1965
|
-
// same constraint independently and neither is mutated by the adapter.
|
|
1966
|
-
_attachWhereHas(Parent, q, relation, true, constrain);
|
|
1967
|
-
const withConstrained = q.withConstrained;
|
|
1968
|
-
if (constraintWheres.length > 0 && typeof withConstrained === 'function') {
|
|
1969
|
-
return withConstrained.call(q, relation, constraintWheres);
|
|
1970
|
-
}
|
|
1971
|
-
return q.with(relation);
|
|
1972
|
-
}
|
|
1973
|
-
function _attachWhereBelongsTo(Self, q, parent, relation) {
|
|
1974
|
-
const ParentCtor = parent.constructor;
|
|
1975
|
-
const def = _resolveBelongsToFor(Self, ParentCtor, relation);
|
|
1976
|
-
const Related = def.model();
|
|
1977
|
-
const fk = def.foreignKey ?? `${_camelHead(Related.name)}Id`;
|
|
1978
|
-
const localCol = def.localKey ?? fk;
|
|
1979
|
-
const parentVal = parent[ParentCtor.primaryKey];
|
|
1980
|
-
if (parentVal === undefined || parentVal === null) {
|
|
1981
|
-
throw new Error(`[RudderJS ORM] whereBelongsTo: parent.${ParentCtor.primaryKey} is unset on ${ParentCtor.name}.`);
|
|
1982
|
-
}
|
|
1983
|
-
return q.where(localCol, parentVal);
|
|
1984
|
-
}
|
|
1985
|
-
const _CHAIN_METHODS = new Set([
|
|
1986
|
-
'where', 'orWhere', 'orderBy', 'limit', 'offset', 'with', 'withTrashed', 'onlyTrashed',
|
|
1987
|
-
]);
|
|
1988
|
-
const _TERMINAL_METHODS = new Set([
|
|
1989
|
-
'first', 'find', 'get', 'all', 'count', 'paginate',
|
|
1990
|
-
]);
|
|
1991
|
-
const _UNSUPPORTED_TERMINALS = new Set([
|
|
1992
|
-
'create', 'update', 'delete', 'restore', 'forceDelete', 'increment', 'decrement', 'insertMany', 'deleteAll', 'updateAll',
|
|
1993
|
-
]);
|
|
1994
|
-
function _replayChain(q, recorded) {
|
|
1995
|
-
let cur = q;
|
|
1996
|
-
for (const [m, args] of recorded) {
|
|
1997
|
-
const fn = cur[m];
|
|
1998
|
-
if (fn)
|
|
1999
|
-
cur = fn.apply(cur, args);
|
|
2000
|
-
}
|
|
2001
|
-
return cur;
|
|
2002
|
-
}
|
|
2003
|
-
function _makeDeferredProxy(buildResolved, recorded, relationKind, hooks = {}) {
|
|
2004
|
-
const proxy = new Proxy({}, {
|
|
2005
|
-
get(_t, prop) {
|
|
2006
|
-
const name = String(prop);
|
|
2007
|
-
if (name === 'withPivot') {
|
|
2008
|
-
return (...cols) => {
|
|
2009
|
-
if (cols.length === 0) {
|
|
2010
|
-
throw new Error('[RudderJS ORM] withPivot() requires at least one column name.');
|
|
2011
|
-
}
|
|
2012
|
-
hooks.onWithPivot?.(cols);
|
|
2013
|
-
return proxy;
|
|
2014
|
-
};
|
|
2015
|
-
}
|
|
2016
|
-
if (_CHAIN_METHODS.has(name)) {
|
|
2017
|
-
return (...args) => {
|
|
2018
|
-
recorded.push([name, args]);
|
|
2019
|
-
return proxy;
|
|
2020
|
-
};
|
|
2021
|
-
}
|
|
2022
|
-
if (_TERMINAL_METHODS.has(name)) {
|
|
2023
|
-
return async (...args) => {
|
|
2024
|
-
const q = await buildResolved();
|
|
2025
|
-
const fn = q[name];
|
|
2026
|
-
const raw = fn ? await fn.apply(q, args) : undefined;
|
|
2027
|
-
return hooks.postProcess ? hooks.postProcess(raw, name) : raw;
|
|
2028
|
-
};
|
|
2029
|
-
}
|
|
2030
|
-
if (_UNSUPPORTED_TERMINALS.has(name)) {
|
|
2031
|
-
return () => {
|
|
2032
|
-
throw new Error(`[RudderJS ORM] "${name}" is not supported on a ${relationKind} lazy-fetch query. ` +
|
|
2033
|
-
`Use Model.${relationKind}(parent, name) for pivot mutations or call methods on the related Model directly.`);
|
|
2034
|
-
};
|
|
2035
|
-
}
|
|
2036
|
-
return undefined;
|
|
2037
|
-
},
|
|
2038
|
-
});
|
|
2039
|
-
return proxy;
|
|
2040
|
-
}
|
|
2041
|
-
/**
|
|
2042
|
-
* Stamp `row.pivot = { col: value, ... }` for every row in `rows` whose
|
|
2043
|
-
* `relatedKey` matches a pivot row. Used by the three deferred QBs after the
|
|
2044
|
-
* second-step `Related` query resolves. Mutates rows in place; rows whose
|
|
2045
|
-
* pivot row is absent are left untouched (`pivot` stays undefined).
|
|
2046
|
-
*/
|
|
2047
|
-
function _stampPivotOnRows(rows, relatedKey, pivotRows, relatedPivotKey, pivotColumns) {
|
|
2048
|
-
if (pivotColumns.length === 0)
|
|
2049
|
-
return;
|
|
2050
|
-
const byId = new Map();
|
|
2051
|
-
for (const p of pivotRows)
|
|
2052
|
-
byId.set(p[relatedPivotKey], p);
|
|
2053
|
-
for (const row of rows) {
|
|
2054
|
-
if (row === null || row === undefined)
|
|
2055
|
-
continue;
|
|
2056
|
-
const pivot = byId.get(row[relatedKey]);
|
|
2057
|
-
if (!pivot)
|
|
2058
|
-
continue;
|
|
2059
|
-
const projected = {};
|
|
2060
|
-
for (const col of pivotColumns)
|
|
2061
|
-
projected[col] = pivot[col];
|
|
2062
|
-
row['pivot'] = projected;
|
|
2063
|
-
}
|
|
2064
|
-
}
|
|
2065
|
-
/**
|
|
2066
|
-
* Stamp `pivot` onto a single result (object or array). Used by the deferred
|
|
2067
|
-
* proxy's `postProcess` hook — the terminal name (`first` / `find` / `get` /
|
|
2068
|
-
* `all` / `paginate`) determines whether to walk the result.
|
|
2069
|
-
*/
|
|
2070
|
-
function _stampPivotOnResult(result, terminal, relatedKey, pivotRows, relatedPivotKey, pivotColumns) {
|
|
2071
|
-
if (pivotColumns.length === 0)
|
|
2072
|
-
return result;
|
|
2073
|
-
if (result === null || result === undefined)
|
|
2074
|
-
return result;
|
|
2075
|
-
if (terminal === 'first' || terminal === 'find') {
|
|
2076
|
-
_stampPivotOnRows([result], relatedKey, pivotRows, relatedPivotKey, pivotColumns);
|
|
2077
|
-
return result;
|
|
2078
|
-
}
|
|
2079
|
-
if (terminal === 'get' || terminal === 'all') {
|
|
2080
|
-
_stampPivotOnRows(result, relatedKey, pivotRows, relatedPivotKey, pivotColumns);
|
|
2081
|
-
return result;
|
|
2082
|
-
}
|
|
2083
|
-
if (terminal === 'paginate') {
|
|
2084
|
-
const page = result;
|
|
2085
|
-
_stampPivotOnRows(page.data, relatedKey, pivotRows, relatedPivotKey, pivotColumns);
|
|
2086
|
-
return result;
|
|
2087
|
-
}
|
|
2088
|
-
return result;
|
|
2089
|
-
}
|
|
2090
|
-
function _belongsToManyDeferredQb(Related, _def, meta, parentVal) {
|
|
2091
|
-
const recorded = [];
|
|
2092
|
-
const pivotColumns = [];
|
|
2093
|
-
let lastPivotRows = [];
|
|
2094
|
-
const buildResolved = async () => {
|
|
2095
|
-
const adapter = ModelRegistry.getAdapter();
|
|
2096
|
-
const pivotRows = await adapter
|
|
2097
|
-
.query(meta.pivotTable)
|
|
2098
|
-
.where(meta.foreignPivotKey, parentVal)
|
|
2099
|
-
.get();
|
|
2100
|
-
lastPivotRows = pivotRows;
|
|
2101
|
-
const ids = pivotRows.map(r => r[meta.relatedPivotKey]);
|
|
2102
|
-
// Empty IN list — short-circuit with a guaranteed-empty query so
|
|
2103
|
-
// adapters don't have to handle the edge case.
|
|
2104
|
-
const q = Related.query()
|
|
2105
|
-
.where(meta.relatedKey, 'IN', ids.length === 0 ? [] : ids);
|
|
2106
|
-
return _replayChain(q, recorded);
|
|
2107
|
-
};
|
|
2108
|
-
return _makeDeferredProxy(buildResolved, recorded, 'belongsToMany', {
|
|
2109
|
-
onWithPivot(cols) { pivotColumns.push(...cols); },
|
|
2110
|
-
postProcess(result, terminal) {
|
|
2111
|
-
return _stampPivotOnResult(result, terminal, meta.relatedKey, lastPivotRows, meta.relatedPivotKey, pivotColumns);
|
|
2112
|
-
},
|
|
2113
|
-
});
|
|
2114
|
-
}
|
|
2115
|
-
function _morphToManyDeferredQb(Related, _def, meta, parentVal) {
|
|
2116
|
-
const recorded = [];
|
|
2117
|
-
const pivotColumns = [];
|
|
2118
|
-
let lastPivotRows = [];
|
|
2119
|
-
const buildResolved = async () => {
|
|
2120
|
-
const adapter = ModelRegistry.getAdapter();
|
|
2121
|
-
const pivotRows = await adapter
|
|
2122
|
-
.query(meta.pivotTable)
|
|
2123
|
-
.where(meta.foreignPivotKey, parentVal)
|
|
2124
|
-
.where(meta.morphTypeKey, meta.morphTypeValue)
|
|
2125
|
-
.get();
|
|
2126
|
-
lastPivotRows = pivotRows;
|
|
2127
|
-
const ids = pivotRows.map(r => r[meta.relatedPivotKey]);
|
|
2128
|
-
const q = Related.query()
|
|
2129
|
-
.where(meta.relatedKey, 'IN', ids.length === 0 ? [] : ids);
|
|
2130
|
-
return _replayChain(q, recorded);
|
|
2131
|
-
};
|
|
2132
|
-
return _makeDeferredProxy(buildResolved, recorded, 'morphToMany', {
|
|
2133
|
-
onWithPivot(cols) { pivotColumns.push(...cols); },
|
|
2134
|
-
postProcess(result, terminal) {
|
|
2135
|
-
return _stampPivotOnResult(result, terminal, meta.relatedKey, lastPivotRows, meta.relatedPivotKey, pivotColumns);
|
|
2136
|
-
},
|
|
2137
|
-
});
|
|
2138
|
-
}
|
|
2139
|
-
function _morphedByManyDeferredQb(Related, _def, meta, parentVal) {
|
|
2140
|
-
const recorded = [];
|
|
2141
|
-
const pivotColumns = [];
|
|
2142
|
-
let lastPivotRows = [];
|
|
2143
|
-
const buildResolved = async () => {
|
|
2144
|
-
const adapter = ModelRegistry.getAdapter();
|
|
2145
|
-
const pivotRows = await adapter
|
|
2146
|
-
.query(meta.pivotTable)
|
|
2147
|
-
.where(meta.foreignPivotKey, parentVal)
|
|
2148
|
-
.where(meta.morphTypeKey, meta.morphTypeValue)
|
|
2149
|
-
.get();
|
|
2150
|
-
lastPivotRows = pivotRows;
|
|
2151
|
-
const ids = pivotRows.map(r => r[meta.relatedPivotKey]);
|
|
2152
|
-
const q = Related.query()
|
|
2153
|
-
.where(meta.relatedKey, 'IN', ids.length === 0 ? [] : ids);
|
|
2154
|
-
return _replayChain(q, recorded);
|
|
2155
|
-
};
|
|
2156
|
-
return _makeDeferredProxy(buildResolved, recorded, 'morphedByMany', {
|
|
2157
|
-
onWithPivot(cols) { pivotColumns.push(...cols); },
|
|
2158
|
-
postProcess(result, terminal) {
|
|
2159
|
-
return _stampPivotOnResult(result, terminal, meta.relatedKey, lastPivotRows, meta.relatedPivotKey, pivotColumns);
|
|
2160
|
-
},
|
|
2161
|
-
});
|
|
2162
|
-
}
|
|
2163
|
-
function _normalizeAttachInput(input, foreignPivotKey, parentVal, relatedPivotKey, flatPivot) {
|
|
2164
|
-
const rows = [];
|
|
2165
|
-
if (Array.isArray(input)) {
|
|
2166
|
-
for (const id of input) {
|
|
2167
|
-
rows.push({
|
|
2168
|
-
...(flatPivot ?? {}),
|
|
2169
|
-
[foreignPivotKey]: parentVal,
|
|
2170
|
-
[relatedPivotKey]: id,
|
|
2171
|
-
});
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
else {
|
|
2175
|
-
for (const [id, perIdPivot] of Object.entries(input)) {
|
|
2176
|
-
// Normalize numeric-string keys back to numbers when possible — JS
|
|
2177
|
-
// object keys are always strings; the pivot column may be int.
|
|
2178
|
-
const idVal = /^\d+$/.test(id) ? Number(id) : id;
|
|
2179
|
-
rows.push({
|
|
2180
|
-
...perIdPivot,
|
|
2181
|
-
[foreignPivotKey]: parentVal,
|
|
2182
|
-
[relatedPivotKey]: idVal,
|
|
2183
|
-
});
|
|
2184
|
-
}
|
|
2185
|
-
}
|
|
2186
|
-
return rows;
|
|
2187
|
-
}
|
|
2188
|
-
function _idsFromAttachInput(input) {
|
|
2189
|
-
if (Array.isArray(input))
|
|
2190
|
-
return [...input];
|
|
2191
|
-
return Object.keys(input).map(k => /^\d+$/.test(k) ? Number(k) : k);
|
|
2192
|
-
}
|
|
2193
|
-
function _makeBelongsToManyAccessor(Parent, Related, def, parentVal) {
|
|
2194
|
-
const meta = _resolveBelongsToManyMeta(Parent, Related, def);
|
|
2195
|
-
const updatePivot = async (relatedId, data) => {
|
|
2196
|
-
return ModelRegistry.getAdapter()
|
|
2197
|
-
.query(meta.pivotTable)
|
|
2198
|
-
.where(meta.foreignPivotKey, parentVal)
|
|
2199
|
-
.where(meta.relatedPivotKey, relatedId)
|
|
2200
|
-
.updateAll(data);
|
|
2201
|
-
};
|
|
2202
|
-
return {
|
|
2203
|
-
async attach(input, flatPivot) {
|
|
2204
|
-
const ids = _idsFromAttachInput(input);
|
|
2205
|
-
if (ids.length === 0)
|
|
2206
|
-
return;
|
|
2207
|
-
const rows = _normalizeAttachInput(input, meta.foreignPivotKey, parentVal, meta.relatedPivotKey, flatPivot);
|
|
2208
|
-
await ModelRegistry.getAdapter()
|
|
2209
|
-
.query(meta.pivotTable)
|
|
2210
|
-
.insertMany(rows);
|
|
2211
|
-
},
|
|
2212
|
-
async detach(ids) {
|
|
2213
|
-
const adapter = ModelRegistry.getAdapter();
|
|
2214
|
-
let q = adapter
|
|
2215
|
-
.query(meta.pivotTable)
|
|
2216
|
-
.where(meta.foreignPivotKey, parentVal);
|
|
2217
|
-
if (ids !== undefined) {
|
|
2218
|
-
if (ids.length === 0)
|
|
2219
|
-
return 0;
|
|
2220
|
-
q = q.where(meta.relatedPivotKey, 'IN', [...ids]);
|
|
2221
|
-
}
|
|
2222
|
-
return q.deleteAll();
|
|
2223
|
-
},
|
|
2224
|
-
updatePivot,
|
|
2225
|
-
async sync(arg1, flatPivot) {
|
|
2226
|
-
const adapter = ModelRegistry.getAdapter();
|
|
2227
|
-
const isMap = !Array.isArray(arg1);
|
|
2228
|
-
const perIdMap = isMap ? arg1 : null;
|
|
2229
|
-
const desiredIds = isMap
|
|
2230
|
-
? Object.keys(perIdMap).map(k => /^\d+$/.test(k) ? Number(k) : k)
|
|
2231
|
-
: [...arg1];
|
|
2232
|
-
const currentRows = await adapter
|
|
2233
|
-
.query(meta.pivotTable)
|
|
2234
|
-
.where(meta.foreignPivotKey, parentVal)
|
|
2235
|
-
.get();
|
|
2236
|
-
const current = new Set(currentRows.map(r => r[meta.relatedPivotKey]));
|
|
2237
|
-
const desired = new Set(desiredIds);
|
|
2238
|
-
const attached = [];
|
|
2239
|
-
const detached = [];
|
|
2240
|
-
const updated = [];
|
|
2241
|
-
for (const id of desired)
|
|
2242
|
-
if (!current.has(id))
|
|
2243
|
-
attached.push(id);
|
|
2244
|
-
for (const id of current)
|
|
2245
|
-
if (!desired.has(id))
|
|
2246
|
-
detached.push(id);
|
|
2247
|
-
if (attached.length > 0) {
|
|
2248
|
-
const rows = attached.map(id => {
|
|
2249
|
-
const perIdPivot = perIdMap ? perIdMap[id] : undefined;
|
|
2250
|
-
return {
|
|
2251
|
-
...(flatPivot ?? {}),
|
|
2252
|
-
...(perIdPivot ?? {}),
|
|
2253
|
-
[meta.foreignPivotKey]: parentVal,
|
|
2254
|
-
[meta.relatedPivotKey]: id,
|
|
2255
|
-
};
|
|
2256
|
-
});
|
|
2257
|
-
await adapter.query(meta.pivotTable).insertMany(rows);
|
|
2258
|
-
}
|
|
2259
|
-
if (detached.length > 0) {
|
|
2260
|
-
await adapter
|
|
2261
|
-
.query(meta.pivotTable)
|
|
2262
|
-
.where(meta.foreignPivotKey, parentVal)
|
|
2263
|
-
.where(meta.relatedPivotKey, 'IN', detached)
|
|
2264
|
-
.deleteAll();
|
|
2265
|
-
}
|
|
2266
|
-
if (perIdMap) {
|
|
2267
|
-
// Reconcile extras on still-present ids by overwriting with the
|
|
2268
|
-
// requested pivot data — matches Filament's posture (the form
|
|
2269
|
-
// value wins). Skip when the supplied pivot is empty.
|
|
2270
|
-
for (const id of desired) {
|
|
2271
|
-
if (!current.has(id))
|
|
2272
|
-
continue;
|
|
2273
|
-
const perIdPivot = perIdMap[id];
|
|
2274
|
-
if (!perIdPivot || Object.keys(perIdPivot).length === 0)
|
|
2275
|
-
continue;
|
|
2276
|
-
await updatePivot(id, perIdPivot);
|
|
2277
|
-
updated.push(id);
|
|
2278
|
-
}
|
|
2279
|
-
}
|
|
2280
|
-
return { attached, detached, updated };
|
|
2281
|
-
},
|
|
2282
|
-
};
|
|
2283
|
-
}
|
|
2284
|
-
/**
|
|
2285
|
-
* Install per-relation prototype methods for every `belongsToMany` entry
|
|
2286
|
-
* declared on `static relations`. Idempotent — won't overwrite a method
|
|
2287
|
-
* the author already defined (typing escape hatch).
|
|
2288
|
-
*
|
|
2289
|
-
* Called on first query (via `ModelRegistry.register`) and once more
|
|
2290
|
-
* defensively from `Model.belongsToMany` so apps that construct instances
|
|
2291
|
-
* without ever querying still get the auto-method.
|
|
2292
|
-
*/
|
|
2293
|
-
function _installBelongsToManyMethods(ModelClass) {
|
|
2294
|
-
for (const [name, def] of Object.entries(ModelClass.relations)) {
|
|
2295
|
-
if (def.type !== 'belongsToMany')
|
|
2296
|
-
continue;
|
|
2297
|
-
if (Object.prototype.hasOwnProperty.call(ModelClass.prototype, name))
|
|
2298
|
-
continue;
|
|
2299
|
-
Object.defineProperty(ModelClass.prototype, name, {
|
|
2300
|
-
configurable: true,
|
|
2301
|
-
writable: true,
|
|
2302
|
-
value() {
|
|
2303
|
-
return Model.belongsToMany(this, name);
|
|
2304
|
-
},
|
|
2305
|
-
});
|
|
2306
|
-
}
|
|
2307
|
-
}
|
|
2308
|
-
function _makeMorphToManyAccessor(Parent, Related, def, parentVal) {
|
|
2309
|
-
const meta = _resolveMorphToManyMeta(Parent, Related, def);
|
|
2310
|
-
const updatePivot = async (relatedId, data) => {
|
|
2311
|
-
return ModelRegistry.getAdapter()
|
|
2312
|
-
.query(meta.pivotTable)
|
|
2313
|
-
.where(meta.foreignPivotKey, parentVal)
|
|
2314
|
-
.where(meta.morphTypeKey, meta.morphTypeValue)
|
|
2315
|
-
.where(meta.relatedPivotKey, relatedId)
|
|
2316
|
-
.updateAll(data);
|
|
2317
|
-
};
|
|
2318
|
-
return {
|
|
2319
|
-
async attach(input, flatPivot) {
|
|
2320
|
-
const ids = _idsFromAttachInput(input);
|
|
2321
|
-
if (ids.length === 0)
|
|
2322
|
-
return;
|
|
2323
|
-
const rows = _normalizeAttachInput(input, meta.foreignPivotKey, parentVal, meta.relatedPivotKey, flatPivot)
|
|
2324
|
-
.map(r => ({ ...r, [meta.morphTypeKey]: meta.morphTypeValue }));
|
|
2325
|
-
await ModelRegistry.getAdapter()
|
|
2326
|
-
.query(meta.pivotTable)
|
|
2327
|
-
.insertMany(rows);
|
|
2328
|
-
},
|
|
2329
|
-
async detach(ids) {
|
|
2330
|
-
const adapter = ModelRegistry.getAdapter();
|
|
2331
|
-
let q = adapter
|
|
2332
|
-
.query(meta.pivotTable)
|
|
2333
|
-
.where(meta.foreignPivotKey, parentVal)
|
|
2334
|
-
.where(meta.morphTypeKey, meta.morphTypeValue);
|
|
2335
|
-
if (ids !== undefined) {
|
|
2336
|
-
if (ids.length === 0)
|
|
2337
|
-
return 0;
|
|
2338
|
-
q = q.where(meta.relatedPivotKey, 'IN', [...ids]);
|
|
2339
|
-
}
|
|
2340
|
-
return q.deleteAll();
|
|
2341
|
-
},
|
|
2342
|
-
updatePivot,
|
|
2343
|
-
async sync(arg1, flatPivot) {
|
|
2344
|
-
const adapter = ModelRegistry.getAdapter();
|
|
2345
|
-
const isMap = !Array.isArray(arg1);
|
|
2346
|
-
const perIdMap = isMap ? arg1 : null;
|
|
2347
|
-
const desiredIds = isMap
|
|
2348
|
-
? Object.keys(perIdMap).map(k => /^\d+$/.test(k) ? Number(k) : k)
|
|
2349
|
-
: [...arg1];
|
|
2350
|
-
const currentRows = await adapter
|
|
2351
|
-
.query(meta.pivotTable)
|
|
2352
|
-
.where(meta.foreignPivotKey, parentVal)
|
|
2353
|
-
.where(meta.morphTypeKey, meta.morphTypeValue)
|
|
2354
|
-
.get();
|
|
2355
|
-
const current = new Set(currentRows.map(r => r[meta.relatedPivotKey]));
|
|
2356
|
-
const desired = new Set(desiredIds);
|
|
2357
|
-
const attached = [];
|
|
2358
|
-
const detached = [];
|
|
2359
|
-
const updated = [];
|
|
2360
|
-
for (const id of desired)
|
|
2361
|
-
if (!current.has(id))
|
|
2362
|
-
attached.push(id);
|
|
2363
|
-
for (const id of current)
|
|
2364
|
-
if (!desired.has(id))
|
|
2365
|
-
detached.push(id);
|
|
2366
|
-
if (attached.length > 0) {
|
|
2367
|
-
const rows = attached.map(id => {
|
|
2368
|
-
const perIdPivot = perIdMap ? perIdMap[id] : undefined;
|
|
2369
|
-
return {
|
|
2370
|
-
...(flatPivot ?? {}),
|
|
2371
|
-
...(perIdPivot ?? {}),
|
|
2372
|
-
[meta.foreignPivotKey]: parentVal,
|
|
2373
|
-
[meta.relatedPivotKey]: id,
|
|
2374
|
-
[meta.morphTypeKey]: meta.morphTypeValue,
|
|
2375
|
-
};
|
|
2376
|
-
});
|
|
2377
|
-
await adapter.query(meta.pivotTable).insertMany(rows);
|
|
2378
|
-
}
|
|
2379
|
-
if (detached.length > 0) {
|
|
2380
|
-
await adapter
|
|
2381
|
-
.query(meta.pivotTable)
|
|
2382
|
-
.where(meta.foreignPivotKey, parentVal)
|
|
2383
|
-
.where(meta.morphTypeKey, meta.morphTypeValue)
|
|
2384
|
-
.where(meta.relatedPivotKey, 'IN', detached)
|
|
2385
|
-
.deleteAll();
|
|
2386
|
-
}
|
|
2387
|
-
if (perIdMap) {
|
|
2388
|
-
for (const id of desired) {
|
|
2389
|
-
if (!current.has(id))
|
|
2390
|
-
continue;
|
|
2391
|
-
const perIdPivot = perIdMap[id];
|
|
2392
|
-
if (!perIdPivot || Object.keys(perIdPivot).length === 0)
|
|
2393
|
-
continue;
|
|
2394
|
-
await updatePivot(id, perIdPivot);
|
|
2395
|
-
updated.push(id);
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
return { attached, detached, updated };
|
|
2399
|
-
},
|
|
2400
|
-
};
|
|
2401
|
-
}
|
|
2402
|
-
function _makeMorphedByManyAccessor(Parent, Related, def, parentVal) {
|
|
2403
|
-
const meta = _resolveMorphedByManyMeta(Parent, Related, def);
|
|
2404
|
-
const updatePivot = async (relatedId, data) => {
|
|
2405
|
-
return ModelRegistry.getAdapter()
|
|
2406
|
-
.query(meta.pivotTable)
|
|
2407
|
-
.where(meta.foreignPivotKey, parentVal)
|
|
2408
|
-
.where(meta.morphTypeKey, meta.morphTypeValue)
|
|
2409
|
-
.where(meta.relatedPivotKey, relatedId)
|
|
2410
|
-
.updateAll(data);
|
|
2411
|
-
};
|
|
2412
|
-
return {
|
|
2413
|
-
async attach(input, flatPivot) {
|
|
2414
|
-
const ids = _idsFromAttachInput(input);
|
|
2415
|
-
if (ids.length === 0)
|
|
2416
|
-
return;
|
|
2417
|
-
// Parent is the strong side, related is the polymorphic side. Each row
|
|
2418
|
-
// carries: parent FK, related FK, related-class discriminator.
|
|
2419
|
-
const rows = _normalizeAttachInput(input, meta.foreignPivotKey, parentVal, meta.relatedPivotKey, flatPivot)
|
|
2420
|
-
.map(r => ({ ...r, [meta.morphTypeKey]: meta.morphTypeValue }));
|
|
2421
|
-
await ModelRegistry.getAdapter()
|
|
2422
|
-
.query(meta.pivotTable)
|
|
2423
|
-
.insertMany(rows);
|
|
2424
|
-
},
|
|
2425
|
-
async detach(ids) {
|
|
2426
|
-
const adapter = ModelRegistry.getAdapter();
|
|
2427
|
-
let q = adapter
|
|
2428
|
-
.query(meta.pivotTable)
|
|
2429
|
-
.where(meta.foreignPivotKey, parentVal)
|
|
2430
|
-
.where(meta.morphTypeKey, meta.morphTypeValue);
|
|
2431
|
-
if (ids !== undefined) {
|
|
2432
|
-
if (ids.length === 0)
|
|
2433
|
-
return 0;
|
|
2434
|
-
q = q.where(meta.relatedPivotKey, 'IN', [...ids]);
|
|
2435
|
-
}
|
|
2436
|
-
return q.deleteAll();
|
|
2437
|
-
},
|
|
2438
|
-
updatePivot,
|
|
2439
|
-
async sync(arg1, flatPivot) {
|
|
2440
|
-
const adapter = ModelRegistry.getAdapter();
|
|
2441
|
-
const isMap = !Array.isArray(arg1);
|
|
2442
|
-
const perIdMap = isMap ? arg1 : null;
|
|
2443
|
-
const desiredIds = isMap
|
|
2444
|
-
? Object.keys(perIdMap).map(k => /^\d+$/.test(k) ? Number(k) : k)
|
|
2445
|
-
: [...arg1];
|
|
2446
|
-
const currentRows = await adapter
|
|
2447
|
-
.query(meta.pivotTable)
|
|
2448
|
-
.where(meta.foreignPivotKey, parentVal)
|
|
2449
|
-
.where(meta.morphTypeKey, meta.morphTypeValue)
|
|
2450
|
-
.get();
|
|
2451
|
-
const current = new Set(currentRows.map(r => r[meta.relatedPivotKey]));
|
|
2452
|
-
const desired = new Set(desiredIds);
|
|
2453
|
-
const attached = [];
|
|
2454
|
-
const detached = [];
|
|
2455
|
-
const updated = [];
|
|
2456
|
-
for (const id of desired)
|
|
2457
|
-
if (!current.has(id))
|
|
2458
|
-
attached.push(id);
|
|
2459
|
-
for (const id of current)
|
|
2460
|
-
if (!desired.has(id))
|
|
2461
|
-
detached.push(id);
|
|
2462
|
-
if (attached.length > 0) {
|
|
2463
|
-
const rows = attached.map(id => {
|
|
2464
|
-
const perIdPivot = perIdMap ? perIdMap[id] : undefined;
|
|
2465
|
-
return {
|
|
2466
|
-
...(flatPivot ?? {}),
|
|
2467
|
-
...(perIdPivot ?? {}),
|
|
2468
|
-
[meta.foreignPivotKey]: parentVal,
|
|
2469
|
-
[meta.relatedPivotKey]: id,
|
|
2470
|
-
[meta.morphTypeKey]: meta.morphTypeValue,
|
|
2471
|
-
};
|
|
2472
|
-
});
|
|
2473
|
-
await adapter.query(meta.pivotTable).insertMany(rows);
|
|
2474
|
-
}
|
|
2475
|
-
if (detached.length > 0) {
|
|
2476
|
-
await adapter
|
|
2477
|
-
.query(meta.pivotTable)
|
|
2478
|
-
.where(meta.foreignPivotKey, parentVal)
|
|
2479
|
-
.where(meta.morphTypeKey, meta.morphTypeValue)
|
|
2480
|
-
.where(meta.relatedPivotKey, 'IN', detached)
|
|
2481
|
-
.deleteAll();
|
|
2482
|
-
}
|
|
2483
|
-
if (perIdMap) {
|
|
2484
|
-
for (const id of desired) {
|
|
2485
|
-
if (!current.has(id))
|
|
2486
|
-
continue;
|
|
2487
|
-
const perIdPivot = perIdMap[id];
|
|
2488
|
-
if (!perIdPivot || Object.keys(perIdPivot).length === 0)
|
|
2489
|
-
continue;
|
|
2490
|
-
await updatePivot(id, perIdPivot);
|
|
2491
|
-
updated.push(id);
|
|
2492
|
-
}
|
|
2493
|
-
}
|
|
2494
|
-
return { attached, detached, updated };
|
|
2495
|
-
},
|
|
2496
|
-
};
|
|
2497
|
-
}
|
|
2498
|
-
/**
|
|
2499
|
-
* Install per-relation prototype methods for every `morphToMany` /
|
|
2500
|
-
* `morphedByMany` entry. Same idempotent shape as
|
|
2501
|
-
* {@link _installBelongsToManyMethods}.
|
|
2502
|
-
*/
|
|
2503
|
-
function _installMorphPivotMethods(ModelClass) {
|
|
2504
|
-
for (const [name, def] of Object.entries(ModelClass.relations)) {
|
|
2505
|
-
if (def.type !== 'morphToMany' && def.type !== 'morphedByMany')
|
|
2506
|
-
continue;
|
|
2507
|
-
if (Object.prototype.hasOwnProperty.call(ModelClass.prototype, name))
|
|
2508
|
-
continue;
|
|
2509
|
-
const isOwning = def.type === 'morphToMany';
|
|
2510
|
-
Object.defineProperty(ModelClass.prototype, name, {
|
|
2511
|
-
configurable: true,
|
|
2512
|
-
writable: true,
|
|
2513
|
-
value() {
|
|
2514
|
-
return isOwning
|
|
2515
|
-
? Model.morphToMany(this, name)
|
|
2516
|
-
: Model.morphedByMany(this, name);
|
|
2517
|
-
},
|
|
2518
|
-
});
|
|
2519
|
-
}
|
|
2520
|
-
}
|
|
2521
1752
|
// ─── Compile-time contract check ───────────────────────────
|
|
2522
1753
|
// Asserts that `Model`'s static surface conforms to the `ModelLike`
|
|
2523
1754
|
// contract from `@rudderjs/contracts`. Downstream tools (admin panels
|
|
@@ -2525,7 +1756,6 @@ function _installMorphPivotMethods(ModelClass) {
|
|
|
2525
1756
|
// `ModelLike` so they don't need to depend on `@rudderjs/orm` directly.
|
|
2526
1757
|
// This line will fail to compile if a future change to Model breaks
|
|
2527
1758
|
// that contract.
|
|
2528
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
2529
1759
|
const _modelSatisfiesContract = Model;
|
|
2530
1760
|
void _modelSatisfiesContract;
|
|
2531
1761
|
//# sourceMappingURL=index.js.map
|