@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/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
- // ─── Global ORM Registry ───────────────────────────────────
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
- this.adapter = adapter;
28
+ _store.adapter = adapter;
19
29
  }
20
30
  static get() {
21
- return this.adapter;
31
+ return _store.adapter;
22
32
  }
23
33
  static getAdapter() {
24
- if (!this.adapter) {
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 this.adapter;
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 || this.models.has(name))
52
+ if (!name || _store.models.has(name))
43
53
  return;
44
- this.models.set(name, ModelClass);
45
- _installBelongsToManyMethods(ModelClass);
46
- _installMorphPivotMethods(ModelClass);
47
- for (const listener of this.listeners)
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 this.models;
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
- this.listeners.add(listener);
63
- return () => { this.listeners.delete(listener); };
72
+ _store.listeners.add(listener);
73
+ return () => { _store.listeners.delete(listener); };
64
74
  }
65
75
  static reset() {
66
- this.adapter = null;
67
- this.models.clear();
68
- this.listeners.clear();
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
- /** @internal — own enumerable column values as of last load/save/refresh. */
360
- #original = {};
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
- static async _fireEvent(event, ...args) {
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
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
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
- instance._syncOriginal();
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
- _attachWhereHas(ModelClass, target, relation, true, constrain);
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
- _attachWhereHas(ModelClass, target, relation, false, constrain);
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
- _attachWithWhereHas(ModelClass, target, relation, constrain);
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
- _attachWhereBelongsTo(ModelClass, target, parent, relation);
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 _attachWhereHas(this, Model._q(this), relation, true, constrain);
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 _attachWhereHas(this, Model._q(this), relation, false, constrain);
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 _attachWithWhereHas(this, Model._q(this), relation, constrain);
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 _attachWhereBelongsTo(this, Model._q(this), parent, relation);
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`/`orWhere` constraints + optional alias override.
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[ctor.primaryKey];
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.#original = this._currentAttrs();
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(this.#original)])) {
1031
- if (!_attrEqual(next[k], this.#original[k]))
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.#original = next;
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
- delete this[k];
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[k] = v;
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 { ...this.#original };
1290
- return this.#original[key];
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
- for (const k of new Set([...Object.keys(current), ...Object.keys(this.#original)])) {
1301
- if (!_attrEqual(current[k], this.#original[k]))
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['deletedAt'];
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[idCol];
1360
- const typeVal = this[typeCol];
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 _morphParentQuery(this, ctor, def, name);
1459
+ return morphParentQuery(this, ctor, def, name);
1390
1460
  }
1391
1461
  if (def.type === 'morphOne') {
1392
- return _morphParentQuery(this, ctor, def, name);
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 = _resolveBelongsToManyMeta(ctor, Related, def);
1398
- const parentVal = this[meta.parentKey];
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 _belongsToManyDeferredQb(Related, def, meta, parentVal);
1472
+ return belongsToManyDeferredQb(Related, def, meta, parentVal);
1403
1473
  }
1404
1474
  if (def.type === 'morphToMany') {
1405
- const meta = _resolveMorphToManyMeta(ctor, Related, def);
1406
- const parentVal = this[meta.parentKey];
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 _morphToManyDeferredQb(Related, def, meta, parentVal);
1480
+ return morphToManyDeferredQb(Related, def, meta, parentVal);
1411
1481
  }
1412
1482
  if (def.type === 'morphedByMany') {
1413
- const meta = _resolveMorphedByManyMeta(ctor, Related, def);
1414
- const parentVal = this[meta.parentKey];
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 _morphedByManyDeferredQb(Related, def, meta, parentVal);
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[localCol];
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[localCol];
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 = _resolveBelongsToManyMeta(ctor, Related, def);
1469
- const parentVal = parent[meta.parentKey];
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
- _installBelongsToManyMethods(ctor);
1476
- return _makeBelongsToManyAccessor(ctor, Related, def, parentVal);
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 = _resolveMorphToManyMeta(ctor, Related, def);
1503
- const parentVal = parent[meta.parentKey];
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
- _installMorphPivotMethods(ctor);
1508
- return _makeMorphToManyAccessor(ctor, Related, def, parentVal);
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 = _resolveMorphedByManyMeta(ctor, Related, def);
1536
- const parentVal = parent[meta.parentKey];
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
- _installMorphPivotMethods(ctor);
1541
- return _makeMorphedByManyAccessor(ctor, Related, def, parentVal);
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[ctor.primaryKey];
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