@getstrata/core 0.5.91 → 0.5.98

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
@@ -120,6 +120,22 @@ class PreconditionFailedError extends HttpError {
120
120
  super(412, message, details);
121
121
  }
122
122
  }
123
+ function isHttpErrorLike(error) {
124
+ if (typeof error !== "object" || error === null) {
125
+ return false;
126
+ }
127
+ const candidate = error;
128
+ return typeof candidate.status === "number" && Number.isInteger(candidate.status) && candidate.status >= 400 && candidate.status < 600 && typeof candidate.message === "string";
129
+ }
130
+ function toHttpError(error) {
131
+ if (error instanceof HttpError) {
132
+ return error;
133
+ }
134
+ if (!isHttpErrorLike(error)) {
135
+ return null;
136
+ }
137
+ return new HttpError(error.status, error.message, error.details);
138
+ }
123
139
 
124
140
  // ../../src/core/runtime/asyncContextStore.ts
125
141
  import { AsyncLocalStorage } from "async_hooks";
@@ -1481,6 +1497,12 @@ class ServiceContainer {
1481
1497
  resolve(key) {
1482
1498
  return this.get(key);
1483
1499
  }
1500
+ make(key) {
1501
+ return this.resolve(key);
1502
+ }
1503
+ instance(key, value) {
1504
+ return this.set(key, value);
1505
+ }
1484
1506
  has(key) {
1485
1507
  return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
1486
1508
  }
@@ -1509,6 +1531,9 @@ class ConfigStore {
1509
1531
  class EventBus {
1510
1532
  constructor() {}
1511
1533
  listeners = new Map;
1534
+ on(event, listener) {
1535
+ return this.listen(event, listener);
1536
+ }
1512
1537
  listen(event, listener) {
1513
1538
  const handlers = this.listeners.get(event) ?? new Set;
1514
1539
  handlers.add(listener);
@@ -1520,6 +1545,9 @@ class EventBus {
1520
1545
  }
1521
1546
  };
1522
1547
  }
1548
+ async emit(event, payload) {
1549
+ await this.dispatch(event, payload);
1550
+ }
1523
1551
  async dispatch(event, payload) {
1524
1552
  const handlers = this.listeners.get(event);
1525
1553
  if (!handlers || handlers.size === 0) {
@@ -1575,8 +1603,9 @@ function getPostgresSqlState(error) {
1575
1603
  return;
1576
1604
  }
1577
1605
  function mapDatabaseError(error) {
1578
- if (error instanceof HttpError) {
1579
- return error;
1606
+ const httpError = toHttpError(error);
1607
+ if (httpError) {
1608
+ return httpError;
1580
1609
  }
1581
1610
  if (!isPostgresError(error)) {
1582
1611
  const message = error instanceof Error ? error.message : "Database operation failed.";
@@ -1720,7 +1749,16 @@ function appendWhereParts(tableName, where, params) {
1720
1749
  }
1721
1750
  return clauses.join(" AND ");
1722
1751
  }
1752
+ function remapExistsSql(sql, existsParams, params) {
1753
+ const offset = params.length;
1754
+ params.push(...existsParams);
1755
+ return sql.replace(/\$(\d+)/g, (_match, index) => `$${offset + Number(index)}`);
1756
+ }
1723
1757
  function buildWhereNodeClause(tableName, node, params) {
1758
+ if ("exists" in node) {
1759
+ const body = remapExistsSql(node.exists.sql, node.exists.params, params);
1760
+ return `${node.exists.not ? "NOT " : ""}EXISTS (${body})`;
1761
+ }
1724
1762
  if ("where" in node) {
1725
1763
  return appendWhereParts(tableName, node.where, params);
1726
1764
  }
@@ -2238,6 +2276,23 @@ class RepositoryQuery {
2238
2276
  this.queryOptions = { ...this.queryOptions, limit };
2239
2277
  return this;
2240
2278
  }
2279
+ whereNull(column) {
2280
+ return this.where({ [column]: null });
2281
+ }
2282
+ whereNotNull(column) {
2283
+ return this.where({ [column]: { isNull: false } });
2284
+ }
2285
+ whereIn(column, values) {
2286
+ return this.where({ [column]: values });
2287
+ }
2288
+ whereExists(sql, params = []) {
2289
+ this.whereNodes.push({ kind: "and", exists: { sql, params } });
2290
+ return this;
2291
+ }
2292
+ whereNotExists(sql, params = []) {
2293
+ this.whereNodes.push({ kind: "and", exists: { sql, params, not: true } });
2294
+ return this;
2295
+ }
2241
2296
  offset(offset) {
2242
2297
  this.queryOptions = { ...this.queryOptions, offset };
2243
2298
  return this;
@@ -2749,6 +2804,162 @@ function createDatabaseConnection(source) {
2749
2804
  }
2750
2805
  };
2751
2806
  }
2807
+ // ../../src/core/database/factory.ts
2808
+ class Factory {
2809
+ quantity = 1;
2810
+ counted = false;
2811
+ sequenceIndex = 0;
2812
+ stateTransforms = [];
2813
+ sequenceItems = [];
2814
+ parentAssociations = [];
2815
+ children = [];
2816
+ afterMakingCallbacks = [];
2817
+ afterCreatingCallbacks = [];
2818
+ definition() {
2819
+ throw new Error("Factory definition must be implemented by subclass.");
2820
+ }
2821
+ clone() {
2822
+ const next = Object.create(Object.getPrototypeOf(this));
2823
+ Object.assign(next, this);
2824
+ next.stateTransforms = [...this.stateTransforms];
2825
+ next.sequenceItems = [...this.sequenceItems];
2826
+ next.parentAssociations = [...this.parentAssociations];
2827
+ next.children = [...this.children];
2828
+ next.afterMakingCallbacks = [...this.afterMakingCallbacks];
2829
+ next.afterCreatingCallbacks = [...this.afterCreatingCallbacks];
2830
+ return next;
2831
+ }
2832
+ count(quantity) {
2833
+ if (!Number.isInteger(quantity) || quantity < 1) {
2834
+ throw new Error("Factory.count() requires a positive integer.");
2835
+ }
2836
+ const next = this.clone();
2837
+ next.quantity = quantity;
2838
+ next.counted = true;
2839
+ return next;
2840
+ }
2841
+ state(state) {
2842
+ const next = this.clone();
2843
+ next.stateTransforms = [...this.stateTransforms, state];
2844
+ return next;
2845
+ }
2846
+ sequence(...items) {
2847
+ if (items.length === 0) {
2848
+ throw new Error("Factory.sequence() requires at least one attribute set.");
2849
+ }
2850
+ const next = this.clone();
2851
+ next.sequenceItems = [...this.sequenceItems, ...items];
2852
+ return next;
2853
+ }
2854
+ for(parent, foreignKey) {
2855
+ if (parent.id === undefined || parent.id === null) {
2856
+ throw new Error("Factory.for() requires a parent with an id.");
2857
+ }
2858
+ const next = this.clone();
2859
+ next.parentAssociations = [...this.parentAssociations, { foreignKey, value: parent.id }];
2860
+ return next;
2861
+ }
2862
+ recycle(parent, foreignKey) {
2863
+ return this.for(parent, foreignKey);
2864
+ }
2865
+ afterMaking(callback) {
2866
+ const next = this.clone();
2867
+ next.afterMakingCallbacks = [...this.afterMakingCallbacks, callback];
2868
+ return next;
2869
+ }
2870
+ afterCreating(callback) {
2871
+ const next = this.clone();
2872
+ next.afterCreatingCallbacks = [...this.afterCreatingCallbacks, callback];
2873
+ return next;
2874
+ }
2875
+ has(factory, foreignKey) {
2876
+ const next = this.clone();
2877
+ next.children = [
2878
+ ...this.children,
2879
+ { factory, foreignKey }
2880
+ ];
2881
+ return next;
2882
+ }
2883
+ make(overrides = {}) {
2884
+ if (!this.counted) {
2885
+ return this.makeOne(overrides);
2886
+ }
2887
+ return Array.from({ length: this.quantity }, () => this.makeOne(overrides));
2888
+ }
2889
+ async create(overrides = {}) {
2890
+ if (!this.counted) {
2891
+ return await this.createOne(overrides);
2892
+ }
2893
+ const records = [];
2894
+ for (let index = 0;index < this.quantity; index += 1) {
2895
+ records.push(await this.createOne(overrides));
2896
+ }
2897
+ return records;
2898
+ }
2899
+ makeOne(overrides = {}) {
2900
+ let record = { ...this.definition() };
2901
+ for (const state of this.stateTransforms) {
2902
+ const patch = typeof state === "function" ? state(record) : state;
2903
+ record = { ...record, ...patch };
2904
+ }
2905
+ if (this.sequenceItems.length > 0) {
2906
+ const item = this.sequenceItems[this.sequenceIndex % this.sequenceItems.length];
2907
+ const patch = typeof item === "function" ? item(this.sequenceIndex) : item;
2908
+ record = { ...record, ...patch };
2909
+ this.sequenceIndex += 1;
2910
+ }
2911
+ for (const association of this.parentAssociations) {
2912
+ record[association.foreignKey] = association.value;
2913
+ }
2914
+ const made = {
2915
+ ...record,
2916
+ ...overrides
2917
+ };
2918
+ for (const callback of this.afterMakingCallbacks) {
2919
+ callback(made);
2920
+ }
2921
+ return made;
2922
+ }
2923
+ async createOne(overrides = {}) {
2924
+ const created = await this.persist(this.insertable(this.makeOne(overrides)));
2925
+ for (const child of this.children) {
2926
+ await child.factory.for(created, child.foreignKey).create();
2927
+ }
2928
+ for (const callback of this.afterCreatingCallbacks) {
2929
+ await callback(created);
2930
+ }
2931
+ return created;
2932
+ }
2933
+ insertable(record) {
2934
+ const values = { ...record };
2935
+ if (values.id === 0 || values.id === undefined || values.id === null) {
2936
+ delete values.id;
2937
+ }
2938
+ return values;
2939
+ }
2940
+ persist(_values) {
2941
+ throw new Error("Factory.persist() must be implemented to use create().");
2942
+ }
2943
+ }
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
+ }
2752
2963
  // ../../src/core/database/migrations/advisoryLock.ts
2753
2964
  var MIGRATION_LOCK_KEY = 42424242;
2754
2965
  async function withMigrationLock(db, callback, lockKey = MIGRATION_LOCK_KEY) {
@@ -2861,10 +3072,479 @@ async function freshDatabase(db, migrations, options = {}) {
2861
3072
  }
2862
3073
  await runFresh();
2863
3074
  }
3075
+ // ../../src/core/database/relationQuery.ts
3076
+ function asWhere(where) {
3077
+ return where;
3078
+ }
3079
+ function ownerId(owner, ownerKey) {
3080
+ if (typeof owner.get === "function") {
3081
+ return owner.get(ownerKey);
3082
+ }
3083
+ if (owner && typeof owner === "object" && "id" in owner && owner.id !== undefined) {
3084
+ return owner.id;
3085
+ }
3086
+ throw new Error("belongsTo.associate() requires a related model or { id }.");
3087
+ }
3088
+
3089
+ class HasManyRelationQuery {
3090
+ parent;
3091
+ related;
3092
+ relation;
3093
+ kind = "hasMany";
3094
+ extraWhere = {};
3095
+ extraOptions = {};
3096
+ constructor(parent, related, relation) {
3097
+ this.parent = parent;
3098
+ this.related = related;
3099
+ this.relation = relation;
3100
+ }
3101
+ where(where) {
3102
+ this.extraWhere = { ...this.extraWhere, ...where };
3103
+ return this;
3104
+ }
3105
+ orderBy(orderBy) {
3106
+ this.extraOptions = { ...this.extraOptions, orderBy };
3107
+ return this;
3108
+ }
3109
+ limit(limit) {
3110
+ this.extraOptions = { ...this.extraOptions, limit };
3111
+ return this;
3112
+ }
3113
+ applyEagerLoad(query, alias) {
3114
+ query.withHasMany(alias, this.relation, this.related.repository(), this.extraOptions);
3115
+ }
3116
+ hydrateEager(row, alias) {
3117
+ const value = row[alias] ?? [];
3118
+ const rows = Array.isArray(value) ? value : [];
3119
+ return rows.map((item) => this.related.newFromRecord(item));
3120
+ }
3121
+ toExistsClause(parentTable) {
3122
+ const childTable = this.related.repository().getTable().name;
3123
+ const extra = buildAdvancedWhereClause(childTable, this.extraWhere, [], []);
3124
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
3125
+ const sql = `SELECT 1 FROM ${quoteIdentifier(childTable)} WHERE ${qualifyColumn(childTable, this.relation.foreignKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
3126
+ return { sql, params: extra.params };
3127
+ }
3128
+ scopedQuery() {
3129
+ const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
3130
+ let query = repository.query(asWhere({
3131
+ [this.relation.foreignKey]: this.parent.get(this.relation.localKey),
3132
+ ...this.extraWhere
3133
+ }));
3134
+ if (this.extraOptions.orderBy) {
3135
+ query = query.orderBy(this.extraOptions.orderBy);
3136
+ }
3137
+ if (this.extraOptions.limit !== undefined) {
3138
+ query = query.limit(this.extraOptions.limit);
3139
+ }
3140
+ return query;
3141
+ }
3142
+ async get() {
3143
+ const rows = await this.scopedQuery().get();
3144
+ return rows.map((row) => this.related.newFromRecord(row));
3145
+ }
3146
+ async first() {
3147
+ const rows = await this.limit(1).get();
3148
+ return rows[0] ?? null;
3149
+ }
3150
+ async count() {
3151
+ return (await this.get()).length;
3152
+ }
3153
+ async create(attributes = {}) {
3154
+ return this.related.create(attributes, {
3155
+ [this.relation.foreignKey]: this.parent.get(this.relation.localKey)
3156
+ });
3157
+ }
3158
+ async createMany(records) {
3159
+ const created = [];
3160
+ for (const attributes of records) {
3161
+ created.push(await this.create(attributes));
3162
+ }
3163
+ return created;
3164
+ }
3165
+ }
3166
+
3167
+ class HasOneRelationQuery {
3168
+ relation;
3169
+ kind = "hasOne";
3170
+ inner;
3171
+ constructor(parent, related, relation) {
3172
+ this.relation = relation;
3173
+ this.inner = new HasManyRelationQuery(parent, related, {
3174
+ type: "hasMany",
3175
+ name: relation.name,
3176
+ localKey: relation.localKey,
3177
+ foreignKey: relation.foreignKey
3178
+ });
3179
+ }
3180
+ where(where) {
3181
+ this.inner.where(where);
3182
+ return this;
3183
+ }
3184
+ orderBy(orderBy) {
3185
+ this.inner.orderBy(orderBy);
3186
+ return this;
3187
+ }
3188
+ applyEagerLoad(query, alias) {
3189
+ this.inner.limit(1).applyEagerLoad(query, alias);
3190
+ }
3191
+ hydrateEager(row, alias) {
3192
+ const hydrated = this.inner.hydrateEager(row, alias);
3193
+ return hydrated[0];
3194
+ }
3195
+ toExistsClause(parentTable) {
3196
+ return this.inner.toExistsClause(parentTable);
3197
+ }
3198
+ async get() {
3199
+ return this.inner.limit(1).first();
3200
+ }
3201
+ async first() {
3202
+ return this.get();
3203
+ }
3204
+ async count() {
3205
+ return await this.get() ? 1 : 0;
3206
+ }
3207
+ async create(attributes = {}) {
3208
+ return this.inner.create(attributes);
3209
+ }
3210
+ }
3211
+
3212
+ class BelongsToRelationQuery {
3213
+ parent;
3214
+ related;
3215
+ relation;
3216
+ kind = "belongsTo";
3217
+ extraOptions = {};
3218
+ constructor(parent, related, relation) {
3219
+ this.parent = parent;
3220
+ this.related = related;
3221
+ this.relation = relation;
3222
+ }
3223
+ orderBy(orderBy) {
3224
+ this.extraOptions = { ...this.extraOptions, orderBy };
3225
+ return this;
3226
+ }
3227
+ applyEagerLoad(query, alias) {
3228
+ query.withBelongsTo(alias, this.relation, this.related.repository(), this.extraOptions);
3229
+ }
3230
+ hydrateEager(row, alias) {
3231
+ const value = row[alias];
3232
+ return value ? this.related.newFromRecord(value) : value;
3233
+ }
3234
+ toExistsClause(parentTable) {
3235
+ 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
+ };
3240
+ }
3241
+ async get() {
3242
+ const foreign = this.parent.get(this.relation.foreignKey);
3243
+ if (foreign === null || foreign === undefined) {
3244
+ return null;
3245
+ }
3246
+ const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
3247
+ let query = repository.query(asWhere({ [this.relation.ownerKey]: foreign }));
3248
+ if (this.extraOptions.orderBy) {
3249
+ query = query.orderBy(this.extraOptions.orderBy);
3250
+ }
3251
+ const row = await query.first();
3252
+ return row ? this.related.newFromRecord(row) : null;
3253
+ }
3254
+ async first() {
3255
+ return this.get();
3256
+ }
3257
+ async associate(owner) {
3258
+ await this.parent.getRepository().updateById(this.parent.id, {
3259
+ [this.relation.foreignKey]: ownerId(owner, this.relation.ownerKey)
3260
+ });
3261
+ }
3262
+ async dissociate() {
3263
+ await this.parent.getRepository().updateById(this.parent.id, {
3264
+ [this.relation.foreignKey]: null
3265
+ });
3266
+ }
3267
+ }
3268
+
3269
+ class BelongsToManyRelationQuery {
3270
+ parent;
3271
+ related;
3272
+ relation;
3273
+ kind = "belongsToMany";
3274
+ extraWhere = {};
3275
+ extraOptions = {};
3276
+ constructor(parent, related, relation) {
3277
+ this.parent = parent;
3278
+ this.related = related;
3279
+ this.relation = relation;
3280
+ }
3281
+ where(where) {
3282
+ this.extraWhere = { ...this.extraWhere, ...where };
3283
+ return this;
3284
+ }
3285
+ orderBy(orderBy) {
3286
+ this.extraOptions = { ...this.extraOptions, orderBy };
3287
+ return this;
3288
+ }
3289
+ applyEagerLoad(_query, _alias) {}
3290
+ hydrateEager(row, alias) {
3291
+ const value = row[alias] ?? [];
3292
+ const rows = Array.isArray(value) ? value : [];
3293
+ return rows.map((item) => this.related.newFromRecord(item));
3294
+ }
3295
+ toExistsClause(parentTable) {
3296
+ const relatedTable = this.related.repository().getTable().name;
3297
+ const extra = buildAdvancedWhereClause(relatedTable, this.extraWhere, [], []);
3298
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
3299
+ const sql = `SELECT 1 FROM ${quoteIdentifier(relatedTable)} INNER JOIN ${quoteIdentifier(this.relation.pivotTable)} ON ${qualifyColumn(this.relation.pivotTable, this.relation.relatedPivotKey)} = ${qualifyColumn(relatedTable, this.relation.relatedKey)} WHERE ${qualifyColumn(this.relation.pivotTable, this.relation.foreignPivotKey)} = ${qualifyColumn(parentTable, this.relation.parentKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
3300
+ return { sql, params: extra.params };
3301
+ }
3302
+ connection() {
3303
+ return this.parent.getRepository().getConnection();
3304
+ }
3305
+ async get() {
3306
+ const parentId = this.parent.get(this.relation.parentKey);
3307
+ const pivotRows = await this.connection().unsafe(`SELECT * FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1`, [parentId]);
3308
+ if (pivotRows.length === 0) {
3309
+ return [];
3310
+ }
3311
+ const relatedIds = [
3312
+ ...new Set(pivotRows.map((row) => row[this.relation.relatedPivotKey]))
3313
+ ];
3314
+ const repository = this.related.repository().withConnection(this.connection());
3315
+ const rows = await repository.findAll({
3316
+ ...this.extraOptions,
3317
+ where: asWhere({
3318
+ [this.relation.relatedKey]: relatedIds,
3319
+ ...this.extraWhere
3320
+ })
3321
+ });
3322
+ return rows.map((row) => this.related.newFromRecord(row));
3323
+ }
3324
+ async first() {
3325
+ const rows = await this.get();
3326
+ return rows[0] ?? null;
3327
+ }
3328
+ async count() {
3329
+ return (await this.get()).length;
3330
+ }
3331
+ async attach(ids) {
3332
+ const list = Array.isArray(ids) ? ids : [ids];
3333
+ const parentId = this.parent.get(this.relation.parentKey);
3334
+ 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]);
3336
+ }
3337
+ }
3338
+ async detach(ids) {
3339
+ const parentId = this.parent.get(this.relation.parentKey);
3340
+ if (ids === undefined) {
3341
+ await this.connection().unsafe(`DELETE FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1`, [parentId]);
3342
+ return;
3343
+ }
3344
+ const list = Array.isArray(ids) ? ids : [ids];
3345
+ await this.connection().unsafe(`DELETE FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1 AND ${String(this.relation.relatedPivotKey)} = ANY($2)`, [parentId, list]);
3346
+ }
3347
+ async sync(ids) {
3348
+ await this.detach();
3349
+ if (ids.length > 0) {
3350
+ await this.attach(ids);
3351
+ }
3352
+ }
3353
+ async create(attributes = {}) {
3354
+ const related = await this.related.create(attributes);
3355
+ await this.attach(related.id);
3356
+ return related;
3357
+ }
3358
+ }
3359
+
3360
+ class MorphManyRelationQuery {
3361
+ parent;
3362
+ related;
3363
+ relation;
3364
+ kind = "morphMany";
3365
+ extraWhere = {};
3366
+ extraOptions = {};
3367
+ constructor(parent, related, relation) {
3368
+ this.parent = parent;
3369
+ this.related = related;
3370
+ this.relation = relation;
3371
+ }
3372
+ where(where) {
3373
+ this.extraWhere = { ...this.extraWhere, ...where };
3374
+ return this;
3375
+ }
3376
+ applyEagerLoad(query, alias) {
3377
+ query.withMorphMany(alias, this.relation, this.related.repository(), this.extraOptions);
3378
+ }
3379
+ hydrateEager(row, alias) {
3380
+ const value = row[alias] ?? [];
3381
+ const rows = Array.isArray(value) ? value : [];
3382
+ return rows.map((item) => this.related.newFromRecord(item));
3383
+ }
3384
+ toExistsClause(parentTable) {
3385
+ const childTable = this.related.repository().getTable().name;
3386
+ const extra = buildAdvancedWhereClause(childTable, {
3387
+ [this.relation.morphTypeKey]: this.relation.morphType,
3388
+ ...this.extraWhere
3389
+ }, [], []);
3390
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
3391
+ const sql = `SELECT 1 FROM ${quoteIdentifier(childTable)} WHERE ${qualifyColumn(childTable, this.relation.morphIdKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
3392
+ return { sql, params: extra.params };
3393
+ }
3394
+ async get() {
3395
+ const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
3396
+ const rows = await repository.query(asWhere({
3397
+ [this.relation.morphTypeKey]: this.relation.morphType,
3398
+ [this.relation.morphIdKey]: this.parent.get(this.relation.localKey),
3399
+ ...this.extraWhere
3400
+ })).get();
3401
+ return rows.map((row) => this.related.newFromRecord(row));
3402
+ }
3403
+ async first() {
3404
+ const rows = await this.get();
3405
+ return rows[0] ?? null;
3406
+ }
3407
+ async create(attributes = {}) {
3408
+ return this.related.create(attributes, {
3409
+ [this.relation.morphTypeKey]: this.relation.morphType,
3410
+ [this.relation.morphIdKey]: this.parent.get(this.relation.localKey)
3411
+ });
3412
+ }
3413
+ }
3414
+
3415
+ class MorphOneRelationQuery {
3416
+ relation;
3417
+ kind = "morphOne";
3418
+ inner;
3419
+ constructor(parent, related, relation) {
3420
+ this.relation = relation;
3421
+ this.inner = new MorphManyRelationQuery(parent, related, {
3422
+ type: "morphMany",
3423
+ name: relation.name,
3424
+ localKey: relation.localKey,
3425
+ morphTypeKey: relation.morphTypeKey,
3426
+ morphIdKey: relation.morphIdKey,
3427
+ morphType: relation.morphType
3428
+ });
3429
+ }
3430
+ where(where) {
3431
+ this.inner.where(where);
3432
+ return this;
3433
+ }
3434
+ applyEagerLoad(query, alias) {
3435
+ this.inner.applyEagerLoad(query, alias);
3436
+ }
3437
+ hydrateEager(row, alias) {
3438
+ const hydrated = this.inner.hydrateEager(row, alias);
3439
+ return hydrated[0];
3440
+ }
3441
+ toExistsClause(parentTable) {
3442
+ return this.inner.toExistsClause(parentTable);
3443
+ }
3444
+ async get() {
3445
+ return this.inner.first();
3446
+ }
3447
+ async create(attributes = {}) {
3448
+ return this.inner.create(attributes);
3449
+ }
3450
+ }
3451
+
3452
+ class MorphToRelationQuery {
3453
+ parent;
3454
+ relatedByType;
3455
+ relation;
3456
+ kind = "morphTo";
3457
+ constructor(parent, relatedByType, relation) {
3458
+ this.parent = parent;
3459
+ this.relatedByType = relatedByType;
3460
+ this.relation = relation;
3461
+ }
3462
+ applyEagerLoad(query, alias) {
3463
+ const repositories = new Map(Object.entries(this.relatedByType).map(([type, model]) => [
3464
+ type,
3465
+ model.repository()
3466
+ ]));
3467
+ query.withMorphTo(alias, this.relation, repositories);
3468
+ }
3469
+ hydrateEager(row, alias) {
3470
+ return row[alias];
3471
+ }
3472
+ toExistsClause(parentTable) {
3473
+ const type = String(this.parent.get(this.relation.morphTypeKey) ?? "");
3474
+ const related = this.relatedByType[type];
3475
+ if (!related) {
3476
+ return { sql: "SELECT 1 WHERE 1 = 0", params: [] };
3477
+ }
3478
+ const relatedTable = related.repository().getTable();
3479
+ return {
3480
+ sql: `SELECT 1 FROM ${quoteIdentifier(relatedTable.name)} WHERE ${qualifyColumn(relatedTable.name, relatedTable.primaryKey)} = ${qualifyColumn(parentTable, this.relation.morphIdKey)}`,
3481
+ params: []
3482
+ };
3483
+ }
3484
+ async get() {
3485
+ const type = String(this.parent.get(this.relation.morphTypeKey) ?? "");
3486
+ const id = this.parent.get(this.relation.morphIdKey);
3487
+ const related = this.relatedByType[type];
3488
+ if (!related || id === null || id === undefined) {
3489
+ return null;
3490
+ }
3491
+ const table = related.repository().getTable();
3492
+ const row = await related.repository().withConnection(this.parent.getRepository().getConnection()).query(asWhere({ [table.primaryKey]: id })).first();
3493
+ return row ? related.newFromRecord(row) : null;
3494
+ }
3495
+ }
3496
+
2864
3497
  // ../../src/core/database/model.ts
2865
3498
  var modelRepositories = new WeakMap;
2866
3499
  var modelGlobalScopes = new WeakMap;
3500
+ var modelObservers = new WeakMap;
2867
3501
  var modelBooted = new WeakSet;
3502
+ async function runObservers(model, hook) {
3503
+ const observers = modelObservers.get(model.constructor) ?? [];
3504
+ for (const observer of observers) {
3505
+ const handler = observer[hook];
3506
+ if (handler && await handler(model) === false) {
3507
+ return false;
3508
+ }
3509
+ }
3510
+ return true;
3511
+ }
3512
+ function accessorName(key) {
3513
+ const pascal = key.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase());
3514
+ return `get${pascal.charAt(0).toUpperCase()}${pascal.slice(1)}Attribute`;
3515
+ }
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);
3530
+ }
3531
+ async function loadNested(model, path) {
3532
+ const [head, ...rest] = path.split(".");
3533
+ if (!head) {
3534
+ return;
3535
+ }
3536
+ await model.load(head);
3537
+ if (rest.length === 0) {
3538
+ return;
3539
+ }
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("."));
3545
+ }
3546
+ }
3547
+ }
2868
3548
  function resolveModelRepository(model) {
2869
3549
  const repository = modelRepositories.get(model);
2870
3550
  if (!repository) {
@@ -2979,12 +3659,22 @@ class Model {
2979
3659
  static $guarded;
2980
3660
  static $casts = {};
2981
3661
  static $timestamps = true;
3662
+ static $hidden;
3663
+ static $visible;
3664
+ static $appends;
2982
3665
  _exists;
3666
+ loadedRelations = {};
3667
+ hiddenOverrides = [];
3668
+ visibleOverrides = [];
3669
+ appended = [];
2983
3670
  constructor(attributes, repository, exists = true) {
2984
3671
  this.attributes = attributes;
2985
3672
  this.repository = repository;
2986
3673
  this._exists = exists;
2987
3674
  }
3675
+ getRepository() {
3676
+ return this.repository;
3677
+ }
2988
3678
  get $exists() {
2989
3679
  return this._exists;
2990
3680
  }
@@ -2997,6 +3687,49 @@ class Model {
2997
3687
  toObject() {
2998
3688
  return { ...this.attributes };
2999
3689
  }
3690
+ toArray() {
3691
+ const ModelClass = modelStatics(this.constructor);
3692
+ const hidden = new Set([...ModelClass.$hidden ?? [], ...this.hiddenOverrides]);
3693
+ const visible = this.visibleOverrides.length > 0 ? this.visibleOverrides : ModelClass.$visible;
3694
+ const data = { ...this.attributes };
3695
+ if (visible && visible.length > 0) {
3696
+ for (const key of Object.keys(data)) {
3697
+ if (!visible.includes(key)) {
3698
+ delete data[key];
3699
+ }
3700
+ }
3701
+ }
3702
+ for (const key of hidden) {
3703
+ delete data[key];
3704
+ }
3705
+ for (const key of [...ModelClass.$appends ?? [], ...this.appended]) {
3706
+ const accessor = this[accessorName(key)];
3707
+ if (typeof accessor === "function") {
3708
+ data[key] = accessor.call(this);
3709
+ }
3710
+ }
3711
+ for (const [name, value] of Object.entries(this.loadedRelations)) {
3712
+ if (!hidden.has(name) && (!visible || visible.includes(name) || this.appended.includes(name))) {
3713
+ data[name] = value;
3714
+ }
3715
+ }
3716
+ return data;
3717
+ }
3718
+ toJSON() {
3719
+ return this.toArray();
3720
+ }
3721
+ makeHidden(...keys) {
3722
+ this.hiddenOverrides.push(...keys);
3723
+ return this;
3724
+ }
3725
+ makeVisible(...keys) {
3726
+ this.visibleOverrides.push(...keys);
3727
+ return this;
3728
+ }
3729
+ append(...keys) {
3730
+ this.appended.push(...keys);
3731
+ return this;
3732
+ }
3000
3733
  primaryKey() {
3001
3734
  throw new Error(`${this.constructor.name}.primaryKey() is not implemented.`);
3002
3735
  }
@@ -3017,6 +3750,10 @@ class Model {
3017
3750
  return new statics(hydrated, repository, exists);
3018
3751
  }
3019
3752
  static boot() {}
3753
+ static observe(observer) {
3754
+ const existing = modelObservers.get(this) ?? [];
3755
+ modelObservers.set(this, [...existing, observer]);
3756
+ }
3020
3757
  static addGlobalScope(_name, scope) {
3021
3758
  ensureBooted(this);
3022
3759
  const existing = modelGlobalScopes.get(this) ?? [];
@@ -3037,17 +3774,89 @@ class Model {
3037
3774
  }
3038
3775
  return query;
3039
3776
  }
3040
- static async create(attributes) {
3777
+ static newFromRecord(record, exists = true) {
3778
+ const repository = resolveModelRepository(this);
3779
+ return modelStatics(this).fromRecord(record, repository, exists);
3780
+ }
3781
+ static async create(attributes, forced = {}) {
3041
3782
  const statics = modelStatics(this);
3042
3783
  ensureBooted(this);
3043
3784
  const repository = resolveModelRepository(this);
3044
3785
  const table = repository.getTable();
3045
3786
  const timestamps = statics.$timestamps ?? true;
3046
- const assignable = filterMassAssignable(statics.$fillable, statics.$guarded, attributes);
3787
+ const assignable = {
3788
+ ...filterMassAssignable(statics.$fillable, statics.$guarded, attributes),
3789
+ ...forced
3790
+ };
3047
3791
  const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
3048
3792
  const payload = statics.dehydrateAttributes(withTimestamps);
3793
+ const pending = statics.newFromRecord({ ...payload }, false);
3794
+ if (await runObservers(pending, "creating") === false) {
3795
+ throw new Error(`${this.name}.create() was cancelled by an observer.`);
3796
+ }
3049
3797
  const record = await repository.create(payload);
3050
- return statics.fromRecord(record, repository, true);
3798
+ const created = statics.fromRecord(record, repository, true);
3799
+ await runObservers(created, "created");
3800
+ return created;
3801
+ }
3802
+ 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
+ };
3848
+ }
3849
+ static whereHas(name, constrain) {
3850
+ return constrainRelationExists(this, name, constrain, false);
3851
+ }
3852
+ static has(name) {
3853
+ return constrainRelationExists(this, name, undefined, false);
3854
+ }
3855
+ static doesntHave(name) {
3856
+ return constrainRelationExists(this, name, undefined, true);
3857
+ }
3858
+ static whereDoesntHave(name, constrain) {
3859
+ return constrainRelationExists(this, name, constrain, true);
3051
3860
  }
3052
3861
  static async find(id) {
3053
3862
  const statics = modelStatics(this);
@@ -3076,6 +3885,9 @@ class Model {
3076
3885
  const rows = await query.get();
3077
3886
  return rows.map((row) => statics.fromRecord(row, repository, true));
3078
3887
  }
3888
+ static where(where) {
3889
+ return Model.query.call(this).where(where);
3890
+ }
3079
3891
  static async firstWhere(where, options = {}) {
3080
3892
  const statics = modelStatics(this);
3081
3893
  const repository = resolveModelRepository(this);
@@ -3086,15 +3898,41 @@ class Model {
3086
3898
  const record = await query.first();
3087
3899
  return record ? statics.fromRecord(record, repository, true) : null;
3088
3900
  }
3901
+ static async firstOrNew(where, values = {}) {
3902
+ const existing = await Model.firstWhere.call(this, where);
3903
+ if (existing) {
3904
+ return existing;
3905
+ }
3906
+ return modelStatics(this).newFromRecord({ ...where, ...values }, false);
3907
+ }
3908
+ static async firstOrCreate(where, values = {}) {
3909
+ const existing = await Model.firstWhere.call(this, where);
3910
+ if (existing) {
3911
+ return existing;
3912
+ }
3913
+ return Model.create.call(this, { ...where, ...values });
3914
+ }
3915
+ static async updateOrCreate(where, values = {}) {
3916
+ const existing = await Model.firstWhere.call(this, where);
3917
+ if (existing) {
3918
+ return existing.update(values);
3919
+ }
3920
+ return Model.create.call(this, { ...where, ...values });
3921
+ }
3089
3922
  async save() {
3090
3923
  const ModelClass = modelStatics(this.constructor);
3091
3924
  const timestamps = ModelClass.$timestamps ?? true;
3092
3925
  const casts = ModelClass.$casts ?? {};
3093
3926
  const table = this.repository.getTable();
3094
- if (this.$exists) {
3927
+ const updating = this.$exists;
3928
+ if (await runObservers(this, updating ? "updating" : "creating") === false) {
3929
+ return this;
3930
+ }
3931
+ if (updating) {
3095
3932
  const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
3096
3933
  const record2 = await this.repository.updateByIdOrThrow(this.id, changes);
3097
3934
  this.attributes = ModelClass.hydrateAttributes(record2);
3935
+ await runObservers(this, "updated");
3098
3936
  return this;
3099
3937
  }
3100
3938
  const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, this.attributes);
@@ -3103,6 +3941,7 @@ class Model {
3103
3941
  const record = await this.repository.create(payload);
3104
3942
  this.attributes = ModelClass.hydrateAttributes(record);
3105
3943
  this._exists = true;
3944
+ await runObservers(this, "created");
3106
3945
  return this;
3107
3946
  }
3108
3947
  async update(changes) {
@@ -3112,10 +3951,14 @@ class Model {
3112
3951
  return await this.save();
3113
3952
  }
3114
3953
  async delete() {
3115
- if (resolveSoftDeleteColumn(this.repository.getTable())) {
3116
- return await this.repository.deleteById(this.id);
3954
+ if (await runObservers(this, "deleting") === false) {
3955
+ return false;
3117
3956
  }
3118
- return await this.repository.forceDeleteById(this.id);
3957
+ const deleted = resolveSoftDeleteColumn(this.repository.getTable()) ? await this.repository.deleteById(this.id) : await this.repository.forceDeleteById(this.id);
3958
+ if (deleted) {
3959
+ await runObservers(this, "deleted");
3960
+ }
3961
+ return deleted;
3119
3962
  }
3120
3963
  async forceDelete() {
3121
3964
  return await this.repository.forceDeleteById(this.id);
@@ -3164,6 +4007,91 @@ class Model {
3164
4007
  const loaded = grouped.get(parentId) ?? [];
3165
4008
  return Object.assign(this, { [as]: loaded });
3166
4009
  }
4010
+ hasMany(related, foreignKey, localKey) {
4011
+ const table = this.repository.getTable();
4012
+ return new HasManyRelationQuery(this, related, hasMany({
4013
+ name: related.repository().getTable().name,
4014
+ localKey: localKey ?? table.primaryKey,
4015
+ foreignKey: foreignKey ?? foreignKeyFromTable(table.name)
4016
+ }));
4017
+ }
4018
+ hasOne(related, foreignKey, localKey) {
4019
+ const table = this.repository.getTable();
4020
+ return new HasOneRelationQuery(this, related, hasOne({
4021
+ name: related.repository().getTable().name,
4022
+ localKey: localKey ?? table.primaryKey,
4023
+ foreignKey: foreignKey ?? foreignKeyFromTable(table.name)
4024
+ }));
4025
+ }
4026
+ belongsTo(related, foreignKey, ownerKey) {
4027
+ const relatedTable = related.repository().getTable();
4028
+ return new BelongsToRelationQuery(this, related, belongsTo({
4029
+ name: relatedTable.name,
4030
+ foreignKey: foreignKey ?? foreignKeyFromTable(relatedTable.name),
4031
+ ownerKey: ownerKey ?? relatedTable.primaryKey
4032
+ }));
4033
+ }
4034
+ belongsToMany(related, pivotTable, foreignPivotKey, relatedPivotKey) {
4035
+ const table = this.repository.getTable();
4036
+ const relatedTable = related.repository().getTable();
4037
+ return new BelongsToManyRelationQuery(this, related, belongsToMany({
4038
+ name: relatedTable.name,
4039
+ pivotTable: pivotTable ?? pivotTableName(table.name, relatedTable.name),
4040
+ parentKey: table.primaryKey,
4041
+ relatedKey: relatedTable.primaryKey,
4042
+ foreignPivotKey: foreignPivotKey ?? foreignKeyFromTable(table.name),
4043
+ relatedPivotKey: relatedPivotKey ?? foreignKeyFromTable(relatedTable.name)
4044
+ }));
4045
+ }
4046
+ morphMany(related, morphName, typeKey, idKey) {
4047
+ const table = this.repository.getTable();
4048
+ return new MorphManyRelationQuery(this, related, morphMany({
4049
+ name: morphName,
4050
+ localKey: table.primaryKey,
4051
+ morphTypeKey: typeKey ?? `${morphName}_type`,
4052
+ morphIdKey: idKey ?? `${morphName}_id`,
4053
+ morphType: table.name
4054
+ }));
4055
+ }
4056
+ morphOne(related, morphName, typeKey, idKey) {
4057
+ const table = this.repository.getTable();
4058
+ return new MorphOneRelationQuery(this, related, morphOne({
4059
+ name: morphName,
4060
+ localKey: table.primaryKey,
4061
+ morphTypeKey: typeKey ?? `${morphName}_type`,
4062
+ morphIdKey: idKey ?? `${morphName}_id`,
4063
+ morphType: table.name
4064
+ }));
4065
+ }
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`
4071
+ }));
4072
+ }
4073
+ async load(...names) {
4074
+ for (const name of names) {
4075
+ if (name.includes(".")) {
4076
+ await loadNested(this, name);
4077
+ continue;
4078
+ }
4079
+ const method = this[name];
4080
+ if (typeof method !== "function") {
4081
+ throw new Error(`${this.constructor.name} has no relation method ${name}().`);
4082
+ }
4083
+ const relationQuery = method.call(this);
4084
+ this.loadedRelations[name] = await relationQuery.get();
4085
+ }
4086
+ return this;
4087
+ }
4088
+ loaded(name) {
4089
+ return this.loadedRelations[name];
4090
+ }
4091
+ setLoaded(name, value) {
4092
+ this.loadedRelations[name] = value;
4093
+ return this;
4094
+ }
3167
4095
  mergeAttributes(patch) {
3168
4096
  Object.assign(this.attributes, patch);
3169
4097
  return this;
@@ -5062,6 +5990,64 @@ function createRequireAuthMiddleware(auth2) {
5062
5990
  function serializeDate(value) {
5063
5991
  return value instanceof Date ? value.toISOString() : value;
5064
5992
  }
5993
+ function whenLoaded(model, relation, transform) {
5994
+ const value = model.loaded(relation);
5995
+ if (value === undefined) {
5996
+ return;
5997
+ }
5998
+ return transform ? transform(value) : value;
5999
+ }
6000
+
6001
+ class JsonResource {
6002
+ resource;
6003
+ static wrap = "data";
6004
+ extra = {};
6005
+ constructor(resource) {
6006
+ this.resource = resource;
6007
+ }
6008
+ static make(resource) {
6009
+ return new JsonResource(resource);
6010
+ }
6011
+ static collection(items) {
6012
+ return new ResourceCollection(items);
6013
+ }
6014
+ additional(data) {
6015
+ this.extra = { ...this.extra, ...data };
6016
+ return this;
6017
+ }
6018
+ when(condition, value) {
6019
+ return condition ? value : undefined;
6020
+ }
6021
+ whenLoaded(relation, transform) {
6022
+ const model = this.resource;
6023
+ if (typeof model.loaded !== "function") {
6024
+ return;
6025
+ }
6026
+ return whenLoaded(model, relation, transform);
6027
+ }
6028
+ toArray() {
6029
+ if (this.resource && typeof this.resource === "object" && "toArray" in this.resource) {
6030
+ return this.resource.toArray();
6031
+ }
6032
+ return { ...this.resource };
6033
+ }
6034
+ toResponse() {
6035
+ const wrap = this.constructor.wrap;
6036
+ const payload = this.toArray();
6037
+ if (wrap === null) {
6038
+ return { ...payload, ...this.extra };
6039
+ }
6040
+ return { [wrap]: payload, ...this.extra };
6041
+ }
6042
+ }
6043
+
6044
+ class ResourceCollection extends JsonResource {
6045
+ toArray() {
6046
+ return {
6047
+ data: this.resource.map((item) => item instanceof JsonResource ? item.toArray() : { ...item })
6048
+ };
6049
+ }
6050
+ }
5065
6051
  function toResourceCollection(items, transformer) {
5066
6052
  return items.map(transformer);
5067
6053
  }
@@ -5298,11 +6284,11 @@ async function webErrorResponse(error, request) {
5298
6284
  if (!request || !isViewsEnabled() || requestPrefersJson(request)) {
5299
6285
  return null;
5300
6286
  }
5301
- const mappedError = error instanceof HttpError ? error : mapDatabaseError(error);
5302
- if (mappedError instanceof UnauthorizedError) {
6287
+ const mappedError = toHttpError(error) ?? mapDatabaseError(error);
6288
+ if (mappedError.status === 401) {
5303
6289
  return Response.redirect(loginRedirectLocation(request), 302);
5304
6290
  }
5305
- const errors = mappedError instanceof ValidationError ? normalizeFieldErrors(mappedError.details) : undefined;
6291
+ const errors = mappedError instanceof ValidationError || mappedError.name === "ValidationError" ? normalizeFieldErrors(mappedError.details) : undefined;
5306
6292
  return htmlErrorResponse({
5307
6293
  status: mappedError.status,
5308
6294
  title: errorPageTitle(mappedError.status, mappedError.message),
@@ -5326,7 +6312,7 @@ function noContentResponse() {
5326
6312
  return new Response(null, { status: 204 });
5327
6313
  }
5328
6314
  function errorResponse(error) {
5329
- const mappedError = error instanceof HttpError ? error : mapDatabaseError(error);
6315
+ const mappedError = toHttpError(error) ?? mapDatabaseError(error);
5330
6316
  return Response.json({
5331
6317
  error: mappedError.message,
5332
6318
  ...mappedError.details === undefined ? {} : { details: mappedError.details }
@@ -6699,8 +7685,9 @@ function createTenantMiddleware() {
6699
7685
  });
6700
7686
  });
6701
7687
  } catch (error) {
6702
- if (error instanceof HttpError) {
6703
- return Response.json({ error: error.message }, { status: error.status });
7688
+ const httpError = toHttpError(error);
7689
+ if (httpError) {
7690
+ return Response.json({ error: httpError.message }, { status: httpError.status });
6704
7691
  }
6705
7692
  throw error;
6706
7693
  }
@@ -7143,6 +8130,8 @@ export {
7143
8130
  AuthManager,
7144
8131
  BadRequestError,
7145
8132
  baseRepository_default as BaseRepository,
8133
+ BelongsToManyRelationQuery,
8134
+ BelongsToRelationQuery,
7146
8135
  Blueprint,
7147
8136
  CACHE_TAGS,
7148
8137
  repository_default as CacheRepository,
@@ -7155,19 +8144,26 @@ export {
7155
8144
  DatabaseTokenGuard,
7156
8145
  EtaViewEngine,
7157
8146
  EventBus,
8147
+ Factory,
7158
8148
  failedJobRepository_default as FailedJobRepository,
7159
8149
  failedJobService_default as FailedJobService,
7160
8150
  ForbiddenError,
7161
8151
  ForeignIdColumnDefinition,
7162
8152
  FormRequest,
7163
8153
  GuestGuard,
8154
+ HasManyRelationQuery,
8155
+ HasOneRelationQuery,
7164
8156
  HttpError,
7165
8157
  Job,
8158
+ JsonResource,
7166
8159
  LocalStorageDriver,
7167
8160
  LogMailDriver,
7168
8161
  Mailer,
7169
8162
  membershipService_default as MembershipService,
7170
8163
  Model,
8164
+ MorphManyRelationQuery,
8165
+ MorphOneRelationQuery,
8166
+ MorphToRelationQuery,
7171
8167
  MySqlGrammar,
7172
8168
  NotFoundError,
7173
8169
  Notification,
@@ -7182,6 +8178,7 @@ export {
7182
8178
  RedisQueue,
7183
8179
  RepositoryQuery,
7184
8180
  ResilientQueue,
8181
+ ResourceCollection,
7185
8182
  Schedule,
7186
8183
  Schema,
7187
8184
  ServiceContainer,
@@ -7279,6 +8276,7 @@ export {
7279
8276
  eventBus,
7280
8277
  events,
7281
8278
  filterMassAssignable,
8279
+ foreignKeyFromTable,
7282
8280
  formatAdminValue,
7283
8281
  freshDatabase,
7284
8282
  generateCspNonce,
@@ -7309,6 +8307,7 @@ export {
7309
8307
  isEtagEnabled,
7310
8308
  isGlobalAdmin,
7311
8309
  isHtmxRequest,
8310
+ isHttpErrorLike,
7312
8311
  isInsideTenantDatabaseScope,
7313
8312
  isPublicReadsEnabled,
7314
8313
  isTenancyEnabled,
@@ -7336,6 +8335,7 @@ export {
7336
8335
  parseMultipartUpload,
7337
8336
  parsePaginationQuery,
7338
8337
  parsePositiveIntParam,
8338
+ pivotTableName,
7339
8339
  policyGate,
7340
8340
  prometheusRegistry,
7341
8341
  queue,
@@ -7371,6 +8371,7 @@ export {
7371
8371
  resolveCsrfTokenForRequest,
7372
8372
  resolveDatabaseDriver,
7373
8373
  resolveHtmlContentSecurityPolicy,
8374
+ resolveMembershipLookup,
7374
8375
  resolveMembershipService,
7375
8376
  resolveOrganizationScope,
7376
8377
  resolveRepositoryConnection,
@@ -7387,6 +8388,7 @@ export {
7387
8388
  runSeedersFromDirectory,
7388
8389
  runWithAuthUser,
7389
8390
  runWithDatabaseConnection,
8391
+ runWithMembershipContext,
7390
8392
  runWithRequestMeta,
7391
8393
  runWithTenant,
7392
8394
  runWithTenantDatabase,
@@ -7402,6 +8404,7 @@ export {
7402
8404
  serverHtmxContentSecurityPolicy,
7403
8405
  setActiveApplicationContext,
7404
8406
  signedUrl,
8407
+ singularize,
7405
8408
  spaContentSecurityPolicy,
7406
8409
  storageFacade as storage,
7407
8410
  strictApiContentSecurityPolicy,
@@ -7409,11 +8412,13 @@ export {
7409
8412
  stripMarkdown,
7410
8413
  temporarySignedUrl,
7411
8414
  textResponse,
8415
+ toHttpError,
7412
8416
  toPaginatedResourceCollection,
7413
8417
  toResourceCollection,
7414
8418
  trustForwardedFor,
7415
8419
  validateObject,
7416
8420
  verifyCsrfToken,
8421
+ whenLoaded,
7417
8422
  withErrorHandling,
7418
8423
  withMiddleware,
7419
8424
  withMigrationLock,