@rudderjs/orm 1.9.0 → 1.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +53 -0
  2. package/boost/skills/orm-models/SKILL.md +24 -251
  3. package/boost/skills/orm-models/rules/crud-and-observers.md +130 -0
  4. package/boost/skills/orm-models/rules/defining-models.md +137 -0
  5. package/boost/skills/orm-models/rules/factories.md +73 -0
  6. package/boost/skills/orm-models/rules/querying.md +117 -0
  7. package/boost/skills/orm-models/rules/resources.md +111 -0
  8. package/dist/aggregate.d.ts +16 -7
  9. package/dist/aggregate.d.ts.map +1 -1
  10. package/dist/aggregate.js +27 -29
  11. package/dist/aggregate.js.map +1 -1
  12. package/dist/index.d.ts +41 -124
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +55 -892
  15. package/dist/index.js.map +1 -1
  16. package/dist/relations/pivot-accessors.d.ts +127 -0
  17. package/dist/relations/pivot-accessors.d.ts.map +1 -0
  18. package/dist/relations/pivot-accessors.js +211 -0
  19. package/dist/relations/pivot-accessors.js.map +1 -0
  20. package/dist/relations/pivot-deferred.d.ts +13 -0
  21. package/dist/relations/pivot-deferred.d.ts.map +1 -0
  22. package/dist/relations/pivot-deferred.js +210 -0
  23. package/dist/relations/pivot-deferred.js.map +1 -0
  24. package/dist/relations/pivot-meta.d.ts +50 -0
  25. package/dist/relations/pivot-meta.d.ts.map +1 -0
  26. package/dist/relations/pivot-meta.js +34 -0
  27. package/dist/relations/pivot-meta.js.map +1 -0
  28. package/dist/relations/where-has.d.ts +53 -0
  29. package/dist/relations/where-has.d.ts.map +1 -0
  30. package/dist/relations/where-has.js +239 -0
  31. package/dist/relations/where-has.js.map +1 -0
  32. package/dist/utils.d.ts +35 -0
  33. package/dist/utils.d.ts.map +1 -0
  34. package/dist/utils.js +63 -0
  35. package/dist/utils.js.map +1 -0
  36. 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';
@@ -42,8 +47,8 @@ export class ModelRegistry {
42
47
  if (!name || this.models.has(name))
43
48
  return;
44
49
  this.models.set(name, ModelClass);
45
- _installBelongsToManyMethods(ModelClass);
46
- _installMorphPivotMethods(ModelClass);
50
+ installBelongsToManyMethods(ModelClass);
51
+ installMorphPivotMethods(ModelClass);
47
52
  for (const listener of this.listeners)
48
53
  listener(name, ModelClass);
49
54
  }
@@ -389,10 +394,8 @@ export class Model {
389
394
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
390
395
  static async _fireEvent(event, ...args) {
391
396
  if (Object.prototype.hasOwnProperty.call(this, '_eventsMuted') && this._eventsMuted) {
392
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
393
397
  return args[0];
394
398
  }
395
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
396
399
  let result = args[0];
397
400
  const observers = Object.prototype.hasOwnProperty.call(this, '_observers') ? this._observers : [];
398
401
  for (const obs of observers) {
@@ -409,7 +412,6 @@ export class Model {
409
412
  ? (this._listeners.get(event) ?? [])
410
413
  : [];
411
414
  for (const fn of listeners) {
412
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
413
415
  const ret = await fn(...args);
414
416
  if (ret === false)
415
417
  return false;
@@ -476,6 +478,10 @@ export class Model {
476
478
  aggregateAliases.add(r.alias);
477
479
  qb.withAggregate(reqs);
478
480
  };
481
+ // The Proxy's `get` handler implements the extra `HydratingQueryBuilder`
482
+ // methods at runtime (whereHas / withCount / etc.). TS can't verify that
483
+ // through the Proxy constructor, so we assert here — the assertion is
484
+ // contained to this one site instead of leaking to every call site.
479
485
  const proxy = new Proxy(qb, {
480
486
  get(target, prop, receiver) {
481
487
  // ORM-side chainables that don't exist on the adapter QB itself —
@@ -483,25 +489,25 @@ export class Model {
483
489
  // are added by this proxy, not by the adapter.
484
490
  if (prop === 'whereHas') {
485
491
  return (relation, constrain) => {
486
- _attachWhereHas(ModelClass, target, relation, true, constrain);
492
+ attachWhereHas(ModelClass, target, relation, true, constrain);
487
493
  return proxy;
488
494
  };
489
495
  }
490
496
  if (prop === 'whereDoesntHave') {
491
497
  return (relation, constrain) => {
492
- _attachWhereHas(ModelClass, target, relation, false, constrain);
498
+ attachWhereHas(ModelClass, target, relation, false, constrain);
493
499
  return proxy;
494
500
  };
495
501
  }
496
502
  if (prop === 'withWhereHas') {
497
503
  return (relation, constrain) => {
498
- _attachWithWhereHas(ModelClass, target, relation, constrain);
504
+ attachWithWhereHas(ModelClass, target, relation, constrain);
499
505
  return proxy;
500
506
  };
501
507
  }
502
508
  if (prop === 'whereBelongsTo') {
503
509
  return (parent, relation) => {
504
- _attachWhereBelongsTo(ModelClass, target, parent, relation);
510
+ attachWhereBelongsTo(ModelClass, target, parent, relation);
505
511
  return proxy;
506
512
  };
507
513
  }
@@ -565,7 +571,6 @@ export class Model {
565
571
  // Chainable methods (where/orderBy/with/...) typically return `target` —
566
572
  // re-wrap so `Model.where('a', 1).first()` keeps hydrating.
567
573
  return (...args) => {
568
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
569
574
  const result = value.apply(target, args);
570
575
  return result === target ? proxy : result;
571
576
  };
@@ -606,7 +611,6 @@ export class Model {
606
611
  excludedScopes.add(name);
607
612
  return enhance(buildScoped());
608
613
  };
609
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
610
614
  return enhanced;
611
615
  };
612
616
  return enhance(buildScoped());
@@ -691,7 +695,7 @@ export class Model {
691
695
  * await Post.whereHas('comments').get() // morphMany — adds the {morph}Type filter automatically
692
696
  */
693
697
  static whereHas(relation, constrain) {
694
- return _attachWhereHas(this, Model._q(this), relation, true, constrain);
698
+ return attachWhereHas(this, Model._q(this), relation, true, constrain);
695
699
  }
696
700
  /**
697
701
  * Inverse of {@link Model.whereHas} — rows whose named relation has zero
@@ -700,7 +704,7 @@ export class Model {
700
704
  * matching the constraint" rather than "no children at all".
701
705
  */
702
706
  static whereDoesntHave(relation, constrain) {
703
- return _attachWhereHas(this, Model._q(this), relation, false, constrain);
707
+ return attachWhereHas(this, Model._q(this), relation, false, constrain);
704
708
  }
705
709
  /**
706
710
  * `whereHas` + `with` — filter by the relation predicate AND eager-load the
@@ -710,7 +714,7 @@ export class Model {
710
714
  * the parent was matched on a narrower predicate.
711
715
  */
712
716
  static withWhereHas(relation, constrain) {
713
- return _attachWithWhereHas(this, Model._q(this), relation, constrain);
717
+ return attachWithWhereHas(this, Model._q(this), relation, constrain);
714
718
  }
715
719
  /**
716
720
  * Filter rows whose `belongsTo` relation points at `parent`. Sugar for
@@ -724,7 +728,7 @@ export class Model {
724
728
  * await Comment.whereBelongsTo(post, 'post').get() // explicit relation name when ambiguous
725
729
  */
726
730
  static whereBelongsTo(parent, relation) {
727
- return _attachWhereBelongsTo(this, Model._q(this), parent, relation);
731
+ return attachWhereBelongsTo(this, Model._q(this), parent, relation);
728
732
  }
729
733
  /**
730
734
  * Aggregate eager-loading — count related rows alongside the parent in a
@@ -736,7 +740,7 @@ export class Model {
736
740
  * - `withCount('posts')` — single relation, no constraint.
737
741
  * - `withCount(['posts', 'comments'])` — multiple, no constraints.
738
742
  * - `withCount({ posts: q => q.where('published', true).as('publishedPosts') })`
739
- * — map form with `where`/`orWhere` constraints + optional alias override.
743
+ * — map form with `where` constraints + optional alias override.
740
744
  *
741
745
  * Closes the N+1 footgun for hot list pages. For a single instance use
742
746
  * `instance.loadCount('posts')` instead.
@@ -969,7 +973,7 @@ export class Model {
969
973
  /** @internal — pull the primary-key value from this instance, or `undefined` if unset. */
970
974
  _getKey() {
971
975
  const ctor = this.constructor;
972
- const value = this[ctor.primaryKey];
976
+ const value = readField(this, ctor.primaryKey);
973
977
  if (value === undefined || value === null)
974
978
  return undefined;
975
979
  return value;
@@ -1028,7 +1032,7 @@ export class Model {
1028
1032
  const next = this._currentAttrs();
1029
1033
  const diff = {};
1030
1034
  for (const k of new Set([...Object.keys(next), ...Object.keys(this.#original)])) {
1031
- if (!_attrEqual(next[k], this.#original[k]))
1035
+ if (!attrEqual(next[k], this.#original[k]))
1032
1036
  diff[k] = next[k];
1033
1037
  }
1034
1038
  this.#changes = diff;
@@ -1073,7 +1077,7 @@ export class Model {
1073
1077
  throw new ModelNotFoundError(ctor.name, id);
1074
1078
  for (const k of Object.keys(this)) {
1075
1079
  if (!k.startsWith('_'))
1076
- delete this[k];
1080
+ deleteField(this, k);
1077
1081
  }
1078
1082
  Object.assign(this, fresh);
1079
1083
  this.#changes = {};
@@ -1096,8 +1100,7 @@ export class Model {
1096
1100
  }
1097
1101
  await ctor.delete(id);
1098
1102
  if (ctor.softDeletes) {
1099
- ;
1100
- this.deletedAt = new Date();
1103
+ writeField(this, 'deletedAt', new Date());
1101
1104
  this._syncOriginal();
1102
1105
  }
1103
1106
  }
@@ -1258,7 +1261,7 @@ export class Model {
1258
1261
  for (const [k, v] of Object.entries(this)) {
1259
1262
  if (k.startsWith('_') || exclude.has(k) || v === undefined)
1260
1263
  continue;
1261
- clone[k] = v;
1264
+ writeField(clone, k, v);
1262
1265
  }
1263
1266
  return clone;
1264
1267
  }
@@ -1298,7 +1301,7 @@ export class Model {
1298
1301
  const out = {};
1299
1302
  const current = this._currentAttrs();
1300
1303
  for (const k of new Set([...Object.keys(current), ...Object.keys(this.#original)])) {
1301
- if (!_attrEqual(current[k], this.#original[k]))
1304
+ if (!attrEqual(current[k], this.#original[k]))
1302
1305
  out[k] = current[k];
1303
1306
  }
1304
1307
  return out;
@@ -1324,7 +1327,7 @@ export class Model {
1324
1327
  }
1325
1328
  /** True when this instance has been soft-deleted (its `deletedAt` is set). */
1326
1329
  trashed() {
1327
- const v = this['deletedAt'];
1330
+ const v = readField(this, 'deletedAt');
1328
1331
  return v !== null && v !== undefined;
1329
1332
  }
1330
1333
  // ── Relations ──────────────────────────────────────────
@@ -1356,8 +1359,8 @@ export class Model {
1356
1359
  if (def.type === 'morphTo') {
1357
1360
  const idCol = `${def.morphName}Id`;
1358
1361
  const typeCol = `${def.morphName}Type`;
1359
- const idVal = this[idCol];
1360
- const typeVal = this[typeCol];
1362
+ const idVal = readField(this, idCol);
1363
+ const typeVal = readField(this, typeCol);
1361
1364
  if (idVal === undefined || idVal === null || typeVal === undefined || typeVal === null) {
1362
1365
  throw new Error(`[RudderJS ORM] Cannot resolve morphTo "${name}" on ${ctor.name} — ${idCol}/${typeCol} unset.`);
1363
1366
  }
@@ -1386,42 +1389,42 @@ export class Model {
1386
1389
  // expectation (`first()` vs `get()`). Split into two ifs so TS can narrow
1387
1390
  // each tag literal out of the union for the fall-through branches below.
1388
1391
  if (def.type === 'morphMany') {
1389
- return _morphParentQuery(this, ctor, def, name);
1392
+ return morphParentQuery(this, ctor, def, name);
1390
1393
  }
1391
1394
  if (def.type === 'morphOne') {
1392
- return _morphParentQuery(this, ctor, def, name);
1395
+ return morphParentQuery(this, ctor, def, name);
1393
1396
  }
1394
1397
  const Related = def.model();
1395
1398
  const fkCamel = (s) => s.charAt(0).toLowerCase() + s.slice(1);
1396
1399
  if (def.type === 'belongsToMany') {
1397
- const meta = _resolveBelongsToManyMeta(ctor, Related, def);
1398
- const parentVal = this[meta.parentKey];
1400
+ const meta = resolveBelongsToManyMeta(ctor, Related, def);
1401
+ const parentVal = readField(this, meta.parentKey);
1399
1402
  if (parentVal === undefined || parentVal === null) {
1400
1403
  throw new Error(`[RudderJS ORM] Cannot resolve "${name}" on ${ctor.name} — ${meta.parentKey} is unset.`);
1401
1404
  }
1402
- return _belongsToManyDeferredQb(Related, def, meta, parentVal);
1405
+ return belongsToManyDeferredQb(Related, def, meta, parentVal);
1403
1406
  }
1404
1407
  if (def.type === 'morphToMany') {
1405
- const meta = _resolveMorphToManyMeta(ctor, Related, def);
1406
- const parentVal = this[meta.parentKey];
1408
+ const meta = resolveMorphToManyMeta(ctor, Related, def);
1409
+ const parentVal = readField(this, meta.parentKey);
1407
1410
  if (parentVal === undefined || parentVal === null) {
1408
1411
  throw new Error(`[RudderJS ORM] Cannot resolve "${name}" on ${ctor.name} — ${meta.parentKey} is unset.`);
1409
1412
  }
1410
- return _morphToManyDeferredQb(Related, def, meta, parentVal);
1413
+ return morphToManyDeferredQb(Related, def, meta, parentVal);
1411
1414
  }
1412
1415
  if (def.type === 'morphedByMany') {
1413
- const meta = _resolveMorphedByManyMeta(ctor, Related, def);
1414
- const parentVal = this[meta.parentKey];
1416
+ const meta = resolveMorphedByManyMeta(ctor, Related, def);
1417
+ const parentVal = readField(this, meta.parentKey);
1415
1418
  if (parentVal === undefined || parentVal === null) {
1416
1419
  throw new Error(`[RudderJS ORM] Cannot resolve "${name}" on ${ctor.name} — ${meta.parentKey} is unset.`);
1417
1420
  }
1418
- return _morphedByManyDeferredQb(Related, def, meta, parentVal);
1421
+ return morphedByManyDeferredQb(Related, def, meta, parentVal);
1419
1422
  }
1420
1423
  if (def.type === 'belongsTo') {
1421
1424
  // This model holds the FK; query the related model's PK.
1422
1425
  const fk = def.foreignKey ?? `${fkCamel(Related.name)}Id`;
1423
1426
  const localCol = def.localKey ?? fk;
1424
- const localVal = this[localCol];
1427
+ const localVal = readField(this, localCol);
1425
1428
  if (localVal === undefined || localVal === null) {
1426
1429
  throw new Error(`[RudderJS ORM] Cannot resolve belongsTo "${name}" — ${ctor.name}.${localCol} is unset.`);
1427
1430
  }
@@ -1430,7 +1433,7 @@ export class Model {
1430
1433
  // hasOne / hasMany — related model holds the FK pointing back to us.
1431
1434
  const fk = def.foreignKey ?? `${fkCamel(ctor.name)}Id`;
1432
1435
  const localCol = def.localKey ?? ctor.primaryKey;
1433
- const localVal = this[localCol];
1436
+ const localVal = readField(this, localCol);
1434
1437
  if (localVal === undefined || localVal === null) {
1435
1438
  throw new Error(`[RudderJS ORM] Cannot resolve "${name}" on ${ctor.name} — ${localCol} is unset.`);
1436
1439
  }
@@ -1465,15 +1468,15 @@ export class Model {
1465
1468
  throw new Error(`[RudderJS ORM] Relation "${name}" on ${ctor.name} is "${def.type}", not "belongsToMany".`);
1466
1469
  }
1467
1470
  const Related = def.model();
1468
- const meta = _resolveBelongsToManyMeta(ctor, Related, def);
1469
- const parentVal = parent[meta.parentKey];
1471
+ const meta = resolveBelongsToManyMeta(ctor, Related, def);
1472
+ const parentVal = readField(parent, meta.parentKey);
1470
1473
  if (parentVal === undefined || parentVal === null) {
1471
1474
  throw new Error(`[RudderJS ORM] Cannot use belongsToMany "${name}" on ${ctor.name} — ${meta.parentKey} is unset.`);
1472
1475
  }
1473
1476
  // Belt-and-suspenders: make sure the auto-method is installed even
1474
1477
  // for instances constructed before any query against this class.
1475
- _installBelongsToManyMethods(ctor);
1476
- return _makeBelongsToManyAccessor(ctor, Related, def, parentVal);
1478
+ installBelongsToManyMethods(ctor);
1479
+ return makeBelongsToManyAccessor(ctor, Related, def, parentVal);
1477
1480
  }
1478
1481
  /**
1479
1482
  * Pivot-mutation accessor for a `morphToMany` relation (the polymorphic
@@ -1499,13 +1502,13 @@ export class Model {
1499
1502
  throw new Error(`[RudderJS ORM] Relation "${name}" on ${ctor.name} is "${def.type}", not "morphToMany".`);
1500
1503
  }
1501
1504
  const Related = def.model();
1502
- const meta = _resolveMorphToManyMeta(ctor, Related, def);
1503
- const parentVal = parent[meta.parentKey];
1505
+ const meta = resolveMorphToManyMeta(ctor, Related, def);
1506
+ const parentVal = readField(parent, meta.parentKey);
1504
1507
  if (parentVal === undefined || parentVal === null) {
1505
1508
  throw new Error(`[RudderJS ORM] Cannot use morphToMany "${name}" on ${ctor.name} — ${meta.parentKey} is unset.`);
1506
1509
  }
1507
- _installMorphPivotMethods(ctor);
1508
- return _makeMorphToManyAccessor(ctor, Related, def, parentVal);
1510
+ installMorphPivotMethods(ctor);
1511
+ return makeMorphToManyAccessor(ctor, Related, def, parentVal);
1509
1512
  }
1510
1513
  /**
1511
1514
  * Pivot-mutation accessor for a `morphedByMany` relation (the inverse
@@ -1532,13 +1535,13 @@ export class Model {
1532
1535
  throw new Error(`[RudderJS ORM] Relation "${name}" on ${ctor.name} is "${def.type}", not "morphedByMany".`);
1533
1536
  }
1534
1537
  const Related = def.model();
1535
- const meta = _resolveMorphedByManyMeta(ctor, Related, def);
1536
- const parentVal = parent[meta.parentKey];
1538
+ const meta = resolveMorphedByManyMeta(ctor, Related, def);
1539
+ const parentVal = readField(parent, meta.parentKey);
1537
1540
  if (parentVal === undefined || parentVal === null) {
1538
1541
  throw new Error(`[RudderJS ORM] Cannot use morphedByMany "${name}" on ${ctor.name} — ${meta.parentKey} is unset.`);
1539
1542
  }
1540
- _installMorphPivotMethods(ctor);
1541
- return _makeMorphedByManyAccessor(ctor, Related, def, parentVal);
1543
+ installMorphPivotMethods(ctor);
1544
+ return makeMorphedByManyAccessor(ctor, Related, def, parentVal);
1542
1545
  }
1543
1546
  /**
1544
1547
  * Build the `{name}Id + {name}Type` payload for a polymorphic write.
@@ -1555,7 +1558,7 @@ export class Model {
1555
1558
  */
1556
1559
  static morph(name, parent) {
1557
1560
  const ctor = parent.constructor;
1558
- const pk = parent[ctor.primaryKey];
1561
+ const pk = readField(parent, ctor.primaryKey);
1559
1562
  if (pk === undefined || pk === null) {
1560
1563
  throw new Error(`[RudderJS ORM] Model.morph("${name}", parent): parent.${ctor.primaryKey} is unset — save the parent first.`);
1561
1564
  }
@@ -1679,845 +1682,6 @@ export class Model {
1679
1682
  return Object.fromEntries(Object.entries(result).filter(([k]) => !effectiveHidden.includes(k)));
1680
1683
  }
1681
1684
  }
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
1685
  // ─── Compile-time contract check ───────────────────────────
2522
1686
  // Asserts that `Model`'s static surface conforms to the `ModelLike`
2523
1687
  // contract from `@rudderjs/contracts`. Downstream tools (admin panels
@@ -2525,7 +1689,6 @@ function _installMorphPivotMethods(ModelClass) {
2525
1689
  // `ModelLike` so they don't need to depend on `@rudderjs/orm` directly.
2526
1690
  // This line will fail to compile if a future change to Model breaks
2527
1691
  // that contract.
2528
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
2529
1692
  const _modelSatisfiesContract = Model;
2530
1693
  void _modelSatisfiesContract;
2531
1694
  //# sourceMappingURL=index.js.map