@getstrata/core 0.5.98 → 0.5.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1719,7 +1719,7 @@ function buildOperatorClauses(column, operator, params) {
1719
1719
  clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
1720
1720
  }
1721
1721
  if (operator.ilike !== undefined) {
1722
- clauses.push(`${column} ILIKE ${pushParam(params, `%${operator.ilike}%`)}`);
1722
+ clauses.push(`${column} ILIKE ${pushParam(params, operator.ilike)}`);
1723
1723
  }
1724
1724
  if (operator.tsMatch !== undefined) {
1725
1725
  clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
@@ -2084,26 +2084,67 @@ function belongsToMany(definition) {
2084
2084
  ...definition
2085
2085
  };
2086
2086
  }
2087
+ function relationMatchKey(value) {
2088
+ if (value === null || value === undefined) {
2089
+ return "";
2090
+ }
2091
+ if (typeof value === "bigint") {
2092
+ return value.toString();
2093
+ }
2094
+ if (typeof value === "number" && Number.isFinite(value)) {
2095
+ return String(value);
2096
+ }
2097
+ if (typeof value === "string" && /^-?\d+$/.test(value)) {
2098
+ return BigInt(value).toString();
2099
+ }
2100
+ return String(value);
2101
+ }
2102
+ function getByRelationKey(map, key) {
2103
+ if (map.has(key)) {
2104
+ return map.get(key);
2105
+ }
2106
+ const want = relationMatchKey(key);
2107
+ if (want === "") {
2108
+ return;
2109
+ }
2110
+ for (const [existing, value] of map) {
2111
+ if (relationMatchKey(existing) === want) {
2112
+ return value;
2113
+ }
2114
+ }
2115
+ return;
2116
+ }
2087
2117
  function indexHasManyRelation(parents, children, relation) {
2088
2118
  const groups = new Map;
2119
+ const originalKeys = new Map;
2089
2120
  for (const parent of parents) {
2090
- groups.set(parent[relation.localKey], []);
2121
+ const key = relationMatchKey(parent[relation.localKey]);
2122
+ if (!groups.has(key)) {
2123
+ groups.set(key, []);
2124
+ originalKeys.set(key, parent[relation.localKey]);
2125
+ }
2091
2126
  }
2092
2127
  for (const child of children) {
2093
- const key = child[relation.foreignKey];
2094
- const group = groups.get(key);
2128
+ const group = groups.get(relationMatchKey(child[relation.foreignKey]));
2095
2129
  if (!group) {
2096
2130
  continue;
2097
2131
  }
2098
2132
  group.push(child);
2099
2133
  }
2100
- return groups;
2134
+ const result = new Map;
2135
+ for (const [key, group] of groups) {
2136
+ const original = originalKeys.get(key);
2137
+ if (original !== undefined) {
2138
+ result.set(original, group);
2139
+ }
2140
+ }
2141
+ return result;
2101
2142
  }
2102
2143
  function indexHasOneRelation(parents, children, relation) {
2103
2144
  const grouped = indexHasManyRelation(parents, children, relation);
2104
2145
  const result = new Map;
2105
2146
  for (const parent of parents) {
2106
- const matches = grouped.get(parent[relation.localKey]) ?? [];
2147
+ const matches = getByRelationKey(grouped, parent[relation.localKey]) ?? [];
2107
2148
  result.set(parent[relation.localKey], matches[0]);
2108
2149
  }
2109
2150
  return result;
@@ -2111,12 +2152,12 @@ function indexHasOneRelation(parents, children, relation) {
2111
2152
  function indexBelongsToRelation(children, parents, relation) {
2112
2153
  const parentsById = new Map;
2113
2154
  for (const parent of parents) {
2114
- parentsById.set(parent[relation.ownerKey], parent);
2155
+ parentsById.set(relationMatchKey(parent[relation.ownerKey]), parent);
2115
2156
  }
2116
2157
  const result = new Map;
2117
2158
  for (const child of children) {
2118
2159
  const foreignKey = child[relation.foreignKey];
2119
- const parent = parentsById.get(foreignKey);
2160
+ const parent = parentsById.get(relationMatchKey(foreignKey));
2120
2161
  if (parent) {
2121
2162
  result.set(foreignKey, parent);
2122
2163
  }
@@ -2126,23 +2167,33 @@ function indexBelongsToRelation(children, parents, relation) {
2126
2167
  function indexBelongsToManyRelation(parents, pivotRows, relatedRows, relation) {
2127
2168
  const relatedById = new Map;
2128
2169
  for (const related of relatedRows) {
2129
- relatedById.set(related[relation.relatedKey], related);
2170
+ relatedById.set(relationMatchKey(related[relation.relatedKey]), related);
2130
2171
  }
2131
2172
  const groups = new Map;
2173
+ const originalKeys = new Map;
2132
2174
  for (const parent of parents) {
2133
- groups.set(parent[relation.parentKey], []);
2175
+ const key = relationMatchKey(parent[relation.parentKey]);
2176
+ if (!groups.has(key)) {
2177
+ groups.set(key, []);
2178
+ originalKeys.set(key, parent[relation.parentKey]);
2179
+ }
2134
2180
  }
2135
2181
  for (const pivot of pivotRows) {
2136
- const parentId = pivot[relation.foreignPivotKey];
2137
- const relatedId = pivot[relation.relatedPivotKey];
2138
- const group = groups.get(parentId);
2139
- const related = relatedById.get(relatedId);
2182
+ const group = groups.get(relationMatchKey(pivot[relation.foreignPivotKey]));
2183
+ const related = relatedById.get(relationMatchKey(pivot[relation.relatedPivotKey]));
2140
2184
  if (!group || !related) {
2141
2185
  continue;
2142
2186
  }
2143
2187
  group.push(related);
2144
2188
  }
2145
- return groups;
2189
+ const result = new Map;
2190
+ for (const [key, group] of groups) {
2191
+ const original = originalKeys.get(key);
2192
+ if (original !== undefined) {
2193
+ result.set(original, group);
2194
+ }
2195
+ }
2196
+ return result;
2146
2197
  }
2147
2198
  function morphMany(definition) {
2148
2199
  return {
@@ -2164,27 +2215,38 @@ function morphTo(definition) {
2164
2215
  }
2165
2216
  function indexMorphManyRelation(parents, children, relation) {
2166
2217
  const groups = new Map;
2218
+ const originalKeys = new Map;
2167
2219
  for (const parent of parents) {
2168
- groups.set(parent[relation.localKey], []);
2220
+ const key = relationMatchKey(parent[relation.localKey]);
2221
+ if (!groups.has(key)) {
2222
+ groups.set(key, []);
2223
+ originalKeys.set(key, parent[relation.localKey]);
2224
+ }
2169
2225
  }
2170
2226
  for (const child of children) {
2171
2227
  if (child[relation.morphTypeKey] !== relation.morphType) {
2172
2228
  continue;
2173
2229
  }
2174
- const key = child[relation.morphIdKey];
2175
- const group = groups.get(key);
2230
+ const group = groups.get(relationMatchKey(child[relation.morphIdKey]));
2176
2231
  if (!group) {
2177
2232
  continue;
2178
2233
  }
2179
2234
  group.push(child);
2180
2235
  }
2181
- return groups;
2236
+ const result = new Map;
2237
+ for (const [key, group] of groups) {
2238
+ const original = originalKeys.get(key);
2239
+ if (original !== undefined) {
2240
+ result.set(original, group);
2241
+ }
2242
+ }
2243
+ return result;
2182
2244
  }
2183
2245
  function indexMorphOneRelation(parents, children, relation) {
2184
2246
  const grouped = indexMorphManyRelation(parents, children, relation);
2185
2247
  const result = new Map;
2186
2248
  for (const parent of parents) {
2187
- const matches = grouped.get(parent[relation.localKey]) ?? [];
2249
+ const matches = getByRelationKey(grouped, parent[relation.localKey]) ?? [];
2188
2250
  result.set(parent[relation.localKey], matches[0]);
2189
2251
  }
2190
2252
  return result;
@@ -2197,7 +2259,7 @@ function indexMorphToRelation(children, parentsByType, relation) {
2197
2259
  if (!parents) {
2198
2260
  continue;
2199
2261
  }
2200
- const parent = parents.get(child[relation.morphIdKey]);
2262
+ const parent = getByRelationKey(parents, child[relation.morphIdKey]);
2201
2263
  if (parent) {
2202
2264
  result.set(child[relation.morphIdKey], parent);
2203
2265
  }
@@ -2362,13 +2424,30 @@ class RepositoryQuery {
2362
2424
  });
2363
2425
  return this;
2364
2426
  }
2427
+ withBelongsToMany(as, relation, relatedRepository, options = {}) {
2428
+ this.eagerLoads.push({
2429
+ kind: "belongsToMany",
2430
+ as,
2431
+ relation,
2432
+ repository: relatedRepository,
2433
+ options
2434
+ });
2435
+ return this;
2436
+ }
2365
2437
  async get() {
2366
2438
  const rows = await this.repository.findAll(this.buildOptions());
2367
2439
  return await this.attach(rows);
2368
2440
  }
2369
2441
  async first() {
2370
- const rows = await this.get();
2371
- return rows[0] ?? null;
2442
+ const rows = await this.repository.findAll({ ...this.buildOptions(), limit: 1 });
2443
+ const attached = await this.attach(rows);
2444
+ return attached[0] ?? null;
2445
+ }
2446
+ async count() {
2447
+ return await this.repository.count(this.buildOptions());
2448
+ }
2449
+ async attachToRows(rows) {
2450
+ return await this.attach(rows);
2372
2451
  }
2373
2452
  async paginate(options) {
2374
2453
  return await this.repository.paginate({
@@ -2418,7 +2497,7 @@ class RepositoryQuery {
2418
2497
  const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation2, load.options);
2419
2498
  result = result.map((row) => ({
2420
2499
  ...row,
2421
- [load.as]: grouped2.get(row[relation2.localKey]) ?? []
2500
+ [load.as]: getByRelationKey(grouped2, row[relation2.localKey]) ?? []
2422
2501
  }));
2423
2502
  continue;
2424
2503
  }
@@ -2427,7 +2506,7 @@ class RepositoryQuery {
2427
2506
  const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation2, load.options);
2428
2507
  result = result.map((row) => ({
2429
2508
  ...row,
2430
- [load.as]: grouped2.get(row[relation2.localKey]) ?? []
2509
+ [load.as]: getByRelationKey(grouped2, row[relation2.localKey]) ?? []
2431
2510
  }));
2432
2511
  continue;
2433
2512
  }
@@ -2436,7 +2515,16 @@ class RepositoryQuery {
2436
2515
  const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation2, load.options);
2437
2516
  result = result.map((row) => ({
2438
2517
  ...row,
2439
- [load.as]: grouped2.get(row[relation2.localKey])
2518
+ [load.as]: getByRelationKey(grouped2, row[relation2.localKey])
2519
+ }));
2520
+ continue;
2521
+ }
2522
+ if (load.kind === "belongsToMany") {
2523
+ const relation2 = load.relation;
2524
+ const grouped2 = await this.repository.loadBelongsToManyForParents(rows, relation2, load.repository, load.options);
2525
+ result = result.map((row) => ({
2526
+ ...row,
2527
+ [load.as]: getByRelationKey(grouped2, row[relation2.parentKey]) ?? []
2440
2528
  }));
2441
2529
  continue;
2442
2530
  }
@@ -2445,7 +2533,7 @@ class RepositoryQuery {
2445
2533
  const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
2446
2534
  result = result.map((row) => ({
2447
2535
  ...row,
2448
- [load.as]: grouped2.get(row[relation2.morphIdKey])
2536
+ [load.as]: getByRelationKey(grouped2, row[relation2.morphIdKey])
2449
2537
  }));
2450
2538
  continue;
2451
2539
  }
@@ -2453,7 +2541,7 @@ class RepositoryQuery {
2453
2541
  const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
2454
2542
  result = result.map((row) => ({
2455
2543
  ...row,
2456
- [load.as]: grouped.get(row[relation.foreignKey])
2544
+ [load.as]: getByRelationKey(grouped, row[relation.foreignKey])
2457
2545
  }));
2458
2546
  }
2459
2547
  return result;
@@ -2468,6 +2556,10 @@ class BaseRepository {
2468
2556
  this.table = table;
2469
2557
  this.connection = connection;
2470
2558
  }
2559
+ async count(options = {}) {
2560
+ const { whereNodes, where, ...rest } = options;
2561
+ return await this.countWhere(where ?? {}, rest, whereNodes ?? []);
2562
+ }
2471
2563
  async findAll(options = {}) {
2472
2564
  return await withDatabaseErrorHandling(async () => {
2473
2565
  const { whereNodes, ...queryOptions } = options;
@@ -2740,7 +2832,7 @@ class BaseRepository {
2740
2832
  const grouped = await this.loadMorphManyForParents(parents, relation, options);
2741
2833
  const result = new Map;
2742
2834
  for (const parent of parents) {
2743
- const matches = grouped.get(parent[relation.localKey]) ?? [];
2835
+ const matches = getByRelationKey(grouped, parent[relation.localKey]) ?? [];
2744
2836
  result.set(parent[relation.localKey], matches[0]);
2745
2837
  }
2746
2838
  return result;
@@ -2769,12 +2861,29 @@ class BaseRepository {
2769
2861
  }, options);
2770
2862
  const indexed = new Map;
2771
2863
  for (const parent of parents) {
2772
- indexed.set(parent[ownerKey], parent);
2864
+ indexed.set(relationMatchKey(parent[ownerKey]), parent);
2773
2865
  }
2774
2866
  parentsByType.set(morphType, indexed);
2775
2867
  }
2776
2868
  return indexMorphToRelation(children, parentsByType, relation);
2777
2869
  }
2870
+ async loadBelongsToManyForParents(parents, relation, relatedRepository, options = {}) {
2871
+ if (parents.length === 0) {
2872
+ return indexBelongsToManyRelation(parents, [], [], relation);
2873
+ }
2874
+ const parentIds = [...new Set(parents.map((parent) => parent[relation.parentKey]))];
2875
+ const pivotRows = await this.connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} = ANY($1)`, [parentIds]);
2876
+ if (pivotRows.length === 0) {
2877
+ return indexBelongsToManyRelation(parents, [], [], relation);
2878
+ }
2879
+ const relatedIds = [
2880
+ ...new Set(pivotRows.map((row) => row[relation.relatedPivotKey]))
2881
+ ];
2882
+ const relatedRows = await relatedRepository.withConnection(this.connection).findWhere({
2883
+ [relation.relatedKey]: relatedIds
2884
+ }, options);
2885
+ return indexBelongsToManyRelation(parents, pivotRows, relatedRows, relation);
2886
+ }
2778
2887
  }
2779
2888
  var baseRepository_default = BaseRepository;
2780
2889
  // ../../src/core/database/bunSql.ts
@@ -2804,7 +2913,40 @@ function createDatabaseConnection(source) {
2804
2913
  }
2805
2914
  };
2806
2915
  }
2916
+ // ../../src/core/database/inflection.ts
2917
+ function singularize(word) {
2918
+ if (word.endsWith("ies") && word.length > 3) {
2919
+ return `${word.slice(0, -3)}y`;
2920
+ }
2921
+ if (/(ses|xes|zes|ches|shes)$/i.test(word)) {
2922
+ return word.slice(0, -2);
2923
+ }
2924
+ if (word.endsWith("s") && !word.endsWith("ss")) {
2925
+ return word.slice(0, -1);
2926
+ }
2927
+ return word;
2928
+ }
2929
+ function foreignKeyFromTable(tableName) {
2930
+ return `${singularize(tableName)}_id`;
2931
+ }
2932
+ function pivotTableName(leftTable, rightTable) {
2933
+ return [singularize(leftTable), singularize(rightTable)].sort().join("_");
2934
+ }
2935
+
2807
2936
  // ../../src/core/database/factory.ts
2937
+ function inferFactoryForeignKey(parent, explicit) {
2938
+ if (explicit) {
2939
+ if (explicit.endsWith("_id")) {
2940
+ return explicit;
2941
+ }
2942
+ return `${explicit}_id`;
2943
+ }
2944
+ if (typeof parent.getRepository === "function") {
2945
+ return foreignKeyFromTable(parent.getRepository().getTable().name);
2946
+ }
2947
+ throw new Error("Factory.for() requires a foreign key or a parent Model.");
2948
+ }
2949
+
2808
2950
  class Factory {
2809
2951
  quantity = 1;
2810
2952
  counted = false;
@@ -2815,6 +2957,7 @@ class Factory {
2815
2957
  children = [];
2816
2958
  afterMakingCallbacks = [];
2817
2959
  afterCreatingCallbacks = [];
2960
+ model;
2818
2961
  definition() {
2819
2962
  throw new Error("Factory definition must be implemented by subclass.");
2820
2963
  }
@@ -2855,8 +2998,9 @@ class Factory {
2855
2998
  if (parent.id === undefined || parent.id === null) {
2856
2999
  throw new Error("Factory.for() requires a parent with an id.");
2857
3000
  }
3001
+ const key = inferFactoryForeignKey(parent, foreignKey);
2858
3002
  const next = this.clone();
2859
- next.parentAssociations = [...this.parentAssociations, { foreignKey, value: parent.id }];
3003
+ next.parentAssociations = [...this.parentAssociations, { foreignKey: key, value: parent.id }];
2860
3004
  return next;
2861
3005
  }
2862
3006
  recycle(parent, foreignKey) {
@@ -2937,29 +3081,17 @@ class Factory {
2937
3081
  }
2938
3082
  return values;
2939
3083
  }
2940
- persist(_values) {
3084
+ async persist(values) {
3085
+ if (this.model) {
3086
+ const created = await this.model.create(values);
3087
+ if (created && typeof created.toObject === "function") {
3088
+ return created.toObject();
3089
+ }
3090
+ return created;
3091
+ }
2941
3092
  throw new Error("Factory.persist() must be implemented to use create().");
2942
3093
  }
2943
3094
  }
2944
- // ../../src/core/database/inflection.ts
2945
- function singularize(word) {
2946
- if (word.endsWith("ies") && word.length > 3) {
2947
- return `${word.slice(0, -3)}y`;
2948
- }
2949
- if (/(ses|xes|zes|ches|shes)$/i.test(word)) {
2950
- return word.slice(0, -2);
2951
- }
2952
- if (word.endsWith("s") && !word.endsWith("ss")) {
2953
- return word.slice(0, -1);
2954
- }
2955
- return word;
2956
- }
2957
- function foreignKeyFromTable(tableName) {
2958
- return `${singularize(tableName)}_id`;
2959
- }
2960
- function pivotTableName(leftTable, rightTable) {
2961
- return [singularize(leftTable), singularize(rightTable)].sort().join("_");
2962
- }
2963
3095
  // ../../src/core/database/migrations/advisoryLock.ts
2964
3096
  var MIGRATION_LOCK_KEY = 42424242;
2965
3097
  async function withMigrationLock(db, callback, lockKey = MIGRATION_LOCK_KEY) {
@@ -3085,6 +3217,9 @@ function ownerId(owner, ownerKey) {
3085
3217
  }
3086
3218
  throw new Error("belongsTo.associate() requires a related model or { id }.");
3087
3219
  }
3220
+ function thenGet(get, onfulfilled, onrejected) {
3221
+ return get().then(onfulfilled ?? undefined, onrejected ?? undefined);
3222
+ }
3088
3223
 
3089
3224
  class HasManyRelationQuery {
3090
3225
  parent;
@@ -3148,13 +3283,28 @@ class HasManyRelationQuery {
3148
3283
  return rows[0] ?? null;
3149
3284
  }
3150
3285
  async count() {
3151
- return (await this.get()).length;
3286
+ return this.scopedQuery().count();
3287
+ }
3288
+ then(onfulfilled, onrejected) {
3289
+ return thenGet(() => this.get(), onfulfilled, onrejected);
3152
3290
  }
3153
3291
  async create(attributes = {}) {
3154
3292
  return this.related.create(attributes, {
3155
3293
  [this.relation.foreignKey]: this.parent.get(this.relation.localKey)
3156
3294
  });
3157
3295
  }
3296
+ async save(related) {
3297
+ const forced = {
3298
+ [this.relation.foreignKey]: this.parent.get(this.relation.localKey)
3299
+ };
3300
+ const savable = related;
3301
+ if (typeof savable.save === "function") {
3302
+ savable.mergeAttributes?.(forced);
3303
+ await savable.save();
3304
+ return related;
3305
+ }
3306
+ return this.create(related);
3307
+ }
3158
3308
  async createMany(records) {
3159
3309
  const created = [];
3160
3310
  for (const attributes of records) {
@@ -3202,11 +3352,17 @@ class HasOneRelationQuery {
3202
3352
  return this.get();
3203
3353
  }
3204
3354
  async count() {
3205
- return await this.get() ? 1 : 0;
3355
+ return this.inner.count();
3356
+ }
3357
+ then(onfulfilled, onrejected) {
3358
+ return thenGet(() => this.get(), onfulfilled, onrejected);
3206
3359
  }
3207
3360
  async create(attributes = {}) {
3208
3361
  return this.inner.create(attributes);
3209
3362
  }
3363
+ async save(related) {
3364
+ return this.inner.save(related);
3365
+ }
3210
3366
  }
3211
3367
 
3212
3368
  class BelongsToRelationQuery {
@@ -3214,12 +3370,17 @@ class BelongsToRelationQuery {
3214
3370
  related;
3215
3371
  relation;
3216
3372
  kind = "belongsTo";
3373
+ extraWhere = {};
3217
3374
  extraOptions = {};
3218
3375
  constructor(parent, related, relation) {
3219
3376
  this.parent = parent;
3220
3377
  this.related = related;
3221
3378
  this.relation = relation;
3222
3379
  }
3380
+ where(where) {
3381
+ this.extraWhere = { ...this.extraWhere, ...where };
3382
+ return this;
3383
+ }
3223
3384
  orderBy(orderBy) {
3224
3385
  this.extraOptions = { ...this.extraOptions, orderBy };
3225
3386
  return this;
@@ -3233,10 +3394,10 @@ class BelongsToRelationQuery {
3233
3394
  }
3234
3395
  toExistsClause(parentTable) {
3235
3396
  const relatedTable = this.related.repository().getTable().name;
3236
- return {
3237
- sql: `SELECT 1 FROM ${quoteIdentifier(relatedTable)} WHERE ${qualifyColumn(relatedTable, this.relation.ownerKey)} = ${qualifyColumn(parentTable, this.relation.foreignKey)}`,
3238
- params: []
3239
- };
3397
+ const extra = buildAdvancedWhereClause(relatedTable, this.extraWhere, [], []);
3398
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
3399
+ const sql = `SELECT 1 FROM ${quoteIdentifier(relatedTable)} WHERE ${qualifyColumn(relatedTable, this.relation.ownerKey)} = ${qualifyColumn(parentTable, this.relation.foreignKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
3400
+ return { sql, params: extra.params };
3240
3401
  }
3241
3402
  async get() {
3242
3403
  const foreign = this.parent.get(this.relation.foreignKey);
@@ -3244,7 +3405,7 @@ class BelongsToRelationQuery {
3244
3405
  return null;
3245
3406
  }
3246
3407
  const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
3247
- let query = repository.query(asWhere({ [this.relation.ownerKey]: foreign }));
3408
+ let query = repository.query(asWhere({ [this.relation.ownerKey]: foreign, ...this.extraWhere }));
3248
3409
  if (this.extraOptions.orderBy) {
3249
3410
  query = query.orderBy(this.extraOptions.orderBy);
3250
3411
  }
@@ -3254,6 +3415,9 @@ class BelongsToRelationQuery {
3254
3415
  async first() {
3255
3416
  return this.get();
3256
3417
  }
3418
+ then(onfulfilled, onrejected) {
3419
+ return thenGet(() => this.get(), onfulfilled, onrejected);
3420
+ }
3257
3421
  async associate(owner) {
3258
3422
  await this.parent.getRepository().updateById(this.parent.id, {
3259
3423
  [this.relation.foreignKey]: ownerId(owner, this.relation.ownerKey)
@@ -3273,6 +3437,7 @@ class BelongsToManyRelationQuery {
3273
3437
  kind = "belongsToMany";
3274
3438
  extraWhere = {};
3275
3439
  extraOptions = {};
3440
+ pivotValues = {};
3276
3441
  constructor(parent, related, relation) {
3277
3442
  this.parent = parent;
3278
3443
  this.related = related;
@@ -3286,7 +3451,13 @@ class BelongsToManyRelationQuery {
3286
3451
  this.extraOptions = { ...this.extraOptions, orderBy };
3287
3452
  return this;
3288
3453
  }
3289
- applyEagerLoad(_query, _alias) {}
3454
+ applyEagerLoad(query, alias) {
3455
+ query.withBelongsToMany(alias, this.relation, this.related.repository(), this.extraOptions);
3456
+ }
3457
+ withPivotValues(values) {
3458
+ this.pivotValues = { ...this.pivotValues, ...values };
3459
+ return this;
3460
+ }
3290
3461
  hydrateEager(row, alias) {
3291
3462
  const value = row[alias] ?? [];
3292
3463
  const rows = Array.isArray(value) ? value : [];
@@ -3326,13 +3497,34 @@ class BelongsToManyRelationQuery {
3326
3497
  return rows[0] ?? null;
3327
3498
  }
3328
3499
  async count() {
3329
- return (await this.get()).length;
3500
+ const parentId = this.parent.get(this.relation.parentKey);
3501
+ const rows = await this.connection().unsafe(`SELECT COUNT(*) AS count FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1`, [parentId]);
3502
+ return Number(rows[0]?.count ?? 0);
3503
+ }
3504
+ then(onfulfilled, onrejected) {
3505
+ return thenGet(() => this.get(), onfulfilled, onrejected);
3330
3506
  }
3331
3507
  async attach(ids) {
3508
+ const list = Array.isArray(ids) ? ids : [ids];
3509
+ const parentId = this.parent.get(this.relation.parentKey);
3510
+ const extraKeys = Object.keys(this.pivotValues);
3511
+ const extraColumns = extraKeys.length > 0 ? `, ${extraKeys.join(", ")}` : "";
3512
+ const extraPlaceholders = extraKeys.map((_, index) => `$${index + 3}`).join(", ");
3513
+ const extraValues = extraKeys.map((key) => this.pivotValues[key]);
3514
+ for (const id of list) {
3515
+ 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]);
3516
+ }
3517
+ }
3518
+ async toggle(ids) {
3332
3519
  const list = Array.isArray(ids) ? ids : [ids];
3333
3520
  const parentId = this.parent.get(this.relation.parentKey);
3334
3521
  for (const id of list) {
3335
- await this.connection().unsafe(`INSERT INTO ${this.relation.pivotTable} (${String(this.relation.foreignPivotKey)}, ${String(this.relation.relatedPivotKey)}) VALUES ($1, $2)`, [parentId, id]);
3522
+ 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]);
3523
+ if (existing.length > 0) {
3524
+ await this.detach(id);
3525
+ } else {
3526
+ await this.attach(id);
3527
+ }
3336
3528
  }
3337
3529
  }
3338
3530
  async detach(ids) {
@@ -3404,6 +3596,17 @@ class MorphManyRelationQuery {
3404
3596
  const rows = await this.get();
3405
3597
  return rows[0] ?? null;
3406
3598
  }
3599
+ async count() {
3600
+ const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
3601
+ return repository.query(asWhere({
3602
+ [this.relation.morphTypeKey]: this.relation.morphType,
3603
+ [this.relation.morphIdKey]: this.parent.get(this.relation.localKey),
3604
+ ...this.extraWhere
3605
+ })).count();
3606
+ }
3607
+ then(onfulfilled, onrejected) {
3608
+ return thenGet(() => this.get(), onfulfilled, onrejected);
3609
+ }
3407
3610
  async create(attributes = {}) {
3408
3611
  return this.related.create(attributes, {
3409
3612
  [this.relation.morphTypeKey]: this.relation.morphType,
@@ -3444,6 +3647,15 @@ class MorphOneRelationQuery {
3444
3647
  async get() {
3445
3648
  return this.inner.first();
3446
3649
  }
3650
+ async first() {
3651
+ return this.get();
3652
+ }
3653
+ async count() {
3654
+ return this.inner.count();
3655
+ }
3656
+ then(onfulfilled, onrejected) {
3657
+ return thenGet(() => this.get(), onfulfilled, onrejected);
3658
+ }
3447
3659
  async create(attributes = {}) {
3448
3660
  return this.inner.create(attributes);
3449
3661
  }
@@ -3454,11 +3666,16 @@ class MorphToRelationQuery {
3454
3666
  relatedByType;
3455
3667
  relation;
3456
3668
  kind = "morphTo";
3669
+ extraWhere = {};
3457
3670
  constructor(parent, relatedByType, relation) {
3458
3671
  this.parent = parent;
3459
3672
  this.relatedByType = relatedByType;
3460
3673
  this.relation = relation;
3461
3674
  }
3675
+ where(where) {
3676
+ this.extraWhere = { ...this.extraWhere, ...where };
3677
+ return this;
3678
+ }
3462
3679
  applyEagerLoad(query, alias) {
3463
3680
  const repositories = new Map(Object.entries(this.relatedByType).map(([type, model]) => [
3464
3681
  type,
@@ -3476,9 +3693,11 @@ class MorphToRelationQuery {
3476
3693
  return { sql: "SELECT 1 WHERE 1 = 0", params: [] };
3477
3694
  }
3478
3695
  const relatedTable = related.repository().getTable();
3696
+ const extra = buildAdvancedWhereClause(relatedTable.name, this.extraWhere, [], []);
3697
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
3479
3698
  return {
3480
- sql: `SELECT 1 FROM ${quoteIdentifier(relatedTable.name)} WHERE ${qualifyColumn(relatedTable.name, relatedTable.primaryKey)} = ${qualifyColumn(parentTable, this.relation.morphIdKey)}`,
3481
- params: []
3699
+ sql: `SELECT 1 FROM ${quoteIdentifier(relatedTable.name)} WHERE ${qualifyColumn(relatedTable.name, relatedTable.primaryKey)} = ${qualifyColumn(parentTable, this.relation.morphIdKey)}${extraSql ? ` AND ${extraSql}` : ""}`,
3700
+ params: extra.params
3482
3701
  };
3483
3702
  }
3484
3703
  async get() {
@@ -3489,13 +3708,17 @@ class MorphToRelationQuery {
3489
3708
  return null;
3490
3709
  }
3491
3710
  const table = related.repository().getTable();
3492
- const row = await related.repository().withConnection(this.parent.getRepository().getConnection()).query(asWhere({ [table.primaryKey]: id })).first();
3711
+ const row = await related.repository().withConnection(this.parent.getRepository().getConnection()).query(asWhere({ [table.primaryKey]: id, ...this.extraWhere })).first();
3493
3712
  return row ? related.newFromRecord(row) : null;
3494
3713
  }
3714
+ then(onfulfilled, onrejected) {
3715
+ return thenGet(() => this.get(), onfulfilled, onrejected);
3716
+ }
3495
3717
  }
3496
3718
 
3497
3719
  // ../../src/core/database/model.ts
3498
3720
  var modelRepositories = new WeakMap;
3721
+ var namedModels = new Map;
3499
3722
  var modelGlobalScopes = new WeakMap;
3500
3723
  var modelObservers = new WeakMap;
3501
3724
  var modelBooted = new WeakSet;
@@ -3513,38 +3736,59 @@ function accessorName(key) {
3513
3736
  const pascal = key.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase());
3514
3737
  return `get${pascal.charAt(0).toUpperCase()}${pascal.slice(1)}Attribute`;
3515
3738
  }
3516
- function constrainRelationExists(model, name, constrain, not) {
3517
- const statics = modelStatics(model);
3518
- ensureBooted(model);
3519
- const repository = resolveModelRepository(model);
3520
- const dummy = statics.newFromRecord({});
3521
- const method = dummy[name];
3522
- if (typeof method !== "function") {
3523
- throw new Error(`${model.name} has no relation method ${name}().`);
3524
- }
3525
- const relationQuery = method.call(dummy);
3526
- constrain?.(relationQuery);
3527
- const exists = relationQuery.toExistsClause(repository.getTable().name);
3528
- const query = Model.query.call(model);
3529
- return not ? query.whereNotExists(exists.sql, exists.params) : query.whereExists(exists.sql, exists.params);
3739
+ function isLoadableModel(value) {
3740
+ return Boolean(value && typeof value === "object" && typeof value.load === "function" && typeof value.loaded === "function" && typeof value.setLoaded === "function");
3530
3741
  }
3531
- async function loadNested(model, path) {
3532
- const [head, ...rest] = path.split(".");
3533
- if (!head) {
3742
+ async function eagerLoadOnModels(models, paths) {
3743
+ if (models.length === 0 || paths.length === 0) {
3534
3744
  return;
3535
3745
  }
3536
- await model.load(head);
3537
- if (rest.length === 0) {
3538
- return;
3746
+ const grouped = new Map;
3747
+ for (const path of paths) {
3748
+ const [head, ...rest] = path.split(".");
3749
+ if (!head) {
3750
+ continue;
3751
+ }
3752
+ const nested = rest.join(".");
3753
+ const existing = grouped.get(head) ?? [];
3754
+ if (nested) {
3755
+ existing.push(nested);
3756
+ }
3757
+ grouped.set(head, existing);
3539
3758
  }
3540
- const loaded = model.loaded(head);
3541
- const children = Array.isArray(loaded) ? loaded : loaded ? [loaded] : [];
3542
- for (const child of children) {
3543
- if (child && typeof child === "object" && typeof child.load === "function") {
3544
- await loadNested(child, rest.join("."));
3759
+ for (const [head, nested] of grouped) {
3760
+ const unloaded = models.filter((model) => model.loaded(head) === undefined);
3761
+ if (unloaded.length > 0) {
3762
+ const first = unloaded[0];
3763
+ if (!first) {
3764
+ continue;
3765
+ }
3766
+ const method = first[head];
3767
+ if (typeof method !== "function") {
3768
+ throw new Error(`${first.constructor.name} has no relation method ${head}().`);
3769
+ }
3770
+ const relationQuery = method.call(first);
3771
+ const query = first.getRepository().query();
3772
+ relationQuery.applyEagerLoad(query, head);
3773
+ const attached = await query.attachToRows(unloaded.map((model) => model.toObject()));
3774
+ for (const [index, model] of unloaded.entries()) {
3775
+ const row = attached[index] ?? model.toObject();
3776
+ model.setLoaded(head, relationQuery.hydrateEager(row, head));
3777
+ }
3778
+ }
3779
+ if (nested.length === 0) {
3780
+ continue;
3545
3781
  }
3782
+ const children = models.flatMap((model) => {
3783
+ const loaded = model.loaded(head);
3784
+ return Array.isArray(loaded) ? loaded : loaded ? [loaded] : [];
3785
+ });
3786
+ await eagerLoadOnModels(children.filter(isLoadableModel), nested);
3546
3787
  }
3547
3788
  }
3789
+ async function loadNested(model, path) {
3790
+ await eagerLoadOnModels([model], [path]);
3791
+ }
3548
3792
  function resolveModelRepository(model) {
3549
3793
  const repository = modelRepositories.get(model);
3550
3794
  if (!repository) {
@@ -3552,6 +3796,48 @@ function resolveModelRepository(model) {
3552
3796
  }
3553
3797
  return repository;
3554
3798
  }
3799
+ function registerModelClass(name, model) {
3800
+ namedModels.set(name, model);
3801
+ }
3802
+ function resolveRelated(related) {
3803
+ if (typeof related === "string") {
3804
+ const found = namedModels.get(related);
3805
+ if (!found) {
3806
+ throw new Error(`Model [${related}] is not registered. Call registerModelClass() first.`);
3807
+ }
3808
+ return found;
3809
+ }
3810
+ if (typeof related === "function" && typeof related.repository !== "function") {
3811
+ return related();
3812
+ }
3813
+ return related;
3814
+ }
3815
+ function inferRelationMethodName(callee) {
3816
+ const stack = new Error().stack ?? "";
3817
+ let seenCallee = false;
3818
+ for (const line of stack.split(`
3819
+ `)) {
3820
+ const match = /at (?:async )?(?:[^.\s]+\.)?(\w+)/.exec(line);
3821
+ const name = match?.[1];
3822
+ if (!name || name === "Error" || name === "inferRelationMethodName") {
3823
+ continue;
3824
+ }
3825
+ if (!seenCallee) {
3826
+ if (name === callee) {
3827
+ seenCallee = true;
3828
+ }
3829
+ continue;
3830
+ }
3831
+ if (name !== callee) {
3832
+ return name;
3833
+ }
3834
+ }
3835
+ return;
3836
+ }
3837
+ function morphClassOf(model) {
3838
+ const statics = modelStatics(model.constructor === Function ? model : model.constructor);
3839
+ return statics.$morphClass ?? (model.constructor === Function ? model.name : model.constructor.name);
3840
+ }
3555
3841
  function modelStatics(model) {
3556
3842
  return model;
3557
3843
  }
@@ -3581,6 +3867,11 @@ function hydrateValue(value, cast) {
3581
3867
  case "bool":
3582
3868
  case "boolean":
3583
3869
  return value === true || value === 1 || value === "1" || value === "true";
3870
+ case "integer":
3871
+ case "int":
3872
+ return value === "" ? null : Number(value);
3873
+ case "hashed":
3874
+ return value;
3584
3875
  default:
3585
3876
  return value;
3586
3877
  }
@@ -3598,6 +3889,11 @@ function dehydrateValue(value, cast) {
3598
3889
  case "bool":
3599
3890
  case "boolean":
3600
3891
  return Boolean(value);
3892
+ case "integer":
3893
+ case "int":
3894
+ return value === "" ? null : Number(value);
3895
+ case "hashed":
3896
+ return value;
3601
3897
  default:
3602
3898
  return value;
3603
3899
  }
@@ -3652,6 +3948,153 @@ function applyTimestampsOnUpdate(columns, values, enabled) {
3652
3948
  return result;
3653
3949
  }
3654
3950
 
3951
+ class ModelQuery {
3952
+ modelClass;
3953
+ query;
3954
+ eager = [];
3955
+ constructor(modelClass, query) {
3956
+ this.modelClass = modelClass;
3957
+ this.query = query;
3958
+ }
3959
+ with(...relations) {
3960
+ const statics = modelStatics(this.modelClass);
3961
+ ensureBooted(this.modelClass);
3962
+ const dummy = statics.newFromRecord({}, false);
3963
+ for (const path of relations) {
3964
+ const name = path.split(".")[0] ?? path;
3965
+ const method = dummy[name];
3966
+ if (typeof method !== "function") {
3967
+ throw new Error(`${this.modelClass.name} has no relation method ${name}().`);
3968
+ }
3969
+ const relationQuery = method.call(dummy);
3970
+ this.eager.push({ name, path, relationQuery });
3971
+ relationQuery.applyEagerLoad(this.query, name);
3972
+ }
3973
+ return this;
3974
+ }
3975
+ where(input) {
3976
+ this.query.where(input);
3977
+ return this;
3978
+ }
3979
+ orWhere(input) {
3980
+ this.query.orWhere(input);
3981
+ return this;
3982
+ }
3983
+ orderBy(orderBy) {
3984
+ this.query.orderBy(orderBy);
3985
+ return this;
3986
+ }
3987
+ limit(limit) {
3988
+ this.query.limit(limit);
3989
+ return this;
3990
+ }
3991
+ offset(offset) {
3992
+ this.query.offset(offset);
3993
+ return this;
3994
+ }
3995
+ whereNull(column) {
3996
+ this.query.whereNull(column);
3997
+ return this;
3998
+ }
3999
+ whereIn(column, values) {
4000
+ this.query.whereIn(column, values);
4001
+ return this;
4002
+ }
4003
+ whereExists(sql, params = []) {
4004
+ this.query.whereExists(sql, params);
4005
+ return this;
4006
+ }
4007
+ whereNotExists(sql, params = []) {
4008
+ this.query.whereNotExists(sql, params);
4009
+ return this;
4010
+ }
4011
+ whereHas(name, constrain) {
4012
+ return this.constrainExists(name, constrain, false);
4013
+ }
4014
+ has(name) {
4015
+ return this.constrainExists(name, undefined, false);
4016
+ }
4017
+ doesntHave(name) {
4018
+ return this.constrainExists(name, undefined, true);
4019
+ }
4020
+ whereDoesntHave(name, constrain) {
4021
+ return this.constrainExists(name, constrain, true);
4022
+ }
4023
+ withHasMany(...args) {
4024
+ this.query.withHasMany(...args);
4025
+ return this;
4026
+ }
4027
+ withBelongsTo(...args) {
4028
+ this.query.withBelongsTo(...args);
4029
+ return this;
4030
+ }
4031
+ withBelongsToMany(...args) {
4032
+ this.query.withBelongsToMany(...args);
4033
+ return this;
4034
+ }
4035
+ withMorphMany(...args) {
4036
+ this.query.withMorphMany(...args);
4037
+ return this;
4038
+ }
4039
+ withMorphOne(...args) {
4040
+ this.query.withMorphOne(...args);
4041
+ return this;
4042
+ }
4043
+ withMorphTo(...args) {
4044
+ this.query.withMorphTo(...args);
4045
+ return this;
4046
+ }
4047
+ async get() {
4048
+ const statics = modelStatics(this.modelClass);
4049
+ const rows = await this.query.get();
4050
+ const models = [];
4051
+ for (const row of rows) {
4052
+ const model = statics.newFromRecord(row, true);
4053
+ await runObservers(model, "retrieved");
4054
+ for (const { name, relationQuery } of this.eager) {
4055
+ model.setLoaded(name, relationQuery.hydrateEager(row, name));
4056
+ }
4057
+ models.push(model);
4058
+ }
4059
+ const nested = this.eager.filter((item) => item.path.includes(".")).map((item) => item.path);
4060
+ await eagerLoadOnModels(models.filter(isLoadableModel), nested);
4061
+ return models;
4062
+ }
4063
+ async first() {
4064
+ this.query.limit(1);
4065
+ const models = await this.get();
4066
+ return models[0] ?? null;
4067
+ }
4068
+ async find(id) {
4069
+ const primaryKey = resolveModelRepository(this.modelClass).getTable().primaryKey;
4070
+ return this.where({ [primaryKey]: id }).first();
4071
+ }
4072
+ async findOrFail(id, errorFactory) {
4073
+ const model = await this.find(id);
4074
+ if (model) {
4075
+ return model;
4076
+ }
4077
+ throw errorFactory?.(id) ?? new NotFoundError(`${this.modelClass.name} ${String(id)} not found.`);
4078
+ }
4079
+ then(onfulfilled, onrejected) {
4080
+ return this.get().then(onfulfilled ?? undefined, onrejected ?? undefined);
4081
+ }
4082
+ constrainExists(name, constrain, not) {
4083
+ const statics = modelStatics(this.modelClass);
4084
+ ensureBooted(this.modelClass);
4085
+ const repository = resolveModelRepository(this.modelClass);
4086
+ const dummy = statics.newFromRecord({});
4087
+ const method = dummy[name];
4088
+ if (typeof method !== "function") {
4089
+ throw new Error(`${this.modelClass.name} has no relation method ${name}().`);
4090
+ }
4091
+ const relationQuery = method.call(dummy);
4092
+ constrain?.(relationQuery);
4093
+ const exists = relationQuery.toExistsClause(repository.getTable().name);
4094
+ return not ? this.whereNotExists(exists.sql, exists.params) : this.whereExists(exists.sql, exists.params);
4095
+ }
4096
+ }
4097
+
3655
4098
  class Model {
3656
4099
  attributes;
3657
4100
  repository;
@@ -3662,6 +4105,7 @@ class Model {
3662
4105
  static $hidden;
3663
4106
  static $visible;
3664
4107
  static $appends;
4108
+ static $morphClass;
3665
4109
  _exists;
3666
4110
  loadedRelations = {};
3667
4111
  hiddenOverrides = [];
@@ -3671,6 +4115,7 @@ class Model {
3671
4115
  this.attributes = attributes;
3672
4116
  this.repository = repository;
3673
4117
  this._exists = exists;
4118
+ this.attributes = modelStatics(this.constructor).hydrateAttributes(attributes);
3674
4119
  }
3675
4120
  getRepository() {
3676
4121
  return this.repository;
@@ -3731,7 +4176,7 @@ class Model {
3731
4176
  return this;
3732
4177
  }
3733
4178
  primaryKey() {
3734
- throw new Error(`${this.constructor.name}.primaryKey() is not implemented.`);
4179
+ return this.repository.getTable().primaryKey;
3735
4180
  }
3736
4181
  static primaryKeyField() {
3737
4182
  return resolveModelRepository(this).getTable().primaryKey;
@@ -3772,7 +4217,7 @@ class Model {
3772
4217
  for (const scope of getGlobalScopes(this)) {
3773
4218
  query = scope(query);
3774
4219
  }
3775
- return query;
4220
+ return new ModelQuery(this, query);
3776
4221
  }
3777
4222
  static newFromRecord(record, exists = true) {
3778
4223
  const repository = resolveModelRepository(this);
@@ -3791,79 +4236,36 @@ class Model {
3791
4236
  const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
3792
4237
  const payload = statics.dehydrateAttributes(withTimestamps);
3793
4238
  const pending = statics.newFromRecord({ ...payload }, false);
4239
+ if (await runObservers(pending, "saving") === false) {
4240
+ throw new Error(`${this.name}.create() was cancelled by an observer.`);
4241
+ }
3794
4242
  if (await runObservers(pending, "creating") === false) {
3795
4243
  throw new Error(`${this.name}.create() was cancelled by an observer.`);
3796
4244
  }
3797
4245
  const record = await repository.create(payload);
3798
4246
  const created = statics.fromRecord(record, repository, true);
3799
4247
  await runObservers(created, "created");
4248
+ await runObservers(created, "saved");
3800
4249
  return created;
3801
4250
  }
3802
4251
  static with(...relations) {
3803
- const statics = modelStatics(this);
3804
- ensureBooted(this);
3805
- const repository = resolveModelRepository(this);
3806
- const dummy = statics.fromRecord({}, repository, false);
3807
- const resolved = relations.map((path) => {
3808
- const name = path.split(".")[0] ?? path;
3809
- const method = dummy[name];
3810
- if (typeof method !== "function") {
3811
- throw new Error(`${this.name} has no relation method ${name}().`);
3812
- }
3813
- const relationQuery = method.call(dummy);
3814
- return { name, path, relationQuery };
3815
- });
3816
- const query = Model.query.call(this);
3817
- for (const { name, relationQuery } of resolved) {
3818
- if (relationQuery.kind !== "belongsToMany") {
3819
- relationQuery.applyEagerLoad(query, name);
3820
- }
3821
- }
3822
- return {
3823
- async get() {
3824
- const rows = await query.get();
3825
- const models = [];
3826
- for (const row of rows) {
3827
- const model = statics.fromRecord(row, repository, true);
3828
- for (const { name, path, relationQuery } of resolved) {
3829
- if (relationQuery.kind === "belongsToMany") {
3830
- await model.load(path.includes(".") ? path : name);
3831
- continue;
3832
- }
3833
- model.setLoaded(name, relationQuery.hydrateEager(row, name));
3834
- const nested = path.split(".").slice(1).join(".");
3835
- if (nested) {
3836
- await loadNested(model, path);
3837
- }
3838
- }
3839
- models.push(model);
3840
- }
3841
- return models;
3842
- },
3843
- async first() {
3844
- const [model] = await this.get();
3845
- return model ?? null;
3846
- }
3847
- };
4252
+ return Model.query.call(this).with(...relations);
3848
4253
  }
3849
4254
  static whereHas(name, constrain) {
3850
- return constrainRelationExists(this, name, constrain, false);
4255
+ return Model.query.call(this).whereHas(name, constrain);
3851
4256
  }
3852
4257
  static has(name) {
3853
- return constrainRelationExists(this, name, undefined, false);
4258
+ return Model.query.call(this).has(name);
3854
4259
  }
3855
4260
  static doesntHave(name) {
3856
- return constrainRelationExists(this, name, undefined, true);
4261
+ return Model.query.call(this).doesntHave(name);
3857
4262
  }
3858
4263
  static whereDoesntHave(name, constrain) {
3859
- return constrainRelationExists(this, name, constrain, true);
4264
+ return Model.query.call(this).whereDoesntHave(name, constrain);
3860
4265
  }
3861
4266
  static async find(id) {
3862
- const statics = modelStatics(this);
3863
- const repository = resolveModelRepository(this);
3864
- const primaryKey = repository.getTable().primaryKey;
3865
- const record = await Model.query.call(this).where({ [primaryKey]: id }).first();
3866
- return record ? statics.fromRecord(record, repository, true) : null;
4267
+ const primaryKey = resolveModelRepository(this).getTable().primaryKey;
4268
+ return Model.query.call(this).where({ [primaryKey]: id }).first();
3867
4269
  }
3868
4270
  static async findOrFail(id, errorFactory) {
3869
4271
  const model = await Model.find.call(this, id);
@@ -3873,8 +4275,6 @@ class Model {
3873
4275
  throw errorFactory?.(id) ?? new NotFoundError(`${this.name} ${String(id)} not found.`);
3874
4276
  }
3875
4277
  static async all(options = {}) {
3876
- const statics = modelStatics(this);
3877
- const repository = resolveModelRepository(this);
3878
4278
  let query = Model.query.call(this);
3879
4279
  if (options.orderBy) {
3880
4280
  query = query.orderBy(options.orderBy);
@@ -3882,21 +4282,17 @@ class Model {
3882
4282
  if (options.limit !== undefined) {
3883
4283
  query = query.limit(options.limit);
3884
4284
  }
3885
- const rows = await query.get();
3886
- return rows.map((row) => statics.fromRecord(row, repository, true));
4285
+ return query.get();
3887
4286
  }
3888
4287
  static where(where) {
3889
4288
  return Model.query.call(this).where(where);
3890
4289
  }
3891
4290
  static async firstWhere(where, options = {}) {
3892
- const statics = modelStatics(this);
3893
- const repository = resolveModelRepository(this);
3894
4291
  let query = Model.query.call(this).where(where);
3895
4292
  if (options.orderBy) {
3896
4293
  query = query.orderBy(options.orderBy);
3897
4294
  }
3898
- const record = await query.first();
3899
- return record ? statics.fromRecord(record, repository, true) : null;
4295
+ return query.first();
3900
4296
  }
3901
4297
  static async firstOrNew(where, values = {}) {
3902
4298
  const existing = await Model.firstWhere.call(this, where);
@@ -3925,6 +4321,9 @@ class Model {
3925
4321
  const casts = ModelClass.$casts ?? {};
3926
4322
  const table = this.repository.getTable();
3927
4323
  const updating = this.$exists;
4324
+ if (await runObservers(this, "saving") === false) {
4325
+ return this;
4326
+ }
3928
4327
  if (await runObservers(this, updating ? "updating" : "creating") === false) {
3929
4328
  return this;
3930
4329
  }
@@ -3933,6 +4332,7 @@ class Model {
3933
4332
  const record2 = await this.repository.updateByIdOrThrow(this.id, changes);
3934
4333
  this.attributes = ModelClass.hydrateAttributes(record2);
3935
4334
  await runObservers(this, "updated");
4335
+ await runObservers(this, "saved");
3936
4336
  return this;
3937
4337
  }
3938
4338
  const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, this.attributes);
@@ -3942,6 +4342,7 @@ class Model {
3942
4342
  this.attributes = ModelClass.hydrateAttributes(record);
3943
4343
  this._exists = true;
3944
4344
  await runObservers(this, "created");
4345
+ await runObservers(this, "saved");
3945
4346
  return this;
3946
4347
  }
3947
4348
  async update(changes) {
@@ -3974,7 +4375,7 @@ class Model {
3974
4375
  }
3975
4376
  async loadHasMany(as, relation, childRepository, options = {}) {
3976
4377
  const grouped = await childRepository.withConnection(this.repository.getConnection()).loadHasManyForParents([this.attributes], relation, options);
3977
- const loaded = grouped.get(this.attributes[relation.localKey]) ?? [];
4378
+ const loaded = getByRelationKey(grouped, this.attributes[relation.localKey]) ?? [];
3978
4379
  return Object.assign(this, { [as]: loaded });
3979
4380
  }
3980
4381
  async loadHasOne(as, relation, childRepository, options = {}) {
@@ -3984,7 +4385,7 @@ class Model {
3984
4385
  }
3985
4386
  async loadBelongsTo(as, relation, parentRepository, options = {}) {
3986
4387
  const grouped = await this.repository.loadBelongsToForParents([this.attributes], relation, parentRepository, options);
3987
- const loaded = grouped.get(this.attributes[relation.foreignKey]);
4388
+ const loaded = getByRelationKey(grouped, this.attributes[relation.foreignKey]);
3988
4389
  return Object.assign(this, { [as]: loaded });
3989
4390
  }
3990
4391
  async loadBelongsToMany(as, relation, relatedRepository, options = {}) {
@@ -4004,28 +4405,31 @@ class Model {
4004
4405
  }
4005
4406
  });
4006
4407
  const grouped = indexBelongsToManyRelation([this.attributes], pivotRows, relatedRows, relation);
4007
- const loaded = grouped.get(parentId) ?? [];
4408
+ const loaded = getByRelationKey(grouped, parentId) ?? [];
4008
4409
  return Object.assign(this, { [as]: loaded });
4009
4410
  }
4010
4411
  hasMany(related, foreignKey, localKey) {
4011
4412
  const table = this.repository.getTable();
4012
- return new HasManyRelationQuery(this, related, hasMany({
4013
- name: related.repository().getTable().name,
4413
+ const relatedClass = resolveRelated(related);
4414
+ return new HasManyRelationQuery(this, relatedClass, hasMany({
4415
+ name: relatedClass.repository().getTable().name,
4014
4416
  localKey: localKey ?? table.primaryKey,
4015
4417
  foreignKey: foreignKey ?? foreignKeyFromTable(table.name)
4016
4418
  }));
4017
4419
  }
4018
4420
  hasOne(related, foreignKey, localKey) {
4019
4421
  const table = this.repository.getTable();
4020
- return new HasOneRelationQuery(this, related, hasOne({
4021
- name: related.repository().getTable().name,
4422
+ const relatedClass = resolveRelated(related);
4423
+ return new HasOneRelationQuery(this, relatedClass, hasOne({
4424
+ name: relatedClass.repository().getTable().name,
4022
4425
  localKey: localKey ?? table.primaryKey,
4023
4426
  foreignKey: foreignKey ?? foreignKeyFromTable(table.name)
4024
4427
  }));
4025
4428
  }
4026
4429
  belongsTo(related, foreignKey, ownerKey) {
4027
- const relatedTable = related.repository().getTable();
4028
- return new BelongsToRelationQuery(this, related, belongsTo({
4430
+ const relatedClass = resolveRelated(related);
4431
+ const relatedTable = relatedClass.repository().getTable();
4432
+ return new BelongsToRelationQuery(this, relatedClass, belongsTo({
4029
4433
  name: relatedTable.name,
4030
4434
  foreignKey: foreignKey ?? foreignKeyFromTable(relatedTable.name),
4031
4435
  ownerKey: ownerKey ?? relatedTable.primaryKey
@@ -4033,8 +4437,9 @@ class Model {
4033
4437
  }
4034
4438
  belongsToMany(related, pivotTable, foreignPivotKey, relatedPivotKey) {
4035
4439
  const table = this.repository.getTable();
4036
- const relatedTable = related.repository().getTable();
4037
- return new BelongsToManyRelationQuery(this, related, belongsToMany({
4440
+ const relatedClass = resolveRelated(related);
4441
+ const relatedTable = relatedClass.repository().getTable();
4442
+ return new BelongsToManyRelationQuery(this, relatedClass, belongsToMany({
4038
4443
  name: relatedTable.name,
4039
4444
  pivotTable: pivotTable ?? pivotTableName(table.name, relatedTable.name),
4040
4445
  parentKey: table.primaryKey,
@@ -4043,31 +4448,38 @@ class Model {
4043
4448
  relatedPivotKey: relatedPivotKey ?? foreignKeyFromTable(relatedTable.name)
4044
4449
  }));
4045
4450
  }
4046
- morphMany(related, morphName, typeKey, idKey) {
4451
+ morphMany(related, morphName, typeKey, idKey, morphType) {
4047
4452
  const table = this.repository.getTable();
4048
- return new MorphManyRelationQuery(this, related, morphMany({
4453
+ const relatedClass = resolveRelated(related);
4454
+ return new MorphManyRelationQuery(this, relatedClass, morphMany({
4049
4455
  name: morphName,
4050
4456
  localKey: table.primaryKey,
4051
4457
  morphTypeKey: typeKey ?? `${morphName}_type`,
4052
4458
  morphIdKey: idKey ?? `${morphName}_id`,
4053
- morphType: table.name
4459
+ morphType: morphType ?? morphClassOf(this)
4054
4460
  }));
4055
4461
  }
4056
- morphOne(related, morphName, typeKey, idKey) {
4462
+ morphOne(related, morphName, typeKey, idKey, morphType) {
4057
4463
  const table = this.repository.getTable();
4058
- return new MorphOneRelationQuery(this, related, morphOne({
4464
+ const relatedClass = resolveRelated(related);
4465
+ return new MorphOneRelationQuery(this, relatedClass, morphOne({
4059
4466
  name: morphName,
4060
4467
  localKey: table.primaryKey,
4061
4468
  morphTypeKey: typeKey ?? `${morphName}_type`,
4062
4469
  morphIdKey: idKey ?? `${morphName}_id`,
4063
- morphType: table.name
4470
+ morphType: morphType ?? morphClassOf(this)
4064
4471
  }));
4065
4472
  }
4066
- morphTo(relatedByType, morphName = "imageable", typeKey, idKey) {
4067
- return new MorphToRelationQuery(this, relatedByType, morphTo({
4068
- name: morphName,
4069
- morphTypeKey: typeKey ?? `${morphName}_type`,
4070
- morphIdKey: idKey ?? `${morphName}_id`
4473
+ morphTo(relatedByType, morphName, typeKey, idKey) {
4474
+ const resolvedName = morphName ?? inferRelationMethodName("morphTo");
4475
+ if (!resolvedName) {
4476
+ throw new Error(`${this.constructor.name}.morphTo() needs an explicit morph name (Laravel infers it from the relation method).`);
4477
+ }
4478
+ const resolvedMap = Object.fromEntries(Object.entries(relatedByType).map(([type, related]) => [type, resolveRelated(related)]));
4479
+ return new MorphToRelationQuery(this, resolvedMap, morphTo({
4480
+ name: resolvedName,
4481
+ morphTypeKey: typeKey ?? `${resolvedName}_type`,
4482
+ morphIdKey: idKey ?? `${resolvedName}_id`
4071
4483
  }));
4072
4484
  }
4073
4485
  async load(...names) {
@@ -4099,6 +4511,14 @@ class Model {
4099
4511
  }
4100
4512
  function registerModelRepository(model, repository) {
4101
4513
  modelRepositories.set(model, repository);
4514
+ const name = model.name;
4515
+ if (name) {
4516
+ namedModels.set(name, model);
4517
+ }
4518
+ const morphClass = model.$morphClass;
4519
+ if (morphClass) {
4520
+ namedModels.set(morphClass, model);
4521
+ }
4102
4522
  ensureBooted(model);
4103
4523
  return model;
4104
4524
  }
@@ -6043,9 +6463,15 @@ class JsonResource {
6043
6463
 
6044
6464
  class ResourceCollection extends JsonResource {
6045
6465
  toArray() {
6046
- return {
6047
- data: this.resource.map((item) => item instanceof JsonResource ? item.toArray() : { ...item })
6048
- };
6466
+ const items = this.resource.map((item) => item instanceof JsonResource ? item.toArray() : { ...item });
6467
+ const wrap = this.constructor.wrap;
6468
+ if (wrap === null) {
6469
+ return { data: items };
6470
+ }
6471
+ return { [wrap]: items };
6472
+ }
6473
+ toResponse() {
6474
+ return { ...this.toArray(), ...this.extra };
6049
6475
  }
6050
6476
  }
6051
6477
  function toResourceCollection(items, transformer) {
@@ -8161,6 +8587,7 @@ export {
8161
8587
  Mailer,
8162
8588
  membershipService_default as MembershipService,
8163
8589
  Model,
8590
+ ModelQuery,
8164
8591
  MorphManyRelationQuery,
8165
8592
  MorphOneRelationQuery,
8166
8593
  MorphToRelationQuery,
@@ -8348,6 +8775,7 @@ export {
8348
8775
  readTenancyDriver,
8349
8776
  redirectResponse,
8350
8777
  registerDefaultDatabasePool,
8778
+ registerModelClass,
8351
8779
  registerModelRepository,
8352
8780
  registerShutdownHandler,
8353
8781
  renderKernelErrorChrome,