@getstrata/core 0.5.97 → 0.5.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1497,6 +1497,12 @@ class ServiceContainer {
1497
1497
  resolve(key) {
1498
1498
  return this.get(key);
1499
1499
  }
1500
+ make(key) {
1501
+ return this.resolve(key);
1502
+ }
1503
+ instance(key, value) {
1504
+ return this.set(key, value);
1505
+ }
1500
1506
  has(key) {
1501
1507
  return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
1502
1508
  }
@@ -1525,6 +1531,9 @@ class ConfigStore {
1525
1531
  class EventBus {
1526
1532
  constructor() {}
1527
1533
  listeners = new Map;
1534
+ on(event, listener) {
1535
+ return this.listen(event, listener);
1536
+ }
1528
1537
  listen(event, listener) {
1529
1538
  const handlers = this.listeners.get(event) ?? new Set;
1530
1539
  handlers.add(listener);
@@ -1536,6 +1545,9 @@ class EventBus {
1536
1545
  }
1537
1546
  };
1538
1547
  }
1548
+ async emit(event, payload) {
1549
+ await this.dispatch(event, payload);
1550
+ }
1539
1551
  async dispatch(event, payload) {
1540
1552
  const handlers = this.listeners.get(event);
1541
1553
  if (!handlers || handlers.size === 0) {
@@ -1707,7 +1719,7 @@ function buildOperatorClauses(column, operator, params) {
1707
1719
  clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
1708
1720
  }
1709
1721
  if (operator.ilike !== undefined) {
1710
- clauses.push(`${column} ILIKE ${pushParam(params, `%${operator.ilike}%`)}`);
1722
+ clauses.push(`${column} ILIKE ${pushParam(params, operator.ilike)}`);
1711
1723
  }
1712
1724
  if (operator.tsMatch !== undefined) {
1713
1725
  clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
@@ -1737,7 +1749,16 @@ function appendWhereParts(tableName, where, params) {
1737
1749
  }
1738
1750
  return clauses.join(" AND ");
1739
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
+ }
1740
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
+ }
1741
1762
  if ("where" in node) {
1742
1763
  return appendWhereParts(tableName, node.where, params);
1743
1764
  }
@@ -2255,6 +2276,23 @@ class RepositoryQuery {
2255
2276
  this.queryOptions = { ...this.queryOptions, limit };
2256
2277
  return this;
2257
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
+ }
2258
2296
  offset(offset) {
2259
2297
  this.queryOptions = { ...this.queryOptions, offset };
2260
2298
  return this;
@@ -2324,13 +2362,30 @@ class RepositoryQuery {
2324
2362
  });
2325
2363
  return this;
2326
2364
  }
2365
+ withBelongsToMany(as, relation, relatedRepository, options = {}) {
2366
+ this.eagerLoads.push({
2367
+ kind: "belongsToMany",
2368
+ as,
2369
+ relation,
2370
+ repository: relatedRepository,
2371
+ options
2372
+ });
2373
+ return this;
2374
+ }
2327
2375
  async get() {
2328
2376
  const rows = await this.repository.findAll(this.buildOptions());
2329
2377
  return await this.attach(rows);
2330
2378
  }
2331
2379
  async first() {
2332
- const rows = await this.get();
2333
- return rows[0] ?? null;
2380
+ const rows = await this.repository.findAll({ ...this.buildOptions(), limit: 1 });
2381
+ const attached = await this.attach(rows);
2382
+ return attached[0] ?? null;
2383
+ }
2384
+ async count() {
2385
+ return await this.repository.count(this.buildOptions());
2386
+ }
2387
+ async attachToRows(rows) {
2388
+ return await this.attach(rows);
2334
2389
  }
2335
2390
  async paginate(options) {
2336
2391
  return await this.repository.paginate({
@@ -2402,6 +2457,15 @@ class RepositoryQuery {
2402
2457
  }));
2403
2458
  continue;
2404
2459
  }
2460
+ if (load.kind === "belongsToMany") {
2461
+ const relation2 = load.relation;
2462
+ const grouped2 = await this.repository.loadBelongsToManyForParents(rows, relation2, load.repository, load.options);
2463
+ result = result.map((row) => ({
2464
+ ...row,
2465
+ [load.as]: grouped2.get(row[relation2.parentKey]) ?? []
2466
+ }));
2467
+ continue;
2468
+ }
2405
2469
  if (load.kind === "morphTo") {
2406
2470
  const relation2 = load.relation;
2407
2471
  const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
@@ -2430,6 +2494,10 @@ class BaseRepository {
2430
2494
  this.table = table;
2431
2495
  this.connection = connection;
2432
2496
  }
2497
+ async count(options = {}) {
2498
+ const { whereNodes, where, ...rest } = options;
2499
+ return await this.countWhere(where ?? {}, rest, whereNodes ?? []);
2500
+ }
2433
2501
  async findAll(options = {}) {
2434
2502
  return await withDatabaseErrorHandling(async () => {
2435
2503
  const { whereNodes, ...queryOptions } = options;
@@ -2737,6 +2805,23 @@ class BaseRepository {
2737
2805
  }
2738
2806
  return indexMorphToRelation(children, parentsByType, relation);
2739
2807
  }
2808
+ async loadBelongsToManyForParents(parents, relation, relatedRepository, options = {}) {
2809
+ if (parents.length === 0) {
2810
+ return indexBelongsToManyRelation(parents, [], [], relation);
2811
+ }
2812
+ const parentIds = [...new Set(parents.map((parent) => parent[relation.parentKey]))];
2813
+ const pivotRows = await this.connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} = ANY($1)`, [parentIds]);
2814
+ if (pivotRows.length === 0) {
2815
+ return indexBelongsToManyRelation(parents, [], [], relation);
2816
+ }
2817
+ const relatedIds = [
2818
+ ...new Set(pivotRows.map((row) => row[relation.relatedPivotKey]))
2819
+ ];
2820
+ const relatedRows = await relatedRepository.withConnection(this.connection).findWhere({
2821
+ [relation.relatedKey]: relatedIds
2822
+ }, options);
2823
+ return indexBelongsToManyRelation(parents, pivotRows, relatedRows, relation);
2824
+ }
2740
2825
  }
2741
2826
  var baseRepository_default = BaseRepository;
2742
2827
  // ../../src/core/database/bunSql.ts
@@ -2766,6 +2851,185 @@ function createDatabaseConnection(source) {
2766
2851
  }
2767
2852
  };
2768
2853
  }
2854
+ // ../../src/core/database/inflection.ts
2855
+ function singularize(word) {
2856
+ if (word.endsWith("ies") && word.length > 3) {
2857
+ return `${word.slice(0, -3)}y`;
2858
+ }
2859
+ if (/(ses|xes|zes|ches|shes)$/i.test(word)) {
2860
+ return word.slice(0, -2);
2861
+ }
2862
+ if (word.endsWith("s") && !word.endsWith("ss")) {
2863
+ return word.slice(0, -1);
2864
+ }
2865
+ return word;
2866
+ }
2867
+ function foreignKeyFromTable(tableName) {
2868
+ return `${singularize(tableName)}_id`;
2869
+ }
2870
+ function pivotTableName(leftTable, rightTable) {
2871
+ return [singularize(leftTable), singularize(rightTable)].sort().join("_");
2872
+ }
2873
+
2874
+ // ../../src/core/database/factory.ts
2875
+ function inferFactoryForeignKey(parent, explicit) {
2876
+ if (explicit) {
2877
+ if (explicit.endsWith("_id")) {
2878
+ return explicit;
2879
+ }
2880
+ return `${explicit}_id`;
2881
+ }
2882
+ if (typeof parent.getRepository === "function") {
2883
+ return foreignKeyFromTable(parent.getRepository().getTable().name);
2884
+ }
2885
+ throw new Error("Factory.for() requires a foreign key or a parent Model.");
2886
+ }
2887
+
2888
+ class Factory {
2889
+ quantity = 1;
2890
+ counted = false;
2891
+ sequenceIndex = 0;
2892
+ stateTransforms = [];
2893
+ sequenceItems = [];
2894
+ parentAssociations = [];
2895
+ children = [];
2896
+ afterMakingCallbacks = [];
2897
+ afterCreatingCallbacks = [];
2898
+ model;
2899
+ definition() {
2900
+ throw new Error("Factory definition must be implemented by subclass.");
2901
+ }
2902
+ clone() {
2903
+ const next = Object.create(Object.getPrototypeOf(this));
2904
+ Object.assign(next, this);
2905
+ next.stateTransforms = [...this.stateTransforms];
2906
+ next.sequenceItems = [...this.sequenceItems];
2907
+ next.parentAssociations = [...this.parentAssociations];
2908
+ next.children = [...this.children];
2909
+ next.afterMakingCallbacks = [...this.afterMakingCallbacks];
2910
+ next.afterCreatingCallbacks = [...this.afterCreatingCallbacks];
2911
+ return next;
2912
+ }
2913
+ count(quantity) {
2914
+ if (!Number.isInteger(quantity) || quantity < 1) {
2915
+ throw new Error("Factory.count() requires a positive integer.");
2916
+ }
2917
+ const next = this.clone();
2918
+ next.quantity = quantity;
2919
+ next.counted = true;
2920
+ return next;
2921
+ }
2922
+ state(state) {
2923
+ const next = this.clone();
2924
+ next.stateTransforms = [...this.stateTransforms, state];
2925
+ return next;
2926
+ }
2927
+ sequence(...items) {
2928
+ if (items.length === 0) {
2929
+ throw new Error("Factory.sequence() requires at least one attribute set.");
2930
+ }
2931
+ const next = this.clone();
2932
+ next.sequenceItems = [...this.sequenceItems, ...items];
2933
+ return next;
2934
+ }
2935
+ for(parent, foreignKey) {
2936
+ if (parent.id === undefined || parent.id === null) {
2937
+ throw new Error("Factory.for() requires a parent with an id.");
2938
+ }
2939
+ const key = inferFactoryForeignKey(parent, foreignKey);
2940
+ const next = this.clone();
2941
+ next.parentAssociations = [...this.parentAssociations, { foreignKey: key, value: parent.id }];
2942
+ return next;
2943
+ }
2944
+ recycle(parent, foreignKey) {
2945
+ return this.for(parent, foreignKey);
2946
+ }
2947
+ afterMaking(callback) {
2948
+ const next = this.clone();
2949
+ next.afterMakingCallbacks = [...this.afterMakingCallbacks, callback];
2950
+ return next;
2951
+ }
2952
+ afterCreating(callback) {
2953
+ const next = this.clone();
2954
+ next.afterCreatingCallbacks = [...this.afterCreatingCallbacks, callback];
2955
+ return next;
2956
+ }
2957
+ has(factory, foreignKey) {
2958
+ const next = this.clone();
2959
+ next.children = [
2960
+ ...this.children,
2961
+ { factory, foreignKey }
2962
+ ];
2963
+ return next;
2964
+ }
2965
+ make(overrides = {}) {
2966
+ if (!this.counted) {
2967
+ return this.makeOne(overrides);
2968
+ }
2969
+ return Array.from({ length: this.quantity }, () => this.makeOne(overrides));
2970
+ }
2971
+ async create(overrides = {}) {
2972
+ if (!this.counted) {
2973
+ return await this.createOne(overrides);
2974
+ }
2975
+ const records = [];
2976
+ for (let index = 0;index < this.quantity; index += 1) {
2977
+ records.push(await this.createOne(overrides));
2978
+ }
2979
+ return records;
2980
+ }
2981
+ makeOne(overrides = {}) {
2982
+ let record = { ...this.definition() };
2983
+ for (const state of this.stateTransforms) {
2984
+ const patch = typeof state === "function" ? state(record) : state;
2985
+ record = { ...record, ...patch };
2986
+ }
2987
+ if (this.sequenceItems.length > 0) {
2988
+ const item = this.sequenceItems[this.sequenceIndex % this.sequenceItems.length];
2989
+ const patch = typeof item === "function" ? item(this.sequenceIndex) : item;
2990
+ record = { ...record, ...patch };
2991
+ this.sequenceIndex += 1;
2992
+ }
2993
+ for (const association of this.parentAssociations) {
2994
+ record[association.foreignKey] = association.value;
2995
+ }
2996
+ const made = {
2997
+ ...record,
2998
+ ...overrides
2999
+ };
3000
+ for (const callback of this.afterMakingCallbacks) {
3001
+ callback(made);
3002
+ }
3003
+ return made;
3004
+ }
3005
+ async createOne(overrides = {}) {
3006
+ const created = await this.persist(this.insertable(this.makeOne(overrides)));
3007
+ for (const child of this.children) {
3008
+ await child.factory.for(created, child.foreignKey).create();
3009
+ }
3010
+ for (const callback of this.afterCreatingCallbacks) {
3011
+ await callback(created);
3012
+ }
3013
+ return created;
3014
+ }
3015
+ insertable(record) {
3016
+ const values = { ...record };
3017
+ if (values.id === 0 || values.id === undefined || values.id === null) {
3018
+ delete values.id;
3019
+ }
3020
+ return values;
3021
+ }
3022
+ async persist(values) {
3023
+ if (this.model) {
3024
+ const created = await this.model.create(values);
3025
+ if (created && typeof created.toObject === "function") {
3026
+ return created.toObject();
3027
+ }
3028
+ return created;
3029
+ }
3030
+ throw new Error("Factory.persist() must be implemented to use create().");
3031
+ }
3032
+ }
2769
3033
  // ../../src/core/database/migrations/advisoryLock.ts
2770
3034
  var MIGRATION_LOCK_KEY = 42424242;
2771
3035
  async function withMigrationLock(db, callback, lockKey = MIGRATION_LOCK_KEY) {
@@ -2878,10 +3142,591 @@ async function freshDatabase(db, migrations, options = {}) {
2878
3142
  }
2879
3143
  await runFresh();
2880
3144
  }
3145
+ // ../../src/core/database/relationQuery.ts
3146
+ function asWhere(where) {
3147
+ return where;
3148
+ }
3149
+ function ownerId(owner, ownerKey) {
3150
+ if (typeof owner.get === "function") {
3151
+ return owner.get(ownerKey);
3152
+ }
3153
+ if (owner && typeof owner === "object" && "id" in owner && owner.id !== undefined) {
3154
+ return owner.id;
3155
+ }
3156
+ throw new Error("belongsTo.associate() requires a related model or { id }.");
3157
+ }
3158
+ function thenGet(get, onfulfilled, onrejected) {
3159
+ return get().then(onfulfilled ?? undefined, onrejected ?? undefined);
3160
+ }
3161
+
3162
+ class HasManyRelationQuery {
3163
+ parent;
3164
+ related;
3165
+ relation;
3166
+ kind = "hasMany";
3167
+ extraWhere = {};
3168
+ extraOptions = {};
3169
+ constructor(parent, related, relation) {
3170
+ this.parent = parent;
3171
+ this.related = related;
3172
+ this.relation = relation;
3173
+ }
3174
+ where(where) {
3175
+ this.extraWhere = { ...this.extraWhere, ...where };
3176
+ return this;
3177
+ }
3178
+ orderBy(orderBy) {
3179
+ this.extraOptions = { ...this.extraOptions, orderBy };
3180
+ return this;
3181
+ }
3182
+ limit(limit) {
3183
+ this.extraOptions = { ...this.extraOptions, limit };
3184
+ return this;
3185
+ }
3186
+ applyEagerLoad(query, alias) {
3187
+ query.withHasMany(alias, this.relation, this.related.repository(), this.extraOptions);
3188
+ }
3189
+ hydrateEager(row, alias) {
3190
+ const value = row[alias] ?? [];
3191
+ const rows = Array.isArray(value) ? value : [];
3192
+ return rows.map((item) => this.related.newFromRecord(item));
3193
+ }
3194
+ toExistsClause(parentTable) {
3195
+ const childTable = this.related.repository().getTable().name;
3196
+ const extra = buildAdvancedWhereClause(childTable, this.extraWhere, [], []);
3197
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
3198
+ const sql = `SELECT 1 FROM ${quoteIdentifier(childTable)} WHERE ${qualifyColumn(childTable, this.relation.foreignKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
3199
+ return { sql, params: extra.params };
3200
+ }
3201
+ scopedQuery() {
3202
+ const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
3203
+ let query = repository.query(asWhere({
3204
+ [this.relation.foreignKey]: this.parent.get(this.relation.localKey),
3205
+ ...this.extraWhere
3206
+ }));
3207
+ if (this.extraOptions.orderBy) {
3208
+ query = query.orderBy(this.extraOptions.orderBy);
3209
+ }
3210
+ if (this.extraOptions.limit !== undefined) {
3211
+ query = query.limit(this.extraOptions.limit);
3212
+ }
3213
+ return query;
3214
+ }
3215
+ async get() {
3216
+ const rows = await this.scopedQuery().get();
3217
+ return rows.map((row) => this.related.newFromRecord(row));
3218
+ }
3219
+ async first() {
3220
+ const rows = await this.limit(1).get();
3221
+ return rows[0] ?? null;
3222
+ }
3223
+ async count() {
3224
+ return this.scopedQuery().count();
3225
+ }
3226
+ then(onfulfilled, onrejected) {
3227
+ return thenGet(() => this.get(), onfulfilled, onrejected);
3228
+ }
3229
+ async create(attributes = {}) {
3230
+ return this.related.create(attributes, {
3231
+ [this.relation.foreignKey]: this.parent.get(this.relation.localKey)
3232
+ });
3233
+ }
3234
+ async save(related) {
3235
+ const forced = {
3236
+ [this.relation.foreignKey]: this.parent.get(this.relation.localKey)
3237
+ };
3238
+ const savable = related;
3239
+ if (typeof savable.save === "function") {
3240
+ savable.mergeAttributes?.(forced);
3241
+ await savable.save();
3242
+ return related;
3243
+ }
3244
+ return this.create(related);
3245
+ }
3246
+ async createMany(records) {
3247
+ const created = [];
3248
+ for (const attributes of records) {
3249
+ created.push(await this.create(attributes));
3250
+ }
3251
+ return created;
3252
+ }
3253
+ }
3254
+
3255
+ class HasOneRelationQuery {
3256
+ relation;
3257
+ kind = "hasOne";
3258
+ inner;
3259
+ constructor(parent, related, relation) {
3260
+ this.relation = relation;
3261
+ this.inner = new HasManyRelationQuery(parent, related, {
3262
+ type: "hasMany",
3263
+ name: relation.name,
3264
+ localKey: relation.localKey,
3265
+ foreignKey: relation.foreignKey
3266
+ });
3267
+ }
3268
+ where(where) {
3269
+ this.inner.where(where);
3270
+ return this;
3271
+ }
3272
+ orderBy(orderBy) {
3273
+ this.inner.orderBy(orderBy);
3274
+ return this;
3275
+ }
3276
+ applyEagerLoad(query, alias) {
3277
+ this.inner.limit(1).applyEagerLoad(query, alias);
3278
+ }
3279
+ hydrateEager(row, alias) {
3280
+ const hydrated = this.inner.hydrateEager(row, alias);
3281
+ return hydrated[0];
3282
+ }
3283
+ toExistsClause(parentTable) {
3284
+ return this.inner.toExistsClause(parentTable);
3285
+ }
3286
+ async get() {
3287
+ return this.inner.limit(1).first();
3288
+ }
3289
+ async first() {
3290
+ return this.get();
3291
+ }
3292
+ async count() {
3293
+ return this.inner.count();
3294
+ }
3295
+ then(onfulfilled, onrejected) {
3296
+ return thenGet(() => this.get(), onfulfilled, onrejected);
3297
+ }
3298
+ async create(attributes = {}) {
3299
+ return this.inner.create(attributes);
3300
+ }
3301
+ async save(related) {
3302
+ return this.inner.save(related);
3303
+ }
3304
+ }
3305
+
3306
+ class BelongsToRelationQuery {
3307
+ parent;
3308
+ related;
3309
+ relation;
3310
+ kind = "belongsTo";
3311
+ extraWhere = {};
3312
+ extraOptions = {};
3313
+ constructor(parent, related, relation) {
3314
+ this.parent = parent;
3315
+ this.related = related;
3316
+ this.relation = relation;
3317
+ }
3318
+ where(where) {
3319
+ this.extraWhere = { ...this.extraWhere, ...where };
3320
+ return this;
3321
+ }
3322
+ orderBy(orderBy) {
3323
+ this.extraOptions = { ...this.extraOptions, orderBy };
3324
+ return this;
3325
+ }
3326
+ applyEagerLoad(query, alias) {
3327
+ query.withBelongsTo(alias, this.relation, this.related.repository(), this.extraOptions);
3328
+ }
3329
+ hydrateEager(row, alias) {
3330
+ const value = row[alias];
3331
+ return value ? this.related.newFromRecord(value) : value;
3332
+ }
3333
+ toExistsClause(parentTable) {
3334
+ const relatedTable = this.related.repository().getTable().name;
3335
+ const extra = buildAdvancedWhereClause(relatedTable, this.extraWhere, [], []);
3336
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
3337
+ const sql = `SELECT 1 FROM ${quoteIdentifier(relatedTable)} WHERE ${qualifyColumn(relatedTable, this.relation.ownerKey)} = ${qualifyColumn(parentTable, this.relation.foreignKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
3338
+ return { sql, params: extra.params };
3339
+ }
3340
+ async get() {
3341
+ const foreign = this.parent.get(this.relation.foreignKey);
3342
+ if (foreign === null || foreign === undefined) {
3343
+ return null;
3344
+ }
3345
+ const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
3346
+ let query = repository.query(asWhere({ [this.relation.ownerKey]: foreign, ...this.extraWhere }));
3347
+ if (this.extraOptions.orderBy) {
3348
+ query = query.orderBy(this.extraOptions.orderBy);
3349
+ }
3350
+ const row = await query.first();
3351
+ return row ? this.related.newFromRecord(row) : null;
3352
+ }
3353
+ async first() {
3354
+ return this.get();
3355
+ }
3356
+ then(onfulfilled, onrejected) {
3357
+ return thenGet(() => this.get(), onfulfilled, onrejected);
3358
+ }
3359
+ async associate(owner) {
3360
+ await this.parent.getRepository().updateById(this.parent.id, {
3361
+ [this.relation.foreignKey]: ownerId(owner, this.relation.ownerKey)
3362
+ });
3363
+ }
3364
+ async dissociate() {
3365
+ await this.parent.getRepository().updateById(this.parent.id, {
3366
+ [this.relation.foreignKey]: null
3367
+ });
3368
+ }
3369
+ }
3370
+
3371
+ class BelongsToManyRelationQuery {
3372
+ parent;
3373
+ related;
3374
+ relation;
3375
+ kind = "belongsToMany";
3376
+ extraWhere = {};
3377
+ extraOptions = {};
3378
+ pivotValues = {};
3379
+ constructor(parent, related, relation) {
3380
+ this.parent = parent;
3381
+ this.related = related;
3382
+ this.relation = relation;
3383
+ }
3384
+ where(where) {
3385
+ this.extraWhere = { ...this.extraWhere, ...where };
3386
+ return this;
3387
+ }
3388
+ orderBy(orderBy) {
3389
+ this.extraOptions = { ...this.extraOptions, orderBy };
3390
+ return this;
3391
+ }
3392
+ applyEagerLoad(query, alias) {
3393
+ query.withBelongsToMany(alias, this.relation, this.related.repository(), this.extraOptions);
3394
+ }
3395
+ withPivotValues(values) {
3396
+ this.pivotValues = { ...this.pivotValues, ...values };
3397
+ return this;
3398
+ }
3399
+ hydrateEager(row, alias) {
3400
+ const value = row[alias] ?? [];
3401
+ const rows = Array.isArray(value) ? value : [];
3402
+ return rows.map((item) => this.related.newFromRecord(item));
3403
+ }
3404
+ toExistsClause(parentTable) {
3405
+ const relatedTable = this.related.repository().getTable().name;
3406
+ const extra = buildAdvancedWhereClause(relatedTable, this.extraWhere, [], []);
3407
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
3408
+ 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}` : ""}`;
3409
+ return { sql, params: extra.params };
3410
+ }
3411
+ connection() {
3412
+ return this.parent.getRepository().getConnection();
3413
+ }
3414
+ async get() {
3415
+ const parentId = this.parent.get(this.relation.parentKey);
3416
+ const pivotRows = await this.connection().unsafe(`SELECT * FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1`, [parentId]);
3417
+ if (pivotRows.length === 0) {
3418
+ return [];
3419
+ }
3420
+ const relatedIds = [
3421
+ ...new Set(pivotRows.map((row) => row[this.relation.relatedPivotKey]))
3422
+ ];
3423
+ const repository = this.related.repository().withConnection(this.connection());
3424
+ const rows = await repository.findAll({
3425
+ ...this.extraOptions,
3426
+ where: asWhere({
3427
+ [this.relation.relatedKey]: relatedIds,
3428
+ ...this.extraWhere
3429
+ })
3430
+ });
3431
+ return rows.map((row) => this.related.newFromRecord(row));
3432
+ }
3433
+ async first() {
3434
+ const rows = await this.get();
3435
+ return rows[0] ?? null;
3436
+ }
3437
+ async count() {
3438
+ const parentId = this.parent.get(this.relation.parentKey);
3439
+ const rows = await this.connection().unsafe(`SELECT COUNT(*) AS count FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1`, [parentId]);
3440
+ return Number(rows[0]?.count ?? 0);
3441
+ }
3442
+ then(onfulfilled, onrejected) {
3443
+ return thenGet(() => this.get(), onfulfilled, onrejected);
3444
+ }
3445
+ async attach(ids) {
3446
+ const list = Array.isArray(ids) ? ids : [ids];
3447
+ const parentId = this.parent.get(this.relation.parentKey);
3448
+ const extraKeys = Object.keys(this.pivotValues);
3449
+ const extraColumns = extraKeys.length > 0 ? `, ${extraKeys.join(", ")}` : "";
3450
+ const extraPlaceholders = extraKeys.map((_, index) => `$${index + 3}`).join(", ");
3451
+ const extraValues = extraKeys.map((key) => this.pivotValues[key]);
3452
+ for (const id of list) {
3453
+ 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]);
3454
+ }
3455
+ }
3456
+ async toggle(ids) {
3457
+ const list = Array.isArray(ids) ? ids : [ids];
3458
+ const parentId = this.parent.get(this.relation.parentKey);
3459
+ for (const id of list) {
3460
+ 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]);
3461
+ if (existing.length > 0) {
3462
+ await this.detach(id);
3463
+ } else {
3464
+ await this.attach(id);
3465
+ }
3466
+ }
3467
+ }
3468
+ async detach(ids) {
3469
+ const parentId = this.parent.get(this.relation.parentKey);
3470
+ if (ids === undefined) {
3471
+ await this.connection().unsafe(`DELETE FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1`, [parentId]);
3472
+ return;
3473
+ }
3474
+ const list = Array.isArray(ids) ? ids : [ids];
3475
+ await this.connection().unsafe(`DELETE FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1 AND ${String(this.relation.relatedPivotKey)} = ANY($2)`, [parentId, list]);
3476
+ }
3477
+ async sync(ids) {
3478
+ await this.detach();
3479
+ if (ids.length > 0) {
3480
+ await this.attach(ids);
3481
+ }
3482
+ }
3483
+ async create(attributes = {}) {
3484
+ const related = await this.related.create(attributes);
3485
+ await this.attach(related.id);
3486
+ return related;
3487
+ }
3488
+ }
3489
+
3490
+ class MorphManyRelationQuery {
3491
+ parent;
3492
+ related;
3493
+ relation;
3494
+ kind = "morphMany";
3495
+ extraWhere = {};
3496
+ extraOptions = {};
3497
+ constructor(parent, related, relation) {
3498
+ this.parent = parent;
3499
+ this.related = related;
3500
+ this.relation = relation;
3501
+ }
3502
+ where(where) {
3503
+ this.extraWhere = { ...this.extraWhere, ...where };
3504
+ return this;
3505
+ }
3506
+ applyEagerLoad(query, alias) {
3507
+ query.withMorphMany(alias, this.relation, this.related.repository(), this.extraOptions);
3508
+ }
3509
+ hydrateEager(row, alias) {
3510
+ const value = row[alias] ?? [];
3511
+ const rows = Array.isArray(value) ? value : [];
3512
+ return rows.map((item) => this.related.newFromRecord(item));
3513
+ }
3514
+ toExistsClause(parentTable) {
3515
+ const childTable = this.related.repository().getTable().name;
3516
+ const extra = buildAdvancedWhereClause(childTable, {
3517
+ [this.relation.morphTypeKey]: this.relation.morphType,
3518
+ ...this.extraWhere
3519
+ }, [], []);
3520
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
3521
+ const sql = `SELECT 1 FROM ${quoteIdentifier(childTable)} WHERE ${qualifyColumn(childTable, this.relation.morphIdKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
3522
+ return { sql, params: extra.params };
3523
+ }
3524
+ async get() {
3525
+ const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
3526
+ const rows = await repository.query(asWhere({
3527
+ [this.relation.morphTypeKey]: this.relation.morphType,
3528
+ [this.relation.morphIdKey]: this.parent.get(this.relation.localKey),
3529
+ ...this.extraWhere
3530
+ })).get();
3531
+ return rows.map((row) => this.related.newFromRecord(row));
3532
+ }
3533
+ async first() {
3534
+ const rows = await this.get();
3535
+ return rows[0] ?? null;
3536
+ }
3537
+ async count() {
3538
+ const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
3539
+ return repository.query(asWhere({
3540
+ [this.relation.morphTypeKey]: this.relation.morphType,
3541
+ [this.relation.morphIdKey]: this.parent.get(this.relation.localKey),
3542
+ ...this.extraWhere
3543
+ })).count();
3544
+ }
3545
+ then(onfulfilled, onrejected) {
3546
+ return thenGet(() => this.get(), onfulfilled, onrejected);
3547
+ }
3548
+ async create(attributes = {}) {
3549
+ return this.related.create(attributes, {
3550
+ [this.relation.morphTypeKey]: this.relation.morphType,
3551
+ [this.relation.morphIdKey]: this.parent.get(this.relation.localKey)
3552
+ });
3553
+ }
3554
+ }
3555
+
3556
+ class MorphOneRelationQuery {
3557
+ relation;
3558
+ kind = "morphOne";
3559
+ inner;
3560
+ constructor(parent, related, relation) {
3561
+ this.relation = relation;
3562
+ this.inner = new MorphManyRelationQuery(parent, related, {
3563
+ type: "morphMany",
3564
+ name: relation.name,
3565
+ localKey: relation.localKey,
3566
+ morphTypeKey: relation.morphTypeKey,
3567
+ morphIdKey: relation.morphIdKey,
3568
+ morphType: relation.morphType
3569
+ });
3570
+ }
3571
+ where(where) {
3572
+ this.inner.where(where);
3573
+ return this;
3574
+ }
3575
+ applyEagerLoad(query, alias) {
3576
+ this.inner.applyEagerLoad(query, alias);
3577
+ }
3578
+ hydrateEager(row, alias) {
3579
+ const hydrated = this.inner.hydrateEager(row, alias);
3580
+ return hydrated[0];
3581
+ }
3582
+ toExistsClause(parentTable) {
3583
+ return this.inner.toExistsClause(parentTable);
3584
+ }
3585
+ async get() {
3586
+ return this.inner.first();
3587
+ }
3588
+ async first() {
3589
+ return this.get();
3590
+ }
3591
+ async count() {
3592
+ return this.inner.count();
3593
+ }
3594
+ then(onfulfilled, onrejected) {
3595
+ return thenGet(() => this.get(), onfulfilled, onrejected);
3596
+ }
3597
+ async create(attributes = {}) {
3598
+ return this.inner.create(attributes);
3599
+ }
3600
+ }
3601
+
3602
+ class MorphToRelationQuery {
3603
+ parent;
3604
+ relatedByType;
3605
+ relation;
3606
+ kind = "morphTo";
3607
+ extraWhere = {};
3608
+ constructor(parent, relatedByType, relation) {
3609
+ this.parent = parent;
3610
+ this.relatedByType = relatedByType;
3611
+ this.relation = relation;
3612
+ }
3613
+ where(where) {
3614
+ this.extraWhere = { ...this.extraWhere, ...where };
3615
+ return this;
3616
+ }
3617
+ applyEagerLoad(query, alias) {
3618
+ const repositories = new Map(Object.entries(this.relatedByType).map(([type, model]) => [
3619
+ type,
3620
+ model.repository()
3621
+ ]));
3622
+ query.withMorphTo(alias, this.relation, repositories);
3623
+ }
3624
+ hydrateEager(row, alias) {
3625
+ return row[alias];
3626
+ }
3627
+ toExistsClause(parentTable) {
3628
+ const type = String(this.parent.get(this.relation.morphTypeKey) ?? "");
3629
+ const related = this.relatedByType[type];
3630
+ if (!related) {
3631
+ return { sql: "SELECT 1 WHERE 1 = 0", params: [] };
3632
+ }
3633
+ const relatedTable = related.repository().getTable();
3634
+ const extra = buildAdvancedWhereClause(relatedTable.name, this.extraWhere, [], []);
3635
+ const extraSql = extra.clause.replace(/^ WHERE /, "");
3636
+ return {
3637
+ sql: `SELECT 1 FROM ${quoteIdentifier(relatedTable.name)} WHERE ${qualifyColumn(relatedTable.name, relatedTable.primaryKey)} = ${qualifyColumn(parentTable, this.relation.morphIdKey)}${extraSql ? ` AND ${extraSql}` : ""}`,
3638
+ params: extra.params
3639
+ };
3640
+ }
3641
+ async get() {
3642
+ const type = String(this.parent.get(this.relation.morphTypeKey) ?? "");
3643
+ const id = this.parent.get(this.relation.morphIdKey);
3644
+ const related = this.relatedByType[type];
3645
+ if (!related || id === null || id === undefined) {
3646
+ return null;
3647
+ }
3648
+ const table = related.repository().getTable();
3649
+ const row = await related.repository().withConnection(this.parent.getRepository().getConnection()).query(asWhere({ [table.primaryKey]: id, ...this.extraWhere })).first();
3650
+ return row ? related.newFromRecord(row) : null;
3651
+ }
3652
+ then(onfulfilled, onrejected) {
3653
+ return thenGet(() => this.get(), onfulfilled, onrejected);
3654
+ }
3655
+ }
3656
+
2881
3657
  // ../../src/core/database/model.ts
2882
3658
  var modelRepositories = new WeakMap;
3659
+ var namedModels = new Map;
2883
3660
  var modelGlobalScopes = new WeakMap;
3661
+ var modelObservers = new WeakMap;
2884
3662
  var modelBooted = new WeakSet;
3663
+ async function runObservers(model, hook) {
3664
+ const observers = modelObservers.get(model.constructor) ?? [];
3665
+ for (const observer of observers) {
3666
+ const handler = observer[hook];
3667
+ if (handler && await handler(model) === false) {
3668
+ return false;
3669
+ }
3670
+ }
3671
+ return true;
3672
+ }
3673
+ function accessorName(key) {
3674
+ const pascal = key.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase());
3675
+ return `get${pascal.charAt(0).toUpperCase()}${pascal.slice(1)}Attribute`;
3676
+ }
3677
+ function isLoadableModel(value) {
3678
+ return Boolean(value && typeof value === "object" && typeof value.load === "function" && typeof value.loaded === "function" && typeof value.setLoaded === "function");
3679
+ }
3680
+ async function eagerLoadOnModels(models, paths) {
3681
+ if (models.length === 0 || paths.length === 0) {
3682
+ return;
3683
+ }
3684
+ const grouped = new Map;
3685
+ for (const path of paths) {
3686
+ const [head, ...rest] = path.split(".");
3687
+ if (!head) {
3688
+ continue;
3689
+ }
3690
+ const nested = rest.join(".");
3691
+ const existing = grouped.get(head) ?? [];
3692
+ if (nested) {
3693
+ existing.push(nested);
3694
+ }
3695
+ grouped.set(head, existing);
3696
+ }
3697
+ for (const [head, nested] of grouped) {
3698
+ const unloaded = models.filter((model) => model.loaded(head) === undefined);
3699
+ if (unloaded.length > 0) {
3700
+ const first = unloaded[0];
3701
+ if (!first) {
3702
+ continue;
3703
+ }
3704
+ const method = first[head];
3705
+ if (typeof method !== "function") {
3706
+ throw new Error(`${first.constructor.name} has no relation method ${head}().`);
3707
+ }
3708
+ const relationQuery = method.call(first);
3709
+ const query = first.getRepository().query();
3710
+ relationQuery.applyEagerLoad(query, head);
3711
+ const attached = await query.attachToRows(unloaded.map((model) => model.toObject()));
3712
+ for (const [index, model] of unloaded.entries()) {
3713
+ const row = attached[index] ?? model.toObject();
3714
+ model.setLoaded(head, relationQuery.hydrateEager(row, head));
3715
+ }
3716
+ }
3717
+ if (nested.length === 0) {
3718
+ continue;
3719
+ }
3720
+ const children = models.flatMap((model) => {
3721
+ const loaded = model.loaded(head);
3722
+ return Array.isArray(loaded) ? loaded : loaded ? [loaded] : [];
3723
+ });
3724
+ await eagerLoadOnModels(children.filter(isLoadableModel), nested);
3725
+ }
3726
+ }
3727
+ async function loadNested(model, path) {
3728
+ await eagerLoadOnModels([model], [path]);
3729
+ }
2885
3730
  function resolveModelRepository(model) {
2886
3731
  const repository = modelRepositories.get(model);
2887
3732
  if (!repository) {
@@ -2889,6 +3734,48 @@ function resolveModelRepository(model) {
2889
3734
  }
2890
3735
  return repository;
2891
3736
  }
3737
+ function registerModelClass(name, model) {
3738
+ namedModels.set(name, model);
3739
+ }
3740
+ function resolveRelated(related) {
3741
+ if (typeof related === "string") {
3742
+ const found = namedModels.get(related);
3743
+ if (!found) {
3744
+ throw new Error(`Model [${related}] is not registered. Call registerModelClass() first.`);
3745
+ }
3746
+ return found;
3747
+ }
3748
+ if (typeof related === "function" && typeof related.repository !== "function") {
3749
+ return related();
3750
+ }
3751
+ return related;
3752
+ }
3753
+ function inferRelationMethodName(callee) {
3754
+ const stack = new Error().stack ?? "";
3755
+ let seenCallee = false;
3756
+ for (const line of stack.split(`
3757
+ `)) {
3758
+ const match = /at (?:async )?(?:[^.\s]+\.)?(\w+)/.exec(line);
3759
+ const name = match?.[1];
3760
+ if (!name || name === "Error" || name === "inferRelationMethodName") {
3761
+ continue;
3762
+ }
3763
+ if (!seenCallee) {
3764
+ if (name === callee) {
3765
+ seenCallee = true;
3766
+ }
3767
+ continue;
3768
+ }
3769
+ if (name !== callee) {
3770
+ return name;
3771
+ }
3772
+ }
3773
+ return;
3774
+ }
3775
+ function morphClassOf(model) {
3776
+ const statics = modelStatics(model.constructor === Function ? model : model.constructor);
3777
+ return statics.$morphClass ?? (model.constructor === Function ? model.name : model.constructor.name);
3778
+ }
2892
3779
  function modelStatics(model) {
2893
3780
  return model;
2894
3781
  }
@@ -2918,6 +3805,11 @@ function hydrateValue(value, cast) {
2918
3805
  case "bool":
2919
3806
  case "boolean":
2920
3807
  return value === true || value === 1 || value === "1" || value === "true";
3808
+ case "integer":
3809
+ case "int":
3810
+ return value === "" ? null : Number(value);
3811
+ case "hashed":
3812
+ return value;
2921
3813
  default:
2922
3814
  return value;
2923
3815
  }
@@ -2935,6 +3827,11 @@ function dehydrateValue(value, cast) {
2935
3827
  case "bool":
2936
3828
  case "boolean":
2937
3829
  return Boolean(value);
3830
+ case "integer":
3831
+ case "int":
3832
+ return value === "" ? null : Number(value);
3833
+ case "hashed":
3834
+ return value;
2938
3835
  default:
2939
3836
  return value;
2940
3837
  }
@@ -2989,6 +3886,153 @@ function applyTimestampsOnUpdate(columns, values, enabled) {
2989
3886
  return result;
2990
3887
  }
2991
3888
 
3889
+ class ModelQuery {
3890
+ modelClass;
3891
+ query;
3892
+ eager = [];
3893
+ constructor(modelClass, query) {
3894
+ this.modelClass = modelClass;
3895
+ this.query = query;
3896
+ }
3897
+ with(...relations) {
3898
+ const statics = modelStatics(this.modelClass);
3899
+ ensureBooted(this.modelClass);
3900
+ const dummy = statics.newFromRecord({}, false);
3901
+ for (const path of relations) {
3902
+ const name = path.split(".")[0] ?? path;
3903
+ const method = dummy[name];
3904
+ if (typeof method !== "function") {
3905
+ throw new Error(`${this.modelClass.name} has no relation method ${name}().`);
3906
+ }
3907
+ const relationQuery = method.call(dummy);
3908
+ this.eager.push({ name, path, relationQuery });
3909
+ relationQuery.applyEagerLoad(this.query, name);
3910
+ }
3911
+ return this;
3912
+ }
3913
+ where(input) {
3914
+ this.query.where(input);
3915
+ return this;
3916
+ }
3917
+ orWhere(input) {
3918
+ this.query.orWhere(input);
3919
+ return this;
3920
+ }
3921
+ orderBy(orderBy) {
3922
+ this.query.orderBy(orderBy);
3923
+ return this;
3924
+ }
3925
+ limit(limit) {
3926
+ this.query.limit(limit);
3927
+ return this;
3928
+ }
3929
+ offset(offset) {
3930
+ this.query.offset(offset);
3931
+ return this;
3932
+ }
3933
+ whereNull(column) {
3934
+ this.query.whereNull(column);
3935
+ return this;
3936
+ }
3937
+ whereIn(column, values) {
3938
+ this.query.whereIn(column, values);
3939
+ return this;
3940
+ }
3941
+ whereExists(sql, params = []) {
3942
+ this.query.whereExists(sql, params);
3943
+ return this;
3944
+ }
3945
+ whereNotExists(sql, params = []) {
3946
+ this.query.whereNotExists(sql, params);
3947
+ return this;
3948
+ }
3949
+ whereHas(name, constrain) {
3950
+ return this.constrainExists(name, constrain, false);
3951
+ }
3952
+ has(name) {
3953
+ return this.constrainExists(name, undefined, false);
3954
+ }
3955
+ doesntHave(name) {
3956
+ return this.constrainExists(name, undefined, true);
3957
+ }
3958
+ whereDoesntHave(name, constrain) {
3959
+ return this.constrainExists(name, constrain, true);
3960
+ }
3961
+ withHasMany(...args) {
3962
+ this.query.withHasMany(...args);
3963
+ return this;
3964
+ }
3965
+ withBelongsTo(...args) {
3966
+ this.query.withBelongsTo(...args);
3967
+ return this;
3968
+ }
3969
+ withBelongsToMany(...args) {
3970
+ this.query.withBelongsToMany(...args);
3971
+ return this;
3972
+ }
3973
+ withMorphMany(...args) {
3974
+ this.query.withMorphMany(...args);
3975
+ return this;
3976
+ }
3977
+ withMorphOne(...args) {
3978
+ this.query.withMorphOne(...args);
3979
+ return this;
3980
+ }
3981
+ withMorphTo(...args) {
3982
+ this.query.withMorphTo(...args);
3983
+ return this;
3984
+ }
3985
+ async get() {
3986
+ const statics = modelStatics(this.modelClass);
3987
+ const rows = await this.query.get();
3988
+ const models = [];
3989
+ for (const row of rows) {
3990
+ const model = statics.newFromRecord(row, true);
3991
+ await runObservers(model, "retrieved");
3992
+ for (const { name, relationQuery } of this.eager) {
3993
+ model.setLoaded(name, relationQuery.hydrateEager(row, name));
3994
+ }
3995
+ models.push(model);
3996
+ }
3997
+ const nested = this.eager.filter((item) => item.path.includes(".")).map((item) => item.path);
3998
+ await eagerLoadOnModels(models.filter(isLoadableModel), nested);
3999
+ return models;
4000
+ }
4001
+ async first() {
4002
+ this.query.limit(1);
4003
+ const models = await this.get();
4004
+ return models[0] ?? null;
4005
+ }
4006
+ async find(id) {
4007
+ const primaryKey = resolveModelRepository(this.modelClass).getTable().primaryKey;
4008
+ return this.where({ [primaryKey]: id }).first();
4009
+ }
4010
+ async findOrFail(id, errorFactory) {
4011
+ const model = await this.find(id);
4012
+ if (model) {
4013
+ return model;
4014
+ }
4015
+ throw errorFactory?.(id) ?? new NotFoundError(`${this.modelClass.name} ${String(id)} not found.`);
4016
+ }
4017
+ then(onfulfilled, onrejected) {
4018
+ return this.get().then(onfulfilled ?? undefined, onrejected ?? undefined);
4019
+ }
4020
+ constrainExists(name, constrain, not) {
4021
+ const statics = modelStatics(this.modelClass);
4022
+ ensureBooted(this.modelClass);
4023
+ const repository = resolveModelRepository(this.modelClass);
4024
+ const dummy = statics.newFromRecord({});
4025
+ const method = dummy[name];
4026
+ if (typeof method !== "function") {
4027
+ throw new Error(`${this.modelClass.name} has no relation method ${name}().`);
4028
+ }
4029
+ const relationQuery = method.call(dummy);
4030
+ constrain?.(relationQuery);
4031
+ const exists = relationQuery.toExistsClause(repository.getTable().name);
4032
+ return not ? this.whereNotExists(exists.sql, exists.params) : this.whereExists(exists.sql, exists.params);
4033
+ }
4034
+ }
4035
+
2992
4036
  class Model {
2993
4037
  attributes;
2994
4038
  repository;
@@ -2996,11 +4040,23 @@ class Model {
2996
4040
  static $guarded;
2997
4041
  static $casts = {};
2998
4042
  static $timestamps = true;
4043
+ static $hidden;
4044
+ static $visible;
4045
+ static $appends;
4046
+ static $morphClass;
2999
4047
  _exists;
4048
+ loadedRelations = {};
4049
+ hiddenOverrides = [];
4050
+ visibleOverrides = [];
4051
+ appended = [];
3000
4052
  constructor(attributes, repository, exists = true) {
3001
4053
  this.attributes = attributes;
3002
4054
  this.repository = repository;
3003
4055
  this._exists = exists;
4056
+ this.attributes = modelStatics(this.constructor).hydrateAttributes(attributes);
4057
+ }
4058
+ getRepository() {
4059
+ return this.repository;
3004
4060
  }
3005
4061
  get $exists() {
3006
4062
  return this._exists;
@@ -3014,8 +4070,51 @@ class Model {
3014
4070
  toObject() {
3015
4071
  return { ...this.attributes };
3016
4072
  }
4073
+ toArray() {
4074
+ const ModelClass = modelStatics(this.constructor);
4075
+ const hidden = new Set([...ModelClass.$hidden ?? [], ...this.hiddenOverrides]);
4076
+ const visible = this.visibleOverrides.length > 0 ? this.visibleOverrides : ModelClass.$visible;
4077
+ const data = { ...this.attributes };
4078
+ if (visible && visible.length > 0) {
4079
+ for (const key of Object.keys(data)) {
4080
+ if (!visible.includes(key)) {
4081
+ delete data[key];
4082
+ }
4083
+ }
4084
+ }
4085
+ for (const key of hidden) {
4086
+ delete data[key];
4087
+ }
4088
+ for (const key of [...ModelClass.$appends ?? [], ...this.appended]) {
4089
+ const accessor = this[accessorName(key)];
4090
+ if (typeof accessor === "function") {
4091
+ data[key] = accessor.call(this);
4092
+ }
4093
+ }
4094
+ for (const [name, value] of Object.entries(this.loadedRelations)) {
4095
+ if (!hidden.has(name) && (!visible || visible.includes(name) || this.appended.includes(name))) {
4096
+ data[name] = value;
4097
+ }
4098
+ }
4099
+ return data;
4100
+ }
4101
+ toJSON() {
4102
+ return this.toArray();
4103
+ }
4104
+ makeHidden(...keys) {
4105
+ this.hiddenOverrides.push(...keys);
4106
+ return this;
4107
+ }
4108
+ makeVisible(...keys) {
4109
+ this.visibleOverrides.push(...keys);
4110
+ return this;
4111
+ }
4112
+ append(...keys) {
4113
+ this.appended.push(...keys);
4114
+ return this;
4115
+ }
3017
4116
  primaryKey() {
3018
- throw new Error(`${this.constructor.name}.primaryKey() is not implemented.`);
4117
+ return this.repository.getTable().primaryKey;
3019
4118
  }
3020
4119
  static primaryKeyField() {
3021
4120
  return resolveModelRepository(this).getTable().primaryKey;
@@ -3034,6 +4133,10 @@ class Model {
3034
4133
  return new statics(hydrated, repository, exists);
3035
4134
  }
3036
4135
  static boot() {}
4136
+ static observe(observer) {
4137
+ const existing = modelObservers.get(this) ?? [];
4138
+ modelObservers.set(this, [...existing, observer]);
4139
+ }
3037
4140
  static addGlobalScope(_name, scope) {
3038
4141
  ensureBooted(this);
3039
4142
  const existing = modelGlobalScopes.get(this) ?? [];
@@ -3052,26 +4155,55 @@ class Model {
3052
4155
  for (const scope of getGlobalScopes(this)) {
3053
4156
  query = scope(query);
3054
4157
  }
3055
- return query;
4158
+ return new ModelQuery(this, query);
3056
4159
  }
3057
- static async create(attributes) {
4160
+ static newFromRecord(record, exists = true) {
4161
+ const repository = resolveModelRepository(this);
4162
+ return modelStatics(this).fromRecord(record, repository, exists);
4163
+ }
4164
+ static async create(attributes, forced = {}) {
3058
4165
  const statics = modelStatics(this);
3059
4166
  ensureBooted(this);
3060
4167
  const repository = resolveModelRepository(this);
3061
4168
  const table = repository.getTable();
3062
4169
  const timestamps = statics.$timestamps ?? true;
3063
- const assignable = filterMassAssignable(statics.$fillable, statics.$guarded, attributes);
4170
+ const assignable = {
4171
+ ...filterMassAssignable(statics.$fillable, statics.$guarded, attributes),
4172
+ ...forced
4173
+ };
3064
4174
  const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
3065
4175
  const payload = statics.dehydrateAttributes(withTimestamps);
4176
+ const pending = statics.newFromRecord({ ...payload }, false);
4177
+ if (await runObservers(pending, "saving") === false) {
4178
+ throw new Error(`${this.name}.create() was cancelled by an observer.`);
4179
+ }
4180
+ if (await runObservers(pending, "creating") === false) {
4181
+ throw new Error(`${this.name}.create() was cancelled by an observer.`);
4182
+ }
3066
4183
  const record = await repository.create(payload);
3067
- return statics.fromRecord(record, repository, true);
4184
+ const created = statics.fromRecord(record, repository, true);
4185
+ await runObservers(created, "created");
4186
+ await runObservers(created, "saved");
4187
+ return created;
4188
+ }
4189
+ static with(...relations) {
4190
+ return Model.query.call(this).with(...relations);
4191
+ }
4192
+ static whereHas(name, constrain) {
4193
+ return Model.query.call(this).whereHas(name, constrain);
4194
+ }
4195
+ static has(name) {
4196
+ return Model.query.call(this).has(name);
4197
+ }
4198
+ static doesntHave(name) {
4199
+ return Model.query.call(this).doesntHave(name);
4200
+ }
4201
+ static whereDoesntHave(name, constrain) {
4202
+ return Model.query.call(this).whereDoesntHave(name, constrain);
3068
4203
  }
3069
4204
  static async find(id) {
3070
- const statics = modelStatics(this);
3071
- const repository = resolveModelRepository(this);
3072
- const primaryKey = repository.getTable().primaryKey;
3073
- const record = await Model.query.call(this).where({ [primaryKey]: id }).first();
3074
- return record ? statics.fromRecord(record, repository, true) : null;
4205
+ const primaryKey = resolveModelRepository(this).getTable().primaryKey;
4206
+ return Model.query.call(this).where({ [primaryKey]: id }).first();
3075
4207
  }
3076
4208
  static async findOrFail(id, errorFactory) {
3077
4209
  const model = await Model.find.call(this, id);
@@ -3081,8 +4213,6 @@ class Model {
3081
4213
  throw errorFactory?.(id) ?? new NotFoundError(`${this.name} ${String(id)} not found.`);
3082
4214
  }
3083
4215
  static async all(options = {}) {
3084
- const statics = modelStatics(this);
3085
- const repository = resolveModelRepository(this);
3086
4216
  let query = Model.query.call(this);
3087
4217
  if (options.orderBy) {
3088
4218
  query = query.orderBy(options.orderBy);
@@ -3090,28 +4220,57 @@ class Model {
3090
4220
  if (options.limit !== undefined) {
3091
4221
  query = query.limit(options.limit);
3092
4222
  }
3093
- const rows = await query.get();
3094
- return rows.map((row) => statics.fromRecord(row, repository, true));
4223
+ return query.get();
4224
+ }
4225
+ static where(where) {
4226
+ return Model.query.call(this).where(where);
3095
4227
  }
3096
4228
  static async firstWhere(where, options = {}) {
3097
- const statics = modelStatics(this);
3098
- const repository = resolveModelRepository(this);
3099
4229
  let query = Model.query.call(this).where(where);
3100
4230
  if (options.orderBy) {
3101
4231
  query = query.orderBy(options.orderBy);
3102
4232
  }
3103
- const record = await query.first();
3104
- return record ? statics.fromRecord(record, repository, true) : null;
4233
+ return query.first();
4234
+ }
4235
+ static async firstOrNew(where, values = {}) {
4236
+ const existing = await Model.firstWhere.call(this, where);
4237
+ if (existing) {
4238
+ return existing;
4239
+ }
4240
+ return modelStatics(this).newFromRecord({ ...where, ...values }, false);
4241
+ }
4242
+ static async firstOrCreate(where, values = {}) {
4243
+ const existing = await Model.firstWhere.call(this, where);
4244
+ if (existing) {
4245
+ return existing;
4246
+ }
4247
+ return Model.create.call(this, { ...where, ...values });
4248
+ }
4249
+ static async updateOrCreate(where, values = {}) {
4250
+ const existing = await Model.firstWhere.call(this, where);
4251
+ if (existing) {
4252
+ return existing.update(values);
4253
+ }
4254
+ return Model.create.call(this, { ...where, ...values });
3105
4255
  }
3106
4256
  async save() {
3107
4257
  const ModelClass = modelStatics(this.constructor);
3108
4258
  const timestamps = ModelClass.$timestamps ?? true;
3109
4259
  const casts = ModelClass.$casts ?? {};
3110
4260
  const table = this.repository.getTable();
3111
- if (this.$exists) {
4261
+ const updating = this.$exists;
4262
+ if (await runObservers(this, "saving") === false) {
4263
+ return this;
4264
+ }
4265
+ if (await runObservers(this, updating ? "updating" : "creating") === false) {
4266
+ return this;
4267
+ }
4268
+ if (updating) {
3112
4269
  const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
3113
4270
  const record2 = await this.repository.updateByIdOrThrow(this.id, changes);
3114
4271
  this.attributes = ModelClass.hydrateAttributes(record2);
4272
+ await runObservers(this, "updated");
4273
+ await runObservers(this, "saved");
3115
4274
  return this;
3116
4275
  }
3117
4276
  const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, this.attributes);
@@ -3120,6 +4279,8 @@ class Model {
3120
4279
  const record = await this.repository.create(payload);
3121
4280
  this.attributes = ModelClass.hydrateAttributes(record);
3122
4281
  this._exists = true;
4282
+ await runObservers(this, "created");
4283
+ await runObservers(this, "saved");
3123
4284
  return this;
3124
4285
  }
3125
4286
  async update(changes) {
@@ -3129,10 +4290,14 @@ class Model {
3129
4290
  return await this.save();
3130
4291
  }
3131
4292
  async delete() {
3132
- if (resolveSoftDeleteColumn(this.repository.getTable())) {
3133
- return await this.repository.deleteById(this.id);
4293
+ if (await runObservers(this, "deleting") === false) {
4294
+ return false;
3134
4295
  }
3135
- return await this.repository.forceDeleteById(this.id);
4296
+ const deleted = resolveSoftDeleteColumn(this.repository.getTable()) ? await this.repository.deleteById(this.id) : await this.repository.forceDeleteById(this.id);
4297
+ if (deleted) {
4298
+ await runObservers(this, "deleted");
4299
+ }
4300
+ return deleted;
3136
4301
  }
3137
4302
  async forceDelete() {
3138
4303
  return await this.repository.forceDeleteById(this.id);
@@ -3181,6 +4346,102 @@ class Model {
3181
4346
  const loaded = grouped.get(parentId) ?? [];
3182
4347
  return Object.assign(this, { [as]: loaded });
3183
4348
  }
4349
+ hasMany(related, foreignKey, localKey) {
4350
+ const table = this.repository.getTable();
4351
+ const relatedClass = resolveRelated(related);
4352
+ return new HasManyRelationQuery(this, relatedClass, hasMany({
4353
+ name: relatedClass.repository().getTable().name,
4354
+ localKey: localKey ?? table.primaryKey,
4355
+ foreignKey: foreignKey ?? foreignKeyFromTable(table.name)
4356
+ }));
4357
+ }
4358
+ hasOne(related, foreignKey, localKey) {
4359
+ const table = this.repository.getTable();
4360
+ const relatedClass = resolveRelated(related);
4361
+ return new HasOneRelationQuery(this, relatedClass, hasOne({
4362
+ name: relatedClass.repository().getTable().name,
4363
+ localKey: localKey ?? table.primaryKey,
4364
+ foreignKey: foreignKey ?? foreignKeyFromTable(table.name)
4365
+ }));
4366
+ }
4367
+ belongsTo(related, foreignKey, ownerKey) {
4368
+ const relatedClass = resolveRelated(related);
4369
+ const relatedTable = relatedClass.repository().getTable();
4370
+ return new BelongsToRelationQuery(this, relatedClass, belongsTo({
4371
+ name: relatedTable.name,
4372
+ foreignKey: foreignKey ?? foreignKeyFromTable(relatedTable.name),
4373
+ ownerKey: ownerKey ?? relatedTable.primaryKey
4374
+ }));
4375
+ }
4376
+ belongsToMany(related, pivotTable, foreignPivotKey, relatedPivotKey) {
4377
+ const table = this.repository.getTable();
4378
+ const relatedClass = resolveRelated(related);
4379
+ const relatedTable = relatedClass.repository().getTable();
4380
+ return new BelongsToManyRelationQuery(this, relatedClass, belongsToMany({
4381
+ name: relatedTable.name,
4382
+ pivotTable: pivotTable ?? pivotTableName(table.name, relatedTable.name),
4383
+ parentKey: table.primaryKey,
4384
+ relatedKey: relatedTable.primaryKey,
4385
+ foreignPivotKey: foreignPivotKey ?? foreignKeyFromTable(table.name),
4386
+ relatedPivotKey: relatedPivotKey ?? foreignKeyFromTable(relatedTable.name)
4387
+ }));
4388
+ }
4389
+ morphMany(related, morphName, typeKey, idKey, morphType) {
4390
+ const table = this.repository.getTable();
4391
+ const relatedClass = resolveRelated(related);
4392
+ return new MorphManyRelationQuery(this, relatedClass, morphMany({
4393
+ name: morphName,
4394
+ localKey: table.primaryKey,
4395
+ morphTypeKey: typeKey ?? `${morphName}_type`,
4396
+ morphIdKey: idKey ?? `${morphName}_id`,
4397
+ morphType: morphType ?? morphClassOf(this)
4398
+ }));
4399
+ }
4400
+ morphOne(related, morphName, typeKey, idKey, morphType) {
4401
+ const table = this.repository.getTable();
4402
+ const relatedClass = resolveRelated(related);
4403
+ return new MorphOneRelationQuery(this, relatedClass, morphOne({
4404
+ name: morphName,
4405
+ localKey: table.primaryKey,
4406
+ morphTypeKey: typeKey ?? `${morphName}_type`,
4407
+ morphIdKey: idKey ?? `${morphName}_id`,
4408
+ morphType: morphType ?? morphClassOf(this)
4409
+ }));
4410
+ }
4411
+ morphTo(relatedByType, morphName, typeKey, idKey) {
4412
+ const resolvedName = morphName ?? inferRelationMethodName("morphTo");
4413
+ if (!resolvedName) {
4414
+ throw new Error(`${this.constructor.name}.morphTo() needs an explicit morph name (Laravel infers it from the relation method).`);
4415
+ }
4416
+ const resolvedMap = Object.fromEntries(Object.entries(relatedByType).map(([type, related]) => [type, resolveRelated(related)]));
4417
+ return new MorphToRelationQuery(this, resolvedMap, morphTo({
4418
+ name: resolvedName,
4419
+ morphTypeKey: typeKey ?? `${resolvedName}_type`,
4420
+ morphIdKey: idKey ?? `${resolvedName}_id`
4421
+ }));
4422
+ }
4423
+ async load(...names) {
4424
+ for (const name of names) {
4425
+ if (name.includes(".")) {
4426
+ await loadNested(this, name);
4427
+ continue;
4428
+ }
4429
+ const method = this[name];
4430
+ if (typeof method !== "function") {
4431
+ throw new Error(`${this.constructor.name} has no relation method ${name}().`);
4432
+ }
4433
+ const relationQuery = method.call(this);
4434
+ this.loadedRelations[name] = await relationQuery.get();
4435
+ }
4436
+ return this;
4437
+ }
4438
+ loaded(name) {
4439
+ return this.loadedRelations[name];
4440
+ }
4441
+ setLoaded(name, value) {
4442
+ this.loadedRelations[name] = value;
4443
+ return this;
4444
+ }
3184
4445
  mergeAttributes(patch) {
3185
4446
  Object.assign(this.attributes, patch);
3186
4447
  return this;
@@ -3188,6 +4449,14 @@ class Model {
3188
4449
  }
3189
4450
  function registerModelRepository(model, repository) {
3190
4451
  modelRepositories.set(model, repository);
4452
+ const name = model.name;
4453
+ if (name) {
4454
+ namedModels.set(name, model);
4455
+ }
4456
+ const morphClass = model.$morphClass;
4457
+ if (morphClass) {
4458
+ namedModels.set(morphClass, model);
4459
+ }
3191
4460
  ensureBooted(model);
3192
4461
  return model;
3193
4462
  }
@@ -5079,6 +6348,70 @@ function createRequireAuthMiddleware(auth2) {
5079
6348
  function serializeDate(value) {
5080
6349
  return value instanceof Date ? value.toISOString() : value;
5081
6350
  }
6351
+ function whenLoaded(model, relation, transform) {
6352
+ const value = model.loaded(relation);
6353
+ if (value === undefined) {
6354
+ return;
6355
+ }
6356
+ return transform ? transform(value) : value;
6357
+ }
6358
+
6359
+ class JsonResource {
6360
+ resource;
6361
+ static wrap = "data";
6362
+ extra = {};
6363
+ constructor(resource) {
6364
+ this.resource = resource;
6365
+ }
6366
+ static make(resource) {
6367
+ return new JsonResource(resource);
6368
+ }
6369
+ static collection(items) {
6370
+ return new ResourceCollection(items);
6371
+ }
6372
+ additional(data) {
6373
+ this.extra = { ...this.extra, ...data };
6374
+ return this;
6375
+ }
6376
+ when(condition, value) {
6377
+ return condition ? value : undefined;
6378
+ }
6379
+ whenLoaded(relation, transform) {
6380
+ const model = this.resource;
6381
+ if (typeof model.loaded !== "function") {
6382
+ return;
6383
+ }
6384
+ return whenLoaded(model, relation, transform);
6385
+ }
6386
+ toArray() {
6387
+ if (this.resource && typeof this.resource === "object" && "toArray" in this.resource) {
6388
+ return this.resource.toArray();
6389
+ }
6390
+ return { ...this.resource };
6391
+ }
6392
+ toResponse() {
6393
+ const wrap = this.constructor.wrap;
6394
+ const payload = this.toArray();
6395
+ if (wrap === null) {
6396
+ return { ...payload, ...this.extra };
6397
+ }
6398
+ return { [wrap]: payload, ...this.extra };
6399
+ }
6400
+ }
6401
+
6402
+ class ResourceCollection extends JsonResource {
6403
+ toArray() {
6404
+ const items = this.resource.map((item) => item instanceof JsonResource ? item.toArray() : { ...item });
6405
+ const wrap = this.constructor.wrap;
6406
+ if (wrap === null) {
6407
+ return { data: items };
6408
+ }
6409
+ return { [wrap]: items };
6410
+ }
6411
+ toResponse() {
6412
+ return { ...this.toArray(), ...this.extra };
6413
+ }
6414
+ }
5082
6415
  function toResourceCollection(items, transformer) {
5083
6416
  return items.map(transformer);
5084
6417
  }
@@ -7161,6 +8494,8 @@ export {
7161
8494
  AuthManager,
7162
8495
  BadRequestError,
7163
8496
  baseRepository_default as BaseRepository,
8497
+ BelongsToManyRelationQuery,
8498
+ BelongsToRelationQuery,
7164
8499
  Blueprint,
7165
8500
  CACHE_TAGS,
7166
8501
  repository_default as CacheRepository,
@@ -7173,19 +8508,27 @@ export {
7173
8508
  DatabaseTokenGuard,
7174
8509
  EtaViewEngine,
7175
8510
  EventBus,
8511
+ Factory,
7176
8512
  failedJobRepository_default as FailedJobRepository,
7177
8513
  failedJobService_default as FailedJobService,
7178
8514
  ForbiddenError,
7179
8515
  ForeignIdColumnDefinition,
7180
8516
  FormRequest,
7181
8517
  GuestGuard,
8518
+ HasManyRelationQuery,
8519
+ HasOneRelationQuery,
7182
8520
  HttpError,
7183
8521
  Job,
8522
+ JsonResource,
7184
8523
  LocalStorageDriver,
7185
8524
  LogMailDriver,
7186
8525
  Mailer,
7187
8526
  membershipService_default as MembershipService,
7188
8527
  Model,
8528
+ ModelQuery,
8529
+ MorphManyRelationQuery,
8530
+ MorphOneRelationQuery,
8531
+ MorphToRelationQuery,
7189
8532
  MySqlGrammar,
7190
8533
  NotFoundError,
7191
8534
  Notification,
@@ -7200,6 +8543,7 @@ export {
7200
8543
  RedisQueue,
7201
8544
  RepositoryQuery,
7202
8545
  ResilientQueue,
8546
+ ResourceCollection,
7203
8547
  Schedule,
7204
8548
  Schema,
7205
8549
  ServiceContainer,
@@ -7297,6 +8641,7 @@ export {
7297
8641
  eventBus,
7298
8642
  events,
7299
8643
  filterMassAssignable,
8644
+ foreignKeyFromTable,
7300
8645
  formatAdminValue,
7301
8646
  freshDatabase,
7302
8647
  generateCspNonce,
@@ -7355,6 +8700,7 @@ export {
7355
8700
  parseMultipartUpload,
7356
8701
  parsePaginationQuery,
7357
8702
  parsePositiveIntParam,
8703
+ pivotTableName,
7358
8704
  policyGate,
7359
8705
  prometheusRegistry,
7360
8706
  queue,
@@ -7367,6 +8713,7 @@ export {
7367
8713
  readTenancyDriver,
7368
8714
  redirectResponse,
7369
8715
  registerDefaultDatabasePool,
8716
+ registerModelClass,
7370
8717
  registerModelRepository,
7371
8718
  registerShutdownHandler,
7372
8719
  renderKernelErrorChrome,
@@ -7423,6 +8770,7 @@ export {
7423
8770
  serverHtmxContentSecurityPolicy,
7424
8771
  setActiveApplicationContext,
7425
8772
  signedUrl,
8773
+ singularize,
7426
8774
  spaContentSecurityPolicy,
7427
8775
  storageFacade as storage,
7428
8776
  strictApiContentSecurityPolicy,
@@ -7436,6 +8784,7 @@ export {
7436
8784
  trustForwardedFor,
7437
8785
  validateObject,
7438
8786
  verifyCsrfToken,
8787
+ whenLoaded,
7439
8788
  withErrorHandling,
7440
8789
  withMiddleware,
7441
8790
  withMigrationLock,