@bunyad/orm 0.1.0-alpha.0

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/model.js ADDED
@@ -0,0 +1,1931 @@
1
+ import { DatabaseManager as Db, setDefaultConnection, connections, LengthAwarePaginator, CursorPaginator, Paginator, dateTimeForStorage, withoutQueryWriteHook, wrapSqlName, } from "@bunyad/database";
2
+ import { Collection } from "@bunyad/common";
3
+ import { OrmCollection } from "./orm-collection.js";
4
+ import { Attribute, castFromStorage, castToStorage, hashCastValue, isAlreadyHashed, enumFromStorage, enumToStorage, isAttribute, isCastType, isEnumCast, } from "./casts.js";
5
+ import { bootIfNotBooted, fireModelEvent, hasAnyModelEventListeners, hasModelEventListeners, observeModel, registerModelEvent, withoutModelEvents, } from "./model-events.js";
6
+ import { resolveModelBuilder } from "./decorators.js";
7
+ import { addGlobalScope as registerGlobalScope, getGlobalScopes, hasGlobalScopes, installLocalScopeCallStatic, removeGlobalScope as unregisterGlobalScope, } from "./scopes.js";
8
+ import { deletedAtColumn, normalizeWithRelations, nowForConnection, parseRelationAlias, singular, usesSoftDeletes, } from "./model-helpers.js";
9
+ import { BelongsTo, BelongsToMany, HasMany, HasManyThrough, HasOne, HasOneThrough, MorphMany, MorphOne, MorphTo, MorphToMany, aggregateRelation, clearMorphMap, morphMap, morphTypeFor, } from "./relations.js";
10
+ import { eagerLoadAggregates, eagerLoadModels, } from "./eager.js";
11
+ import { ModelQuery, MODEL_QUERY_TARGET } from "./model-query.js";
12
+ import { LazyLoadingViolationException, MassAssignmentException, MissingAttributeException, isIgnoringTouch, isInsideEagerLoad, modelsShouldPreventAccessingMissingAttributes, modelsShouldPreventLazyLoading, modelsShouldPreventSilentlyDiscardingAttributes, preventAccessingMissingAttributes as setPreventAccessingMissingAttributes, preventLazyLoading as setPreventLazyLoading, preventSilentlyDiscardingAttributes as setPreventSilentlyDiscardingAttributes, shouldBeStrict as setShouldBeStrict, withoutTouching as runWithoutTouching, } from "./model-strictness.js";
13
+ export { Attribute, AsCollection, AsFluent, AsEnumCollection, } from "./casts.js";
14
+ export { resetModelEventsForTests } from "./model-events.js";
15
+ /** Thrown when a model query returns no results. */
16
+ export class ModelNotFoundException extends Error {
17
+ constructor(message = "No query results for model.") {
18
+ super(message);
19
+ this.name = "ModelNotFoundException";
20
+ }
21
+ }
22
+ /** Resolve a connection name or handle to a live Connection. */
23
+ export function resolveConnection(connection) {
24
+ if (connection == null) {
25
+ return defaultConnection ?? connections.connection();
26
+ }
27
+ if (typeof connection === "string") {
28
+ return connections.connection(connection);
29
+ }
30
+ return connection;
31
+ }
32
+ let defaultConnection;
33
+ const fillableSetCache = new WeakMap();
34
+ const syncInsertColsCache = new WeakMap();
35
+ /** Reused bind list for multi-column sync inserts (single-threaded). */
36
+ const persistValueScratch = [];
37
+ function cachedSyncInsertColumns(ctor) {
38
+ const fillables = ctor.fillable ?? [];
39
+ if (fillables.length === 0)
40
+ return null;
41
+ const usesTimestamps = ctor.timestamps !== false;
42
+ const incrementing = ctor.incrementing !== false;
43
+ const key = ctor.primaryKey;
44
+ let cached = syncInsertColsCache.get(ctor);
45
+ if (cached &&
46
+ cached.timestamps === usesTimestamps &&
47
+ cached.incrementing === incrementing &&
48
+ cached.key === key) {
49
+ return cached.cols;
50
+ }
51
+ const cols = [];
52
+ for (let i = 0; i < fillables.length; i++) {
53
+ const col = fillables[i];
54
+ if (incrementing && col === key)
55
+ continue;
56
+ cols.push(col);
57
+ }
58
+ if (usesTimestamps) {
59
+ if (!cols.includes("created_at"))
60
+ cols.push("created_at");
61
+ if (!cols.includes("updated_at"))
62
+ cols.push("updated_at");
63
+ }
64
+ syncInsertColsCache.set(ctor, {
65
+ cols,
66
+ timestamps: usesTimestamps,
67
+ incrementing,
68
+ key,
69
+ });
70
+ return cols;
71
+ }
72
+ export function filterFillable(model, attributes) {
73
+ const fillables = model.fillable ?? [];
74
+ const guardeds = model.guarded ?? ["*"];
75
+ const strictDiscard = modelsShouldPreventSilentlyDiscardingAttributes();
76
+ if (fillables.length > 0) {
77
+ let cached = fillableSetCache.get(model);
78
+ if (!cached || cached.list !== fillables) {
79
+ cached = { list: fillables, set: new Set(fillables) };
80
+ fillableSetCache.set(model, cached);
81
+ }
82
+ const allowed = cached.set;
83
+ const discarded = [];
84
+ for (const key of Object.keys(attributes)) {
85
+ if (!allowed.has(key))
86
+ discarded.push(key);
87
+ }
88
+ if (discarded.length === 0)
89
+ return attributes;
90
+ if (strictDiscard) {
91
+ throw new MassAssignmentException(`Add [${discarded.join(", ")}] to fillable property to allow mass assignment on [${model.name ?? "Model"}].`);
92
+ }
93
+ const out = {};
94
+ for (const key of allowed) {
95
+ if (key in attributes)
96
+ out[key] = attributes[key];
97
+ }
98
+ return out;
99
+ }
100
+ if (!guardeds.includes("*")) {
101
+ const blocked = new Set(guardeds);
102
+ const discarded = [];
103
+ const out = {};
104
+ for (const [key, value] of Object.entries(attributes)) {
105
+ if (blocked.has(key))
106
+ discarded.push(key);
107
+ else
108
+ out[key] = value;
109
+ }
110
+ if (discarded.length > 0 && strictDiscard) {
111
+ throw new MassAssignmentException(`Add [${discarded.join(", ")}] to fillable property to allow mass assignment on [${model.name ?? "Model"}].`);
112
+ }
113
+ return out;
114
+ }
115
+ // Empty fillable + guarded ["*"] → allow all (make:model DX; set fillable in apps)
116
+ return { ...attributes };
117
+ }
118
+ /** Expose `scopeFoo` methods as `.foo()` on model queries. */
119
+ /** Per-class: does this model define any `scopeFoo` local scopes? */
120
+ const hasLocalScopesCache = new WeakMap();
121
+ function modelHasLocalScopes(model) {
122
+ const cached = hasLocalScopesCache.get(model);
123
+ if (cached !== undefined)
124
+ return cached;
125
+ let has = false;
126
+ let proto = model;
127
+ while (proto && proto !== Function.prototype) {
128
+ for (const key of Object.getOwnPropertyNames(proto)) {
129
+ if (key.length > 5 &&
130
+ key.startsWith("scope") &&
131
+ typeof proto[key] === "function") {
132
+ has = true;
133
+ break;
134
+ }
135
+ }
136
+ if (has)
137
+ break;
138
+ proto = Object.getPrototypeOf(proto);
139
+ }
140
+ hasLocalScopesCache.set(model, has);
141
+ return has;
142
+ }
143
+ /**
144
+ * Expose `scopeFoo` methods as `.foo()` on model queries.
145
+ * Skip the Proxy entirely when the model has no local scopes (list/get hot path).
146
+ */
147
+ function withLocalScopeProxy(query) {
148
+ const model = query.model;
149
+ if (!modelHasLocalScopes(model))
150
+ return query;
151
+ return new Proxy(query, {
152
+ get(target, prop, receiver) {
153
+ if (prop === MODEL_QUERY_TARGET) {
154
+ return target;
155
+ }
156
+ if (typeof prop === "symbol") {
157
+ return Reflect.get(target, prop, receiver);
158
+ }
159
+ if (prop in target) {
160
+ const value = Reflect.get(target, prop, receiver);
161
+ if (typeof value === "function") {
162
+ return (...args) => {
163
+ const result = value.apply(target, args);
164
+ // Re-wrap ModelQuery results (e.g. clone) so scopes stay available.
165
+ if (result instanceof ModelQuery && result !== target) {
166
+ return withLocalScopeProxy(result);
167
+ }
168
+ return result === target ? receiver : result;
169
+ };
170
+ }
171
+ return value;
172
+ }
173
+ const scopeModel = target.model;
174
+ const scopeName = `scope${String(prop).charAt(0).toUpperCase()}${String(prop).slice(1)}`;
175
+ const fn = scopeModel[scopeName];
176
+ if (typeof fn === "function") {
177
+ return (...args) => {
178
+ fn.call(scopeModel, target, ...args);
179
+ return receiver;
180
+ };
181
+ }
182
+ return undefined;
183
+ },
184
+ });
185
+ }
186
+ const noCastsCache = new WeakMap();
187
+ const castsResultCache = new WeakMap();
188
+ const hashedColumnsCache = new WeakMap();
189
+ const castShapeCache = new WeakMap();
190
+ /** Reuse DatabaseManager per Connection — avoids `new Db()` on every list build. */
191
+ const dbManagerByConnection = new WeakMap();
192
+ function classHasNoCasts(ctor) {
193
+ const cached = noCastsCache.get(ctor);
194
+ if (cached !== undefined)
195
+ return cached;
196
+ const casts = ctor.getCasts();
197
+ let empty = true;
198
+ for (const _ in casts) {
199
+ empty = false;
200
+ break;
201
+ }
202
+ noCastsCache.set(ctor, empty);
203
+ return empty;
204
+ }
205
+ /** True when this row must run get-casts (a cast column is present, or an accessor always runs). */
206
+ function rowNeedsCasts(ctor, attributes) {
207
+ let shape = castShapeCache.get(ctor);
208
+ if (shape === undefined) {
209
+ const casts = ctor.getCasts();
210
+ const keys = [];
211
+ let force = false;
212
+ for (const key in casts) {
213
+ keys.push(key);
214
+ const definition = casts[key];
215
+ if (isAttribute(definition) && definition.get)
216
+ force = true;
217
+ }
218
+ shape = keys.length === 0 ? null : { keys, force };
219
+ castShapeCache.set(ctor, shape);
220
+ }
221
+ if (shape == null)
222
+ return false;
223
+ if (shape.force)
224
+ return true;
225
+ for (const key of shape.keys) {
226
+ if (attributes[key] !== undefined)
227
+ return true;
228
+ }
229
+ return false;
230
+ }
231
+ /** True when hydrating this row must run cast getters (partial selects often do not). */
232
+ export function attributesNeedModelCasts(ctor, attributes) {
233
+ return rowNeedsCasts(ctor, attributes);
234
+ }
235
+ /**
236
+ * Incrementing integer keys (`keyType` `int`) become numbers.
237
+ * Postgres BIGINT often arrives as a string; UUID / `keyType` `string` is left as-is.
238
+ * An explicit `casts` entry for the primary key wins.
239
+ */
240
+ function applyIncrementingKeyType(ctor, attrs, hasExplicitCasts) {
241
+ if (ctor.incrementing === false)
242
+ return;
243
+ const key = ctor.primaryKey;
244
+ if (!(key in attrs) || attrs[key] == null)
245
+ return;
246
+ if (hasExplicitCasts && ctor.getCasts()[key])
247
+ return;
248
+ const value = attrs[key];
249
+ if (ctor.keyType === "string") {
250
+ if (typeof value !== "string")
251
+ attrs[key] = String(value);
252
+ return;
253
+ }
254
+ if (typeof value === "number" && Number.isFinite(value)) {
255
+ attrs[key] = Math.trunc(value);
256
+ return;
257
+ }
258
+ const n = typeof value === "bigint" ? Number(value) : Number(value);
259
+ if (Number.isSafeInteger(n)) {
260
+ attrs[key] = n;
261
+ }
262
+ }
263
+ /** Faster than Object.assign onto class instances with private fields (Bun). */
264
+ function assignOwn(target, source) {
265
+ for (const key in source) {
266
+ target[key] = source[key];
267
+ }
268
+ }
269
+ const relationNamesCache = new WeakMap();
270
+ function relationNames(ctor) {
271
+ const cached = relationNamesCache.get(ctor);
272
+ if (cached)
273
+ return cached;
274
+ const raw = ctor.relations;
275
+ const map = typeof raw === "function" ? raw.call(ctor) : raw;
276
+ const names = map != null ? new Set(Object.keys(map)) : new Set();
277
+ relationNamesCache.set(ctor, names);
278
+ return names;
279
+ }
280
+ function isModelInstance(value) {
281
+ return (value != null &&
282
+ typeof value === "object" &&
283
+ typeof value.getDirty === "function" &&
284
+ typeof value.save === "function");
285
+ }
286
+ function isRelationValue(value) {
287
+ if (value == null)
288
+ return false;
289
+ if (value instanceof Collection || isModelInstance(value))
290
+ return true;
291
+ if (Array.isArray(value)) {
292
+ for (let i = 0; i < value.length; i++) {
293
+ const item = value[i];
294
+ if (item instanceof Collection || isModelInstance(item))
295
+ return true;
296
+ }
297
+ }
298
+ return false;
299
+ }
300
+ /** Column attributes only — skip eager-loaded relations and related model bags. */
301
+ function persistableAttributes(ctor, bag, omitKey) {
302
+ const relations = relationNames(ctor);
303
+ const casts = ctor.getCasts();
304
+ const out = {};
305
+ for (const key of Object.keys(bag)) {
306
+ if (omitKey != null && key === omitKey)
307
+ continue;
308
+ if (key === "pivot")
309
+ continue;
310
+ if (relations.has(key))
311
+ continue;
312
+ const value = bag[key];
313
+ // A cast column holds a Collection or object on purpose (`tags: "collection"`); only
314
+ // uncast values that look like loaded relations are skipped.
315
+ if (!(key in casts) && (typeof value === "function" || isRelationValue(value)))
316
+ continue;
317
+ out[key] = value;
318
+ }
319
+ return out;
320
+ }
321
+ const findSqlCache = new WeakMap();
322
+ export function canFastFind(ctor) {
323
+ if (usesSoftDeletes(ctor) || hasGlobalScopes(ctor))
324
+ return false;
325
+ if (hasModelEventListeners(ctor, "retrieved"))
326
+ return false;
327
+ const conn = ctor.getConnection();
328
+ return (typeof conn.getSync1 === "function" || typeof conn.getSync === "function");
329
+ }
330
+ function canPersistSync(ctor, conn) {
331
+ return (typeof conn.insertGetIdSync === "function" &&
332
+ typeof conn.runSync === "function" &&
333
+ !hasAnyModelEventListeners(ctor));
334
+ }
335
+ /**
336
+ * Active Record model — familiar query/relation APIs for app code.
337
+ */
338
+ export class Model {
339
+ static table;
340
+ static primaryKey = "id";
341
+ /**
342
+ * `$incrementing` — false for UUID / ULID / assigned keys.
343
+ * When false, `save()` inserts the primary key instead of `insertGetId`.
344
+ */
345
+ static incrementing = true;
346
+ /** `$keyType` (`int` | `string`). */
347
+ static keyType = "int";
348
+ /** Connection name (`"secondary"`) or Connection handle. */
349
+ static connection;
350
+ /** SoftDeletes stand-in (`@SoftDeletes()` sets this). */
351
+ static softDeletes = false;
352
+ static deletedAt = "deleted_at";
353
+ /** Set by `@HasFactory(...)` — call `Model.factory().create()`. */
354
+ static factory;
355
+ /** When false, `save()` skips `created_at` / `updated_at`. */
356
+ static timestamps = true;
357
+ /**
358
+ * Laravel `$casts` property — prefer `static casts()` when adding enums / Attribute.
359
+ * May be a plain object or a method returning the map.
360
+ */
361
+ static casts = {};
362
+ /**
363
+ * Relation definitions. Prefer this over instance methods so you can
364
+ * `declare product: Product | null` and use `model.product` after eager load,
365
+ * with `model.related('product')` for the relation query.
366
+ */
367
+ static relations = {};
368
+ /**
369
+ * Laravel `$touches` — relation names whose related models get `touch()` after
370
+ * this model is saved (or soft-deleted). Honor `withoutTouching`.
371
+ */
372
+ static touches = [];
373
+ /** Laravel `$hidden`. */
374
+ static hidden = [];
375
+ /** Laravel `$visible` allow-list (empty = all non-hidden). */
376
+ static visible = [];
377
+ /** Laravel `$appends` — accessors included in `toArray()`. */
378
+ static appends = [];
379
+ /** Laravel `$fillable`. */
380
+ static fillable = [];
381
+ /** Laravel `$guarded` — default guard all until fillable is set. */
382
+ static guarded = ["*"];
383
+ /** Set in constructor / hydrate — avoid empty `{}` alloc before assign. */
384
+ #original;
385
+ /** Lazy: allocated on first mutation tracking. */
386
+ #changes;
387
+ #wasRecentlyCreated = false;
388
+ /**
389
+ * Whether this instance is persisted (`$exists`).
390
+ * Not derived from primary key — non-incrementing models assign UUIDs before insert.
391
+ */
392
+ #exists = false;
393
+ #appends = null;
394
+ #hidden = null;
395
+ #visible = null;
396
+ /** Per-instance connection override (`$model->setConnection()`). */
397
+ #connection;
398
+ /** Laravel `$exists` — true after retrieve / successful insert. */
399
+ get exists() {
400
+ return this.#exists;
401
+ }
402
+ set exists(value) {
403
+ this.#exists = Boolean(value);
404
+ }
405
+ constructor(attributes = {}) {
406
+ const ctor = this.constructor;
407
+ const self = this;
408
+ // Hot path: no casts, or this row omitted every cast column (partial select).
409
+ if (classHasNoCasts(ctor) ||
410
+ !rowNeedsCasts(ctor, attributes)) {
411
+ applyIncrementingKeyType(ctor, attributes, false);
412
+ assignOwn(self, attributes);
413
+ this.#original = attributes;
414
+ return;
415
+ }
416
+ const casted = ctor.castAttributes(attributes, "get");
417
+ applyIncrementingKeyType(ctor, casted, true);
418
+ assignOwn(self, casted);
419
+ // Avoid a second own-key scan via syncOriginal()/getAttributes() on hydrate.
420
+ this.#original = { ...casted };
421
+ }
422
+ /**
423
+ * `newFromBuilder` — hydrate a persisted row (sets `$exists`).
424
+ * Used by ModelQuery list/find paths.
425
+ */
426
+ static newFromBuilder(attributes) {
427
+ const model = new this(attributes);
428
+ model.#exists = true;
429
+ return model;
430
+ }
431
+ /** Snapshot current attributes as the original set. */
432
+ syncOriginal() {
433
+ this.#original = this.getAttributes();
434
+ return this;
435
+ }
436
+ /** Current attribute bag (non-function own properties). */
437
+ getAttributes() {
438
+ const out = {};
439
+ for (const key of Object.keys(this)) {
440
+ const value = this[key];
441
+ if (typeof value !== "function")
442
+ out[key] = value;
443
+ }
444
+ return out;
445
+ }
446
+ /**
447
+ * Laravel `getAttribute` — throws when `preventAccessingMissingAttributes` is on
448
+ * and the key was not retrieved / does not exist (relations exempt).
449
+ */
450
+ getAttribute(key) {
451
+ const self = this;
452
+ if (Object.prototype.hasOwnProperty.call(self, key)) {
453
+ return self[key];
454
+ }
455
+ const original = this.#original ?? {};
456
+ if (Object.prototype.hasOwnProperty.call(original, key)) {
457
+ return original[key];
458
+ }
459
+ if (modelsShouldPreventAccessingMissingAttributes()) {
460
+ const ctor = this.constructor;
461
+ const raw = ctor.relations;
462
+ const map = typeof raw === "function" ? raw.call(ctor) : raw;
463
+ let isRelation = map != null && typeof map[key] === "function";
464
+ if (!isRelation) {
465
+ let proto = Object.getPrototypeOf(this);
466
+ while (proto && proto !== Object.prototype) {
467
+ const desc = Object.getOwnPropertyDescriptor(proto, key);
468
+ if (desc && typeof desc.value === "function") {
469
+ isRelation = true;
470
+ break;
471
+ }
472
+ proto = Object.getPrototypeOf(proto);
473
+ }
474
+ }
475
+ if (!isRelation) {
476
+ throw new MissingAttributeException(ctor.name ?? "Model", key);
477
+ }
478
+ }
479
+ return undefined;
480
+ }
481
+ /** Laravel `isDirty`. */
482
+ isDirty(...attributes) {
483
+ const dirty = this.getDirty();
484
+ if (attributes.length === 0)
485
+ return Object.keys(dirty).length > 0;
486
+ return attributes.some((key) => Object.prototype.hasOwnProperty.call(dirty, key));
487
+ }
488
+ /** Laravel `isClean`. */
489
+ isClean(...attributes) {
490
+ return !this.isDirty(...attributes);
491
+ }
492
+ /** Laravel `getDirty`. */
493
+ getDirty() {
494
+ const dirty = {};
495
+ const original = this.#original ?? {};
496
+ for (const [key, value] of Object.entries(this.getAttributes())) {
497
+ if (!Object.is(value, original[key]))
498
+ dirty[key] = value;
499
+ }
500
+ return dirty;
501
+ }
502
+ getOriginal(key) {
503
+ const original = this.#original ?? {};
504
+ if (key === undefined)
505
+ return { ...original };
506
+ return original[key];
507
+ }
508
+ /** Laravel `getChanges` — attributes changed by the last save. */
509
+ getChanges() {
510
+ return this.#changes ? { ...this.#changes } : {};
511
+ }
512
+ /** Laravel `wasChanged`. */
513
+ wasChanged(...attributes) {
514
+ const changes = this.#changes;
515
+ if (!changes)
516
+ return false;
517
+ if (attributes.length === 0)
518
+ return Object.keys(changes).length > 0;
519
+ return attributes.some((key) => Object.prototype.hasOwnProperty.call(changes, key));
520
+ }
521
+ /** Laravel `wasRecentlyCreated`. */
522
+ wasRecentlyCreated() {
523
+ return this.#wasRecentlyCreated;
524
+ }
525
+ /** Laravel `append`. */
526
+ append(...attributes) {
527
+ const current = this.getAppends();
528
+ this.#appends = [...new Set([...current, ...attributes.flat()])];
529
+ return this;
530
+ }
531
+ /** Laravel `setAppends`. */
532
+ setAppends(attributes) {
533
+ this.#appends = [...attributes];
534
+ return this;
535
+ }
536
+ /** Laravel `getAppends`. */
537
+ getAppends() {
538
+ if (this.#appends)
539
+ return [...this.#appends];
540
+ return [...(this.constructor.appends ?? [])];
541
+ }
542
+ /** Laravel `getHidden`. */
543
+ getHidden() {
544
+ if (this.#hidden)
545
+ return [...this.#hidden];
546
+ return [...(this.constructor.hidden ?? [])];
547
+ }
548
+ /** Laravel `getVisible`. */
549
+ getVisible() {
550
+ if (this.#visible)
551
+ return [...this.#visible];
552
+ return [...(this.constructor.visible ?? [])];
553
+ }
554
+ /** Laravel `setHidden`. */
555
+ setHidden(attributes) {
556
+ this.#hidden = [...attributes];
557
+ return this;
558
+ }
559
+ /** Laravel `setVisible`. */
560
+ setVisible(attributes) {
561
+ this.#visible = [...attributes];
562
+ return this;
563
+ }
564
+ /** Laravel `makeVisible` — show attributes for this instance. */
565
+ makeVisible(...attributes) {
566
+ const attrs = attributes.flat();
567
+ const hidden = new Set(this.getHidden());
568
+ for (const key of attrs)
569
+ hidden.delete(key);
570
+ this.#hidden = [...hidden];
571
+ const visible = this.getVisible();
572
+ if (visible.length > 0) {
573
+ this.#visible = [...new Set([...visible, ...attrs])];
574
+ }
575
+ return this;
576
+ }
577
+ /** Laravel `makeHidden` — hide attributes for this instance. */
578
+ makeHidden(...attributes) {
579
+ const attrs = attributes.flat();
580
+ this.#hidden = [...new Set([...this.getHidden(), ...attrs])];
581
+ const visible = this.getVisible();
582
+ if (visible.length > 0) {
583
+ const hide = new Set(attrs);
584
+ this.#visible = visible.filter((key) => !hide.has(key));
585
+ }
586
+ return this;
587
+ }
588
+ /**
589
+ * Run a callback without touching timestamps (Laravel `Model::withoutTimestamps`).
590
+ */
591
+ static async withoutTimestamps(callback) {
592
+ const previous = this.timestamps;
593
+ this.timestamps = false;
594
+ try {
595
+ return await callback();
596
+ }
597
+ finally {
598
+ this.timestamps = previous;
599
+ }
600
+ }
601
+ static withoutTouching(modelsOrCallback, callback) {
602
+ return runWithoutTouching(modelsOrCallback, callback);
603
+ }
604
+ /** Whether touch is currently suppressed for this model class. */
605
+ static isIgnoringTouch() {
606
+ return isIgnoringTouch(this);
607
+ }
608
+ /** Laravel `Model::preventLazyLoading`. */
609
+ static preventLazyLoading(prevent = true) {
610
+ setPreventLazyLoading(prevent);
611
+ }
612
+ /** Laravel `Model::preventSilentlyDiscardingAttributes`. */
613
+ static preventSilentlyDiscardingAttributes(prevent = true) {
614
+ setPreventSilentlyDiscardingAttributes(prevent);
615
+ }
616
+ /** Laravel `Model::preventAccessingMissingAttributes`. */
617
+ static preventAccessingMissingAttributes(prevent = true) {
618
+ setPreventAccessingMissingAttributes(prevent);
619
+ }
620
+ /** Laravel `Model::shouldBeStrict` — enable/disable the strictness trio. */
621
+ static shouldBeStrict(should = true) {
622
+ setShouldBeStrict(should);
623
+ }
624
+ /** Laravel `Model::addGlobalScope`. */
625
+ static addGlobalScope(nameOrScope, scope) {
626
+ if (typeof nameOrScope === "function") {
627
+ registerGlobalScope(this, nameOrScope.name || `scope_${Date.now()}`, nameOrScope);
628
+ return;
629
+ }
630
+ if (!scope)
631
+ throw new Error("addGlobalScope requires a callback.");
632
+ registerGlobalScope(this, nameOrScope, scope);
633
+ }
634
+ /** Laravel `Model::addGlobalScope` removal helper. */
635
+ static removeGlobalScope(name) {
636
+ unregisterGlobalScope(this, name);
637
+ }
638
+ /**
639
+ * Laravel `boot` — runs once per model class before first use.
640
+ * Prefer registering listeners in `booted()`.
641
+ */
642
+ static boot() { }
643
+ /**
644
+ * Laravel `booted` — runs once after `boot`. Typical place for `static::created(...)`.
645
+ */
646
+ static booted() { }
647
+ /** Ensure this model class has been booted. */
648
+ static bootIfNotBooted() {
649
+ bootIfNotBooted(this);
650
+ }
651
+ /** Laravel `Model::observe`. */
652
+ static observe(observer) {
653
+ observeModel(this, observer);
654
+ }
655
+ /** Laravel `Model::withoutEvents`. */
656
+ static withoutEvents(callback) {
657
+ return withoutModelEvents(this, callback);
658
+ }
659
+ static retrieved(callback) {
660
+ registerModelEvent(this, "retrieved", callback);
661
+ }
662
+ static creating(callback) {
663
+ registerModelEvent(this, "creating", callback);
664
+ }
665
+ static created(callback) {
666
+ registerModelEvent(this, "created", callback);
667
+ }
668
+ static updating(callback) {
669
+ registerModelEvent(this, "updating", callback);
670
+ }
671
+ static updated(callback) {
672
+ registerModelEvent(this, "updated", callback);
673
+ }
674
+ static saving(callback) {
675
+ registerModelEvent(this, "saving", callback);
676
+ }
677
+ static saved(callback) {
678
+ registerModelEvent(this, "saved", callback);
679
+ }
680
+ static deleting(callback) {
681
+ registerModelEvent(this, "deleting", callback);
682
+ }
683
+ static deleted(callback) {
684
+ registerModelEvent(this, "deleted", callback);
685
+ }
686
+ static trashed(callback) {
687
+ registerModelEvent(this, "trashed", callback);
688
+ }
689
+ static forceDeleting(callback) {
690
+ registerModelEvent(this, "forceDeleting", callback);
691
+ }
692
+ static forceDeleted(callback) {
693
+ registerModelEvent(this, "forceDeleted", callback);
694
+ }
695
+ static restoring(callback) {
696
+ registerModelEvent(this, "restoring", callback);
697
+ }
698
+ static restored(callback) {
699
+ registerModelEvent(this, "restored", callback);
700
+ }
701
+ static replicating(callback) {
702
+ registerModelEvent(this, "replicating", callback);
703
+ }
704
+ /** Fire a lifecycle event on this instance. */
705
+ fireModelEvent(event) {
706
+ return fireModelEvent(this, event);
707
+ }
708
+ /** Resolve casts from `casts()` method or `$casts` property. */
709
+ static getCasts() {
710
+ const cached = castsResultCache.get(this);
711
+ if (cached)
712
+ return cached;
713
+ const field = this.casts;
714
+ const value = typeof field === "function" ? (field.call(this) ?? {}) : (field ?? {});
715
+ castsResultCache.set(this, value);
716
+ return value;
717
+ }
718
+ static castAttributes(attributes, direction) {
719
+ const casts = this.getCasts();
720
+ let hasCast = false;
721
+ for (const _ in casts) {
722
+ hasCast = true;
723
+ break;
724
+ }
725
+ if (!hasCast) {
726
+ return attributes;
727
+ }
728
+ const driver = (this.connection != null
729
+ ? resolveConnection(this.connection)
730
+ : defaultConnection)?.driver;
731
+ let out = { ...attributes };
732
+ for (const [key, definition] of Object.entries(casts)) {
733
+ if (isAttribute(definition)) {
734
+ if (direction === "get") {
735
+ if (!(key in out) && definition.get === undefined)
736
+ continue;
737
+ if (definition.get) {
738
+ out[key] = definition.get(out[key], out);
739
+ }
740
+ }
741
+ else if (definition.set && key in out) {
742
+ const result = definition.set(out[key], out);
743
+ delete out[key];
744
+ if (result !== null &&
745
+ typeof result === "object" &&
746
+ !Array.isArray(result) &&
747
+ !(result instanceof Date)) {
748
+ out = { ...out, ...result };
749
+ }
750
+ else {
751
+ out[key] = result;
752
+ }
753
+ }
754
+ continue;
755
+ }
756
+ if (!(key in out) || out[key] === undefined)
757
+ continue;
758
+ if (isCastType(definition)) {
759
+ out[key] =
760
+ direction === "get"
761
+ ? castFromStorage(out[key], definition)
762
+ : castToStorage(out[key], definition, driver);
763
+ continue;
764
+ }
765
+ if (isEnumCast(definition)) {
766
+ out[key] =
767
+ direction === "get"
768
+ ? enumFromStorage(out[key], definition)
769
+ : enumToStorage(out[key], definition);
770
+ }
771
+ }
772
+ return out;
773
+ }
774
+ static setConnection(connection) {
775
+ defaultConnection = connection;
776
+ const name = connection.getName() || "default";
777
+ setDefaultConnection(connection, name);
778
+ }
779
+ static getConnection() {
780
+ return resolveConnection(this.connection);
781
+ }
782
+ /** Laravel `$model->setConnection($name)`. */
783
+ setConnection(connection) {
784
+ this.#connection = connection;
785
+ return this;
786
+ }
787
+ /** Active connection for this instance (instance override → model → default). */
788
+ getConnection() {
789
+ const ctor = this.constructor;
790
+ return resolveConnection(this.#connection ?? ctor.connection);
791
+ }
792
+ static db() {
793
+ const conn = this.getConnection();
794
+ let manager = dbManagerByConnection.get(conn);
795
+ if (!manager) {
796
+ manager = new Db(conn);
797
+ dbManagerByConnection.set(conn, manager);
798
+ }
799
+ return manager;
800
+ }
801
+ /**
802
+ * Laravel `Model::on($connection)` — same model, different named connection.
803
+ */
804
+ static on(connection) {
805
+ return this.newQuery({ connection });
806
+ }
807
+ /**
808
+ * Create a model query (honors `@UseBuilder` when present).
809
+ * Local scopes (`scopeActive` → `.active()`) are available via proxy.
810
+ */
811
+ static newQuery(options) {
812
+ this.bootIfNotBooted();
813
+ const Ctor = (resolveModelBuilder(this) ?? ModelQuery);
814
+ const query = new Ctor(this, options);
815
+ return withLocalScopeProxy(query);
816
+ }
817
+ /** Laravel `withoutGlobalScopes()`. */
818
+ static withoutGlobalScopes(names) {
819
+ return this.newQuery({
820
+ withoutGlobalScopes: names ?? true,
821
+ });
822
+ }
823
+ /** Laravel `withoutGlobalScope($name)`. */
824
+ static withoutGlobalScope(name) {
825
+ return this.newQuery({ withoutGlobalScopes: [name] });
826
+ }
827
+ /** Laravel `Model::query()` — scoped `ModelQuery` (not a raw table builder). */
828
+ static query(options) {
829
+ return this.newQuery(options);
830
+ }
831
+ /** Laravel `Model::find`. */
832
+ static find(id) {
833
+ this.bootIfNotBooted();
834
+ if (canFastFind(this)) {
835
+ // `.call` is required so subclasses can use Model's #fastFind; assert InstanceType<T>.
836
+ return Model.#fastFind.call(this, id);
837
+ }
838
+ return this.newQuery()
839
+ .where(this.primaryKey, id)
840
+ .first();
841
+ }
842
+ /** Laravel `getRouteKeyName()` — column used for implicit route binding. */
843
+ static getRouteKeyName() {
844
+ return this.primaryKey;
845
+ }
846
+ /**
847
+ * Laravel `resolveRouteBinding` — find by PK or custom `{param:column}` field.
848
+ */
849
+ static resolveRouteBinding(value, field) {
850
+ const column = field ?? this.getRouteKeyName();
851
+ if (column === this.primaryKey) {
852
+ return this.find(value);
853
+ }
854
+ this.bootIfNotBooted();
855
+ return this.newQuery().where(column, value).first();
856
+ }
857
+ /**
858
+ * Laravel SoftDeletes `resolveSoftDeletableRouteBinding` — same as
859
+ * `resolveRouteBinding` but includes soft-deleted rows (`withTrashed()`).
860
+ * Used by the router when the route opts in via `withTrashed()`.
861
+ */
862
+ static resolveSoftDeletableRouteBinding(value, field) {
863
+ const column = field ?? this.getRouteKeyName();
864
+ this.bootIfNotBooted();
865
+ return this.newQuery({ withTrashed: true }).where(column, value).first();
866
+ }
867
+ static #fastFind(id) {
868
+ const conn = this.getConnection();
869
+ let cached = findSqlCache.get(this);
870
+ if (!cached || cached.conn !== conn) {
871
+ const table = wrapSqlName(conn.dialect, this.table);
872
+ const pk = wrapSqlName(conn.dialect, this.primaryKey);
873
+ cached = {
874
+ sql: `SELECT * FROM ${table} WHERE ${pk} = ? LIMIT 1`,
875
+ conn,
876
+ };
877
+ findSqlCache.set(this, cached);
878
+ }
879
+ const row = conn.getSync1
880
+ ? conn.getSync1(cached.sql, id)
881
+ : conn.getSync(cached.sql, [id]);
882
+ if (!row)
883
+ return null;
884
+ const model = new this(row);
885
+ model.#exists = true;
886
+ return model;
887
+ }
888
+ /** Laravel `Model::findOrFail`. */
889
+ static async findOrFail(id) {
890
+ const model = await this.find(id);
891
+ if (!model)
892
+ throw new ModelNotFoundException();
893
+ return model;
894
+ }
895
+ /** Laravel `Model::all`. */
896
+ static async all() {
897
+ this.bootIfNotBooted();
898
+ return this.newQuery().get();
899
+ }
900
+ /**
901
+ * Laravel `Model::get` — same as `query().get()` / `all()`.
902
+ * Typed helper for autocomplete; Proxy also forwards other terminals.
903
+ */
904
+ static get() {
905
+ this.bootIfNotBooted();
906
+ return this.newQuery().get();
907
+ }
908
+ static pluck(column, key) {
909
+ this.bootIfNotBooted();
910
+ if (key === undefined)
911
+ return this.newQuery().pluck(column);
912
+ return this.newQuery().pluck(column, key);
913
+ }
914
+ /** SQLite-only sync `all()` — see `ModelQuery.getSync`. */
915
+ static allSync() {
916
+ this.bootIfNotBooted();
917
+ return this.newQuery().getSync();
918
+ }
919
+ /** SQLite-only sync `find` — see `ModelQuery.firstSync`. */
920
+ static findSync(id) {
921
+ this.bootIfNotBooted();
922
+ return this.newQuery().findSync(id);
923
+ }
924
+ static where(columnOrCallback, opOrValue, value) {
925
+ const q = this.newQuery();
926
+ if (typeof columnOrCallback === "function") {
927
+ return q.where(columnOrCallback);
928
+ }
929
+ if (value === undefined)
930
+ return q.where(columnOrCallback, opOrValue);
931
+ return q.where(columnOrCallback, String(opOrValue), value);
932
+ }
933
+ /** Karobar-style multi-column LIKE search. */
934
+ static search(columns, term) {
935
+ this.bootIfNotBooted();
936
+ return this.newQuery().search(columns, term);
937
+ }
938
+ static orWhere(column, opOrValue, value) {
939
+ this.bootIfNotBooted();
940
+ const q = this.newQuery();
941
+ if (value === undefined)
942
+ return q.orWhere(column, opOrValue);
943
+ return q.orWhere(column, String(opOrValue), value);
944
+ }
945
+ static whereIn(column, values) {
946
+ this.bootIfNotBooted();
947
+ return this.newQuery().whereIn(column, values);
948
+ }
949
+ static whereNull(column) {
950
+ this.bootIfNotBooted();
951
+ return this.newQuery().whereNull(column);
952
+ }
953
+ static whereNotNull(column) {
954
+ this.bootIfNotBooted();
955
+ return this.newQuery().whereNotNull(column);
956
+ }
957
+ static orderBy(column, direction = "asc") {
958
+ this.bootIfNotBooted();
959
+ return this.newQuery().orderBy(column, direction);
960
+ }
961
+ static orderByDesc(column) {
962
+ return this.orderBy(column, "desc");
963
+ }
964
+ static orderByRaw(sql) {
965
+ this.bootIfNotBooted();
966
+ return this.newQuery().orderByRaw(sql);
967
+ }
968
+ /** Karobar alias for `orderByRaw`. */
969
+ static orderBySql(sql) {
970
+ return this.orderByRaw(sql);
971
+ }
972
+ static latest(column = "created_at") {
973
+ this.bootIfNotBooted();
974
+ return this.newQuery().latest(column);
975
+ }
976
+ static oldest(column = "created_at") {
977
+ this.bootIfNotBooted();
978
+ return this.newQuery().oldest(column);
979
+ }
980
+ static limit(value) {
981
+ this.bootIfNotBooted();
982
+ return this.newQuery().limit(value);
983
+ }
984
+ static take(value) {
985
+ return this.limit(value);
986
+ }
987
+ static select(...columns) {
988
+ this.bootIfNotBooted();
989
+ return this.newQuery().select(...columns);
990
+ }
991
+ static whereDate(column, opOrValue, value) {
992
+ this.bootIfNotBooted();
993
+ const q = this.newQuery();
994
+ if (value === undefined)
995
+ return q.whereDate(column, opOrValue);
996
+ return q.whereDate(column, String(opOrValue), value);
997
+ }
998
+ /** Laravel `Model::whereNot(closure)`. */
999
+ static whereNot(callback) {
1000
+ this.bootIfNotBooted();
1001
+ return this.newQuery().whereNot(callback);
1002
+ }
1003
+ /** Laravel `Model::orWhereNot(closure)`. */
1004
+ static orWhereNot(callback) {
1005
+ this.bootIfNotBooted();
1006
+ return this.newQuery().orWhereNot(callback);
1007
+ }
1008
+ /** Laravel `withTrashed()`. */
1009
+ static withTrashed() {
1010
+ this.bootIfNotBooted();
1011
+ return this.newQuery({
1012
+ withTrashed: true,
1013
+ });
1014
+ }
1015
+ /** Laravel `onlyTrashed()`. */
1016
+ static onlyTrashed() {
1017
+ return this.newQuery({
1018
+ onlyTrashed: true,
1019
+ });
1020
+ }
1021
+ /** Laravel `withoutTrashed()`. */
1022
+ static withoutTrashed() {
1023
+ return this.newQuery({
1024
+ withTrashed: false,
1025
+ onlyTrashed: false,
1026
+ });
1027
+ }
1028
+ /** `get()` returns plain row objects. Models are not built. */
1029
+ static rows() {
1030
+ return this.newQuery().rows();
1031
+ }
1032
+ /** Eager-load relations (`with('posts')`, `with({ posts: (q) => q.where(…) })`). */
1033
+ static with(...relations) {
1034
+ return this.newQuery({
1035
+ eagerLoad: normalizeWithRelations(relations),
1036
+ });
1037
+ }
1038
+ /** Laravel `Model::whereHas`. */
1039
+ static whereHas(relation, callback) {
1040
+ this.bootIfNotBooted();
1041
+ return this.newQuery().whereHas(relation, callback);
1042
+ }
1043
+ /** Laravel `Model::whereDoesntHave`. */
1044
+ static whereDoesntHave(relation, callback) {
1045
+ this.bootIfNotBooted();
1046
+ return this.newQuery().whereDoesntHave(relation, callback);
1047
+ }
1048
+ /** Laravel `Model::orWhereHas`. */
1049
+ static orWhereHas(relation, callback) {
1050
+ this.bootIfNotBooted();
1051
+ return this.newQuery().orWhereHas(relation, callback);
1052
+ }
1053
+ /** Laravel `Model::whereRelation`. */
1054
+ static whereRelation(relation, column, opOrValue, value) {
1055
+ this.bootIfNotBooted();
1056
+ return this.newQuery().whereRelation(relation, column, opOrValue, value);
1057
+ }
1058
+ /** Laravel `Model::orWhereRelation`. */
1059
+ static orWhereRelation(relation, column, opOrValue, value) {
1060
+ this.bootIfNotBooted();
1061
+ return this.newQuery().orWhereRelation(relation, column, opOrValue, value);
1062
+ }
1063
+ /** `withCount` — `'posts'`, `'posts as post_total'`, a string list, or `{ products: { as: 'productsCount' } }`. */
1064
+ static withCount(...relations) {
1065
+ this.bootIfNotBooted();
1066
+ return this.newQuery().withCount(...relations);
1067
+ }
1068
+ /** Laravel `Model::withSum`. */
1069
+ static withSum(relation, column) {
1070
+ this.bootIfNotBooted();
1071
+ return this.newQuery().withSum(relation, column);
1072
+ }
1073
+ static withAvg(relation, column) {
1074
+ this.bootIfNotBooted();
1075
+ return this.newQuery().withAvg(relation, column);
1076
+ }
1077
+ static withMin(relation, column) {
1078
+ this.bootIfNotBooted();
1079
+ return this.newQuery().withMin(relation, column);
1080
+ }
1081
+ static withMax(relation, column) {
1082
+ this.bootIfNotBooted();
1083
+ return this.newQuery().withMax(relation, column);
1084
+ }
1085
+ static withExists(...relations) {
1086
+ this.bootIfNotBooted();
1087
+ return this.newQuery().withExists(...relations);
1088
+ }
1089
+ static withWhereHas(relation, callback) {
1090
+ this.bootIfNotBooted();
1091
+ return this.newQuery().withWhereHas(relation, callback);
1092
+ }
1093
+ static whereBelongsTo(related, relationName) {
1094
+ this.bootIfNotBooted();
1095
+ return this.newQuery().whereBelongsTo(related, relationName);
1096
+ }
1097
+ static whereAny(columns, opOrValue, value) {
1098
+ this.bootIfNotBooted();
1099
+ return this.newQuery().whereAny(columns, opOrValue, value);
1100
+ }
1101
+ static whereAll(columns, opOrValue, value) {
1102
+ this.bootIfNotBooted();
1103
+ return this.newQuery().whereAll(columns, opOrValue, value);
1104
+ }
1105
+ static whereNone(columns, opOrValue, value) {
1106
+ this.bootIfNotBooted();
1107
+ return this.newQuery().whereNone(columns, opOrValue, value);
1108
+ }
1109
+ static whereKey(id) {
1110
+ this.bootIfNotBooted();
1111
+ return this.newQuery().whereKey(id);
1112
+ }
1113
+ /** Laravel `Model::whereUuid`. */
1114
+ static whereUuid(column, value) {
1115
+ this.bootIfNotBooted();
1116
+ return this.newQuery().whereUuid(column, value);
1117
+ }
1118
+ /** Laravel `Model::whereUlid`. */
1119
+ static whereUlid(column, value) {
1120
+ this.bootIfNotBooted();
1121
+ return this.newQuery().whereUlid(column, value);
1122
+ }
1123
+ /** Laravel `Model::join`. */
1124
+ static join(table, first, op, second) {
1125
+ this.bootIfNotBooted();
1126
+ return this.newQuery().join(table, first, op, second);
1127
+ }
1128
+ /** Laravel `Model::leftJoin`. */
1129
+ static leftJoin(table, first, op, second) {
1130
+ this.bootIfNotBooted();
1131
+ return this.newQuery().leftJoin(table, first, op, second);
1132
+ }
1133
+ /** Laravel `Model::paginate`. */
1134
+ static async paginate(perPage = 15, page, options = {}) {
1135
+ return this.newQuery().paginate(perPage, page, options);
1136
+ }
1137
+ /** Laravel `Model::cursorPaginate`. */
1138
+ static async cursorPaginate(perPage = 15, cursor = null, options = {}) {
1139
+ return this.newQuery().cursorPaginate(perPage, cursor, options);
1140
+ }
1141
+ /** Laravel `Model::create` (respects `$fillable` / `$guarded`). */
1142
+ static create(attributes) {
1143
+ this.bootIfNotBooted();
1144
+ return this.forceCreate(filterFillable(this, attributes));
1145
+ }
1146
+ /** Laravel `Model::forceCreate` — insert every given attribute, ignoring `$fillable`. */
1147
+ static forceCreate(useAttrs) {
1148
+ this.bootIfNotBooted();
1149
+ // Assign raw attributes (skip get-casts) so mutators like Attribute.set can run on save.
1150
+ const model = classHasNoCasts(this)
1151
+ ? new this(useAttrs)
1152
+ : Object.assign(new this(), useAttrs);
1153
+ const saved = model.save();
1154
+ const after = (row) => {
1155
+ if (classHasNoCasts(this)) {
1156
+ return row;
1157
+ }
1158
+ const key = this.primaryKey;
1159
+ const id = row[key];
1160
+ // Re-hydrate get-casts from storage (same end state as insert + find).
1161
+ const fresh = this.withoutEvents(() => this.find(id));
1162
+ const use = (found) => {
1163
+ if (found) {
1164
+ found.#wasRecentlyCreated = true;
1165
+ return found;
1166
+ }
1167
+ return row;
1168
+ };
1169
+ if (fresh instanceof Promise) {
1170
+ return fresh.then(use);
1171
+ }
1172
+ return use(fresh);
1173
+ };
1174
+ if (saved instanceof Promise) {
1175
+ return saved.then(after);
1176
+ }
1177
+ return after(saved);
1178
+ }
1179
+ /** Laravel `firstOrNew`. */
1180
+ static async firstOrNew(attributes, values = {}) {
1181
+ let q = this.newQuery();
1182
+ for (const [column, value] of Object.entries(attributes)) {
1183
+ q = q.where(column, value);
1184
+ }
1185
+ const existing = await q.first();
1186
+ if (existing)
1187
+ return existing;
1188
+ return new this({ ...attributes, ...values });
1189
+ }
1190
+ /** Laravel `firstOrCreate`. */
1191
+ static async firstOrCreate(attributes, values = {}) {
1192
+ let q = this.newQuery();
1193
+ for (const [column, value] of Object.entries(attributes)) {
1194
+ q = q.where(column, value);
1195
+ }
1196
+ const existing = await q.first();
1197
+ if (existing)
1198
+ return existing;
1199
+ return this.create({ ...attributes, ...values });
1200
+ }
1201
+ /** Laravel `updateOrCreate`. */
1202
+ static async updateOrCreate(attributes, values = {}) {
1203
+ let q = this.newQuery();
1204
+ for (const [column, value] of Object.entries(attributes)) {
1205
+ q = q.where(column, value);
1206
+ }
1207
+ const existing = await q.first();
1208
+ if (existing) {
1209
+ await existing.update(values);
1210
+ return existing;
1211
+ }
1212
+ return this.create({ ...attributes, ...values });
1213
+ }
1214
+ /**
1215
+ * Bulk insert. Delegates to `ModelQuery.insert` (timestamps stamped when enabled).
1216
+ * Does not fire creating/created events — set tenant/client ids on the rows.
1217
+ */
1218
+ static async insert(values) {
1219
+ this.bootIfNotBooted();
1220
+ return this.newQuery().insert(values);
1221
+ }
1222
+ /**
1223
+ * Laravel `Model::upsert` — bulk insert-or-update on unique conflict.
1224
+ * Delegates to `ModelQuery.upsert` (timestamps stamped when enabled).
1225
+ */
1226
+ static async upsert(values, uniqueBy, update = null) {
1227
+ this.bootIfNotBooted();
1228
+ return this.newQuery().upsert(values, uniqueBy, update);
1229
+ }
1230
+ /** Laravel `$model->fill($attributes)`. */
1231
+ fill(attributes) {
1232
+ const ctor = this.constructor;
1233
+ const finalAttrs = filterFillable(ctor, attributes);
1234
+ assignOwn(this, ctor.castAttributes(finalAttrs, "get"));
1235
+ return this;
1236
+ }
1237
+ /** Laravel `$model->update($attributes)`. */
1238
+ async update(attributes) {
1239
+ this.fill(attributes);
1240
+ await this.save();
1241
+ return this;
1242
+ }
1243
+ save() {
1244
+ const ctor = this.constructor;
1245
+ ctor.bootIfNotBooted();
1246
+ if (!classHasNoCasts(ctor)) {
1247
+ const pending = this.#pendingHashes(ctor);
1248
+ if (pending.length > 0)
1249
+ return this.#hashThenSave(pending);
1250
+ }
1251
+ const conn = this.getConnection();
1252
+ if (canPersistSync(ctor, conn)) {
1253
+ return this.#persistSync(conn);
1254
+ }
1255
+ return this.#persistAsync();
1256
+ }
1257
+ /** `hashed` cast columns that hold a plain string, so `save()` can hash them off the event loop. */
1258
+ #pendingHashes(ctor) {
1259
+ let columns = hashedColumnsCache.get(ctor);
1260
+ if (!columns) {
1261
+ const casts = ctor.getCasts();
1262
+ columns = Object.keys(casts).filter((key) => casts[key] === "hashed");
1263
+ hashedColumnsCache.set(ctor, columns);
1264
+ }
1265
+ if (columns.length === 0)
1266
+ return columns;
1267
+ const row = this;
1268
+ return columns.filter((key) => {
1269
+ const value = row[key];
1270
+ return typeof value === "string" && value !== "" && !isAlreadyHashed(value);
1271
+ });
1272
+ }
1273
+ async #hashThenSave(columns) {
1274
+ const row = this;
1275
+ const hashes = await Promise.all(columns.map((key) => hashCastValue(String(row[key]))));
1276
+ columns.forEach((key, i) => {
1277
+ row[key] = hashes[i];
1278
+ });
1279
+ return this.save();
1280
+ }
1281
+ #persistSync(conn) {
1282
+ const ctor = this.constructor;
1283
+ const key = ctor.primaryKey;
1284
+ const row = this;
1285
+ const id = row[key];
1286
+ const exists = this.#exists;
1287
+ const dialect = conn.dialect;
1288
+ const table = ctor.table;
1289
+ const incrementing = ctor.incrementing !== false;
1290
+ const usesTimestamps = ctor.timestamps !== false;
1291
+ if (usesTimestamps) {
1292
+ const now = nowForConnection(conn);
1293
+ row.updated_at = now;
1294
+ if (!exists) {
1295
+ row.created_at = row.created_at ?? now;
1296
+ }
1297
+ }
1298
+ if (!exists) {
1299
+ const noCasts = classHasNoCasts(ctor);
1300
+ const cachedCols = noCasts
1301
+ ? cachedSyncInsertColumns(ctor)
1302
+ : null;
1303
+ let writeColumns;
1304
+ let writeValues;
1305
+ if (cachedCols) {
1306
+ writeColumns = cachedCols;
1307
+ const n = cachedCols.length;
1308
+ if (n === 1) {
1309
+ writeValues = persistValueScratch;
1310
+ persistValueScratch[0] = row[cachedCols[0]];
1311
+ persistValueScratch.length = 1;
1312
+ }
1313
+ else {
1314
+ writeValues = persistValueScratch;
1315
+ persistValueScratch.length = n;
1316
+ for (let i = 0; i < n; i++) {
1317
+ persistValueScratch[i] = row[cachedCols[i]];
1318
+ }
1319
+ }
1320
+ }
1321
+ else {
1322
+ const persistBag = persistableAttributes(ctor, row, incrementing || row[key] == null ? key : undefined);
1323
+ writeColumns = Object.keys(persistBag);
1324
+ writeValues = Object.values(persistBag);
1325
+ if (!noCasts) {
1326
+ const payload = ctor.castAttributes(persistBag, "set");
1327
+ writeColumns = Object.keys(payload);
1328
+ writeValues = Object.values(payload);
1329
+ }
1330
+ }
1331
+ const inserted = withoutQueryWriteHook(() => {
1332
+ if (incrementing) {
1333
+ if (writeColumns.length === 1 && conn.insertGetIdSync1) {
1334
+ return conn.insertGetIdSync1(table, writeColumns[0], writeValues[0]);
1335
+ }
1336
+ return conn.insertGetIdSync(table, writeColumns, writeValues);
1337
+ }
1338
+ const placeholders = writeColumns.map(() => "?").join(", ");
1339
+ const wrapped = writeColumns.map((c) => wrapSqlName(dialect, c));
1340
+ conn.runSync(`INSERT INTO ${table} (${wrapped.join(", ")}) VALUES (${placeholders})`, writeValues);
1341
+ return null;
1342
+ });
1343
+ if (incrementing) {
1344
+ row[key] = inserted;
1345
+ applyIncrementingKeyType(ctor, row, !noCasts);
1346
+ }
1347
+ this.#exists = true;
1348
+ this.#wasRecentlyCreated = true;
1349
+ const original = {};
1350
+ if (cachedCols) {
1351
+ for (let i = 0; i < cachedCols.length; i++) {
1352
+ const col = cachedCols[i];
1353
+ original[col] = row[col];
1354
+ }
1355
+ if (incrementing)
1356
+ original[key] = row[key];
1357
+ }
1358
+ else {
1359
+ for (const attr of Object.keys(row)) {
1360
+ const value = row[attr];
1361
+ if (typeof value !== "function")
1362
+ original[attr] = value;
1363
+ }
1364
+ }
1365
+ this.#original = original;
1366
+ this.#changes = original;
1367
+ this.#touchOwnersSync();
1368
+ return this;
1369
+ }
1370
+ const dirty = this.getDirty();
1371
+ const attributes = persistableAttributes(ctor, dirty, key);
1372
+ const payload = ctor.castAttributes(attributes, "set");
1373
+ const columns = Object.keys(payload);
1374
+ if (columns.length > 0) {
1375
+ const sets = columns
1376
+ .map((c) => `${wrapSqlName(dialect, c)} = ?`)
1377
+ .join(", ");
1378
+ withoutQueryWriteHook(() => conn.runSync(`UPDATE ${table} SET ${sets} WHERE ${wrapSqlName(dialect, key)} = ?`, [...Object.values(payload), id]));
1379
+ }
1380
+ this.#wasRecentlyCreated = false;
1381
+ this.#changes = dirty;
1382
+ this.syncOriginal();
1383
+ this.#touchOwnersSync();
1384
+ return this;
1385
+ }
1386
+ async #persistAsync() {
1387
+ const ctor = this.constructor;
1388
+ const key = ctor.primaryKey;
1389
+ const row = this;
1390
+ const id = row[key];
1391
+ const exists = this.#exists;
1392
+ if ((await fireModelEvent(this, "saving")) === false)
1393
+ return this;
1394
+ const usesTimestamps = ctor.timestamps !== false;
1395
+ if (usesTimestamps) {
1396
+ const now = nowForConnection(this.getConnection());
1397
+ row.updated_at = now;
1398
+ if (!exists) {
1399
+ row.created_at = row.created_at ?? now;
1400
+ }
1401
+ }
1402
+ const dirty = this.getDirty();
1403
+ const persistCtor = ctor;
1404
+ const table = new Db(this.getConnection()).table(ctor.table);
1405
+ if (!exists) {
1406
+ if ((await fireModelEvent(this, "creating")) === false)
1407
+ return this;
1408
+ const attributes = persistableAttributes(persistCtor, row, ctor.incrementing !== false || row[key] == null ? key : undefined);
1409
+ if (ctor.incrementing !== false) {
1410
+ const newId = await withoutQueryWriteHook(() => table.insertGetId(ctor.castAttributes(attributes, "set")));
1411
+ row[key] = newId;
1412
+ applyIncrementingKeyType(ctor, row, !classHasNoCasts(ctor));
1413
+ }
1414
+ else {
1415
+ if (row[key] != null && attributes[key] == null) {
1416
+ attributes[key] = row[key];
1417
+ }
1418
+ await withoutQueryWriteHook(() => table.insert(ctor.castAttributes(attributes, "set")));
1419
+ }
1420
+ this.#exists = true;
1421
+ this.#wasRecentlyCreated = true;
1422
+ await fireModelEvent(this, "created");
1423
+ }
1424
+ else {
1425
+ if ((await fireModelEvent(this, "updating")) === false)
1426
+ return this;
1427
+ const attributes = persistableAttributes(persistCtor, dirty, key);
1428
+ if (Object.keys(attributes).length > 0) {
1429
+ await withoutQueryWriteHook(() => table.where(key, id).update(ctor.castAttributes(attributes, "set")));
1430
+ }
1431
+ this.#wasRecentlyCreated = false;
1432
+ await fireModelEvent(this, "updated");
1433
+ }
1434
+ this.#changes = { ...dirty };
1435
+ if (usesTimestamps) {
1436
+ if ("updated_at" in row)
1437
+ this.#changes.updated_at = row.updated_at;
1438
+ if (!exists && "created_at" in row) {
1439
+ this.#changes.created_at = row.created_at;
1440
+ }
1441
+ }
1442
+ this.syncOriginal();
1443
+ await fireModelEvent(this, "saved");
1444
+ // Laravel finishSave → touchOwners (Relation.touch honors withoutTouching on related).
1445
+ await this.touchOwners();
1446
+ return this;
1447
+ }
1448
+ /** Laravel `$model->saveQuietly()`. */
1449
+ async saveQuietly() {
1450
+ const ctor = this.constructor;
1451
+ return ctor.withoutEvents(() => this.save());
1452
+ }
1453
+ /** Laravel `$model->refresh()`. */
1454
+ async refresh() {
1455
+ const ctor = this.constructor;
1456
+ const key = ctor.primaryKey;
1457
+ const id = this[key];
1458
+ const fresh = await ctor.find(id);
1459
+ if (fresh) {
1460
+ Object.assign(this, fresh.getAttributes());
1461
+ this.#original = { ...fresh.#original };
1462
+ this.#exists = true;
1463
+ }
1464
+ return this;
1465
+ }
1466
+ /**
1467
+ * Laravel-style refresh under row lock (`lockForUpdate`).
1468
+ * Reloads this instance from a locked select of the same primary key.
1469
+ */
1470
+ async refreshForUpdate() {
1471
+ const ctor = this.constructor;
1472
+ const key = ctor.primaryKey;
1473
+ const id = this[key];
1474
+ const fresh = await ctor
1475
+ .newQuery()
1476
+ .lockForUpdate()
1477
+ .find(id);
1478
+ if (fresh) {
1479
+ Object.assign(this, fresh.getAttributes());
1480
+ this.#original = { ...fresh.#original };
1481
+ this.#exists = true;
1482
+ }
1483
+ return this;
1484
+ }
1485
+ /** Laravel `$model->fresh()`. */
1486
+ async fresh() {
1487
+ const ctor = this.constructor;
1488
+ const key = ctor.primaryKey;
1489
+ const id = this[key];
1490
+ return (await ctor.find(id));
1491
+ }
1492
+ /** Eager-load relations onto this model (`load('posts')` / constrained maps). */
1493
+ async load(...relations) {
1494
+ await eagerLoadModels([this], normalizeWithRelations(relations));
1495
+ return this;
1496
+ }
1497
+ /** Load only relations that are not already present. */
1498
+ async loadMissing(...relations) {
1499
+ const normalized = normalizeWithRelations(relations);
1500
+ const missing = normalized.filter((entry) => !this.relationLoaded(entry.name));
1501
+ if (missing.length > 0)
1502
+ await eagerLoadModels([this], missing);
1503
+ return this;
1504
+ }
1505
+ /** Laravel `$model->relationLoaded($key)`. */
1506
+ relationLoaded(key) {
1507
+ return Object.prototype.hasOwnProperty.call(this, key);
1508
+ }
1509
+ /**
1510
+ * Sync-save path: touch `$touches` via BelongsTo.touchSync when available.
1511
+ */
1512
+ #touchOwnersSync() {
1513
+ const ctor = this.constructor;
1514
+ const names = ctor.touches ?? [];
1515
+ if (names.length === 0)
1516
+ return;
1517
+ for (const name of names) {
1518
+ const rel = this.related(name);
1519
+ if (typeof rel.touchSync === "function") {
1520
+ rel.touchSync();
1521
+ continue;
1522
+ }
1523
+ if (typeof rel.touch === "function") {
1524
+ void rel.touch();
1525
+ }
1526
+ }
1527
+ }
1528
+ /**
1529
+ * Laravel `$model->touches($relation)` — whether `$touches` lists this relation.
1530
+ */
1531
+ touches(relation) {
1532
+ const ctor = this.constructor;
1533
+ return (ctor.touches ?? []).includes(relation);
1534
+ }
1535
+ /**
1536
+ * Laravel `$model->touchOwners()` — touch each relation in `$touches`.
1537
+ * Uses `relation.touch()` when available (BelongsTo / HasOne / HasMany);
1538
+ * otherwise loads the related model and calls `touch()` on it.
1539
+ */
1540
+ async touchOwners() {
1541
+ const ctor = this.constructor;
1542
+ const names = ctor.touches ?? [];
1543
+ if (names.length === 0)
1544
+ return;
1545
+ for (const name of names) {
1546
+ const rel = this.related(name);
1547
+ if (typeof rel.touch === "function") {
1548
+ await rel.touch();
1549
+ continue;
1550
+ }
1551
+ if (typeof rel.first === "function") {
1552
+ const related = await rel.first();
1553
+ if (related)
1554
+ await related.touch();
1555
+ }
1556
+ }
1557
+ }
1558
+ /**
1559
+ * Laravel `$model->touch()` — bump `updated_at` (no-op while `withoutTouching`).
1560
+ * Saving also runs `$touches` / `touchOwners()` for listed relations.
1561
+ */
1562
+ async touch() {
1563
+ const ctor = this.constructor;
1564
+ if (isIgnoringTouch(ctor))
1565
+ return false;
1566
+ if (ctor.timestamps === false)
1567
+ return false;
1568
+ if (!this.#exists)
1569
+ return false;
1570
+ const conn = this.getConnection();
1571
+ const now = nowForConnection(conn);
1572
+ this.updated_at = now;
1573
+ await this.save();
1574
+ return true;
1575
+ }
1576
+ /**
1577
+ * Relation query builder (`$model->product()` in Laravel).
1578
+ *
1579
+ * Define relations on `static relations` and declare the loaded property:
1580
+ * `declare product: Product | null` — then use `model.product` after `with('product')`,
1581
+ * and `model.related('product')` when you need the relation query.
1582
+ *
1583
+ * Legacy instance methods named `product()` remain supported via the prototype.
1584
+ */
1585
+ related(name) {
1586
+ const ctor = this.constructor;
1587
+ if (modelsShouldPreventLazyLoading() &&
1588
+ !isInsideEagerLoad() &&
1589
+ this.#exists &&
1590
+ !this.relationLoaded(name)) {
1591
+ throw new LazyLoadingViolationException(ctor.name ?? "Model", name);
1592
+ }
1593
+ const raw = ctor.relations;
1594
+ const map = typeof raw === "function" ? raw.call(ctor) : raw;
1595
+ if (map != null && typeof map[name] === "function") {
1596
+ return map[name](this);
1597
+ }
1598
+ let proto = Object.getPrototypeOf(this);
1599
+ while (proto && proto !== Object.prototype) {
1600
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
1601
+ if (desc && typeof desc.value === "function") {
1602
+ return desc.value.call(this);
1603
+ }
1604
+ proto = Object.getPrototypeOf(proto);
1605
+ }
1606
+ throw new Error(`Call to undefined relationship [${name}] on model [${ctor.name ?? "Model"}].`);
1607
+ }
1608
+ /** Laravel `$model->replicate()`. */
1609
+ replicate(except = []) {
1610
+ const ctor = this.constructor;
1611
+ ctor.bootIfNotBooted();
1612
+ const key = ctor.primaryKey;
1613
+ const skip = new Set([key, "created_at", "updated_at", ...except]);
1614
+ const attrs = {};
1615
+ for (const [k, v] of Object.entries(this)) {
1616
+ if (skip.has(k) || typeof v === "function")
1617
+ continue;
1618
+ attrs[k] = v;
1619
+ }
1620
+ const copy = new ctor(attrs);
1621
+ // Fire sync-style; listeners may be async but replicate is sync in Laravel.
1622
+ void fireModelEvent(copy, "replicating");
1623
+ return copy;
1624
+ }
1625
+ async delete() {
1626
+ const ctor = this.constructor;
1627
+ ctor.bootIfNotBooted();
1628
+ if ((await fireModelEvent(this, "deleting")) === false)
1629
+ return;
1630
+ if (usesSoftDeletes(ctor)) {
1631
+ await this.#performSoftDelete();
1632
+ await fireModelEvent(this, "deleted");
1633
+ await fireModelEvent(this, "trashed");
1634
+ return;
1635
+ }
1636
+ // Laravel delete(): touchOwners before performDeleteOnModel.
1637
+ await this.touchOwners();
1638
+ await this.#performForceDelete();
1639
+ await fireModelEvent(this, "deleted");
1640
+ }
1641
+ async restore() {
1642
+ const ctor = this.constructor;
1643
+ if (!usesSoftDeletes(ctor))
1644
+ return;
1645
+ ctor.bootIfNotBooted();
1646
+ if ((await fireModelEvent(this, "restoring")) === false)
1647
+ return;
1648
+ const key = ctor.primaryKey;
1649
+ const row = this;
1650
+ const col = deletedAtColumn(ctor);
1651
+ row[col] = null;
1652
+ await new Db(this.getConnection())
1653
+ .table(ctor.table)
1654
+ .where(key, row[key])
1655
+ .update({ [col]: null });
1656
+ await fireModelEvent(this, "restored");
1657
+ }
1658
+ async forceDelete() {
1659
+ const ctor = this.constructor;
1660
+ ctor.bootIfNotBooted();
1661
+ if ((await fireModelEvent(this, "forceDeleting")) === false)
1662
+ return;
1663
+ await this.#performForceDelete();
1664
+ await fireModelEvent(this, "forceDeleted");
1665
+ await fireModelEvent(this, "deleted");
1666
+ }
1667
+ /** Laravel `$model->deleteQuietly()`. */
1668
+ async deleteQuietly() {
1669
+ const ctor = this.constructor;
1670
+ await ctor.withoutEvents(() => this.delete());
1671
+ }
1672
+ async #performSoftDelete() {
1673
+ const ctor = this.constructor;
1674
+ const key = ctor.primaryKey;
1675
+ const row = this;
1676
+ const col = deletedAtColumn(ctor);
1677
+ const now = nowForConnection(this.getConnection());
1678
+ row[col] = now;
1679
+ const payload = { [col]: now };
1680
+ if (ctor.timestamps !== false) {
1681
+ row.updated_at = now;
1682
+ payload.updated_at = now;
1683
+ }
1684
+ await withoutQueryWriteHook(() => new Db(this.getConnection())
1685
+ .table(ctor.table)
1686
+ .where(key, row[key])
1687
+ .update(payload));
1688
+ await this.touchOwners();
1689
+ }
1690
+ async #performForceDelete() {
1691
+ const ctor = this.constructor;
1692
+ const key = ctor.primaryKey;
1693
+ const row = this;
1694
+ await withoutQueryWriteHook(() => new Db(this.getConnection())
1695
+ .table(ctor.table)
1696
+ .where(key, row[key])
1697
+ .delete());
1698
+ this.#exists = false;
1699
+ }
1700
+ trashed() {
1701
+ const ctor = this.constructor;
1702
+ if (!usesSoftDeletes(ctor))
1703
+ return false;
1704
+ const col = deletedAtColumn(ctor);
1705
+ const value = this[col];
1706
+ return value != null && value !== "";
1707
+ }
1708
+ /** Laravel `$model->toArray()`. */
1709
+ toArray() {
1710
+ const ctor = this.constructor;
1711
+ const hiddenList = this.getHidden();
1712
+ const visibleList = this.getVisible();
1713
+ const appends = this.getAppends();
1714
+ const self = this;
1715
+ const hidden = new Set(hiddenList);
1716
+ const visible = visibleList.length > 0 ? new Set(visibleList) : null;
1717
+ const out = {};
1718
+ for (const key in self) {
1719
+ const value = self[key];
1720
+ if (typeof value === "function")
1721
+ continue;
1722
+ if (hidden.has(key))
1723
+ continue;
1724
+ if (visible && !visible.has(key))
1725
+ continue;
1726
+ out[key] = value instanceof Date ? value.toISOString() : value;
1727
+ }
1728
+ for (const key of appends) {
1729
+ if (hidden.has(key))
1730
+ continue;
1731
+ if (visible && !visible.has(key))
1732
+ continue;
1733
+ out[key] = this.#mutateAppendedAttribute(key);
1734
+ }
1735
+ return out;
1736
+ }
1737
+ #mutateAppendedAttribute(key) {
1738
+ const studly = key
1739
+ .split("_")
1740
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
1741
+ .join("");
1742
+ const getter = `get${studly}Attribute`;
1743
+ const fn = this[getter];
1744
+ if (typeof fn === "function") {
1745
+ return fn.call(this);
1746
+ }
1747
+ return this[key];
1748
+ }
1749
+ /** Laravel `$model->loadCount(...$relations)`. */
1750
+ async loadCount(...relations) {
1751
+ const names = relations.flat();
1752
+ if (names.length === 0)
1753
+ return this;
1754
+ for (const relation of names) {
1755
+ const parsed = parseRelationAlias(relation);
1756
+ const count = await aggregateRelation(this, parsed.relation, "count");
1757
+ this[parsed.alias ?? `${parsed.relation}_count`] = count;
1758
+ }
1759
+ return this;
1760
+ }
1761
+ /** Laravel `$model->loadSum($relation, $column)`. */
1762
+ async loadSum(relation, column) {
1763
+ const parsed = parseRelationAlias(relation);
1764
+ const value = await aggregateRelation(this, parsed.relation, "sum", column);
1765
+ this[parsed.alias ?? `${parsed.relation}_sum_${column}`] = value;
1766
+ return this;
1767
+ }
1768
+ async loadAvg(relation, column) {
1769
+ const parsed = parseRelationAlias(relation);
1770
+ const value = await aggregateRelation(this, parsed.relation, "avg", column);
1771
+ this[parsed.alias ?? `${parsed.relation}_avg_${column}`] = value;
1772
+ return this;
1773
+ }
1774
+ async loadMin(relation, column) {
1775
+ const parsed = parseRelationAlias(relation);
1776
+ const value = await aggregateRelation(this, parsed.relation, "min", column);
1777
+ this[parsed.alias ?? `${parsed.relation}_min_${column}`] = value;
1778
+ return this;
1779
+ }
1780
+ async loadMax(relation, column) {
1781
+ const parsed = parseRelationAlias(relation);
1782
+ const value = await aggregateRelation(this, parsed.relation, "max", column);
1783
+ this[parsed.alias ?? `${parsed.relation}_max_${column}`] = value;
1784
+ return this;
1785
+ }
1786
+ /** Laravel `$model->loadExists(...$relations)`. */
1787
+ async loadExists(...relations) {
1788
+ for (const relation of relations.flat()) {
1789
+ const parsed = parseRelationAlias(relation);
1790
+ const value = await aggregateRelation(this, parsed.relation, "exists");
1791
+ this[parsed.alias ?? `${parsed.relation}_exists`] = value;
1792
+ }
1793
+ return this;
1794
+ }
1795
+ toJSON() {
1796
+ return this.toArray();
1797
+ }
1798
+ /** Alias of {@link toJSON}. */
1799
+ toJson() {
1800
+ return this.toJSON();
1801
+ }
1802
+ /** Laravel `$model->increment($column, $amount)`. */
1803
+ async increment(column, amount = 1) {
1804
+ const row = this;
1805
+ const current = Number(row[column] ?? 0);
1806
+ row[column] = current + amount;
1807
+ await this.save();
1808
+ return this;
1809
+ }
1810
+ /** Laravel `$model->decrement($column, $amount)`. */
1811
+ async decrement(column, amount = 1) {
1812
+ return this.increment(column, -amount);
1813
+ }
1814
+ /** Laravel `Model::destroy($ids)`. */
1815
+ static async destroy(ids) {
1816
+ const list = Array.isArray(ids) ? ids : [ids];
1817
+ let n = 0;
1818
+ for (const id of list) {
1819
+ const model = await this.find(id);
1820
+ if (!model)
1821
+ continue;
1822
+ await model.delete();
1823
+ n += 1;
1824
+ }
1825
+ return n;
1826
+ }
1827
+ /**
1828
+ * Laravel `Model::forceDestroy($ids)` — permanent delete by key(s).
1829
+ * Soft-delete models are found via `withTrashed()` so trashed rows are included.
1830
+ */
1831
+ static async forceDestroy(ids) {
1832
+ const list = Array.isArray(ids) ? ids : [ids];
1833
+ if (list.length === 0)
1834
+ return 0;
1835
+ this.bootIfNotBooted();
1836
+ const key = this.primaryKey;
1837
+ const models = await this.newQuery().withTrashed().whereIn(key, list).get();
1838
+ let n = 0;
1839
+ for (const model of models) {
1840
+ await model.forceDelete();
1841
+ n += 1;
1842
+ }
1843
+ return n;
1844
+ }
1845
+ /** Laravel `$this->hasMany(Related::class)`. */
1846
+ hasMany(related, foreignKey, localKey) {
1847
+ const key = foreignKey ?? `${singular(this.constructor.table)}_id`;
1848
+ return new HasMany(this, related, key, localKey);
1849
+ }
1850
+ /** Laravel `$this->hasOne(Related::class)`. */
1851
+ hasOne(related, foreignKey, localKey) {
1852
+ const key = foreignKey ?? `${singular(this.constructor.table)}_id`;
1853
+ return new HasOne(this, related, key, localKey);
1854
+ }
1855
+ /**
1856
+ * Laravel `$this->hasManyThrough(Related::class, Through::class)`.
1857
+ * Parent → Through (firstKey) → Related (secondKey).
1858
+ */
1859
+ hasManyThrough(related, through, firstKey, secondKey, localKey, secondLocalKey) {
1860
+ const parentCtor = this.constructor;
1861
+ return new HasManyThrough(this, related, through, firstKey ?? `${singular(parentCtor.table)}_id`, secondKey ?? `${singular(through.table)}_id`, localKey ?? parentCtor.primaryKey, secondLocalKey ?? through.primaryKey);
1862
+ }
1863
+ /**
1864
+ * Laravel `$this->hasOneThrough(Related::class, Through::class)`.
1865
+ */
1866
+ hasOneThrough(related, through, firstKey, secondKey, localKey, secondLocalKey) {
1867
+ const parentCtor = this.constructor;
1868
+ return new HasOneThrough(this, related, through, firstKey ?? `${singular(parentCtor.table)}_id`, secondKey ?? `${singular(through.table)}_id`, localKey ?? parentCtor.primaryKey, secondLocalKey ?? through.primaryKey);
1869
+ }
1870
+ /** Laravel `$this->belongsTo(Related::class)`. */
1871
+ belongsTo(related, foreignKey, ownerKey) {
1872
+ const key = foreignKey ?? `${singular(related.table)}_id`;
1873
+ return new BelongsTo(this, related, key, ownerKey);
1874
+ }
1875
+ /** Laravel `$this->belongsToMany(Related::class, pivot)`. */
1876
+ belongsToMany(related, table, foreignPivotKey, relatedPivotKey) {
1877
+ const parent = this.constructor;
1878
+ const pivot = table ??
1879
+ [singular(parent.table), singular(related.table)].sort().join("_");
1880
+ const foreign = foreignPivotKey ?? `${singular(parent.table)}_id`;
1881
+ const relatedKey = relatedPivotKey ?? `${singular(related.table)}_id`;
1882
+ return new BelongsToMany(this, related, pivot, foreign, relatedKey);
1883
+ }
1884
+ /**
1885
+ * Polymorphic many-to-many (`model_has_roles`-style pivots).
1886
+ * Third argument may be the pivot table name or an options bag (tenant/legacy morph types).
1887
+ */
1888
+ morphToMany(related, name, tableOrOptions, foreignPivotKey, relatedPivotKey) {
1889
+ const options = typeof tableOrOptions === "string" || tableOrOptions == null
1890
+ ? {
1891
+ table: tableOrOptions,
1892
+ foreignPivotKey,
1893
+ relatedPivotKey,
1894
+ }
1895
+ : tableOrOptions;
1896
+ const table = options.table ?? "model_has_roles";
1897
+ const foreign = options.foreignPivotKey ?? "model_id";
1898
+ const relatedKey = options.relatedPivotKey ?? `${singular(related.table)}_id`;
1899
+ const morphTypeColumn = options.morphTypeColumn ?? "model_type";
1900
+ const primaryMorph = morphTypeFor(this.constructor);
1901
+ const morphTypes = options.morphTypes && options.morphTypes.length > 0
1902
+ ? options.morphTypes
1903
+ : [name, primaryMorph].filter((value, index, all) => all.indexOf(value) === index);
1904
+ return new MorphToMany(this, related, table, foreign, relatedKey, morphTypeColumn, morphTypes, options.pivotTenantKey, options.parentTenantKey ?? options.pivotTenantKey);
1905
+ }
1906
+ /** Laravel `$this->morphMany(Related::class, name)`. */
1907
+ morphMany(related, name, type, id, localKey) {
1908
+ return new MorphMany(this, related, type ?? `${name}_type`, id ?? `${name}_id`, morphTypeFor(this.constructor), localKey);
1909
+ }
1910
+ /** Laravel `$this->morphOne(Related::class, name)`. */
1911
+ morphOne(related, name, type, id, localKey) {
1912
+ return new MorphOne(this, related, type ?? `${name}_type`, id ?? `${name}_id`, morphTypeFor(this.constructor), localKey);
1913
+ }
1914
+ /** Laravel `$this->morphTo($name)`. */
1915
+ morphTo(name, type, id, ownerKey) {
1916
+ return new MorphTo(this, type ?? `${name}_type`, id ?? `${name}_id`, ownerKey);
1917
+ }
1918
+ }
1919
+ installLocalScopeCallStatic(Model);
1920
+ /** True when `value` is a Model subclass constructor (O(1) prototype check). */
1921
+ export function isModelCtor(value) {
1922
+ return (typeof value === "function" &&
1923
+ value !== Model &&
1924
+ Boolean(value.prototype) &&
1925
+ value.prototype instanceof Model);
1926
+ }
1927
+ // Re-exports — public API stays on ./model.ts for index.ts
1928
+ export { BelongsTo, BelongsToMany, HasMany, HasManyThrough, HasOne, HasOneThrough, MorphMany, MorphOne, MorphTo, MorphToMany, clearMorphMap, morphMap, } from "./relations.js";
1929
+ export { eagerLoadAggregates, eagerLoadModels, } from "./eager.js";
1930
+ export { ModelQuery } from "./model-query.js";
1931
+ export { LazyLoadingViolationException, MassAssignmentException, MissingAttributeException, resetModelStrictnessForTests, } from "./model-strictness.js";