@bunyad/orm 0.2.0-beta.1 → 0.2.0-beta.2
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/README.md +1 -1
- package/dist/casts.d.ts +16 -3
- package/dist/casts.js +150 -4
- package/dist/eager.js +130 -11
- package/dist/factory.d.ts +20 -1
- package/dist/factory.js +143 -37
- package/dist/index.d.ts +21 -1
- package/dist/index.js +3 -1
- package/dist/model-events.d.ts +8 -2
- package/dist/model-events.js +27 -4
- package/dist/model-query.d.ts +75 -29
- package/dist/model-query.js +252 -133
- package/dist/model.d.ts +164 -34
- package/dist/model.js +708 -44
- package/dist/relations.d.ts +187 -11
- package/dist/relations.js +815 -89
- package/dist/schema-types.d.ts +27 -0
- package/dist/schema-types.js +145 -0
- package/dist/test-drivers.d.ts +10 -0
- package/dist/test-drivers.js +40 -0
- package/dist/typed-names.d.ts +53 -0
- package/dist/typed-names.js +4 -0
- package/package.json +3 -3
package/dist/model.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { DatabaseManager as Db, setDefaultConnection, connections, LengthAwarePaginator, CursorPaginator, Paginator, dateTimeForStorage, withoutQueryWriteHook, wrapSqlName, } from "@bunyad/database";
|
|
2
2
|
import { Collection, LazyCollection } from "@bunyad/common";
|
|
3
3
|
import { OrmCollection } from "./orm-collection.js";
|
|
4
|
-
import { Attribute, castFromStorage, castToStorage, hashCastValue, isAlreadyHashed, enumFromStorage, enumToStorage, isAttribute, isCastType, isEnumCast, } from "./casts.js";
|
|
4
|
+
import { Attribute, castFromStorage, castToStorage, hashCastValue, isAlreadyHashed, enumFromStorage, enumToStorage, isAttribute, isCastType, parseCast, formatPhpDate, isEnumCast, } from "./casts.js";
|
|
5
5
|
import { bootIfNotBooted, fireModelEvent, hasAnyModelEventListeners, hasModelEventListeners, observeModel, registerModelEvent, withoutModelEvents, } from "./model-events.js";
|
|
6
6
|
import { resolveModelBuilder } from "./decorators.js";
|
|
7
7
|
import { addGlobalScope as registerGlobalScope, getGlobalScopes, hasGlobalScopes, installLocalScopeCallStatic, removeGlobalScope as unregisterGlobalScope, } from "./scopes.js";
|
|
8
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";
|
|
9
|
+
import { BelongsTo, BelongsToMany, HasMany, HasManyThrough, HasOne, HasOneThrough, MorphMany, MorphOne, MorphTo, MorphToMany, MorphedByMany, aggregateRelation, clearMorphMap, getMorphedModel, enforceMorphMap, morphMap, registerMorphClass, requireMorphMap, morphTypeFor, } from "./relations.js";
|
|
10
10
|
import { eagerLoadAggregates, eagerLoadModels, } from "./eager.js";
|
|
11
11
|
import { ModelQuery, MODEL_QUERY_TARGET } from "./model-query.js";
|
|
12
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";
|
|
@@ -69,7 +69,23 @@ function cachedSyncInsertColumns(ctor) {
|
|
|
69
69
|
});
|
|
70
70
|
return cols;
|
|
71
71
|
}
|
|
72
|
+
/** `Model::unguard()` — global switch. */
|
|
73
|
+
let massAssignmentUnguarded = false;
|
|
74
|
+
/** Whether `key` may be mass assigned (mirrors {@link filterFillable}'s rules). */
|
|
75
|
+
export function canMassAssign(model, key) {
|
|
76
|
+
if (massAssignmentUnguarded)
|
|
77
|
+
return true;
|
|
78
|
+
const fillables = model.fillable ?? [];
|
|
79
|
+
if (fillables.length > 0)
|
|
80
|
+
return fillables.includes(key);
|
|
81
|
+
const guardeds = model.guarded ?? ["*"];
|
|
82
|
+
if (!guardeds.includes("*"))
|
|
83
|
+
return !guardeds.includes(key);
|
|
84
|
+
return true; // empty fillable + guarded ["*"] → allow all (see filterFillable)
|
|
85
|
+
}
|
|
72
86
|
export function filterFillable(model, attributes) {
|
|
87
|
+
if (massAssignmentUnguarded)
|
|
88
|
+
return { ...attributes };
|
|
73
89
|
const fillables = model.fillable ?? [];
|
|
74
90
|
const guardeds = model.guarded ?? ["*"];
|
|
75
91
|
const strictDiscard = modelsShouldPreventSilentlyDiscardingAttributes();
|
|
@@ -187,6 +203,32 @@ const noCastsCache = new WeakMap();
|
|
|
187
203
|
const castsResultCache = new WeakMap();
|
|
188
204
|
const hashedColumnsCache = new WeakMap();
|
|
189
205
|
const castShapeCache = new WeakMap();
|
|
206
|
+
/**
|
|
207
|
+
* Attributes whose cast value is a mutable object (`json`, `array`, `object`,
|
|
208
|
+
* `collection`). They are compared by content, so an in-place edit
|
|
209
|
+
* (`row.meta.theme = "dark"`) is dirty and an identical re-assignment is not.
|
|
210
|
+
*/
|
|
211
|
+
const objectCastKeysCache = new WeakMap();
|
|
212
|
+
function objectCastKeys(ctor) {
|
|
213
|
+
let keys = objectCastKeysCache.get(ctor);
|
|
214
|
+
if (!keys) {
|
|
215
|
+
keys = [];
|
|
216
|
+
for (const [key, definition] of Object.entries(ctor.getCasts())) {
|
|
217
|
+
if (typeof definition !== "string")
|
|
218
|
+
continue;
|
|
219
|
+
const type = parseCast(definition)?.type;
|
|
220
|
+
if (type === "json" || type === "array" || type === "collection")
|
|
221
|
+
keys.push(key);
|
|
222
|
+
}
|
|
223
|
+
objectCastKeysCache.set(ctor, keys);
|
|
224
|
+
}
|
|
225
|
+
return keys;
|
|
226
|
+
}
|
|
227
|
+
function jsonSnapshot(value) {
|
|
228
|
+
if (value === undefined)
|
|
229
|
+
return undefined;
|
|
230
|
+
return JSON.stringify(value);
|
|
231
|
+
}
|
|
190
232
|
/** Reuse DatabaseManager per Connection — avoids `new Db()` on every list build. */
|
|
191
233
|
const dbManagerByConnection = new WeakMap();
|
|
192
234
|
function classHasNoCasts(ctor) {
|
|
@@ -382,8 +424,13 @@ export class Model {
|
|
|
382
424
|
static guarded = ["*"];
|
|
383
425
|
/** Set in constructor / hydrate — avoid empty `{}` alloc before assign. */
|
|
384
426
|
#original;
|
|
427
|
+
/** Raw (pre-cast) row this model was hydrated from; lets object casts be compared lazily. */
|
|
428
|
+
#rawRow;
|
|
429
|
+
/** JSON snapshots of object-cast attributes taken at the last `syncOriginal()`. */
|
|
430
|
+
#snapshots;
|
|
385
431
|
/** Lazy: allocated on first mutation tracking. */
|
|
386
432
|
#changes;
|
|
433
|
+
#previous;
|
|
387
434
|
#wasRecentlyCreated = false;
|
|
388
435
|
/**
|
|
389
436
|
* Whether this instance is persisted (`$exists`).
|
|
@@ -418,6 +465,8 @@ export class Model {
|
|
|
418
465
|
assignOwn(self, casted);
|
|
419
466
|
// Avoid a second own-key scan via syncOriginal()/getAttributes() on hydrate.
|
|
420
467
|
this.#original = { ...casted };
|
|
468
|
+
if (objectCastKeys(ctor).length > 0)
|
|
469
|
+
this.#rawRow = attributes;
|
|
421
470
|
}
|
|
422
471
|
/**
|
|
423
472
|
* `newFromBuilder` — hydrate a persisted row (sets `$exists`).
|
|
@@ -431,8 +480,54 @@ export class Model {
|
|
|
431
480
|
/** Snapshot current attributes as the original set. */
|
|
432
481
|
syncOriginal() {
|
|
433
482
|
this.#original = this.getAttributes();
|
|
483
|
+
this.#rawRow = undefined;
|
|
484
|
+
const keys = objectCastKeys(this.constructor);
|
|
485
|
+
if (keys.length > 0) {
|
|
486
|
+
const snapshots = new Map();
|
|
487
|
+
for (const key of keys) {
|
|
488
|
+
if (key in this.#original)
|
|
489
|
+
snapshots.set(key, jsonSnapshot(this.#original[key]));
|
|
490
|
+
}
|
|
491
|
+
this.#snapshots = snapshots;
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
this.#snapshots = undefined;
|
|
495
|
+
}
|
|
434
496
|
return this;
|
|
435
497
|
}
|
|
498
|
+
/** JSON of an object-cast attribute as it was when hydrated / last synced. */
|
|
499
|
+
#objectSnapshot(key) {
|
|
500
|
+
if (this.#snapshots?.has(key))
|
|
501
|
+
return this.#snapshots.get(key);
|
|
502
|
+
const raw = this.#rawRow;
|
|
503
|
+
if (raw && key in raw) {
|
|
504
|
+
const definition = this.constructor.getCasts()[key];
|
|
505
|
+
const parsed = castFromStorage(raw[key], definition);
|
|
506
|
+
const snapshot = jsonSnapshot(parsed);
|
|
507
|
+
(this.#snapshots ??= new Map()).set(key, snapshot);
|
|
508
|
+
return snapshot;
|
|
509
|
+
}
|
|
510
|
+
return jsonSnapshot(this.#original?.[key]);
|
|
511
|
+
}
|
|
512
|
+
/** The pristine value of `key` (a fresh copy for object casts, so in-place edits don't leak in). */
|
|
513
|
+
#originalValue(key) {
|
|
514
|
+
const original = this.#original ?? {};
|
|
515
|
+
if (objectCastKeys(this.constructor).includes(key) && key in original) {
|
|
516
|
+
const snapshot = this.#objectSnapshot(key);
|
|
517
|
+
return snapshot === undefined ? undefined : JSON.parse(snapshot);
|
|
518
|
+
}
|
|
519
|
+
return original[key];
|
|
520
|
+
}
|
|
521
|
+
/** `$model->only($keys)` — a subset of the attributes (array or varargs; missing keys are skipped). */
|
|
522
|
+
only(...keys) {
|
|
523
|
+
const attributes = this.getAttributes();
|
|
524
|
+
const out = {};
|
|
525
|
+
for (const key of keys.flat()) {
|
|
526
|
+
if (Object.prototype.hasOwnProperty.call(attributes, key))
|
|
527
|
+
out[key] = attributes[key];
|
|
528
|
+
}
|
|
529
|
+
return out;
|
|
530
|
+
}
|
|
436
531
|
/** Current attribute bag (non-function own properties). */
|
|
437
532
|
getAttributes() {
|
|
438
533
|
const out = {};
|
|
@@ -493,22 +588,73 @@ export class Model {
|
|
|
493
588
|
getDirty() {
|
|
494
589
|
const dirty = {};
|
|
495
590
|
const original = this.#original ?? {};
|
|
591
|
+
const objectKeys = objectCastKeys(this.constructor);
|
|
496
592
|
for (const [key, value] of Object.entries(this.getAttributes())) {
|
|
497
|
-
if (
|
|
593
|
+
if (objectKeys.length > 0 && objectKeys.includes(key) && key in original) {
|
|
594
|
+
if (jsonSnapshot(value) !== this.#objectSnapshot(key))
|
|
595
|
+
dirty[key] = value;
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
const before = original[key];
|
|
599
|
+
// Equal instants are not a change (`row.born = new Date(sameTime)`).
|
|
600
|
+
if (value instanceof Date && before instanceof Date) {
|
|
601
|
+
if (value.getTime() !== before.getTime())
|
|
602
|
+
dirty[key] = value;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (!Object.is(value, before))
|
|
498
606
|
dirty[key] = value;
|
|
499
607
|
}
|
|
500
608
|
return dirty;
|
|
501
609
|
}
|
|
502
610
|
getOriginal(key) {
|
|
503
611
|
const original = this.#original ?? {};
|
|
504
|
-
if (key === undefined)
|
|
505
|
-
|
|
506
|
-
|
|
612
|
+
if (key === undefined) {
|
|
613
|
+
const out = { ...original };
|
|
614
|
+
for (const objectKey of objectCastKeys(this.constructor)) {
|
|
615
|
+
if (objectKey in out)
|
|
616
|
+
out[objectKey] = this.#originalValue(objectKey);
|
|
617
|
+
}
|
|
618
|
+
return out;
|
|
619
|
+
}
|
|
620
|
+
return this.#originalValue(key);
|
|
507
621
|
}
|
|
508
622
|
/** `getChanges` — attributes changed by the last save. */
|
|
509
623
|
getChanges() {
|
|
510
624
|
return this.#changes ? { ...this.#changes } : {};
|
|
511
625
|
}
|
|
626
|
+
getPrevious(key) {
|
|
627
|
+
const previous = this.#previous ?? {};
|
|
628
|
+
if (key === undefined)
|
|
629
|
+
return { ...previous };
|
|
630
|
+
return previous[key];
|
|
631
|
+
}
|
|
632
|
+
/** `getChanges()` / `getPrevious()` for the write that just happened (set before `created` / `updated`). */
|
|
633
|
+
#recordChanges(dirty, exists, usesTimestamps) {
|
|
634
|
+
const row = this;
|
|
635
|
+
this.#changes = { ...dirty };
|
|
636
|
+
if (usesTimestamps) {
|
|
637
|
+
if ("updated_at" in row && (!exists || "updated_at" in dirty || Object.keys(dirty).length > 0)) {
|
|
638
|
+
this.#changes.updated_at = row.updated_at;
|
|
639
|
+
}
|
|
640
|
+
if (!exists && "created_at" in row) {
|
|
641
|
+
this.#changes.created_at = row.created_at;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
this.#capturePrevious(!exists);
|
|
645
|
+
}
|
|
646
|
+
#capturePrevious(created = false) {
|
|
647
|
+
const original = this.#original ?? {};
|
|
648
|
+
const previous = {};
|
|
649
|
+
if (!created) {
|
|
650
|
+
for (const key of Object.keys(this.#changes ?? {})) {
|
|
651
|
+
if (Object.prototype.hasOwnProperty.call(original, key)) {
|
|
652
|
+
previous[key] = this.#originalValue(key);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
this.#previous = previous;
|
|
657
|
+
}
|
|
512
658
|
/** `wasChanged`. */
|
|
513
659
|
wasChanged(...attributes) {
|
|
514
660
|
const changes = this.#changes;
|
|
@@ -609,6 +755,39 @@ export class Model {
|
|
|
609
755
|
static preventLazyLoading(prevent = true) {
|
|
610
756
|
setPreventLazyLoading(prevent);
|
|
611
757
|
}
|
|
758
|
+
/** `Model::unguard()` — disable mass-assignment protection (seeders, imports). */
|
|
759
|
+
static unguard(state = true) {
|
|
760
|
+
massAssignmentUnguarded = state;
|
|
761
|
+
}
|
|
762
|
+
/** `Model::reguard()`. */
|
|
763
|
+
static reguard() {
|
|
764
|
+
massAssignmentUnguarded = false;
|
|
765
|
+
}
|
|
766
|
+
/** `Model::isUnguarded()`. */
|
|
767
|
+
static isUnguarded() {
|
|
768
|
+
return massAssignmentUnguarded;
|
|
769
|
+
}
|
|
770
|
+
/** `Model::unguarded(callback)` — run with protection off, then restore it. */
|
|
771
|
+
static unguarded(callback) {
|
|
772
|
+
if (massAssignmentUnguarded)
|
|
773
|
+
return callback();
|
|
774
|
+
massAssignmentUnguarded = true;
|
|
775
|
+
let result;
|
|
776
|
+
try {
|
|
777
|
+
result = callback();
|
|
778
|
+
}
|
|
779
|
+
catch (error) {
|
|
780
|
+
massAssignmentUnguarded = false;
|
|
781
|
+
throw error;
|
|
782
|
+
}
|
|
783
|
+
if (result instanceof Promise) {
|
|
784
|
+
return result.finally(() => {
|
|
785
|
+
massAssignmentUnguarded = false;
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
massAssignmentUnguarded = false;
|
|
789
|
+
return result;
|
|
790
|
+
}
|
|
612
791
|
/** `Model::preventSilentlyDiscardingAttributes`. */
|
|
613
792
|
static preventSilentlyDiscardingAttributes(prevent = true) {
|
|
614
793
|
setPreventSilentlyDiscardingAttributes(prevent);
|
|
@@ -646,6 +825,7 @@ export class Model {
|
|
|
646
825
|
static booted() { }
|
|
647
826
|
/** Ensure this model class has been booted. */
|
|
648
827
|
static bootIfNotBooted() {
|
|
828
|
+
registerMorphClass(this);
|
|
649
829
|
bootIfNotBooted(this);
|
|
650
830
|
}
|
|
651
831
|
/** `Model::observe`. */
|
|
@@ -654,7 +834,8 @@ export class Model {
|
|
|
654
834
|
}
|
|
655
835
|
/** `Model::withoutEvents`. */
|
|
656
836
|
static withoutEvents(callback) {
|
|
657
|
-
|
|
837
|
+
// Called on `Model` itself it mutes every model class.
|
|
838
|
+
return withoutModelEvents(this, callback, this === Model);
|
|
658
839
|
}
|
|
659
840
|
static retrieved(callback) {
|
|
660
841
|
registerModelEvent(this, "retrieved", callback);
|
|
@@ -712,6 +893,13 @@ export class Model {
|
|
|
712
893
|
return cached;
|
|
713
894
|
const field = this.casts;
|
|
714
895
|
const value = typeof field === "function" ? (field.call(this) ?? {}) : (field ?? {});
|
|
896
|
+
for (const [key, definition] of Object.entries(value)) {
|
|
897
|
+
if (typeof definition === "string" && !isCastType(definition)) {
|
|
898
|
+
throw new Error(`Unknown cast type "${definition}" for ${this.name ?? "Model"}.${key}. ` +
|
|
899
|
+
`Supported: boolean, integer, float, decimal[:N], string, bigint, json, array, object, collection, ` +
|
|
900
|
+
`date[:FORMAT], datetime[:FORMAT], immutable_date, immutable_datetime, encrypted[:json|:array|:collection], hashed.`);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
715
903
|
castsResultCache.set(this, value);
|
|
716
904
|
return value;
|
|
717
905
|
}
|
|
@@ -930,11 +1118,387 @@ export class Model {
|
|
|
930
1118
|
return q.where(columnOrCallback, opOrValue);
|
|
931
1119
|
return q.where(columnOrCallback, String(opOrValue), value);
|
|
932
1120
|
}
|
|
933
|
-
/**
|
|
1121
|
+
/** Multi-column LIKE search. */
|
|
934
1122
|
static search(columns, term) {
|
|
935
1123
|
this.bootIfNotBooted();
|
|
936
1124
|
return this.newQuery().search(columns, term);
|
|
937
1125
|
}
|
|
1126
|
+
// Terminal forwarders take `any[]` on purpose: an app may define its own static with the same name
|
|
1127
|
+
// (`static exists(id)`), and a stricter base signature would make `typeof User` stop satisfying `typeof Model`.
|
|
1128
|
+
// ── Query-builder forwarding (`__callStatic`): `User.whereBetween(...)`, `User.has('posts')`, … ──
|
|
1129
|
+
static whereBetween(...args) {
|
|
1130
|
+
this.bootIfNotBooted();
|
|
1131
|
+
return this.newQuery()["whereBetween"](...args);
|
|
1132
|
+
}
|
|
1133
|
+
static whereNotBetween(...args) {
|
|
1134
|
+
this.bootIfNotBooted();
|
|
1135
|
+
return this.newQuery()["whereNotBetween"](...args);
|
|
1136
|
+
}
|
|
1137
|
+
static whereNotIn(...args) {
|
|
1138
|
+
this.bootIfNotBooted();
|
|
1139
|
+
return this.newQuery()["whereNotIn"](...args);
|
|
1140
|
+
}
|
|
1141
|
+
static whereRaw(...args) {
|
|
1142
|
+
this.bootIfNotBooted();
|
|
1143
|
+
return this.newQuery()["whereRaw"](...args);
|
|
1144
|
+
}
|
|
1145
|
+
static whereColumn(...args) {
|
|
1146
|
+
this.bootIfNotBooted();
|
|
1147
|
+
return this.newQuery()["whereColumn"](...args);
|
|
1148
|
+
}
|
|
1149
|
+
static whereLike(...args) {
|
|
1150
|
+
this.bootIfNotBooted();
|
|
1151
|
+
return this.newQuery()["whereLike"](...args);
|
|
1152
|
+
}
|
|
1153
|
+
static whereNotLike(...args) {
|
|
1154
|
+
this.bootIfNotBooted();
|
|
1155
|
+
return this.newQuery()["whereNotLike"](...args);
|
|
1156
|
+
}
|
|
1157
|
+
static whereDay(...args) {
|
|
1158
|
+
this.bootIfNotBooted();
|
|
1159
|
+
return this.newQuery()["whereDay"](...args);
|
|
1160
|
+
}
|
|
1161
|
+
static whereMonth(...args) {
|
|
1162
|
+
this.bootIfNotBooted();
|
|
1163
|
+
return this.newQuery()["whereMonth"](...args);
|
|
1164
|
+
}
|
|
1165
|
+
static whereYear(...args) {
|
|
1166
|
+
this.bootIfNotBooted();
|
|
1167
|
+
return this.newQuery()["whereYear"](...args);
|
|
1168
|
+
}
|
|
1169
|
+
static orWhereIn(...args) {
|
|
1170
|
+
this.bootIfNotBooted();
|
|
1171
|
+
return this.newQuery()["orWhereIn"](...args);
|
|
1172
|
+
}
|
|
1173
|
+
static orWhereNotIn(...args) {
|
|
1174
|
+
this.bootIfNotBooted();
|
|
1175
|
+
return this.newQuery()["orWhereNotIn"](...args);
|
|
1176
|
+
}
|
|
1177
|
+
static orWhereNull(...args) {
|
|
1178
|
+
this.bootIfNotBooted();
|
|
1179
|
+
return this.newQuery()["orWhereNull"](...args);
|
|
1180
|
+
}
|
|
1181
|
+
static orWhereNotNull(...args) {
|
|
1182
|
+
this.bootIfNotBooted();
|
|
1183
|
+
return this.newQuery()["orWhereNotNull"](...args);
|
|
1184
|
+
}
|
|
1185
|
+
static orWhereBetween(...args) {
|
|
1186
|
+
this.bootIfNotBooted();
|
|
1187
|
+
return this.newQuery()["orWhereBetween"](...args);
|
|
1188
|
+
}
|
|
1189
|
+
static orWhereNotBetween(...args) {
|
|
1190
|
+
this.bootIfNotBooted();
|
|
1191
|
+
return this.newQuery()["orWhereNotBetween"](...args);
|
|
1192
|
+
}
|
|
1193
|
+
static orWhereColumn(...args) {
|
|
1194
|
+
this.bootIfNotBooted();
|
|
1195
|
+
return this.newQuery()["orWhereColumn"](...args);
|
|
1196
|
+
}
|
|
1197
|
+
static orWhereRaw(...args) {
|
|
1198
|
+
this.bootIfNotBooted();
|
|
1199
|
+
return this.newQuery()["orWhereRaw"](...args);
|
|
1200
|
+
}
|
|
1201
|
+
static orWhereLike(...args) {
|
|
1202
|
+
this.bootIfNotBooted();
|
|
1203
|
+
return this.newQuery()["orWhereLike"](...args);
|
|
1204
|
+
}
|
|
1205
|
+
static orWhereNotLike(...args) {
|
|
1206
|
+
this.bootIfNotBooted();
|
|
1207
|
+
return this.newQuery()["orWhereNotLike"](...args);
|
|
1208
|
+
}
|
|
1209
|
+
static groupBy(...args) {
|
|
1210
|
+
this.bootIfNotBooted();
|
|
1211
|
+
return this.newQuery()["groupBy"](...args);
|
|
1212
|
+
}
|
|
1213
|
+
static having(...args) {
|
|
1214
|
+
this.bootIfNotBooted();
|
|
1215
|
+
return this.newQuery()["having"](...args);
|
|
1216
|
+
}
|
|
1217
|
+
static havingRaw(...args) {
|
|
1218
|
+
this.bootIfNotBooted();
|
|
1219
|
+
return this.newQuery()["havingRaw"](...args);
|
|
1220
|
+
}
|
|
1221
|
+
static selectRaw(...args) {
|
|
1222
|
+
this.bootIfNotBooted();
|
|
1223
|
+
return this.newQuery()["selectRaw"](...args);
|
|
1224
|
+
}
|
|
1225
|
+
static addSelect(...args) {
|
|
1226
|
+
this.bootIfNotBooted();
|
|
1227
|
+
return this.newQuery()["addSelect"](...args);
|
|
1228
|
+
}
|
|
1229
|
+
static distinct(...args) {
|
|
1230
|
+
this.bootIfNotBooted();
|
|
1231
|
+
return this.newQuery()["distinct"](...args);
|
|
1232
|
+
}
|
|
1233
|
+
static inRandomOrder(...args) {
|
|
1234
|
+
this.bootIfNotBooted();
|
|
1235
|
+
return this.newQuery()["inRandomOrder"](...args);
|
|
1236
|
+
}
|
|
1237
|
+
static reorder(...args) {
|
|
1238
|
+
this.bootIfNotBooted();
|
|
1239
|
+
return this.newQuery()["reorder"](...args);
|
|
1240
|
+
}
|
|
1241
|
+
static offset(...args) {
|
|
1242
|
+
this.bootIfNotBooted();
|
|
1243
|
+
return this.newQuery()["offset"](...args);
|
|
1244
|
+
}
|
|
1245
|
+
static skip(...args) {
|
|
1246
|
+
this.bootIfNotBooted();
|
|
1247
|
+
return this.newQuery()["skip"](...args);
|
|
1248
|
+
}
|
|
1249
|
+
static lockForUpdate(...args) {
|
|
1250
|
+
this.bootIfNotBooted();
|
|
1251
|
+
return this.newQuery()["lockForUpdate"](...args);
|
|
1252
|
+
}
|
|
1253
|
+
static sharedLock(...args) {
|
|
1254
|
+
this.bootIfNotBooted();
|
|
1255
|
+
return this.newQuery()["sharedLock"](...args);
|
|
1256
|
+
}
|
|
1257
|
+
static has(...args) {
|
|
1258
|
+
this.bootIfNotBooted();
|
|
1259
|
+
return this.newQuery()["has"](...args);
|
|
1260
|
+
}
|
|
1261
|
+
static doesntHave(...args) {
|
|
1262
|
+
this.bootIfNotBooted();
|
|
1263
|
+
return this.newQuery()["doesntHave"](...args);
|
|
1264
|
+
}
|
|
1265
|
+
static orHas(...args) {
|
|
1266
|
+
this.bootIfNotBooted();
|
|
1267
|
+
return this.newQuery()["orHas"](...args);
|
|
1268
|
+
}
|
|
1269
|
+
static orDoesntHave(...args) {
|
|
1270
|
+
this.bootIfNotBooted();
|
|
1271
|
+
return this.newQuery()["orDoesntHave"](...args);
|
|
1272
|
+
}
|
|
1273
|
+
static orWhereDoesntHave(...args) {
|
|
1274
|
+
this.bootIfNotBooted();
|
|
1275
|
+
return this.newQuery()["orWhereDoesntHave"](...args);
|
|
1276
|
+
}
|
|
1277
|
+
static whereHasMorph(...args) {
|
|
1278
|
+
this.bootIfNotBooted();
|
|
1279
|
+
return this.newQuery()["whereHasMorph"](...args);
|
|
1280
|
+
}
|
|
1281
|
+
static orWhereHasMorph(...args) {
|
|
1282
|
+
this.bootIfNotBooted();
|
|
1283
|
+
return this.newQuery()["orWhereHasMorph"](...args);
|
|
1284
|
+
}
|
|
1285
|
+
static whereDoesntHaveMorph(...args) {
|
|
1286
|
+
this.bootIfNotBooted();
|
|
1287
|
+
return this.newQuery()["whereDoesntHaveMorph"](...args);
|
|
1288
|
+
}
|
|
1289
|
+
static orWhereDoesntHaveMorph(...args) {
|
|
1290
|
+
this.bootIfNotBooted();
|
|
1291
|
+
return this.newQuery()["orWhereDoesntHaveMorph"](...args);
|
|
1292
|
+
}
|
|
1293
|
+
static hasMorph(...args) {
|
|
1294
|
+
this.bootIfNotBooted();
|
|
1295
|
+
return this.newQuery()["hasMorph"](...args);
|
|
1296
|
+
}
|
|
1297
|
+
static doesntHaveMorph(...args) {
|
|
1298
|
+
this.bootIfNotBooted();
|
|
1299
|
+
return this.newQuery()["doesntHaveMorph"](...args);
|
|
1300
|
+
}
|
|
1301
|
+
static whereMorphedTo(...args) {
|
|
1302
|
+
this.bootIfNotBooted();
|
|
1303
|
+
return this.newQuery()["whereMorphedTo"](...args);
|
|
1304
|
+
}
|
|
1305
|
+
static whereNotMorphedTo(...args) {
|
|
1306
|
+
this.bootIfNotBooted();
|
|
1307
|
+
return this.newQuery()["whereNotMorphedTo"](...args);
|
|
1308
|
+
}
|
|
1309
|
+
static orWhereMorphedTo(...args) {
|
|
1310
|
+
this.bootIfNotBooted();
|
|
1311
|
+
return this.newQuery()["orWhereMorphedTo"](...args);
|
|
1312
|
+
}
|
|
1313
|
+
static orWhereNotMorphedTo(...args) {
|
|
1314
|
+
this.bootIfNotBooted();
|
|
1315
|
+
return this.newQuery()["orWhereNotMorphedTo"](...args);
|
|
1316
|
+
}
|
|
1317
|
+
static whereMorphRelation(...args) {
|
|
1318
|
+
this.bootIfNotBooted();
|
|
1319
|
+
return this.newQuery()["whereMorphRelation"](...args);
|
|
1320
|
+
}
|
|
1321
|
+
static orWhereMorphRelation(...args) {
|
|
1322
|
+
this.bootIfNotBooted();
|
|
1323
|
+
return this.newQuery()["orWhereMorphRelation"](...args);
|
|
1324
|
+
}
|
|
1325
|
+
static whereKeyNot(...args) {
|
|
1326
|
+
this.bootIfNotBooted();
|
|
1327
|
+
return this.newQuery()["whereKeyNot"](...args);
|
|
1328
|
+
}
|
|
1329
|
+
static orWhereBelongsTo(...args) {
|
|
1330
|
+
this.bootIfNotBooted();
|
|
1331
|
+
return this.newQuery()["orWhereBelongsTo"](...args);
|
|
1332
|
+
}
|
|
1333
|
+
static orWhereAny(...args) {
|
|
1334
|
+
this.bootIfNotBooted();
|
|
1335
|
+
return this.newQuery()["orWhereAny"](...args);
|
|
1336
|
+
}
|
|
1337
|
+
static orWhereAll(...args) {
|
|
1338
|
+
this.bootIfNotBooted();
|
|
1339
|
+
return this.newQuery()["orWhereAll"](...args);
|
|
1340
|
+
}
|
|
1341
|
+
static orWhereNone(...args) {
|
|
1342
|
+
this.bootIfNotBooted();
|
|
1343
|
+
return this.newQuery()["orWhereNone"](...args);
|
|
1344
|
+
}
|
|
1345
|
+
static withAggregate(...args) {
|
|
1346
|
+
this.bootIfNotBooted();
|
|
1347
|
+
return this.newQuery()["withAggregate"](...args);
|
|
1348
|
+
}
|
|
1349
|
+
static withAttributes(...args) {
|
|
1350
|
+
this.bootIfNotBooted();
|
|
1351
|
+
return this.newQuery()["withAttributes"](...args);
|
|
1352
|
+
}
|
|
1353
|
+
static withCasts(...args) {
|
|
1354
|
+
this.bootIfNotBooted();
|
|
1355
|
+
return this.newQuery()["withCasts"](...args);
|
|
1356
|
+
}
|
|
1357
|
+
static rightJoin(...args) {
|
|
1358
|
+
this.bootIfNotBooted();
|
|
1359
|
+
return this.newQuery()["rightJoin"](...args);
|
|
1360
|
+
}
|
|
1361
|
+
static when(...args) {
|
|
1362
|
+
this.bootIfNotBooted();
|
|
1363
|
+
return this.newQuery()["when"](...args);
|
|
1364
|
+
}
|
|
1365
|
+
static unless(...args) {
|
|
1366
|
+
this.bootIfNotBooted();
|
|
1367
|
+
return this.newQuery()["unless"](...args);
|
|
1368
|
+
}
|
|
1369
|
+
static tap(...args) {
|
|
1370
|
+
this.bootIfNotBooted();
|
|
1371
|
+
return this.newQuery()["tap"](...args);
|
|
1372
|
+
}
|
|
1373
|
+
static forPage(...args) {
|
|
1374
|
+
this.bootIfNotBooted();
|
|
1375
|
+
return this.newQuery()["forPage"](...args);
|
|
1376
|
+
}
|
|
1377
|
+
static firstWhere(...args) {
|
|
1378
|
+
this.bootIfNotBooted();
|
|
1379
|
+
const q = this.newQuery();
|
|
1380
|
+
return q["firstWhere"].apply(q, args);
|
|
1381
|
+
}
|
|
1382
|
+
static findMany(...args) {
|
|
1383
|
+
this.bootIfNotBooted();
|
|
1384
|
+
const q = this.newQuery();
|
|
1385
|
+
return q["findMany"].apply(q, args);
|
|
1386
|
+
}
|
|
1387
|
+
static findOr(...args) {
|
|
1388
|
+
this.bootIfNotBooted();
|
|
1389
|
+
const q = this.newQuery();
|
|
1390
|
+
return q["findOr"].apply(q, args);
|
|
1391
|
+
}
|
|
1392
|
+
static findOrNew(...args) {
|
|
1393
|
+
this.bootIfNotBooted();
|
|
1394
|
+
const q = this.newQuery();
|
|
1395
|
+
return q["findOrNew"].apply(q, args);
|
|
1396
|
+
}
|
|
1397
|
+
static firstOr(...args) {
|
|
1398
|
+
this.bootIfNotBooted();
|
|
1399
|
+
const q = this.newQuery();
|
|
1400
|
+
return q["firstOr"].apply(q, args);
|
|
1401
|
+
}
|
|
1402
|
+
static chunk(...args) {
|
|
1403
|
+
this.bootIfNotBooted();
|
|
1404
|
+
const q = this.newQuery();
|
|
1405
|
+
return q["chunk"].apply(q, args);
|
|
1406
|
+
}
|
|
1407
|
+
static chunkById(...args) {
|
|
1408
|
+
this.bootIfNotBooted();
|
|
1409
|
+
const q = this.newQuery();
|
|
1410
|
+
return q["chunkById"].apply(q, args);
|
|
1411
|
+
}
|
|
1412
|
+
static chunkByIdDesc(...args) {
|
|
1413
|
+
this.bootIfNotBooted();
|
|
1414
|
+
const q = this.newQuery();
|
|
1415
|
+
return q["chunkByIdDesc"].apply(q, args);
|
|
1416
|
+
}
|
|
1417
|
+
static dd(...args) {
|
|
1418
|
+
this.bootIfNotBooted();
|
|
1419
|
+
const q = this.newQuery();
|
|
1420
|
+
return q["dd"].apply(q, args);
|
|
1421
|
+
}
|
|
1422
|
+
static dump(...args) {
|
|
1423
|
+
this.bootIfNotBooted();
|
|
1424
|
+
const q = this.newQuery();
|
|
1425
|
+
return q["dump"].apply(q, args);
|
|
1426
|
+
}
|
|
1427
|
+
static toBase(...args) {
|
|
1428
|
+
this.bootIfNotBooted();
|
|
1429
|
+
const q = this.newQuery();
|
|
1430
|
+
return q["toBase"].apply(q, args);
|
|
1431
|
+
}
|
|
1432
|
+
static count(...args) {
|
|
1433
|
+
this.bootIfNotBooted();
|
|
1434
|
+
const q = this.newQuery();
|
|
1435
|
+
return q["count"].apply(q, args);
|
|
1436
|
+
}
|
|
1437
|
+
static sum(...args) {
|
|
1438
|
+
this.bootIfNotBooted();
|
|
1439
|
+
const q = this.newQuery();
|
|
1440
|
+
return q["sum"].apply(q, args);
|
|
1441
|
+
}
|
|
1442
|
+
static avg(...args) {
|
|
1443
|
+
this.bootIfNotBooted();
|
|
1444
|
+
const q = this.newQuery();
|
|
1445
|
+
return q["avg"].apply(q, args);
|
|
1446
|
+
}
|
|
1447
|
+
static min(...args) {
|
|
1448
|
+
this.bootIfNotBooted();
|
|
1449
|
+
const q = this.newQuery();
|
|
1450
|
+
return q["min"].apply(q, args);
|
|
1451
|
+
}
|
|
1452
|
+
static max(...args) {
|
|
1453
|
+
this.bootIfNotBooted();
|
|
1454
|
+
const q = this.newQuery();
|
|
1455
|
+
return q["max"].apply(q, args);
|
|
1456
|
+
}
|
|
1457
|
+
static exists(...args) {
|
|
1458
|
+
this.bootIfNotBooted();
|
|
1459
|
+
const q = this.newQuery();
|
|
1460
|
+
return q["exists"].apply(q, args);
|
|
1461
|
+
}
|
|
1462
|
+
static doesntExist(...args) {
|
|
1463
|
+
this.bootIfNotBooted();
|
|
1464
|
+
const q = this.newQuery();
|
|
1465
|
+
return q["doesntExist"].apply(q, args);
|
|
1466
|
+
}
|
|
1467
|
+
static sole(...args) {
|
|
1468
|
+
this.bootIfNotBooted();
|
|
1469
|
+
const q = this.newQuery();
|
|
1470
|
+
return q["sole"].apply(q, args);
|
|
1471
|
+
}
|
|
1472
|
+
static soleValue(...args) {
|
|
1473
|
+
this.bootIfNotBooted();
|
|
1474
|
+
const q = this.newQuery();
|
|
1475
|
+
return q["soleValue"].apply(q, args);
|
|
1476
|
+
}
|
|
1477
|
+
static valueOrFail(...args) {
|
|
1478
|
+
this.bootIfNotBooted();
|
|
1479
|
+
const q = this.newQuery();
|
|
1480
|
+
return q["valueOrFail"].apply(q, args);
|
|
1481
|
+
}
|
|
1482
|
+
static toSql(...args) {
|
|
1483
|
+
this.bootIfNotBooted();
|
|
1484
|
+
const q = this.newQuery();
|
|
1485
|
+
return q["toSql"].apply(q, args);
|
|
1486
|
+
}
|
|
1487
|
+
static toRawSql(...args) {
|
|
1488
|
+
this.bootIfNotBooted();
|
|
1489
|
+
const q = this.newQuery();
|
|
1490
|
+
return q["toRawSql"].apply(q, args);
|
|
1491
|
+
}
|
|
1492
|
+
static getBindings(...args) {
|
|
1493
|
+
this.bootIfNotBooted();
|
|
1494
|
+
const q = this.newQuery();
|
|
1495
|
+
return q["getBindings"].apply(q, args);
|
|
1496
|
+
}
|
|
1497
|
+
static firstOrFail(...args) {
|
|
1498
|
+
this.bootIfNotBooted();
|
|
1499
|
+
const q = this.newQuery();
|
|
1500
|
+
return q["firstOrFail"].apply(q, args);
|
|
1501
|
+
}
|
|
938
1502
|
static orWhere(column, opOrValue, value) {
|
|
939
1503
|
this.bootIfNotBooted();
|
|
940
1504
|
const q = this.newQuery();
|
|
@@ -965,7 +1529,7 @@ export class Model {
|
|
|
965
1529
|
this.bootIfNotBooted();
|
|
966
1530
|
return this.newQuery().orderByRaw(sql);
|
|
967
1531
|
}
|
|
968
|
-
/**
|
|
1532
|
+
/** Alias for `orderByRaw`. */
|
|
969
1533
|
static orderBySql(sql) {
|
|
970
1534
|
return this.orderByRaw(sql);
|
|
971
1535
|
}
|
|
@@ -995,15 +1559,15 @@ export class Model {
|
|
|
995
1559
|
return q.whereDate(column, opOrValue);
|
|
996
1560
|
return q.whereDate(column, String(opOrValue), value);
|
|
997
1561
|
}
|
|
998
|
-
|
|
999
|
-
static whereNot(callback) {
|
|
1562
|
+
static whereNot(...args) {
|
|
1000
1563
|
this.bootIfNotBooted();
|
|
1001
|
-
|
|
1564
|
+
const q = this.newQuery();
|
|
1565
|
+
return q.whereNot(...args);
|
|
1002
1566
|
}
|
|
1003
|
-
|
|
1004
|
-
static orWhereNot(callback) {
|
|
1567
|
+
static orWhereNot(...args) {
|
|
1005
1568
|
this.bootIfNotBooted();
|
|
1006
|
-
|
|
1569
|
+
const q = this.newQuery();
|
|
1570
|
+
return q.orWhereNot(...args);
|
|
1007
1571
|
}
|
|
1008
1572
|
/** `withTrashed()`. */
|
|
1009
1573
|
static withTrashed() {
|
|
@@ -1160,12 +1724,20 @@ export class Model {
|
|
|
1160
1724
|
return this.forceCreate(filterFillable(this, attributes));
|
|
1161
1725
|
}
|
|
1162
1726
|
/** `Model::forceCreate` — insert every given attribute, ignoring `$fillable`. */
|
|
1163
|
-
static forceCreate(useAttrs) {
|
|
1727
|
+
static forceCreate(useAttrs, beforeSave) {
|
|
1164
1728
|
this.bootIfNotBooted();
|
|
1165
1729
|
// Assign raw attributes (skip get-casts) so mutators like Attribute.set can run on save.
|
|
1166
1730
|
const model = classHasNoCasts(this)
|
|
1167
1731
|
? new this(useAttrs)
|
|
1168
1732
|
: Object.assign(new this(), useAttrs);
|
|
1733
|
+
const pre = beforeSave?.(model);
|
|
1734
|
+
if (pre instanceof Promise) {
|
|
1735
|
+
return pre.then(() => this.forceCreateSaved(model));
|
|
1736
|
+
}
|
|
1737
|
+
return this.forceCreateSaved(model);
|
|
1738
|
+
}
|
|
1739
|
+
/** Persist a model built by {@link forceCreate} and re-hydrate get-casts. */
|
|
1740
|
+
static forceCreateSaved(model) {
|
|
1169
1741
|
const saved = model.save();
|
|
1170
1742
|
const after = (row) => {
|
|
1171
1743
|
if (classHasNoCasts(this)) {
|
|
@@ -1250,6 +1822,20 @@ export class Model {
|
|
|
1250
1822
|
assignOwn(this, ctor.castAttributes(finalAttrs, "get"));
|
|
1251
1823
|
return this;
|
|
1252
1824
|
}
|
|
1825
|
+
/** `$model->forceFill($attributes)` — assign without checking `fillable` / `guarded`. */
|
|
1826
|
+
forceFill(attributes) {
|
|
1827
|
+
const ctor = this.constructor;
|
|
1828
|
+
assignOwn(this, ctor.castAttributes({ ...attributes }, "get"));
|
|
1829
|
+
return this;
|
|
1830
|
+
}
|
|
1831
|
+
/** `$model->isFillable($key)`. */
|
|
1832
|
+
isFillable(key) {
|
|
1833
|
+
return canMassAssign(this.constructor, key);
|
|
1834
|
+
}
|
|
1835
|
+
/** `$model->isGuarded($key)`. */
|
|
1836
|
+
isGuarded(key) {
|
|
1837
|
+
return !this.isFillable(key);
|
|
1838
|
+
}
|
|
1253
1839
|
/** `$model->update($attributes)`. */
|
|
1254
1840
|
async update(attributes) {
|
|
1255
1841
|
this.fill(attributes);
|
|
@@ -1294,6 +1880,20 @@ export class Model {
|
|
|
1294
1880
|
});
|
|
1295
1881
|
return this.save();
|
|
1296
1882
|
}
|
|
1883
|
+
/**
|
|
1884
|
+
* Bump `updated_at` only when the model is dirty
|
|
1885
|
+
* and the caller did not set `updated_at` themselves. A no-op `save()` must
|
|
1886
|
+
* not issue an UPDATE.
|
|
1887
|
+
*/
|
|
1888
|
+
#stampUpdatedAt(now) {
|
|
1889
|
+
const dirty = this.getDirty();
|
|
1890
|
+
for (const _ in dirty) {
|
|
1891
|
+
if (!("updated_at" in dirty)) {
|
|
1892
|
+
this.updated_at = now;
|
|
1893
|
+
}
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1297
1897
|
#persistSync(conn) {
|
|
1298
1898
|
const ctor = this.constructor;
|
|
1299
1899
|
const key = ctor.primaryKey;
|
|
@@ -1306,16 +1906,39 @@ export class Model {
|
|
|
1306
1906
|
const usesTimestamps = ctor.timestamps !== false;
|
|
1307
1907
|
if (usesTimestamps) {
|
|
1308
1908
|
const now = nowForConnection(conn);
|
|
1309
|
-
row.updated_at = now;
|
|
1310
1909
|
if (!exists) {
|
|
1910
|
+
row.updated_at = now;
|
|
1311
1911
|
row.created_at = row.created_at ?? now;
|
|
1312
1912
|
}
|
|
1913
|
+
else {
|
|
1914
|
+
this.#stampUpdatedAt(now);
|
|
1915
|
+
}
|
|
1313
1916
|
}
|
|
1314
1917
|
if (!exists) {
|
|
1315
1918
|
const noCasts = classHasNoCasts(ctor);
|
|
1316
|
-
|
|
1919
|
+
let cachedCols = noCasts
|
|
1317
1920
|
? cachedSyncInsertColumns(ctor)
|
|
1318
1921
|
: null;
|
|
1922
|
+
// The cached column list is only valid when the row sets exactly those columns:
|
|
1923
|
+
// an unset fillable column must stay absent (so the DB default applies) and an
|
|
1924
|
+
// attribute outside `fillable` (forceFill, direct assignment) must still be saved.
|
|
1925
|
+
if (cachedCols) {
|
|
1926
|
+
let present = 0;
|
|
1927
|
+
for (let i = 0; i < cachedCols.length; i++) {
|
|
1928
|
+
if (row[cachedCols[i]] === undefined) {
|
|
1929
|
+
cachedCols = null;
|
|
1930
|
+
break;
|
|
1931
|
+
}
|
|
1932
|
+
present++;
|
|
1933
|
+
}
|
|
1934
|
+
if (cachedCols) {
|
|
1935
|
+
let own = 0;
|
|
1936
|
+
for (const _ in row)
|
|
1937
|
+
own++;
|
|
1938
|
+
if (own !== present)
|
|
1939
|
+
cachedCols = null;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1319
1942
|
let writeColumns;
|
|
1320
1943
|
let writeValues;
|
|
1321
1944
|
if (cachedCols) {
|
|
@@ -1380,6 +2003,7 @@ export class Model {
|
|
|
1380
2003
|
}
|
|
1381
2004
|
this.#original = original;
|
|
1382
2005
|
this.#changes = original;
|
|
2006
|
+
this.#previous = undefined;
|
|
1383
2007
|
this.#touchOwnersSync();
|
|
1384
2008
|
return this;
|
|
1385
2009
|
}
|
|
@@ -1395,6 +2019,7 @@ export class Model {
|
|
|
1395
2019
|
}
|
|
1396
2020
|
this.#wasRecentlyCreated = false;
|
|
1397
2021
|
this.#changes = dirty;
|
|
2022
|
+
this.#capturePrevious();
|
|
1398
2023
|
this.syncOriginal();
|
|
1399
2024
|
this.#touchOwnersSync();
|
|
1400
2025
|
return this;
|
|
@@ -1408,19 +2033,19 @@ export class Model {
|
|
|
1408
2033
|
if ((await fireModelEvent(this, "saving")) === false)
|
|
1409
2034
|
return this;
|
|
1410
2035
|
const usesTimestamps = ctor.timestamps !== false;
|
|
1411
|
-
if (usesTimestamps) {
|
|
1412
|
-
const now = nowForConnection(this.getConnection());
|
|
1413
|
-
row.updated_at = now;
|
|
1414
|
-
if (!exists) {
|
|
1415
|
-
row.created_at = row.created_at ?? now;
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
const dirty = this.getDirty();
|
|
1419
2036
|
const persistCtor = ctor;
|
|
1420
2037
|
const table = new Db(this.getConnection()).table(ctor.table);
|
|
2038
|
+
// Order: saving → creating/updating → timestamps → write → created/updated → saved.
|
|
2039
|
+
let dirty;
|
|
1421
2040
|
if (!exists) {
|
|
1422
2041
|
if ((await fireModelEvent(this, "creating")) === false)
|
|
1423
2042
|
return this;
|
|
2043
|
+
if (usesTimestamps) {
|
|
2044
|
+
const now = nowForConnection(this.getConnection());
|
|
2045
|
+
row.updated_at = now;
|
|
2046
|
+
row.created_at = row.created_at ?? now;
|
|
2047
|
+
}
|
|
2048
|
+
dirty = this.getDirty();
|
|
1424
2049
|
const attributes = persistableAttributes(persistCtor, row, ctor.incrementing !== false || row[key] == null ? key : undefined);
|
|
1425
2050
|
if (ctor.incrementing !== false) {
|
|
1426
2051
|
const newId = await withoutQueryWriteHook(() => table.insertGetId(ctor.castAttributes(attributes, "set")));
|
|
@@ -1435,28 +2060,34 @@ export class Model {
|
|
|
1435
2060
|
}
|
|
1436
2061
|
this.#exists = true;
|
|
1437
2062
|
this.#wasRecentlyCreated = true;
|
|
2063
|
+
this.#recordChanges(dirty, false, usesTimestamps);
|
|
1438
2064
|
await fireModelEvent(this, "created");
|
|
1439
2065
|
}
|
|
1440
2066
|
else {
|
|
1441
2067
|
if ((await fireModelEvent(this, "updating")) === false)
|
|
1442
2068
|
return this;
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
2069
|
+
if (usesTimestamps)
|
|
2070
|
+
this.#stampUpdatedAt(nowForConnection(this.getConnection()));
|
|
2071
|
+
dirty = this.getDirty();
|
|
2072
|
+
// `updated` fires only when something was written.
|
|
2073
|
+
if (Object.keys(dirty).length > 0) {
|
|
2074
|
+
const attributes = persistableAttributes(persistCtor, dirty, key);
|
|
2075
|
+
if (Object.keys(attributes).length > 0) {
|
|
2076
|
+
await withoutQueryWriteHook(() => table.where(key, id).update(ctor.castAttributes(attributes, "set")));
|
|
2077
|
+
}
|
|
2078
|
+
this.#wasRecentlyCreated = false;
|
|
2079
|
+
this.#recordChanges(dirty, true, usesTimestamps);
|
|
2080
|
+
await fireModelEvent(this, "updated");
|
|
1446
2081
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
this.#changes = { ...dirty };
|
|
1451
|
-
if (usesTimestamps) {
|
|
1452
|
-
if ("updated_at" in row)
|
|
1453
|
-
this.#changes.updated_at = row.updated_at;
|
|
1454
|
-
if (!exists && "created_at" in row) {
|
|
1455
|
-
this.#changes.created_at = row.created_at;
|
|
2082
|
+
else {
|
|
2083
|
+
this.#wasRecentlyCreated = false;
|
|
2084
|
+
this.#recordChanges(dirty, true, usesTimestamps);
|
|
1456
2085
|
}
|
|
1457
2086
|
}
|
|
1458
|
-
|
|
2087
|
+
// `saved` fires first, then the original set is synced,
|
|
2088
|
+
// so `isDirty()` / `getOriginal()` still describe the save inside `updated` and `saved`.
|
|
1459
2089
|
await fireModelEvent(this, "saved");
|
|
2090
|
+
this.syncOriginal();
|
|
1460
2091
|
// finishSave → touchOwners (Relation.touch honors withoutTouching on related).
|
|
1461
2092
|
await this.touchOwners();
|
|
1462
2093
|
return this;
|
|
@@ -1475,6 +2106,8 @@ export class Model {
|
|
|
1475
2106
|
if (fresh) {
|
|
1476
2107
|
Object.assign(this, fresh.getAttributes());
|
|
1477
2108
|
this.#original = { ...fresh.#original };
|
|
2109
|
+
this.#rawRow = fresh.#rawRow;
|
|
2110
|
+
this.#snapshots = fresh.#snapshots;
|
|
1478
2111
|
this.#exists = true;
|
|
1479
2112
|
}
|
|
1480
2113
|
return this;
|
|
@@ -1494,6 +2127,8 @@ export class Model {
|
|
|
1494
2127
|
if (fresh) {
|
|
1495
2128
|
Object.assign(this, fresh.getAttributes());
|
|
1496
2129
|
this.#original = { ...fresh.#original };
|
|
2130
|
+
this.#rawRow = fresh.#rawRow;
|
|
2131
|
+
this.#snapshots = fresh.#snapshots;
|
|
1497
2132
|
this.#exists = true;
|
|
1498
2133
|
}
|
|
1499
2134
|
return this;
|
|
@@ -1645,8 +2280,8 @@ export class Model {
|
|
|
1645
2280
|
return;
|
|
1646
2281
|
if (usesSoftDeletes(ctor)) {
|
|
1647
2282
|
await this.#performSoftDelete();
|
|
1648
|
-
await fireModelEvent(this, "deleted");
|
|
1649
2283
|
await fireModelEvent(this, "trashed");
|
|
2284
|
+
await fireModelEvent(this, "deleted");
|
|
1650
2285
|
return;
|
|
1651
2286
|
}
|
|
1652
2287
|
// delete(): touchOwners before performDeleteOnModel.
|
|
@@ -1676,9 +2311,12 @@ export class Model {
|
|
|
1676
2311
|
ctor.bootIfNotBooted();
|
|
1677
2312
|
if ((await fireModelEvent(this, "forceDeleting")) === false)
|
|
1678
2313
|
return;
|
|
2314
|
+
if ((await fireModelEvent(this, "deleting")) === false)
|
|
2315
|
+
return;
|
|
2316
|
+
await this.touchOwners();
|
|
1679
2317
|
await this.#performForceDelete();
|
|
1680
|
-
await fireModelEvent(this, "forceDeleted");
|
|
1681
2318
|
await fireModelEvent(this, "deleted");
|
|
2319
|
+
await fireModelEvent(this, "forceDeleted");
|
|
1682
2320
|
}
|
|
1683
2321
|
/** `$model->deleteQuietly()`. */
|
|
1684
2322
|
async deleteQuietly() {
|
|
@@ -1739,7 +2377,8 @@ export class Model {
|
|
|
1739
2377
|
continue;
|
|
1740
2378
|
if (visible && !visible.has(key))
|
|
1741
2379
|
continue;
|
|
1742
|
-
out[key] =
|
|
2380
|
+
out[key] =
|
|
2381
|
+
value instanceof Date ? serializeDate(ctor, key, value) : value;
|
|
1743
2382
|
}
|
|
1744
2383
|
for (const key of appends) {
|
|
1745
2384
|
if (hidden.has(key))
|
|
@@ -1808,8 +2447,14 @@ export class Model {
|
|
|
1808
2447
|
}
|
|
1809
2448
|
return this;
|
|
1810
2449
|
}
|
|
2450
|
+
/** `toArray()` with `bigint` values as decimal strings, since JSON has no `bigint`. */
|
|
1811
2451
|
toJSON() {
|
|
1812
|
-
|
|
2452
|
+
const out = this.toArray();
|
|
2453
|
+
for (const key in out) {
|
|
2454
|
+
if (typeof out[key] === "bigint")
|
|
2455
|
+
out[key] = String(out[key]);
|
|
2456
|
+
}
|
|
2457
|
+
return out;
|
|
1813
2458
|
}
|
|
1814
2459
|
/** Alias of {@link toJSON}. */
|
|
1815
2460
|
toJson() {
|
|
@@ -1919,6 +2564,14 @@ export class Model {
|
|
|
1919
2564
|
: [name, primaryMorph].filter((value, index, all) => all.indexOf(value) === index);
|
|
1920
2565
|
return new MorphToMany(this, related, table, foreign, relatedKey, morphTypeColumn, morphTypes, options.pivotTenantKey, options.parentTenantKey ?? options.pivotTenantKey);
|
|
1921
2566
|
}
|
|
2567
|
+
/**
|
|
2568
|
+
* `$this->morphedByMany(Related::class, name)` — inverse of `morphToMany`.
|
|
2569
|
+
* Defaults: pivot `{name}s`, `{name}_id`, `{name}_type`, parent key `{parent}_id`.
|
|
2570
|
+
*/
|
|
2571
|
+
morphedByMany(related, name, table, foreignPivotKey, relatedPivotKey) {
|
|
2572
|
+
const parent = this.constructor;
|
|
2573
|
+
return new MorphedByMany(this, related, table ?? `${name}s`, foreignPivotKey ?? `${singular(parent.table)}_id`, relatedPivotKey ?? `${name}_id`, `${name}_type`, morphTypeFor(related));
|
|
2574
|
+
}
|
|
1922
2575
|
/** `$this->morphMany(Related::class, name)`. */
|
|
1923
2576
|
morphMany(related, name, type, id, localKey) {
|
|
1924
2577
|
return new MorphMany(this, related, type ?? `${name}_type`, id ?? `${name}_id`, morphTypeFor(this.constructor), localKey);
|
|
@@ -1932,6 +2585,17 @@ export class Model {
|
|
|
1932
2585
|
return new MorphTo(this, type ?? `${name}_type`, id ?? `${name}_id`, ownerKey);
|
|
1933
2586
|
}
|
|
1934
2587
|
}
|
|
2588
|
+
/** ISO by default; `date:Y-m-d` / `datetime:FORMAT` casts format the serialized value. */
|
|
2589
|
+
function serializeDate(ctor, key, value) {
|
|
2590
|
+
const definition = ctor.getCasts()[key];
|
|
2591
|
+
if (typeof definition === "string") {
|
|
2592
|
+
const arg = parseCast(definition)?.arg;
|
|
2593
|
+
if (arg !== undefined && definition !== "decimal" && !definition.startsWith("decimal:")) {
|
|
2594
|
+
return formatPhpDate(value, arg);
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
return value.toISOString();
|
|
2598
|
+
}
|
|
1935
2599
|
installLocalScopeCallStatic(Model);
|
|
1936
2600
|
/** True when `value` is a Model subclass constructor (O(1) prototype check). */
|
|
1937
2601
|
export function isModelCtor(value) {
|
|
@@ -1941,7 +2605,7 @@ export function isModelCtor(value) {
|
|
|
1941
2605
|
value.prototype instanceof Model);
|
|
1942
2606
|
}
|
|
1943
2607
|
// Re-exports — public API stays on ./model.ts for index.ts
|
|
1944
|
-
export { BelongsTo, BelongsToMany, HasMany, HasManyThrough, HasOne, HasOneThrough, MorphMany, MorphOne, MorphTo, MorphToMany, clearMorphMap, morphMap, } from "./relations.js";
|
|
2608
|
+
export { BelongsTo, BelongsToMany, HasMany, HasManyThrough, HasOne, HasOneThrough, MorphMany, MorphOne, MorphTo, MorphToMany, MorphedByMany, clearMorphMap, morphMap, enforceMorphMap, requireMorphMap, getMorphedModel, } from "./relations.js";
|
|
1945
2609
|
export { eagerLoadAggregates, eagerLoadModels, } from "./eager.js";
|
|
1946
2610
|
export { ModelQuery } from "./model-query.js";
|
|
1947
2611
|
export { LazyLoadingViolationException, MassAssignmentException, MissingAttributeException, resetModelStrictnessForTests, } from "./model-strictness.js";
|