@getstrata/core 0.5.97 → 0.5.99

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.
@@ -2,6 +2,26 @@
2
2
  // ../../src/core/database/model.ts
3
3
  import { NotFoundError } from "@getstrata/core/errors/http";
4
4
 
5
+ // ../../src/core/database/inflection.ts
6
+ function singularize(word) {
7
+ if (word.endsWith("ies") && word.length > 3) {
8
+ return `${word.slice(0, -3)}y`;
9
+ }
10
+ if (/(ses|xes|zes|ches|shes)$/i.test(word)) {
11
+ return word.slice(0, -2);
12
+ }
13
+ if (word.endsWith("s") && !word.endsWith("ss")) {
14
+ return word.slice(0, -1);
15
+ }
16
+ return word;
17
+ }
18
+ function foreignKeyFromTable(tableName) {
19
+ return `${singularize(tableName)}_id`;
20
+ }
21
+ function pivotTableName(leftTable, rightTable) {
22
+ return [singularize(leftTable), singularize(rightTable)].sort().join("_");
23
+ }
24
+
5
25
  // ../../src/core/database/query.ts
6
26
  function quoteIdentifier(identifier) {
7
27
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
@@ -77,7 +97,7 @@ function buildOperatorClauses(column, operator, params) {
77
97
  clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
78
98
  }
79
99
  if (operator.ilike !== undefined) {
80
- clauses.push(`${column} ILIKE ${pushParam(params, `%${operator.ilike}%`)}`);
100
+ clauses.push(`${column} ILIKE ${pushParam(params, operator.ilike)}`);
81
101
  }
82
102
  if (operator.tsMatch !== undefined) {
83
103
  clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
@@ -115,7 +135,16 @@ function buildWhereClause(tableName, where = {}) {
115
135
  params
116
136
  };
117
137
  }
138
+ function remapExistsSql(sql, existsParams, params) {
139
+ const offset = params.length;
140
+ params.push(...existsParams);
141
+ return sql.replace(/\$(\d+)/g, (_match, index) => `$${offset + Number(index)}`);
142
+ }
118
143
  function buildWhereNodeClause(tableName, node, params) {
144
+ if ("exists" in node) {
145
+ const body = remapExistsSql(node.exists.sql, node.exists.params, params);
146
+ return `${node.exists.not ? "NOT " : ""}EXISTS (${body})`;
147
+ }
119
148
  if ("where" in node) {
120
149
  return appendWhereParts(tableName, node.where, params);
121
150
  }
@@ -416,6 +445,518 @@ function buildDeleteByIdQuery(table, id) {
416
445
  };
417
446
  }
418
447
 
448
+ // ../../src/core/database/relationQuery.ts
449
+ function asWhere(where) {
450
+ return where;
451
+ }
452
+ function ownerId(owner, ownerKey) {
453
+ if (typeof owner.get === "function") {
454
+ return owner.get(ownerKey);
455
+ }
456
+ if (owner && typeof owner === "object" && "id" in owner && owner.id !== undefined) {
457
+ return owner.id;
458
+ }
459
+ throw new Error("belongsTo.associate() requires a related model or { id }.");
460
+ }
461
+ function thenGet(get, onfulfilled, onrejected) {
462
+ return get().then(onfulfilled ?? undefined, onrejected ?? undefined);
463
+ }
464
+
465
+ class HasManyRelationQuery {
466
+ parent;
467
+ related;
468
+ relation;
469
+ kind = "hasMany";
470
+ extraWhere = {};
471
+ extraOptions = {};
472
+ constructor(parent, related, relation) {
473
+ this.parent = parent;
474
+ this.related = related;
475
+ this.relation = relation;
476
+ }
477
+ where(where) {
478
+ this.extraWhere = { ...this.extraWhere, ...where };
479
+ return this;
480
+ }
481
+ orderBy(orderBy) {
482
+ this.extraOptions = { ...this.extraOptions, orderBy };
483
+ return this;
484
+ }
485
+ limit(limit) {
486
+ this.extraOptions = { ...this.extraOptions, limit };
487
+ return this;
488
+ }
489
+ applyEagerLoad(query, alias) {
490
+ query.withHasMany(alias, this.relation, this.related.repository(), this.extraOptions);
491
+ }
492
+ hydrateEager(row, alias) {
493
+ const value = row[alias] ?? [];
494
+ const rows = Array.isArray(value) ? value : [];
495
+ return rows.map((item) => this.related.newFromRecord(item));
496
+ }
497
+ toExistsClause(parentTable) {
498
+ const childTable = this.related.repository().getTable().name;
499
+ const extra = buildAdvancedWhereClause(childTable, this.extraWhere, [], []);
500
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
501
+ const sql = `SELECT 1 FROM ${quoteIdentifier(childTable)} WHERE ${qualifyColumn(childTable, this.relation.foreignKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
502
+ return { sql, params: extra.params };
503
+ }
504
+ scopedQuery() {
505
+ const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
506
+ let query = repository.query(asWhere({
507
+ [this.relation.foreignKey]: this.parent.get(this.relation.localKey),
508
+ ...this.extraWhere
509
+ }));
510
+ if (this.extraOptions.orderBy) {
511
+ query = query.orderBy(this.extraOptions.orderBy);
512
+ }
513
+ if (this.extraOptions.limit !== undefined) {
514
+ query = query.limit(this.extraOptions.limit);
515
+ }
516
+ return query;
517
+ }
518
+ async get() {
519
+ const rows = await this.scopedQuery().get();
520
+ return rows.map((row) => this.related.newFromRecord(row));
521
+ }
522
+ async first() {
523
+ const rows = await this.limit(1).get();
524
+ return rows[0] ?? null;
525
+ }
526
+ async count() {
527
+ return this.scopedQuery().count();
528
+ }
529
+ then(onfulfilled, onrejected) {
530
+ return thenGet(() => this.get(), onfulfilled, onrejected);
531
+ }
532
+ async create(attributes = {}) {
533
+ return this.related.create(attributes, {
534
+ [this.relation.foreignKey]: this.parent.get(this.relation.localKey)
535
+ });
536
+ }
537
+ async save(related) {
538
+ const forced = {
539
+ [this.relation.foreignKey]: this.parent.get(this.relation.localKey)
540
+ };
541
+ const savable = related;
542
+ if (typeof savable.save === "function") {
543
+ savable.mergeAttributes?.(forced);
544
+ await savable.save();
545
+ return related;
546
+ }
547
+ return this.create(related);
548
+ }
549
+ async createMany(records) {
550
+ const created = [];
551
+ for (const attributes of records) {
552
+ created.push(await this.create(attributes));
553
+ }
554
+ return created;
555
+ }
556
+ }
557
+
558
+ class HasOneRelationQuery {
559
+ relation;
560
+ kind = "hasOne";
561
+ inner;
562
+ constructor(parent, related, relation) {
563
+ this.relation = relation;
564
+ this.inner = new HasManyRelationQuery(parent, related, {
565
+ type: "hasMany",
566
+ name: relation.name,
567
+ localKey: relation.localKey,
568
+ foreignKey: relation.foreignKey
569
+ });
570
+ }
571
+ where(where) {
572
+ this.inner.where(where);
573
+ return this;
574
+ }
575
+ orderBy(orderBy) {
576
+ this.inner.orderBy(orderBy);
577
+ return this;
578
+ }
579
+ applyEagerLoad(query, alias) {
580
+ this.inner.limit(1).applyEagerLoad(query, alias);
581
+ }
582
+ hydrateEager(row, alias) {
583
+ const hydrated = this.inner.hydrateEager(row, alias);
584
+ return hydrated[0];
585
+ }
586
+ toExistsClause(parentTable) {
587
+ return this.inner.toExistsClause(parentTable);
588
+ }
589
+ async get() {
590
+ return this.inner.limit(1).first();
591
+ }
592
+ async first() {
593
+ return this.get();
594
+ }
595
+ async count() {
596
+ return this.inner.count();
597
+ }
598
+ then(onfulfilled, onrejected) {
599
+ return thenGet(() => this.get(), onfulfilled, onrejected);
600
+ }
601
+ async create(attributes = {}) {
602
+ return this.inner.create(attributes);
603
+ }
604
+ async save(related) {
605
+ return this.inner.save(related);
606
+ }
607
+ }
608
+
609
+ class BelongsToRelationQuery {
610
+ parent;
611
+ related;
612
+ relation;
613
+ kind = "belongsTo";
614
+ extraWhere = {};
615
+ extraOptions = {};
616
+ constructor(parent, related, relation) {
617
+ this.parent = parent;
618
+ this.related = related;
619
+ this.relation = relation;
620
+ }
621
+ where(where) {
622
+ this.extraWhere = { ...this.extraWhere, ...where };
623
+ return this;
624
+ }
625
+ orderBy(orderBy) {
626
+ this.extraOptions = { ...this.extraOptions, orderBy };
627
+ return this;
628
+ }
629
+ applyEagerLoad(query, alias) {
630
+ query.withBelongsTo(alias, this.relation, this.related.repository(), this.extraOptions);
631
+ }
632
+ hydrateEager(row, alias) {
633
+ const value = row[alias];
634
+ return value ? this.related.newFromRecord(value) : value;
635
+ }
636
+ toExistsClause(parentTable) {
637
+ const relatedTable = this.related.repository().getTable().name;
638
+ const extra = buildAdvancedWhereClause(relatedTable, this.extraWhere, [], []);
639
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
640
+ const sql = `SELECT 1 FROM ${quoteIdentifier(relatedTable)} WHERE ${qualifyColumn(relatedTable, this.relation.ownerKey)} = ${qualifyColumn(parentTable, this.relation.foreignKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
641
+ return { sql, params: extra.params };
642
+ }
643
+ async get() {
644
+ const foreign = this.parent.get(this.relation.foreignKey);
645
+ if (foreign === null || foreign === undefined) {
646
+ return null;
647
+ }
648
+ const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
649
+ let query = repository.query(asWhere({ [this.relation.ownerKey]: foreign, ...this.extraWhere }));
650
+ if (this.extraOptions.orderBy) {
651
+ query = query.orderBy(this.extraOptions.orderBy);
652
+ }
653
+ const row = await query.first();
654
+ return row ? this.related.newFromRecord(row) : null;
655
+ }
656
+ async first() {
657
+ return this.get();
658
+ }
659
+ then(onfulfilled, onrejected) {
660
+ return thenGet(() => this.get(), onfulfilled, onrejected);
661
+ }
662
+ async associate(owner) {
663
+ await this.parent.getRepository().updateById(this.parent.id, {
664
+ [this.relation.foreignKey]: ownerId(owner, this.relation.ownerKey)
665
+ });
666
+ }
667
+ async dissociate() {
668
+ await this.parent.getRepository().updateById(this.parent.id, {
669
+ [this.relation.foreignKey]: null
670
+ });
671
+ }
672
+ }
673
+
674
+ class BelongsToManyRelationQuery {
675
+ parent;
676
+ related;
677
+ relation;
678
+ kind = "belongsToMany";
679
+ extraWhere = {};
680
+ extraOptions = {};
681
+ pivotValues = {};
682
+ constructor(parent, related, relation) {
683
+ this.parent = parent;
684
+ this.related = related;
685
+ this.relation = relation;
686
+ }
687
+ where(where) {
688
+ this.extraWhere = { ...this.extraWhere, ...where };
689
+ return this;
690
+ }
691
+ orderBy(orderBy) {
692
+ this.extraOptions = { ...this.extraOptions, orderBy };
693
+ return this;
694
+ }
695
+ applyEagerLoad(query, alias) {
696
+ query.withBelongsToMany(alias, this.relation, this.related.repository(), this.extraOptions);
697
+ }
698
+ withPivotValues(values) {
699
+ this.pivotValues = { ...this.pivotValues, ...values };
700
+ return this;
701
+ }
702
+ hydrateEager(row, alias) {
703
+ const value = row[alias] ?? [];
704
+ const rows = Array.isArray(value) ? value : [];
705
+ return rows.map((item) => this.related.newFromRecord(item));
706
+ }
707
+ toExistsClause(parentTable) {
708
+ const relatedTable = this.related.repository().getTable().name;
709
+ const extra = buildAdvancedWhereClause(relatedTable, this.extraWhere, [], []);
710
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
711
+ const sql = `SELECT 1 FROM ${quoteIdentifier(relatedTable)} INNER JOIN ${quoteIdentifier(this.relation.pivotTable)} ON ${qualifyColumn(this.relation.pivotTable, this.relation.relatedPivotKey)} = ${qualifyColumn(relatedTable, this.relation.relatedKey)} WHERE ${qualifyColumn(this.relation.pivotTable, this.relation.foreignPivotKey)} = ${qualifyColumn(parentTable, this.relation.parentKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
712
+ return { sql, params: extra.params };
713
+ }
714
+ connection() {
715
+ return this.parent.getRepository().getConnection();
716
+ }
717
+ async get() {
718
+ const parentId = this.parent.get(this.relation.parentKey);
719
+ const pivotRows = await this.connection().unsafe(`SELECT * FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1`, [parentId]);
720
+ if (pivotRows.length === 0) {
721
+ return [];
722
+ }
723
+ const relatedIds = [
724
+ ...new Set(pivotRows.map((row) => row[this.relation.relatedPivotKey]))
725
+ ];
726
+ const repository = this.related.repository().withConnection(this.connection());
727
+ const rows = await repository.findAll({
728
+ ...this.extraOptions,
729
+ where: asWhere({
730
+ [this.relation.relatedKey]: relatedIds,
731
+ ...this.extraWhere
732
+ })
733
+ });
734
+ return rows.map((row) => this.related.newFromRecord(row));
735
+ }
736
+ async first() {
737
+ const rows = await this.get();
738
+ return rows[0] ?? null;
739
+ }
740
+ async count() {
741
+ const parentId = this.parent.get(this.relation.parentKey);
742
+ const rows = await this.connection().unsafe(`SELECT COUNT(*) AS count FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1`, [parentId]);
743
+ return Number(rows[0]?.count ?? 0);
744
+ }
745
+ then(onfulfilled, onrejected) {
746
+ return thenGet(() => this.get(), onfulfilled, onrejected);
747
+ }
748
+ async attach(ids) {
749
+ const list = Array.isArray(ids) ? ids : [ids];
750
+ const parentId = this.parent.get(this.relation.parentKey);
751
+ const extraKeys = Object.keys(this.pivotValues);
752
+ const extraColumns = extraKeys.length > 0 ? `, ${extraKeys.join(", ")}` : "";
753
+ const extraPlaceholders = extraKeys.map((_, index) => `$${index + 3}`).join(", ");
754
+ const extraValues = extraKeys.map((key) => this.pivotValues[key]);
755
+ for (const id of list) {
756
+ await this.connection().unsafe(extraKeys.length > 0 ? `INSERT INTO ${this.relation.pivotTable} (${String(this.relation.foreignPivotKey)}, ${String(this.relation.relatedPivotKey)}${extraColumns}) VALUES ($1, $2, ${extraPlaceholders})` : `INSERT INTO ${this.relation.pivotTable} (${String(this.relation.foreignPivotKey)}, ${String(this.relation.relatedPivotKey)}) VALUES ($1, $2)`, [parentId, id, ...extraValues]);
757
+ }
758
+ }
759
+ async toggle(ids) {
760
+ const list = Array.isArray(ids) ? ids : [ids];
761
+ const parentId = this.parent.get(this.relation.parentKey);
762
+ for (const id of list) {
763
+ const existing = await this.connection().unsafe(`SELECT 1 FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1 AND ${String(this.relation.relatedPivotKey)} = $2 LIMIT 1`, [parentId, id]);
764
+ if (existing.length > 0) {
765
+ await this.detach(id);
766
+ } else {
767
+ await this.attach(id);
768
+ }
769
+ }
770
+ }
771
+ async detach(ids) {
772
+ const parentId = this.parent.get(this.relation.parentKey);
773
+ if (ids === undefined) {
774
+ await this.connection().unsafe(`DELETE FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1`, [parentId]);
775
+ return;
776
+ }
777
+ const list = Array.isArray(ids) ? ids : [ids];
778
+ await this.connection().unsafe(`DELETE FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1 AND ${String(this.relation.relatedPivotKey)} = ANY($2)`, [parentId, list]);
779
+ }
780
+ async sync(ids) {
781
+ await this.detach();
782
+ if (ids.length > 0) {
783
+ await this.attach(ids);
784
+ }
785
+ }
786
+ async create(attributes = {}) {
787
+ const related = await this.related.create(attributes);
788
+ await this.attach(related.id);
789
+ return related;
790
+ }
791
+ }
792
+
793
+ class MorphManyRelationQuery {
794
+ parent;
795
+ related;
796
+ relation;
797
+ kind = "morphMany";
798
+ extraWhere = {};
799
+ extraOptions = {};
800
+ constructor(parent, related, relation) {
801
+ this.parent = parent;
802
+ this.related = related;
803
+ this.relation = relation;
804
+ }
805
+ where(where) {
806
+ this.extraWhere = { ...this.extraWhere, ...where };
807
+ return this;
808
+ }
809
+ applyEagerLoad(query, alias) {
810
+ query.withMorphMany(alias, this.relation, this.related.repository(), this.extraOptions);
811
+ }
812
+ hydrateEager(row, alias) {
813
+ const value = row[alias] ?? [];
814
+ const rows = Array.isArray(value) ? value : [];
815
+ return rows.map((item) => this.related.newFromRecord(item));
816
+ }
817
+ toExistsClause(parentTable) {
818
+ const childTable = this.related.repository().getTable().name;
819
+ const extra = buildAdvancedWhereClause(childTable, {
820
+ [this.relation.morphTypeKey]: this.relation.morphType,
821
+ ...this.extraWhere
822
+ }, [], []);
823
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
824
+ const sql = `SELECT 1 FROM ${quoteIdentifier(childTable)} WHERE ${qualifyColumn(childTable, this.relation.morphIdKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
825
+ return { sql, params: extra.params };
826
+ }
827
+ async get() {
828
+ const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
829
+ const rows = await repository.query(asWhere({
830
+ [this.relation.morphTypeKey]: this.relation.morphType,
831
+ [this.relation.morphIdKey]: this.parent.get(this.relation.localKey),
832
+ ...this.extraWhere
833
+ })).get();
834
+ return rows.map((row) => this.related.newFromRecord(row));
835
+ }
836
+ async first() {
837
+ const rows = await this.get();
838
+ return rows[0] ?? null;
839
+ }
840
+ async count() {
841
+ const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
842
+ return repository.query(asWhere({
843
+ [this.relation.morphTypeKey]: this.relation.morphType,
844
+ [this.relation.morphIdKey]: this.parent.get(this.relation.localKey),
845
+ ...this.extraWhere
846
+ })).count();
847
+ }
848
+ then(onfulfilled, onrejected) {
849
+ return thenGet(() => this.get(), onfulfilled, onrejected);
850
+ }
851
+ async create(attributes = {}) {
852
+ return this.related.create(attributes, {
853
+ [this.relation.morphTypeKey]: this.relation.morphType,
854
+ [this.relation.morphIdKey]: this.parent.get(this.relation.localKey)
855
+ });
856
+ }
857
+ }
858
+
859
+ class MorphOneRelationQuery {
860
+ relation;
861
+ kind = "morphOne";
862
+ inner;
863
+ constructor(parent, related, relation) {
864
+ this.relation = relation;
865
+ this.inner = new MorphManyRelationQuery(parent, related, {
866
+ type: "morphMany",
867
+ name: relation.name,
868
+ localKey: relation.localKey,
869
+ morphTypeKey: relation.morphTypeKey,
870
+ morphIdKey: relation.morphIdKey,
871
+ morphType: relation.morphType
872
+ });
873
+ }
874
+ where(where) {
875
+ this.inner.where(where);
876
+ return this;
877
+ }
878
+ applyEagerLoad(query, alias) {
879
+ this.inner.applyEagerLoad(query, alias);
880
+ }
881
+ hydrateEager(row, alias) {
882
+ const hydrated = this.inner.hydrateEager(row, alias);
883
+ return hydrated[0];
884
+ }
885
+ toExistsClause(parentTable) {
886
+ return this.inner.toExistsClause(parentTable);
887
+ }
888
+ async get() {
889
+ return this.inner.first();
890
+ }
891
+ async first() {
892
+ return this.get();
893
+ }
894
+ async count() {
895
+ return this.inner.count();
896
+ }
897
+ then(onfulfilled, onrejected) {
898
+ return thenGet(() => this.get(), onfulfilled, onrejected);
899
+ }
900
+ async create(attributes = {}) {
901
+ return this.inner.create(attributes);
902
+ }
903
+ }
904
+
905
+ class MorphToRelationQuery {
906
+ parent;
907
+ relatedByType;
908
+ relation;
909
+ kind = "morphTo";
910
+ extraWhere = {};
911
+ constructor(parent, relatedByType, relation) {
912
+ this.parent = parent;
913
+ this.relatedByType = relatedByType;
914
+ this.relation = relation;
915
+ }
916
+ where(where) {
917
+ this.extraWhere = { ...this.extraWhere, ...where };
918
+ return this;
919
+ }
920
+ applyEagerLoad(query, alias) {
921
+ const repositories = new Map(Object.entries(this.relatedByType).map(([type, model]) => [
922
+ type,
923
+ model.repository()
924
+ ]));
925
+ query.withMorphTo(alias, this.relation, repositories);
926
+ }
927
+ hydrateEager(row, alias) {
928
+ return row[alias];
929
+ }
930
+ toExistsClause(parentTable) {
931
+ const type = String(this.parent.get(this.relation.morphTypeKey) ?? "");
932
+ const related = this.relatedByType[type];
933
+ if (!related) {
934
+ return { sql: "SELECT 1 WHERE 1 = 0", params: [] };
935
+ }
936
+ const relatedTable = related.repository().getTable();
937
+ const extra = buildAdvancedWhereClause(relatedTable.name, this.extraWhere, [], []);
938
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
939
+ return {
940
+ sql: `SELECT 1 FROM ${quoteIdentifier(relatedTable.name)} WHERE ${qualifyColumn(relatedTable.name, relatedTable.primaryKey)} = ${qualifyColumn(parentTable, this.relation.morphIdKey)}${extraSql ? ` AND ${extraSql}` : ""}`,
941
+ params: extra.params
942
+ };
943
+ }
944
+ async get() {
945
+ const type = String(this.parent.get(this.relation.morphTypeKey) ?? "");
946
+ const id = this.parent.get(this.relation.morphIdKey);
947
+ const related = this.relatedByType[type];
948
+ if (!related || id === null || id === undefined) {
949
+ return null;
950
+ }
951
+ const table = related.repository().getTable();
952
+ const row = await related.repository().withConnection(this.parent.getRepository().getConnection()).query(asWhere({ [table.primaryKey]: id, ...this.extraWhere })).first();
953
+ return row ? related.newFromRecord(row) : null;
954
+ }
955
+ then(onfulfilled, onrejected) {
956
+ return thenGet(() => this.get(), onfulfilled, onrejected);
957
+ }
958
+ }
959
+
419
960
  // ../../src/core/database/relationships.ts
420
961
  function hasMany(definition) {
421
962
  return {
@@ -564,8 +1105,77 @@ function indexMorphToRelation(children, parentsByType, relation) {
564
1105
 
565
1106
  // ../../src/core/database/model.ts
566
1107
  var modelRepositories = new WeakMap;
1108
+ var namedModels = new Map;
567
1109
  var modelGlobalScopes = new WeakMap;
1110
+ var modelObservers = new WeakMap;
568
1111
  var modelBooted = new WeakSet;
1112
+ async function runObservers(model, hook) {
1113
+ const observers = modelObservers.get(model.constructor) ?? [];
1114
+ for (const observer of observers) {
1115
+ const handler = observer[hook];
1116
+ if (handler && await handler(model) === false) {
1117
+ return false;
1118
+ }
1119
+ }
1120
+ return true;
1121
+ }
1122
+ function accessorName(key) {
1123
+ const pascal = key.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase());
1124
+ return `get${pascal.charAt(0).toUpperCase()}${pascal.slice(1)}Attribute`;
1125
+ }
1126
+ function isLoadableModel(value) {
1127
+ return Boolean(value && typeof value === "object" && typeof value.load === "function" && typeof value.loaded === "function" && typeof value.setLoaded === "function");
1128
+ }
1129
+ async function eagerLoadOnModels(models, paths) {
1130
+ if (models.length === 0 || paths.length === 0) {
1131
+ return;
1132
+ }
1133
+ const grouped = new Map;
1134
+ for (const path of paths) {
1135
+ const [head, ...rest] = path.split(".");
1136
+ if (!head) {
1137
+ continue;
1138
+ }
1139
+ const nested = rest.join(".");
1140
+ const existing = grouped.get(head) ?? [];
1141
+ if (nested) {
1142
+ existing.push(nested);
1143
+ }
1144
+ grouped.set(head, existing);
1145
+ }
1146
+ for (const [head, nested] of grouped) {
1147
+ const unloaded = models.filter((model) => model.loaded(head) === undefined);
1148
+ if (unloaded.length > 0) {
1149
+ const first = unloaded[0];
1150
+ if (!first) {
1151
+ continue;
1152
+ }
1153
+ const method = first[head];
1154
+ if (typeof method !== "function") {
1155
+ throw new Error(`${first.constructor.name} has no relation method ${head}().`);
1156
+ }
1157
+ const relationQuery = method.call(first);
1158
+ const query = first.getRepository().query();
1159
+ relationQuery.applyEagerLoad(query, head);
1160
+ const attached = await query.attachToRows(unloaded.map((model) => model.toObject()));
1161
+ for (const [index, model] of unloaded.entries()) {
1162
+ const row = attached[index] ?? model.toObject();
1163
+ model.setLoaded(head, relationQuery.hydrateEager(row, head));
1164
+ }
1165
+ }
1166
+ if (nested.length === 0) {
1167
+ continue;
1168
+ }
1169
+ const children = models.flatMap((model) => {
1170
+ const loaded = model.loaded(head);
1171
+ return Array.isArray(loaded) ? loaded : loaded ? [loaded] : [];
1172
+ });
1173
+ await eagerLoadOnModels(children.filter(isLoadableModel), nested);
1174
+ }
1175
+ }
1176
+ async function loadNested(model, path) {
1177
+ await eagerLoadOnModels([model], [path]);
1178
+ }
569
1179
  function resolveModelRepository(model) {
570
1180
  const repository = modelRepositories.get(model);
571
1181
  if (!repository) {
@@ -573,6 +1183,48 @@ function resolveModelRepository(model) {
573
1183
  }
574
1184
  return repository;
575
1185
  }
1186
+ function registerModelClass(name, model) {
1187
+ namedModels.set(name, model);
1188
+ }
1189
+ function resolveRelated(related) {
1190
+ if (typeof related === "string") {
1191
+ const found = namedModels.get(related);
1192
+ if (!found) {
1193
+ throw new Error(`Model [${related}] is not registered. Call registerModelClass() first.`);
1194
+ }
1195
+ return found;
1196
+ }
1197
+ if (typeof related === "function" && typeof related.repository !== "function") {
1198
+ return related();
1199
+ }
1200
+ return related;
1201
+ }
1202
+ function inferRelationMethodName(callee) {
1203
+ const stack = new Error().stack ?? "";
1204
+ let seenCallee = false;
1205
+ for (const line of stack.split(`
1206
+ `)) {
1207
+ const match = /at (?:async )?(?:[^.\s]+\.)?(\w+)/.exec(line);
1208
+ const name = match?.[1];
1209
+ if (!name || name === "Error" || name === "inferRelationMethodName") {
1210
+ continue;
1211
+ }
1212
+ if (!seenCallee) {
1213
+ if (name === callee) {
1214
+ seenCallee = true;
1215
+ }
1216
+ continue;
1217
+ }
1218
+ if (name !== callee) {
1219
+ return name;
1220
+ }
1221
+ }
1222
+ return;
1223
+ }
1224
+ function morphClassOf(model) {
1225
+ const statics = modelStatics(model.constructor === Function ? model : model.constructor);
1226
+ return statics.$morphClass ?? (model.constructor === Function ? model.name : model.constructor.name);
1227
+ }
576
1228
  function modelStatics(model) {
577
1229
  return model;
578
1230
  }
@@ -602,6 +1254,11 @@ function hydrateValue(value, cast) {
602
1254
  case "bool":
603
1255
  case "boolean":
604
1256
  return value === true || value === 1 || value === "1" || value === "true";
1257
+ case "integer":
1258
+ case "int":
1259
+ return value === "" ? null : Number(value);
1260
+ case "hashed":
1261
+ return value;
605
1262
  default:
606
1263
  return value;
607
1264
  }
@@ -619,6 +1276,11 @@ function dehydrateValue(value, cast) {
619
1276
  case "bool":
620
1277
  case "boolean":
621
1278
  return Boolean(value);
1279
+ case "integer":
1280
+ case "int":
1281
+ return value === "" ? null : Number(value);
1282
+ case "hashed":
1283
+ return value;
622
1284
  default:
623
1285
  return value;
624
1286
  }
@@ -673,6 +1335,153 @@ function applyTimestampsOnUpdate(columns, values, enabled) {
673
1335
  return result;
674
1336
  }
675
1337
 
1338
+ class ModelQuery {
1339
+ modelClass;
1340
+ query;
1341
+ eager = [];
1342
+ constructor(modelClass, query) {
1343
+ this.modelClass = modelClass;
1344
+ this.query = query;
1345
+ }
1346
+ with(...relations) {
1347
+ const statics = modelStatics(this.modelClass);
1348
+ ensureBooted(this.modelClass);
1349
+ const dummy = statics.newFromRecord({}, false);
1350
+ for (const path of relations) {
1351
+ const name = path.split(".")[0] ?? path;
1352
+ const method = dummy[name];
1353
+ if (typeof method !== "function") {
1354
+ throw new Error(`${this.modelClass.name} has no relation method ${name}().`);
1355
+ }
1356
+ const relationQuery = method.call(dummy);
1357
+ this.eager.push({ name, path, relationQuery });
1358
+ relationQuery.applyEagerLoad(this.query, name);
1359
+ }
1360
+ return this;
1361
+ }
1362
+ where(input) {
1363
+ this.query.where(input);
1364
+ return this;
1365
+ }
1366
+ orWhere(input) {
1367
+ this.query.orWhere(input);
1368
+ return this;
1369
+ }
1370
+ orderBy(orderBy) {
1371
+ this.query.orderBy(orderBy);
1372
+ return this;
1373
+ }
1374
+ limit(limit) {
1375
+ this.query.limit(limit);
1376
+ return this;
1377
+ }
1378
+ offset(offset) {
1379
+ this.query.offset(offset);
1380
+ return this;
1381
+ }
1382
+ whereNull(column) {
1383
+ this.query.whereNull(column);
1384
+ return this;
1385
+ }
1386
+ whereIn(column, values) {
1387
+ this.query.whereIn(column, values);
1388
+ return this;
1389
+ }
1390
+ whereExists(sql, params = []) {
1391
+ this.query.whereExists(sql, params);
1392
+ return this;
1393
+ }
1394
+ whereNotExists(sql, params = []) {
1395
+ this.query.whereNotExists(sql, params);
1396
+ return this;
1397
+ }
1398
+ whereHas(name, constrain) {
1399
+ return this.constrainExists(name, constrain, false);
1400
+ }
1401
+ has(name) {
1402
+ return this.constrainExists(name, undefined, false);
1403
+ }
1404
+ doesntHave(name) {
1405
+ return this.constrainExists(name, undefined, true);
1406
+ }
1407
+ whereDoesntHave(name, constrain) {
1408
+ return this.constrainExists(name, constrain, true);
1409
+ }
1410
+ withHasMany(...args) {
1411
+ this.query.withHasMany(...args);
1412
+ return this;
1413
+ }
1414
+ withBelongsTo(...args) {
1415
+ this.query.withBelongsTo(...args);
1416
+ return this;
1417
+ }
1418
+ withBelongsToMany(...args) {
1419
+ this.query.withBelongsToMany(...args);
1420
+ return this;
1421
+ }
1422
+ withMorphMany(...args) {
1423
+ this.query.withMorphMany(...args);
1424
+ return this;
1425
+ }
1426
+ withMorphOne(...args) {
1427
+ this.query.withMorphOne(...args);
1428
+ return this;
1429
+ }
1430
+ withMorphTo(...args) {
1431
+ this.query.withMorphTo(...args);
1432
+ return this;
1433
+ }
1434
+ async get() {
1435
+ const statics = modelStatics(this.modelClass);
1436
+ const rows = await this.query.get();
1437
+ const models = [];
1438
+ for (const row of rows) {
1439
+ const model = statics.newFromRecord(row, true);
1440
+ await runObservers(model, "retrieved");
1441
+ for (const { name, relationQuery } of this.eager) {
1442
+ model.setLoaded(name, relationQuery.hydrateEager(row, name));
1443
+ }
1444
+ models.push(model);
1445
+ }
1446
+ const nested = this.eager.filter((item) => item.path.includes(".")).map((item) => item.path);
1447
+ await eagerLoadOnModels(models.filter(isLoadableModel), nested);
1448
+ return models;
1449
+ }
1450
+ async first() {
1451
+ this.query.limit(1);
1452
+ const models = await this.get();
1453
+ return models[0] ?? null;
1454
+ }
1455
+ async find(id) {
1456
+ const primaryKey = resolveModelRepository(this.modelClass).getTable().primaryKey;
1457
+ return this.where({ [primaryKey]: id }).first();
1458
+ }
1459
+ async findOrFail(id, errorFactory) {
1460
+ const model = await this.find(id);
1461
+ if (model) {
1462
+ return model;
1463
+ }
1464
+ throw errorFactory?.(id) ?? new NotFoundError(`${this.modelClass.name} ${String(id)} not found.`);
1465
+ }
1466
+ then(onfulfilled, onrejected) {
1467
+ return this.get().then(onfulfilled ?? undefined, onrejected ?? undefined);
1468
+ }
1469
+ constrainExists(name, constrain, not) {
1470
+ const statics = modelStatics(this.modelClass);
1471
+ ensureBooted(this.modelClass);
1472
+ const repository = resolveModelRepository(this.modelClass);
1473
+ const dummy = statics.newFromRecord({});
1474
+ const method = dummy[name];
1475
+ if (typeof method !== "function") {
1476
+ throw new Error(`${this.modelClass.name} has no relation method ${name}().`);
1477
+ }
1478
+ const relationQuery = method.call(dummy);
1479
+ constrain?.(relationQuery);
1480
+ const exists = relationQuery.toExistsClause(repository.getTable().name);
1481
+ return not ? this.whereNotExists(exists.sql, exists.params) : this.whereExists(exists.sql, exists.params);
1482
+ }
1483
+ }
1484
+
676
1485
  class Model {
677
1486
  attributes;
678
1487
  repository;
@@ -680,11 +1489,23 @@ class Model {
680
1489
  static $guarded;
681
1490
  static $casts = {};
682
1491
  static $timestamps = true;
1492
+ static $hidden;
1493
+ static $visible;
1494
+ static $appends;
1495
+ static $morphClass;
683
1496
  _exists;
1497
+ loadedRelations = {};
1498
+ hiddenOverrides = [];
1499
+ visibleOverrides = [];
1500
+ appended = [];
684
1501
  constructor(attributes, repository, exists = true) {
685
1502
  this.attributes = attributes;
686
1503
  this.repository = repository;
687
1504
  this._exists = exists;
1505
+ this.attributes = modelStatics(this.constructor).hydrateAttributes(attributes);
1506
+ }
1507
+ getRepository() {
1508
+ return this.repository;
688
1509
  }
689
1510
  get $exists() {
690
1511
  return this._exists;
@@ -698,8 +1519,51 @@ class Model {
698
1519
  toObject() {
699
1520
  return { ...this.attributes };
700
1521
  }
1522
+ toArray() {
1523
+ const ModelClass = modelStatics(this.constructor);
1524
+ const hidden = new Set([...ModelClass.$hidden ?? [], ...this.hiddenOverrides]);
1525
+ const visible = this.visibleOverrides.length > 0 ? this.visibleOverrides : ModelClass.$visible;
1526
+ const data = { ...this.attributes };
1527
+ if (visible && visible.length > 0) {
1528
+ for (const key of Object.keys(data)) {
1529
+ if (!visible.includes(key)) {
1530
+ delete data[key];
1531
+ }
1532
+ }
1533
+ }
1534
+ for (const key of hidden) {
1535
+ delete data[key];
1536
+ }
1537
+ for (const key of [...ModelClass.$appends ?? [], ...this.appended]) {
1538
+ const accessor = this[accessorName(key)];
1539
+ if (typeof accessor === "function") {
1540
+ data[key] = accessor.call(this);
1541
+ }
1542
+ }
1543
+ for (const [name, value] of Object.entries(this.loadedRelations)) {
1544
+ if (!hidden.has(name) && (!visible || visible.includes(name) || this.appended.includes(name))) {
1545
+ data[name] = value;
1546
+ }
1547
+ }
1548
+ return data;
1549
+ }
1550
+ toJSON() {
1551
+ return this.toArray();
1552
+ }
1553
+ makeHidden(...keys) {
1554
+ this.hiddenOverrides.push(...keys);
1555
+ return this;
1556
+ }
1557
+ makeVisible(...keys) {
1558
+ this.visibleOverrides.push(...keys);
1559
+ return this;
1560
+ }
1561
+ append(...keys) {
1562
+ this.appended.push(...keys);
1563
+ return this;
1564
+ }
701
1565
  primaryKey() {
702
- throw new Error(`${this.constructor.name}.primaryKey() is not implemented.`);
1566
+ return this.repository.getTable().primaryKey;
703
1567
  }
704
1568
  static primaryKeyField() {
705
1569
  return resolveModelRepository(this).getTable().primaryKey;
@@ -718,6 +1582,10 @@ class Model {
718
1582
  return new statics(hydrated, repository, exists);
719
1583
  }
720
1584
  static boot() {}
1585
+ static observe(observer) {
1586
+ const existing = modelObservers.get(this) ?? [];
1587
+ modelObservers.set(this, [...existing, observer]);
1588
+ }
721
1589
  static addGlobalScope(_name, scope) {
722
1590
  ensureBooted(this);
723
1591
  const existing = modelGlobalScopes.get(this) ?? [];
@@ -736,26 +1604,55 @@ class Model {
736
1604
  for (const scope of getGlobalScopes(this)) {
737
1605
  query = scope(query);
738
1606
  }
739
- return query;
1607
+ return new ModelQuery(this, query);
1608
+ }
1609
+ static newFromRecord(record, exists = true) {
1610
+ const repository = resolveModelRepository(this);
1611
+ return modelStatics(this).fromRecord(record, repository, exists);
740
1612
  }
741
- static async create(attributes) {
1613
+ static async create(attributes, forced = {}) {
742
1614
  const statics = modelStatics(this);
743
1615
  ensureBooted(this);
744
1616
  const repository = resolveModelRepository(this);
745
1617
  const table = repository.getTable();
746
1618
  const timestamps = statics.$timestamps ?? true;
747
- const assignable = filterMassAssignable(statics.$fillable, statics.$guarded, attributes);
1619
+ const assignable = {
1620
+ ...filterMassAssignable(statics.$fillable, statics.$guarded, attributes),
1621
+ ...forced
1622
+ };
748
1623
  const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
749
1624
  const payload = statics.dehydrateAttributes(withTimestamps);
1625
+ const pending = statics.newFromRecord({ ...payload }, false);
1626
+ if (await runObservers(pending, "saving") === false) {
1627
+ throw new Error(`${this.name}.create() was cancelled by an observer.`);
1628
+ }
1629
+ if (await runObservers(pending, "creating") === false) {
1630
+ throw new Error(`${this.name}.create() was cancelled by an observer.`);
1631
+ }
750
1632
  const record = await repository.create(payload);
751
- return statics.fromRecord(record, repository, true);
1633
+ const created = statics.fromRecord(record, repository, true);
1634
+ await runObservers(created, "created");
1635
+ await runObservers(created, "saved");
1636
+ return created;
1637
+ }
1638
+ static with(...relations) {
1639
+ return Model.query.call(this).with(...relations);
1640
+ }
1641
+ static whereHas(name, constrain) {
1642
+ return Model.query.call(this).whereHas(name, constrain);
1643
+ }
1644
+ static has(name) {
1645
+ return Model.query.call(this).has(name);
1646
+ }
1647
+ static doesntHave(name) {
1648
+ return Model.query.call(this).doesntHave(name);
1649
+ }
1650
+ static whereDoesntHave(name, constrain) {
1651
+ return Model.query.call(this).whereDoesntHave(name, constrain);
752
1652
  }
753
1653
  static async find(id) {
754
- const statics = modelStatics(this);
755
- const repository = resolveModelRepository(this);
756
- const primaryKey = repository.getTable().primaryKey;
757
- const record = await Model.query.call(this).where({ [primaryKey]: id }).first();
758
- return record ? statics.fromRecord(record, repository, true) : null;
1654
+ const primaryKey = resolveModelRepository(this).getTable().primaryKey;
1655
+ return Model.query.call(this).where({ [primaryKey]: id }).first();
759
1656
  }
760
1657
  static async findOrFail(id, errorFactory) {
761
1658
  const model = await Model.find.call(this, id);
@@ -765,8 +1662,6 @@ class Model {
765
1662
  throw errorFactory?.(id) ?? new NotFoundError(`${this.name} ${String(id)} not found.`);
766
1663
  }
767
1664
  static async all(options = {}) {
768
- const statics = modelStatics(this);
769
- const repository = resolveModelRepository(this);
770
1665
  let query = Model.query.call(this);
771
1666
  if (options.orderBy) {
772
1667
  query = query.orderBy(options.orderBy);
@@ -774,28 +1669,57 @@ class Model {
774
1669
  if (options.limit !== undefined) {
775
1670
  query = query.limit(options.limit);
776
1671
  }
777
- const rows = await query.get();
778
- return rows.map((row) => statics.fromRecord(row, repository, true));
1672
+ return query.get();
1673
+ }
1674
+ static where(where) {
1675
+ return Model.query.call(this).where(where);
779
1676
  }
780
1677
  static async firstWhere(where, options = {}) {
781
- const statics = modelStatics(this);
782
- const repository = resolveModelRepository(this);
783
1678
  let query = Model.query.call(this).where(where);
784
1679
  if (options.orderBy) {
785
1680
  query = query.orderBy(options.orderBy);
786
1681
  }
787
- const record = await query.first();
788
- return record ? statics.fromRecord(record, repository, true) : null;
1682
+ return query.first();
1683
+ }
1684
+ static async firstOrNew(where, values = {}) {
1685
+ const existing = await Model.firstWhere.call(this, where);
1686
+ if (existing) {
1687
+ return existing;
1688
+ }
1689
+ return modelStatics(this).newFromRecord({ ...where, ...values }, false);
1690
+ }
1691
+ static async firstOrCreate(where, values = {}) {
1692
+ const existing = await Model.firstWhere.call(this, where);
1693
+ if (existing) {
1694
+ return existing;
1695
+ }
1696
+ return Model.create.call(this, { ...where, ...values });
1697
+ }
1698
+ static async updateOrCreate(where, values = {}) {
1699
+ const existing = await Model.firstWhere.call(this, where);
1700
+ if (existing) {
1701
+ return existing.update(values);
1702
+ }
1703
+ return Model.create.call(this, { ...where, ...values });
789
1704
  }
790
1705
  async save() {
791
1706
  const ModelClass = modelStatics(this.constructor);
792
1707
  const timestamps = ModelClass.$timestamps ?? true;
793
1708
  const casts = ModelClass.$casts ?? {};
794
1709
  const table = this.repository.getTable();
795
- if (this.$exists) {
1710
+ const updating = this.$exists;
1711
+ if (await runObservers(this, "saving") === false) {
1712
+ return this;
1713
+ }
1714
+ if (await runObservers(this, updating ? "updating" : "creating") === false) {
1715
+ return this;
1716
+ }
1717
+ if (updating) {
796
1718
  const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
797
1719
  const record2 = await this.repository.updateByIdOrThrow(this.id, changes);
798
1720
  this.attributes = ModelClass.hydrateAttributes(record2);
1721
+ await runObservers(this, "updated");
1722
+ await runObservers(this, "saved");
799
1723
  return this;
800
1724
  }
801
1725
  const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, this.attributes);
@@ -804,6 +1728,8 @@ class Model {
804
1728
  const record = await this.repository.create(payload);
805
1729
  this.attributes = ModelClass.hydrateAttributes(record);
806
1730
  this._exists = true;
1731
+ await runObservers(this, "created");
1732
+ await runObservers(this, "saved");
807
1733
  return this;
808
1734
  }
809
1735
  async update(changes) {
@@ -813,10 +1739,14 @@ class Model {
813
1739
  return await this.save();
814
1740
  }
815
1741
  async delete() {
816
- if (resolveSoftDeleteColumn(this.repository.getTable())) {
817
- return await this.repository.deleteById(this.id);
1742
+ if (await runObservers(this, "deleting") === false) {
1743
+ return false;
818
1744
  }
819
- return await this.repository.forceDeleteById(this.id);
1745
+ const deleted = resolveSoftDeleteColumn(this.repository.getTable()) ? await this.repository.deleteById(this.id) : await this.repository.forceDeleteById(this.id);
1746
+ if (deleted) {
1747
+ await runObservers(this, "deleted");
1748
+ }
1749
+ return deleted;
820
1750
  }
821
1751
  async forceDelete() {
822
1752
  return await this.repository.forceDeleteById(this.id);
@@ -865,6 +1795,102 @@ class Model {
865
1795
  const loaded = grouped.get(parentId) ?? [];
866
1796
  return Object.assign(this, { [as]: loaded });
867
1797
  }
1798
+ hasMany(related, foreignKey, localKey) {
1799
+ const table = this.repository.getTable();
1800
+ const relatedClass = resolveRelated(related);
1801
+ return new HasManyRelationQuery(this, relatedClass, hasMany({
1802
+ name: relatedClass.repository().getTable().name,
1803
+ localKey: localKey ?? table.primaryKey,
1804
+ foreignKey: foreignKey ?? foreignKeyFromTable(table.name)
1805
+ }));
1806
+ }
1807
+ hasOne(related, foreignKey, localKey) {
1808
+ const table = this.repository.getTable();
1809
+ const relatedClass = resolveRelated(related);
1810
+ return new HasOneRelationQuery(this, relatedClass, hasOne({
1811
+ name: relatedClass.repository().getTable().name,
1812
+ localKey: localKey ?? table.primaryKey,
1813
+ foreignKey: foreignKey ?? foreignKeyFromTable(table.name)
1814
+ }));
1815
+ }
1816
+ belongsTo(related, foreignKey, ownerKey) {
1817
+ const relatedClass = resolveRelated(related);
1818
+ const relatedTable = relatedClass.repository().getTable();
1819
+ return new BelongsToRelationQuery(this, relatedClass, belongsTo({
1820
+ name: relatedTable.name,
1821
+ foreignKey: foreignKey ?? foreignKeyFromTable(relatedTable.name),
1822
+ ownerKey: ownerKey ?? relatedTable.primaryKey
1823
+ }));
1824
+ }
1825
+ belongsToMany(related, pivotTable, foreignPivotKey, relatedPivotKey) {
1826
+ const table = this.repository.getTable();
1827
+ const relatedClass = resolveRelated(related);
1828
+ const relatedTable = relatedClass.repository().getTable();
1829
+ return new BelongsToManyRelationQuery(this, relatedClass, belongsToMany({
1830
+ name: relatedTable.name,
1831
+ pivotTable: pivotTable ?? pivotTableName(table.name, relatedTable.name),
1832
+ parentKey: table.primaryKey,
1833
+ relatedKey: relatedTable.primaryKey,
1834
+ foreignPivotKey: foreignPivotKey ?? foreignKeyFromTable(table.name),
1835
+ relatedPivotKey: relatedPivotKey ?? foreignKeyFromTable(relatedTable.name)
1836
+ }));
1837
+ }
1838
+ morphMany(related, morphName, typeKey, idKey, morphType) {
1839
+ const table = this.repository.getTable();
1840
+ const relatedClass = resolveRelated(related);
1841
+ return new MorphManyRelationQuery(this, relatedClass, morphMany({
1842
+ name: morphName,
1843
+ localKey: table.primaryKey,
1844
+ morphTypeKey: typeKey ?? `${morphName}_type`,
1845
+ morphIdKey: idKey ?? `${morphName}_id`,
1846
+ morphType: morphType ?? morphClassOf(this)
1847
+ }));
1848
+ }
1849
+ morphOne(related, morphName, typeKey, idKey, morphType) {
1850
+ const table = this.repository.getTable();
1851
+ const relatedClass = resolveRelated(related);
1852
+ return new MorphOneRelationQuery(this, relatedClass, morphOne({
1853
+ name: morphName,
1854
+ localKey: table.primaryKey,
1855
+ morphTypeKey: typeKey ?? `${morphName}_type`,
1856
+ morphIdKey: idKey ?? `${morphName}_id`,
1857
+ morphType: morphType ?? morphClassOf(this)
1858
+ }));
1859
+ }
1860
+ morphTo(relatedByType, morphName, typeKey, idKey) {
1861
+ const resolvedName = morphName ?? inferRelationMethodName("morphTo");
1862
+ if (!resolvedName) {
1863
+ throw new Error(`${this.constructor.name}.morphTo() needs an explicit morph name (Laravel infers it from the relation method).`);
1864
+ }
1865
+ const resolvedMap = Object.fromEntries(Object.entries(relatedByType).map(([type, related]) => [type, resolveRelated(related)]));
1866
+ return new MorphToRelationQuery(this, resolvedMap, morphTo({
1867
+ name: resolvedName,
1868
+ morphTypeKey: typeKey ?? `${resolvedName}_type`,
1869
+ morphIdKey: idKey ?? `${resolvedName}_id`
1870
+ }));
1871
+ }
1872
+ async load(...names) {
1873
+ for (const name of names) {
1874
+ if (name.includes(".")) {
1875
+ await loadNested(this, name);
1876
+ continue;
1877
+ }
1878
+ const method = this[name];
1879
+ if (typeof method !== "function") {
1880
+ throw new Error(`${this.constructor.name} has no relation method ${name}().`);
1881
+ }
1882
+ const relationQuery = method.call(this);
1883
+ this.loadedRelations[name] = await relationQuery.get();
1884
+ }
1885
+ return this;
1886
+ }
1887
+ loaded(name) {
1888
+ return this.loadedRelations[name];
1889
+ }
1890
+ setLoaded(name, value) {
1891
+ this.loadedRelations[name] = value;
1892
+ return this;
1893
+ }
868
1894
  mergeAttributes(patch) {
869
1895
  Object.assign(this.attributes, patch);
870
1896
  return this;
@@ -872,14 +1898,31 @@ class Model {
872
1898
  }
873
1899
  function registerModelRepository(model, repository) {
874
1900
  modelRepositories.set(model, repository);
1901
+ const name = model.name;
1902
+ if (name) {
1903
+ namedModels.set(name, model);
1904
+ }
1905
+ const morphClass = model.$morphClass;
1906
+ if (morphClass) {
1907
+ namedModels.set(morphClass, model);
1908
+ }
875
1909
  ensureBooted(model);
876
1910
  return model;
877
1911
  }
878
1912
  export {
1913
+ BelongsToManyRelationQuery,
1914
+ BelongsToRelationQuery,
1915
+ HasManyRelationQuery,
1916
+ HasOneRelationQuery,
879
1917
  Model,
1918
+ ModelQuery,
1919
+ MorphManyRelationQuery,
1920
+ MorphOneRelationQuery,
1921
+ MorphToRelationQuery,
880
1922
  applyCasts,
881
1923
  dehydrateValue,
882
1924
  filterMassAssignable,
883
1925
  hydrateValue,
1926
+ registerModelClass,
884
1927
  registerModelRepository
885
1928
  };