@bunyad/orm 0.2.0-beta.0 → 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 +23 -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 +85 -33
- package/dist/model-query.js +295 -139
- package/dist/model.d.ts +173 -35
- package/dist/model.js +725 -45
- 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
|
-
import { Collection } from "@bunyad/common";
|
|
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() {
|
|
@@ -1138,18 +1702,42 @@ export class Model {
|
|
|
1138
1702
|
static async cursorPaginate(perPage = 15, cursor = null, options = {}) {
|
|
1139
1703
|
return this.newQuery().cursorPaginate(perPage, cursor, options);
|
|
1140
1704
|
}
|
|
1705
|
+
/** `Model::cursor()`: stream models from a single query. */
|
|
1706
|
+
static cursor(chunkSize) {
|
|
1707
|
+
return this.newQuery().cursor(chunkSize);
|
|
1708
|
+
}
|
|
1709
|
+
/** `Model::lazy()`: chunked iteration, one query per chunk. */
|
|
1710
|
+
static lazy(chunkSize) {
|
|
1711
|
+
return this.newQuery().lazy(chunkSize);
|
|
1712
|
+
}
|
|
1713
|
+
/** `Model::lazyById()`: chunked iteration by primary key (safe while updating). */
|
|
1714
|
+
static lazyById(chunkSize, column) {
|
|
1715
|
+
return this.newQuery().lazyById(chunkSize, column);
|
|
1716
|
+
}
|
|
1717
|
+
/** `Model::lazyByIdDesc()`. */
|
|
1718
|
+
static lazyByIdDesc(chunkSize, column) {
|
|
1719
|
+
return this.newQuery().lazyByIdDesc(chunkSize, column);
|
|
1720
|
+
}
|
|
1141
1721
|
/** `Model::create` (respects `$fillable` / `$guarded`). */
|
|
1142
1722
|
static create(attributes) {
|
|
1143
1723
|
this.bootIfNotBooted();
|
|
1144
1724
|
return this.forceCreate(filterFillable(this, attributes));
|
|
1145
1725
|
}
|
|
1146
1726
|
/** `Model::forceCreate` — insert every given attribute, ignoring `$fillable`. */
|
|
1147
|
-
static forceCreate(useAttrs) {
|
|
1727
|
+
static forceCreate(useAttrs, beforeSave) {
|
|
1148
1728
|
this.bootIfNotBooted();
|
|
1149
1729
|
// Assign raw attributes (skip get-casts) so mutators like Attribute.set can run on save.
|
|
1150
1730
|
const model = classHasNoCasts(this)
|
|
1151
1731
|
? new this(useAttrs)
|
|
1152
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) {
|
|
1153
1741
|
const saved = model.save();
|
|
1154
1742
|
const after = (row) => {
|
|
1155
1743
|
if (classHasNoCasts(this)) {
|
|
@@ -1234,6 +1822,20 @@ export class Model {
|
|
|
1234
1822
|
assignOwn(this, ctor.castAttributes(finalAttrs, "get"));
|
|
1235
1823
|
return this;
|
|
1236
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
|
+
}
|
|
1237
1839
|
/** `$model->update($attributes)`. */
|
|
1238
1840
|
async update(attributes) {
|
|
1239
1841
|
this.fill(attributes);
|
|
@@ -1278,6 +1880,20 @@ export class Model {
|
|
|
1278
1880
|
});
|
|
1279
1881
|
return this.save();
|
|
1280
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
|
+
}
|
|
1281
1897
|
#persistSync(conn) {
|
|
1282
1898
|
const ctor = this.constructor;
|
|
1283
1899
|
const key = ctor.primaryKey;
|
|
@@ -1290,16 +1906,39 @@ export class Model {
|
|
|
1290
1906
|
const usesTimestamps = ctor.timestamps !== false;
|
|
1291
1907
|
if (usesTimestamps) {
|
|
1292
1908
|
const now = nowForConnection(conn);
|
|
1293
|
-
row.updated_at = now;
|
|
1294
1909
|
if (!exists) {
|
|
1910
|
+
row.updated_at = now;
|
|
1295
1911
|
row.created_at = row.created_at ?? now;
|
|
1296
1912
|
}
|
|
1913
|
+
else {
|
|
1914
|
+
this.#stampUpdatedAt(now);
|
|
1915
|
+
}
|
|
1297
1916
|
}
|
|
1298
1917
|
if (!exists) {
|
|
1299
1918
|
const noCasts = classHasNoCasts(ctor);
|
|
1300
|
-
|
|
1919
|
+
let cachedCols = noCasts
|
|
1301
1920
|
? cachedSyncInsertColumns(ctor)
|
|
1302
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
|
+
}
|
|
1303
1942
|
let writeColumns;
|
|
1304
1943
|
let writeValues;
|
|
1305
1944
|
if (cachedCols) {
|
|
@@ -1364,6 +2003,7 @@ export class Model {
|
|
|
1364
2003
|
}
|
|
1365
2004
|
this.#original = original;
|
|
1366
2005
|
this.#changes = original;
|
|
2006
|
+
this.#previous = undefined;
|
|
1367
2007
|
this.#touchOwnersSync();
|
|
1368
2008
|
return this;
|
|
1369
2009
|
}
|
|
@@ -1379,6 +2019,7 @@ export class Model {
|
|
|
1379
2019
|
}
|
|
1380
2020
|
this.#wasRecentlyCreated = false;
|
|
1381
2021
|
this.#changes = dirty;
|
|
2022
|
+
this.#capturePrevious();
|
|
1382
2023
|
this.syncOriginal();
|
|
1383
2024
|
this.#touchOwnersSync();
|
|
1384
2025
|
return this;
|
|
@@ -1392,19 +2033,19 @@ export class Model {
|
|
|
1392
2033
|
if ((await fireModelEvent(this, "saving")) === false)
|
|
1393
2034
|
return this;
|
|
1394
2035
|
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
2036
|
const persistCtor = ctor;
|
|
1404
2037
|
const table = new Db(this.getConnection()).table(ctor.table);
|
|
2038
|
+
// Order: saving → creating/updating → timestamps → write → created/updated → saved.
|
|
2039
|
+
let dirty;
|
|
1405
2040
|
if (!exists) {
|
|
1406
2041
|
if ((await fireModelEvent(this, "creating")) === false)
|
|
1407
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();
|
|
1408
2049
|
const attributes = persistableAttributes(persistCtor, row, ctor.incrementing !== false || row[key] == null ? key : undefined);
|
|
1409
2050
|
if (ctor.incrementing !== false) {
|
|
1410
2051
|
const newId = await withoutQueryWriteHook(() => table.insertGetId(ctor.castAttributes(attributes, "set")));
|
|
@@ -1419,28 +2060,34 @@ export class Model {
|
|
|
1419
2060
|
}
|
|
1420
2061
|
this.#exists = true;
|
|
1421
2062
|
this.#wasRecentlyCreated = true;
|
|
2063
|
+
this.#recordChanges(dirty, false, usesTimestamps);
|
|
1422
2064
|
await fireModelEvent(this, "created");
|
|
1423
2065
|
}
|
|
1424
2066
|
else {
|
|
1425
2067
|
if ((await fireModelEvent(this, "updating")) === false)
|
|
1426
2068
|
return this;
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
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");
|
|
1430
2081
|
}
|
|
1431
|
-
|
|
1432
|
-
|
|
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;
|
|
2082
|
+
else {
|
|
2083
|
+
this.#wasRecentlyCreated = false;
|
|
2084
|
+
this.#recordChanges(dirty, true, usesTimestamps);
|
|
1440
2085
|
}
|
|
1441
2086
|
}
|
|
1442
|
-
|
|
2087
|
+
// `saved` fires first, then the original set is synced,
|
|
2088
|
+
// so `isDirty()` / `getOriginal()` still describe the save inside `updated` and `saved`.
|
|
1443
2089
|
await fireModelEvent(this, "saved");
|
|
2090
|
+
this.syncOriginal();
|
|
1444
2091
|
// finishSave → touchOwners (Relation.touch honors withoutTouching on related).
|
|
1445
2092
|
await this.touchOwners();
|
|
1446
2093
|
return this;
|
|
@@ -1459,6 +2106,8 @@ export class Model {
|
|
|
1459
2106
|
if (fresh) {
|
|
1460
2107
|
Object.assign(this, fresh.getAttributes());
|
|
1461
2108
|
this.#original = { ...fresh.#original };
|
|
2109
|
+
this.#rawRow = fresh.#rawRow;
|
|
2110
|
+
this.#snapshots = fresh.#snapshots;
|
|
1462
2111
|
this.#exists = true;
|
|
1463
2112
|
}
|
|
1464
2113
|
return this;
|
|
@@ -1478,6 +2127,8 @@ export class Model {
|
|
|
1478
2127
|
if (fresh) {
|
|
1479
2128
|
Object.assign(this, fresh.getAttributes());
|
|
1480
2129
|
this.#original = { ...fresh.#original };
|
|
2130
|
+
this.#rawRow = fresh.#rawRow;
|
|
2131
|
+
this.#snapshots = fresh.#snapshots;
|
|
1481
2132
|
this.#exists = true;
|
|
1482
2133
|
}
|
|
1483
2134
|
return this;
|
|
@@ -1629,8 +2280,8 @@ export class Model {
|
|
|
1629
2280
|
return;
|
|
1630
2281
|
if (usesSoftDeletes(ctor)) {
|
|
1631
2282
|
await this.#performSoftDelete();
|
|
1632
|
-
await fireModelEvent(this, "deleted");
|
|
1633
2283
|
await fireModelEvent(this, "trashed");
|
|
2284
|
+
await fireModelEvent(this, "deleted");
|
|
1634
2285
|
return;
|
|
1635
2286
|
}
|
|
1636
2287
|
// delete(): touchOwners before performDeleteOnModel.
|
|
@@ -1660,9 +2311,12 @@ export class Model {
|
|
|
1660
2311
|
ctor.bootIfNotBooted();
|
|
1661
2312
|
if ((await fireModelEvent(this, "forceDeleting")) === false)
|
|
1662
2313
|
return;
|
|
2314
|
+
if ((await fireModelEvent(this, "deleting")) === false)
|
|
2315
|
+
return;
|
|
2316
|
+
await this.touchOwners();
|
|
1663
2317
|
await this.#performForceDelete();
|
|
1664
|
-
await fireModelEvent(this, "forceDeleted");
|
|
1665
2318
|
await fireModelEvent(this, "deleted");
|
|
2319
|
+
await fireModelEvent(this, "forceDeleted");
|
|
1666
2320
|
}
|
|
1667
2321
|
/** `$model->deleteQuietly()`. */
|
|
1668
2322
|
async deleteQuietly() {
|
|
@@ -1723,7 +2377,8 @@ export class Model {
|
|
|
1723
2377
|
continue;
|
|
1724
2378
|
if (visible && !visible.has(key))
|
|
1725
2379
|
continue;
|
|
1726
|
-
out[key] =
|
|
2380
|
+
out[key] =
|
|
2381
|
+
value instanceof Date ? serializeDate(ctor, key, value) : value;
|
|
1727
2382
|
}
|
|
1728
2383
|
for (const key of appends) {
|
|
1729
2384
|
if (hidden.has(key))
|
|
@@ -1792,8 +2447,14 @@ export class Model {
|
|
|
1792
2447
|
}
|
|
1793
2448
|
return this;
|
|
1794
2449
|
}
|
|
2450
|
+
/** `toArray()` with `bigint` values as decimal strings, since JSON has no `bigint`. */
|
|
1795
2451
|
toJSON() {
|
|
1796
|
-
|
|
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;
|
|
1797
2458
|
}
|
|
1798
2459
|
/** Alias of {@link toJSON}. */
|
|
1799
2460
|
toJson() {
|
|
@@ -1903,6 +2564,14 @@ export class Model {
|
|
|
1903
2564
|
: [name, primaryMorph].filter((value, index, all) => all.indexOf(value) === index);
|
|
1904
2565
|
return new MorphToMany(this, related, table, foreign, relatedKey, morphTypeColumn, morphTypes, options.pivotTenantKey, options.parentTenantKey ?? options.pivotTenantKey);
|
|
1905
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
|
+
}
|
|
1906
2575
|
/** `$this->morphMany(Related::class, name)`. */
|
|
1907
2576
|
morphMany(related, name, type, id, localKey) {
|
|
1908
2577
|
return new MorphMany(this, related, type ?? `${name}_type`, id ?? `${name}_id`, morphTypeFor(this.constructor), localKey);
|
|
@@ -1916,6 +2585,17 @@ export class Model {
|
|
|
1916
2585
|
return new MorphTo(this, type ?? `${name}_type`, id ?? `${name}_id`, ownerKey);
|
|
1917
2586
|
}
|
|
1918
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
|
+
}
|
|
1919
2599
|
installLocalScopeCallStatic(Model);
|
|
1920
2600
|
/** True when `value` is a Model subclass constructor (O(1) prototype check). */
|
|
1921
2601
|
export function isModelCtor(value) {
|
|
@@ -1925,7 +2605,7 @@ export function isModelCtor(value) {
|
|
|
1925
2605
|
value.prototype instanceof Model);
|
|
1926
2606
|
}
|
|
1927
2607
|
// 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";
|
|
2608
|
+
export { BelongsTo, BelongsToMany, HasMany, HasManyThrough, HasOne, HasOneThrough, MorphMany, MorphOne, MorphTo, MorphToMany, MorphedByMany, clearMorphMap, morphMap, enforceMorphMap, requireMorphMap, getMorphedModel, } from "./relations.js";
|
|
1929
2609
|
export { eagerLoadAggregates, eagerLoadModels, } from "./eager.js";
|
|
1930
2610
|
export { ModelQuery } from "./model-query.js";
|
|
1931
2611
|
export { LazyLoadingViolationException, MassAssignmentException, MissingAttributeException, resetModelStrictnessForTests, } from "./model-strictness.js";
|