@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.
- package/README.md +53 -0
- package/boost/skills/orm-models/SKILL.md +24 -251
- package/boost/skills/orm-models/rules/crud-and-observers.md +130 -0
- package/boost/skills/orm-models/rules/defining-models.md +137 -0
- package/boost/skills/orm-models/rules/factories.md +73 -0
- package/boost/skills/orm-models/rules/querying.md +117 -0
- package/boost/skills/orm-models/rules/resources.md +111 -0
- package/dist/aggregate.d.ts +16 -7
- package/dist/aggregate.d.ts.map +1 -1
- package/dist/aggregate.js +27 -29
- package/dist/aggregate.js.map +1 -1
- package/dist/index.d.ts +41 -124
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +55 -892
- package/dist/index.js.map +1 -1
- package/dist/relations/pivot-accessors.d.ts +127 -0
- package/dist/relations/pivot-accessors.d.ts.map +1 -0
- package/dist/relations/pivot-accessors.js +211 -0
- package/dist/relations/pivot-accessors.js.map +1 -0
- package/dist/relations/pivot-deferred.d.ts +13 -0
- package/dist/relations/pivot-deferred.d.ts.map +1 -0
- package/dist/relations/pivot-deferred.js +210 -0
- package/dist/relations/pivot-deferred.js.map +1 -0
- package/dist/relations/pivot-meta.d.ts +50 -0
- package/dist/relations/pivot-meta.d.ts.map +1 -0
- package/dist/relations/pivot-meta.js +34 -0
- package/dist/relations/pivot-meta.js.map +1 -0
- package/dist/relations/where-has.d.ts +53 -0
- package/dist/relations/where-has.d.ts.map +1 -0
- package/dist/relations/where-has.js +239 -0
- package/dist/relations/where-has.js.map +1 -0
- package/dist/utils.d.ts +35 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +63 -0
- package/dist/utils.js.map +1 -0
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { castGet, castSet } from './cast.js';
|
|
2
2
|
import { AGGREGATES_SYMBOL, aggregateKeysOf, loadCountOrExists, loadMissingRelations, loadNumericAggregate, normalizeWithCount, normalizeWithExists, normalizeWithNumericAggregate, } from './aggregate.js';
|
|
3
|
+
import { attrEqual, readField, writeField, deleteField } from './utils.js';
|
|
4
|
+
import { resolveBelongsToManyMeta, resolveMorphToManyMeta, resolveMorphedByManyMeta, } from './relations/pivot-meta.js';
|
|
5
|
+
import { attachWhereHas, attachWithWhereHas, attachWhereBelongsTo, } from './relations/where-has.js';
|
|
6
|
+
import { morphParentQuery, belongsToManyDeferredQb, morphToManyDeferredQb, morphedByManyDeferredQb, } from './relations/pivot-deferred.js';
|
|
7
|
+
import { makeBelongsToManyAccessor, makeMorphToManyAccessor, makeMorphedByManyAccessor, installBelongsToManyMethods, installMorphPivotMethods, } from './relations/pivot-accessors.js';
|
|
3
8
|
export { vector } from './cast.js';
|
|
4
9
|
export { VectorDimensionMismatchError, VectorStorageUnsupportedError, MissingEmbedderError, } from './vector-errors.js';
|
|
5
10
|
export { Attribute } from './attribute.js';
|
|
@@ -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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
1360
|
-
const typeVal = this
|
|
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
|
|
1392
|
+
return morphParentQuery(this, ctor, def, name);
|
|
1390
1393
|
}
|
|
1391
1394
|
if (def.type === 'morphOne') {
|
|
1392
|
-
return
|
|
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 =
|
|
1398
|
-
const parentVal = this
|
|
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
|
|
1405
|
+
return belongsToManyDeferredQb(Related, def, meta, parentVal);
|
|
1403
1406
|
}
|
|
1404
1407
|
if (def.type === 'morphToMany') {
|
|
1405
|
-
const meta =
|
|
1406
|
-
const parentVal = this
|
|
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
|
|
1413
|
+
return morphToManyDeferredQb(Related, def, meta, parentVal);
|
|
1411
1414
|
}
|
|
1412
1415
|
if (def.type === 'morphedByMany') {
|
|
1413
|
-
const meta =
|
|
1414
|
-
const parentVal = this
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
1469
|
-
const parentVal = parent
|
|
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
|
-
|
|
1476
|
-
return
|
|
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 =
|
|
1503
|
-
const parentVal = parent
|
|
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
|
-
|
|
1508
|
-
return
|
|
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 =
|
|
1536
|
-
const parentVal = parent
|
|
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
|
-
|
|
1541
|
-
return
|
|
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
|
|
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
|