@getstrata/core 0.5.98 → 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.
@@ -97,7 +97,7 @@ function buildOperatorClauses(column, operator, params) {
97
97
  clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
98
98
  }
99
99
  if (operator.ilike !== undefined) {
100
- clauses.push(`${column} ILIKE ${pushParam(params, `%${operator.ilike}%`)}`);
100
+ clauses.push(`${column} ILIKE ${pushParam(params, operator.ilike)}`);
101
101
  }
102
102
  if (operator.tsMatch !== undefined) {
103
103
  clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
@@ -458,6 +458,9 @@ function ownerId(owner, ownerKey) {
458
458
  }
459
459
  throw new Error("belongsTo.associate() requires a related model or { id }.");
460
460
  }
461
+ function thenGet(get, onfulfilled, onrejected) {
462
+ return get().then(onfulfilled ?? undefined, onrejected ?? undefined);
463
+ }
461
464
 
462
465
  class HasManyRelationQuery {
463
466
  parent;
@@ -521,13 +524,28 @@ class HasManyRelationQuery {
521
524
  return rows[0] ?? null;
522
525
  }
523
526
  async count() {
524
- return (await this.get()).length;
527
+ return this.scopedQuery().count();
528
+ }
529
+ then(onfulfilled, onrejected) {
530
+ return thenGet(() => this.get(), onfulfilled, onrejected);
525
531
  }
526
532
  async create(attributes = {}) {
527
533
  return this.related.create(attributes, {
528
534
  [this.relation.foreignKey]: this.parent.get(this.relation.localKey)
529
535
  });
530
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
+ }
531
549
  async createMany(records) {
532
550
  const created = [];
533
551
  for (const attributes of records) {
@@ -575,11 +593,17 @@ class HasOneRelationQuery {
575
593
  return this.get();
576
594
  }
577
595
  async count() {
578
- return await this.get() ? 1 : 0;
596
+ return this.inner.count();
597
+ }
598
+ then(onfulfilled, onrejected) {
599
+ return thenGet(() => this.get(), onfulfilled, onrejected);
579
600
  }
580
601
  async create(attributes = {}) {
581
602
  return this.inner.create(attributes);
582
603
  }
604
+ async save(related) {
605
+ return this.inner.save(related);
606
+ }
583
607
  }
584
608
 
585
609
  class BelongsToRelationQuery {
@@ -587,12 +611,17 @@ class BelongsToRelationQuery {
587
611
  related;
588
612
  relation;
589
613
  kind = "belongsTo";
614
+ extraWhere = {};
590
615
  extraOptions = {};
591
616
  constructor(parent, related, relation) {
592
617
  this.parent = parent;
593
618
  this.related = related;
594
619
  this.relation = relation;
595
620
  }
621
+ where(where) {
622
+ this.extraWhere = { ...this.extraWhere, ...where };
623
+ return this;
624
+ }
596
625
  orderBy(orderBy) {
597
626
  this.extraOptions = { ...this.extraOptions, orderBy };
598
627
  return this;
@@ -606,10 +635,10 @@ class BelongsToRelationQuery {
606
635
  }
607
636
  toExistsClause(parentTable) {
608
637
  const relatedTable = this.related.repository().getTable().name;
609
- return {
610
- sql: `SELECT 1 FROM ${quoteIdentifier(relatedTable)} WHERE ${qualifyColumn(relatedTable, this.relation.ownerKey)} = ${qualifyColumn(parentTable, this.relation.foreignKey)}`,
611
- params: []
612
- };
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 };
613
642
  }
614
643
  async get() {
615
644
  const foreign = this.parent.get(this.relation.foreignKey);
@@ -617,7 +646,7 @@ class BelongsToRelationQuery {
617
646
  return null;
618
647
  }
619
648
  const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
620
- let query = repository.query(asWhere({ [this.relation.ownerKey]: foreign }));
649
+ let query = repository.query(asWhere({ [this.relation.ownerKey]: foreign, ...this.extraWhere }));
621
650
  if (this.extraOptions.orderBy) {
622
651
  query = query.orderBy(this.extraOptions.orderBy);
623
652
  }
@@ -627,6 +656,9 @@ class BelongsToRelationQuery {
627
656
  async first() {
628
657
  return this.get();
629
658
  }
659
+ then(onfulfilled, onrejected) {
660
+ return thenGet(() => this.get(), onfulfilled, onrejected);
661
+ }
630
662
  async associate(owner) {
631
663
  await this.parent.getRepository().updateById(this.parent.id, {
632
664
  [this.relation.foreignKey]: ownerId(owner, this.relation.ownerKey)
@@ -646,6 +678,7 @@ class BelongsToManyRelationQuery {
646
678
  kind = "belongsToMany";
647
679
  extraWhere = {};
648
680
  extraOptions = {};
681
+ pivotValues = {};
649
682
  constructor(parent, related, relation) {
650
683
  this.parent = parent;
651
684
  this.related = related;
@@ -659,7 +692,13 @@ class BelongsToManyRelationQuery {
659
692
  this.extraOptions = { ...this.extraOptions, orderBy };
660
693
  return this;
661
694
  }
662
- applyEagerLoad(_query, _alias) {}
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
+ }
663
702
  hydrateEager(row, alias) {
664
703
  const value = row[alias] ?? [];
665
704
  const rows = Array.isArray(value) ? value : [];
@@ -699,13 +738,34 @@ class BelongsToManyRelationQuery {
699
738
  return rows[0] ?? null;
700
739
  }
701
740
  async count() {
702
- return (await this.get()).length;
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);
703
747
  }
704
748
  async attach(ids) {
705
749
  const list = Array.isArray(ids) ? ids : [ids];
706
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]);
707
755
  for (const id of list) {
708
- await this.connection().unsafe(`INSERT INTO ${this.relation.pivotTable} (${String(this.relation.foreignPivotKey)}, ${String(this.relation.relatedPivotKey)}) VALUES ($1, $2)`, [parentId, id]);
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
+ }
709
769
  }
710
770
  }
711
771
  async detach(ids) {
@@ -777,6 +837,17 @@ class MorphManyRelationQuery {
777
837
  const rows = await this.get();
778
838
  return rows[0] ?? null;
779
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
+ }
780
851
  async create(attributes = {}) {
781
852
  return this.related.create(attributes, {
782
853
  [this.relation.morphTypeKey]: this.relation.morphType,
@@ -817,6 +888,15 @@ class MorphOneRelationQuery {
817
888
  async get() {
818
889
  return this.inner.first();
819
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
+ }
820
900
  async create(attributes = {}) {
821
901
  return this.inner.create(attributes);
822
902
  }
@@ -827,11 +907,16 @@ class MorphToRelationQuery {
827
907
  relatedByType;
828
908
  relation;
829
909
  kind = "morphTo";
910
+ extraWhere = {};
830
911
  constructor(parent, relatedByType, relation) {
831
912
  this.parent = parent;
832
913
  this.relatedByType = relatedByType;
833
914
  this.relation = relation;
834
915
  }
916
+ where(where) {
917
+ this.extraWhere = { ...this.extraWhere, ...where };
918
+ return this;
919
+ }
835
920
  applyEagerLoad(query, alias) {
836
921
  const repositories = new Map(Object.entries(this.relatedByType).map(([type, model]) => [
837
922
  type,
@@ -849,9 +934,11 @@ class MorphToRelationQuery {
849
934
  return { sql: "SELECT 1 WHERE 1 = 0", params: [] };
850
935
  }
851
936
  const relatedTable = related.repository().getTable();
937
+ const extra = buildAdvancedWhereClause(relatedTable.name, this.extraWhere, [], []);
938
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
852
939
  return {
853
- sql: `SELECT 1 FROM ${quoteIdentifier(relatedTable.name)} WHERE ${qualifyColumn(relatedTable.name, relatedTable.primaryKey)} = ${qualifyColumn(parentTable, this.relation.morphIdKey)}`,
854
- params: []
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
855
942
  };
856
943
  }
857
944
  async get() {
@@ -862,9 +949,12 @@ class MorphToRelationQuery {
862
949
  return null;
863
950
  }
864
951
  const table = related.repository().getTable();
865
- const row = await related.repository().withConnection(this.parent.getRepository().getConnection()).query(asWhere({ [table.primaryKey]: id })).first();
952
+ const row = await related.repository().withConnection(this.parent.getRepository().getConnection()).query(asWhere({ [table.primaryKey]: id, ...this.extraWhere })).first();
866
953
  return row ? related.newFromRecord(row) : null;
867
954
  }
955
+ then(onfulfilled, onrejected) {
956
+ return thenGet(() => this.get(), onfulfilled, onrejected);
957
+ }
868
958
  }
869
959
 
870
960
  // ../../src/core/database/relationships.ts
@@ -1015,6 +1105,7 @@ function indexMorphToRelation(children, parentsByType, relation) {
1015
1105
 
1016
1106
  // ../../src/core/database/model.ts
1017
1107
  var modelRepositories = new WeakMap;
1108
+ var namedModels = new Map;
1018
1109
  var modelGlobalScopes = new WeakMap;
1019
1110
  var modelObservers = new WeakMap;
1020
1111
  var modelBooted = new WeakSet;
@@ -1032,38 +1123,59 @@ function accessorName(key) {
1032
1123
  const pascal = key.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase());
1033
1124
  return `get${pascal.charAt(0).toUpperCase()}${pascal.slice(1)}Attribute`;
1034
1125
  }
1035
- function constrainRelationExists(model, name, constrain, not) {
1036
- const statics = modelStatics(model);
1037
- ensureBooted(model);
1038
- const repository = resolveModelRepository(model);
1039
- const dummy = statics.newFromRecord({});
1040
- const method = dummy[name];
1041
- if (typeof method !== "function") {
1042
- throw new Error(`${model.name} has no relation method ${name}().`);
1043
- }
1044
- const relationQuery = method.call(dummy);
1045
- constrain?.(relationQuery);
1046
- const exists = relationQuery.toExistsClause(repository.getTable().name);
1047
- const query = Model.query.call(model);
1048
- return not ? query.whereNotExists(exists.sql, exists.params) : query.whereExists(exists.sql, exists.params);
1126
+ function isLoadableModel(value) {
1127
+ return Boolean(value && typeof value === "object" && typeof value.load === "function" && typeof value.loaded === "function" && typeof value.setLoaded === "function");
1049
1128
  }
1050
- async function loadNested(model, path) {
1051
- const [head, ...rest] = path.split(".");
1052
- if (!head) {
1129
+ async function eagerLoadOnModels(models, paths) {
1130
+ if (models.length === 0 || paths.length === 0) {
1053
1131
  return;
1054
1132
  }
1055
- await model.load(head);
1056
- if (rest.length === 0) {
1057
- return;
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);
1058
1145
  }
1059
- const loaded = model.loaded(head);
1060
- const children = Array.isArray(loaded) ? loaded : loaded ? [loaded] : [];
1061
- for (const child of children) {
1062
- if (child && typeof child === "object" && typeof child.load === "function") {
1063
- await loadNested(child, rest.join("."));
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
+ }
1064
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);
1065
1174
  }
1066
1175
  }
1176
+ async function loadNested(model, path) {
1177
+ await eagerLoadOnModels([model], [path]);
1178
+ }
1067
1179
  function resolveModelRepository(model) {
1068
1180
  const repository = modelRepositories.get(model);
1069
1181
  if (!repository) {
@@ -1071,6 +1183,48 @@ function resolveModelRepository(model) {
1071
1183
  }
1072
1184
  return repository;
1073
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
+ }
1074
1228
  function modelStatics(model) {
1075
1229
  return model;
1076
1230
  }
@@ -1100,6 +1254,11 @@ function hydrateValue(value, cast) {
1100
1254
  case "bool":
1101
1255
  case "boolean":
1102
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;
1103
1262
  default:
1104
1263
  return value;
1105
1264
  }
@@ -1117,6 +1276,11 @@ function dehydrateValue(value, cast) {
1117
1276
  case "bool":
1118
1277
  case "boolean":
1119
1278
  return Boolean(value);
1279
+ case "integer":
1280
+ case "int":
1281
+ return value === "" ? null : Number(value);
1282
+ case "hashed":
1283
+ return value;
1120
1284
  default:
1121
1285
  return value;
1122
1286
  }
@@ -1171,6 +1335,153 @@ function applyTimestampsOnUpdate(columns, values, enabled) {
1171
1335
  return result;
1172
1336
  }
1173
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
+
1174
1485
  class Model {
1175
1486
  attributes;
1176
1487
  repository;
@@ -1181,6 +1492,7 @@ class Model {
1181
1492
  static $hidden;
1182
1493
  static $visible;
1183
1494
  static $appends;
1495
+ static $morphClass;
1184
1496
  _exists;
1185
1497
  loadedRelations = {};
1186
1498
  hiddenOverrides = [];
@@ -1190,6 +1502,7 @@ class Model {
1190
1502
  this.attributes = attributes;
1191
1503
  this.repository = repository;
1192
1504
  this._exists = exists;
1505
+ this.attributes = modelStatics(this.constructor).hydrateAttributes(attributes);
1193
1506
  }
1194
1507
  getRepository() {
1195
1508
  return this.repository;
@@ -1250,7 +1563,7 @@ class Model {
1250
1563
  return this;
1251
1564
  }
1252
1565
  primaryKey() {
1253
- throw new Error(`${this.constructor.name}.primaryKey() is not implemented.`);
1566
+ return this.repository.getTable().primaryKey;
1254
1567
  }
1255
1568
  static primaryKeyField() {
1256
1569
  return resolveModelRepository(this).getTable().primaryKey;
@@ -1291,7 +1604,7 @@ class Model {
1291
1604
  for (const scope of getGlobalScopes(this)) {
1292
1605
  query = scope(query);
1293
1606
  }
1294
- return query;
1607
+ return new ModelQuery(this, query);
1295
1608
  }
1296
1609
  static newFromRecord(record, exists = true) {
1297
1610
  const repository = resolveModelRepository(this);
@@ -1310,79 +1623,36 @@ class Model {
1310
1623
  const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
1311
1624
  const payload = statics.dehydrateAttributes(withTimestamps);
1312
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
+ }
1313
1629
  if (await runObservers(pending, "creating") === false) {
1314
1630
  throw new Error(`${this.name}.create() was cancelled by an observer.`);
1315
1631
  }
1316
1632
  const record = await repository.create(payload);
1317
1633
  const created = statics.fromRecord(record, repository, true);
1318
1634
  await runObservers(created, "created");
1635
+ await runObservers(created, "saved");
1319
1636
  return created;
1320
1637
  }
1321
1638
  static with(...relations) {
1322
- const statics = modelStatics(this);
1323
- ensureBooted(this);
1324
- const repository = resolveModelRepository(this);
1325
- const dummy = statics.fromRecord({}, repository, false);
1326
- const resolved = relations.map((path) => {
1327
- const name = path.split(".")[0] ?? path;
1328
- const method = dummy[name];
1329
- if (typeof method !== "function") {
1330
- throw new Error(`${this.name} has no relation method ${name}().`);
1331
- }
1332
- const relationQuery = method.call(dummy);
1333
- return { name, path, relationQuery };
1334
- });
1335
- const query = Model.query.call(this);
1336
- for (const { name, relationQuery } of resolved) {
1337
- if (relationQuery.kind !== "belongsToMany") {
1338
- relationQuery.applyEagerLoad(query, name);
1339
- }
1340
- }
1341
- return {
1342
- async get() {
1343
- const rows = await query.get();
1344
- const models = [];
1345
- for (const row of rows) {
1346
- const model = statics.fromRecord(row, repository, true);
1347
- for (const { name, path, relationQuery } of resolved) {
1348
- if (relationQuery.kind === "belongsToMany") {
1349
- await model.load(path.includes(".") ? path : name);
1350
- continue;
1351
- }
1352
- model.setLoaded(name, relationQuery.hydrateEager(row, name));
1353
- const nested = path.split(".").slice(1).join(".");
1354
- if (nested) {
1355
- await loadNested(model, path);
1356
- }
1357
- }
1358
- models.push(model);
1359
- }
1360
- return models;
1361
- },
1362
- async first() {
1363
- const [model] = await this.get();
1364
- return model ?? null;
1365
- }
1366
- };
1639
+ return Model.query.call(this).with(...relations);
1367
1640
  }
1368
1641
  static whereHas(name, constrain) {
1369
- return constrainRelationExists(this, name, constrain, false);
1642
+ return Model.query.call(this).whereHas(name, constrain);
1370
1643
  }
1371
1644
  static has(name) {
1372
- return constrainRelationExists(this, name, undefined, false);
1645
+ return Model.query.call(this).has(name);
1373
1646
  }
1374
1647
  static doesntHave(name) {
1375
- return constrainRelationExists(this, name, undefined, true);
1648
+ return Model.query.call(this).doesntHave(name);
1376
1649
  }
1377
1650
  static whereDoesntHave(name, constrain) {
1378
- return constrainRelationExists(this, name, constrain, true);
1651
+ return Model.query.call(this).whereDoesntHave(name, constrain);
1379
1652
  }
1380
1653
  static async find(id) {
1381
- const statics = modelStatics(this);
1382
- const repository = resolveModelRepository(this);
1383
- const primaryKey = repository.getTable().primaryKey;
1384
- const record = await Model.query.call(this).where({ [primaryKey]: id }).first();
1385
- 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();
1386
1656
  }
1387
1657
  static async findOrFail(id, errorFactory) {
1388
1658
  const model = await Model.find.call(this, id);
@@ -1392,8 +1662,6 @@ class Model {
1392
1662
  throw errorFactory?.(id) ?? new NotFoundError(`${this.name} ${String(id)} not found.`);
1393
1663
  }
1394
1664
  static async all(options = {}) {
1395
- const statics = modelStatics(this);
1396
- const repository = resolveModelRepository(this);
1397
1665
  let query = Model.query.call(this);
1398
1666
  if (options.orderBy) {
1399
1667
  query = query.orderBy(options.orderBy);
@@ -1401,21 +1669,17 @@ class Model {
1401
1669
  if (options.limit !== undefined) {
1402
1670
  query = query.limit(options.limit);
1403
1671
  }
1404
- const rows = await query.get();
1405
- return rows.map((row) => statics.fromRecord(row, repository, true));
1672
+ return query.get();
1406
1673
  }
1407
1674
  static where(where) {
1408
1675
  return Model.query.call(this).where(where);
1409
1676
  }
1410
1677
  static async firstWhere(where, options = {}) {
1411
- const statics = modelStatics(this);
1412
- const repository = resolveModelRepository(this);
1413
1678
  let query = Model.query.call(this).where(where);
1414
1679
  if (options.orderBy) {
1415
1680
  query = query.orderBy(options.orderBy);
1416
1681
  }
1417
- const record = await query.first();
1418
- return record ? statics.fromRecord(record, repository, true) : null;
1682
+ return query.first();
1419
1683
  }
1420
1684
  static async firstOrNew(where, values = {}) {
1421
1685
  const existing = await Model.firstWhere.call(this, where);
@@ -1444,6 +1708,9 @@ class Model {
1444
1708
  const casts = ModelClass.$casts ?? {};
1445
1709
  const table = this.repository.getTable();
1446
1710
  const updating = this.$exists;
1711
+ if (await runObservers(this, "saving") === false) {
1712
+ return this;
1713
+ }
1447
1714
  if (await runObservers(this, updating ? "updating" : "creating") === false) {
1448
1715
  return this;
1449
1716
  }
@@ -1452,6 +1719,7 @@ class Model {
1452
1719
  const record2 = await this.repository.updateByIdOrThrow(this.id, changes);
1453
1720
  this.attributes = ModelClass.hydrateAttributes(record2);
1454
1721
  await runObservers(this, "updated");
1722
+ await runObservers(this, "saved");
1455
1723
  return this;
1456
1724
  }
1457
1725
  const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, this.attributes);
@@ -1461,6 +1729,7 @@ class Model {
1461
1729
  this.attributes = ModelClass.hydrateAttributes(record);
1462
1730
  this._exists = true;
1463
1731
  await runObservers(this, "created");
1732
+ await runObservers(this, "saved");
1464
1733
  return this;
1465
1734
  }
1466
1735
  async update(changes) {
@@ -1528,23 +1797,26 @@ class Model {
1528
1797
  }
1529
1798
  hasMany(related, foreignKey, localKey) {
1530
1799
  const table = this.repository.getTable();
1531
- return new HasManyRelationQuery(this, related, hasMany({
1532
- name: related.repository().getTable().name,
1800
+ const relatedClass = resolveRelated(related);
1801
+ return new HasManyRelationQuery(this, relatedClass, hasMany({
1802
+ name: relatedClass.repository().getTable().name,
1533
1803
  localKey: localKey ?? table.primaryKey,
1534
1804
  foreignKey: foreignKey ?? foreignKeyFromTable(table.name)
1535
1805
  }));
1536
1806
  }
1537
1807
  hasOne(related, foreignKey, localKey) {
1538
1808
  const table = this.repository.getTable();
1539
- return new HasOneRelationQuery(this, related, hasOne({
1540
- name: related.repository().getTable().name,
1809
+ const relatedClass = resolveRelated(related);
1810
+ return new HasOneRelationQuery(this, relatedClass, hasOne({
1811
+ name: relatedClass.repository().getTable().name,
1541
1812
  localKey: localKey ?? table.primaryKey,
1542
1813
  foreignKey: foreignKey ?? foreignKeyFromTable(table.name)
1543
1814
  }));
1544
1815
  }
1545
1816
  belongsTo(related, foreignKey, ownerKey) {
1546
- const relatedTable = related.repository().getTable();
1547
- return new BelongsToRelationQuery(this, related, belongsTo({
1817
+ const relatedClass = resolveRelated(related);
1818
+ const relatedTable = relatedClass.repository().getTable();
1819
+ return new BelongsToRelationQuery(this, relatedClass, belongsTo({
1548
1820
  name: relatedTable.name,
1549
1821
  foreignKey: foreignKey ?? foreignKeyFromTable(relatedTable.name),
1550
1822
  ownerKey: ownerKey ?? relatedTable.primaryKey
@@ -1552,8 +1824,9 @@ class Model {
1552
1824
  }
1553
1825
  belongsToMany(related, pivotTable, foreignPivotKey, relatedPivotKey) {
1554
1826
  const table = this.repository.getTable();
1555
- const relatedTable = related.repository().getTable();
1556
- return new BelongsToManyRelationQuery(this, related, belongsToMany({
1827
+ const relatedClass = resolveRelated(related);
1828
+ const relatedTable = relatedClass.repository().getTable();
1829
+ return new BelongsToManyRelationQuery(this, relatedClass, belongsToMany({
1557
1830
  name: relatedTable.name,
1558
1831
  pivotTable: pivotTable ?? pivotTableName(table.name, relatedTable.name),
1559
1832
  parentKey: table.primaryKey,
@@ -1562,31 +1835,38 @@ class Model {
1562
1835
  relatedPivotKey: relatedPivotKey ?? foreignKeyFromTable(relatedTable.name)
1563
1836
  }));
1564
1837
  }
1565
- morphMany(related, morphName, typeKey, idKey) {
1838
+ morphMany(related, morphName, typeKey, idKey, morphType) {
1566
1839
  const table = this.repository.getTable();
1567
- return new MorphManyRelationQuery(this, related, morphMany({
1840
+ const relatedClass = resolveRelated(related);
1841
+ return new MorphManyRelationQuery(this, relatedClass, morphMany({
1568
1842
  name: morphName,
1569
1843
  localKey: table.primaryKey,
1570
1844
  morphTypeKey: typeKey ?? `${morphName}_type`,
1571
1845
  morphIdKey: idKey ?? `${morphName}_id`,
1572
- morphType: table.name
1846
+ morphType: morphType ?? morphClassOf(this)
1573
1847
  }));
1574
1848
  }
1575
- morphOne(related, morphName, typeKey, idKey) {
1849
+ morphOne(related, morphName, typeKey, idKey, morphType) {
1576
1850
  const table = this.repository.getTable();
1577
- return new MorphOneRelationQuery(this, related, morphOne({
1851
+ const relatedClass = resolveRelated(related);
1852
+ return new MorphOneRelationQuery(this, relatedClass, morphOne({
1578
1853
  name: morphName,
1579
1854
  localKey: table.primaryKey,
1580
1855
  morphTypeKey: typeKey ?? `${morphName}_type`,
1581
1856
  morphIdKey: idKey ?? `${morphName}_id`,
1582
- morphType: table.name
1857
+ morphType: morphType ?? morphClassOf(this)
1583
1858
  }));
1584
1859
  }
1585
- morphTo(relatedByType, morphName = "imageable", typeKey, idKey) {
1586
- return new MorphToRelationQuery(this, relatedByType, morphTo({
1587
- name: morphName,
1588
- morphTypeKey: typeKey ?? `${morphName}_type`,
1589
- morphIdKey: idKey ?? `${morphName}_id`
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`
1590
1870
  }));
1591
1871
  }
1592
1872
  async load(...names) {
@@ -1618,6 +1898,14 @@ class Model {
1618
1898
  }
1619
1899
  function registerModelRepository(model, repository) {
1620
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
+ }
1621
1909
  ensureBooted(model);
1622
1910
  return model;
1623
1911
  }
@@ -1627,6 +1915,7 @@ export {
1627
1915
  HasManyRelationQuery,
1628
1916
  HasOneRelationQuery,
1629
1917
  Model,
1918
+ ModelQuery,
1630
1919
  MorphManyRelationQuery,
1631
1920
  MorphOneRelationQuery,
1632
1921
  MorphToRelationQuery,
@@ -1634,5 +1923,6 @@ export {
1634
1923
  dehydrateValue,
1635
1924
  filterMassAssignable,
1636
1925
  hydrateValue,
1926
+ registerModelClass,
1637
1927
  registerModelRepository
1638
1928
  };