@zerotal/orm 1.14.3 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/api-surface.md +7 -4
- package/package.json +3 -3
- package/src/model/BaseModel.ts +26 -8
- package/src/model/SoftDeletes.ts +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,27 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.15.0] — 2026-09-04
|
|
12
|
+
|
|
13
|
+
### Changed — **BREAKING**
|
|
14
|
+
|
|
15
|
+
- **BREAKING: `createdAt`, `updatedAt` and `deletedAt` are `Carbon`, not `Date`.** Every
|
|
16
|
+
`datetime` column hydrates as `Carbon`, these three included — so the declared type was
|
|
17
|
+
wrong on a freshly loaded row, and `order.createdAt.getTime()` type-checked and threw,
|
|
18
|
+
because `Carbon` has `toISOString()` and `valueOf()` but no `getTime()`.
|
|
19
|
+
|
|
20
|
+
Worse, the runtime class depended on how you got there: `Carbon` after a load, a raw
|
|
21
|
+
`Date` after a `save()`, a `touch()`, or a soft `delete()` wrote one straight back onto
|
|
22
|
+
the instance. The same property on the same model, matching its declared type in neither
|
|
23
|
+
case, and with no `@column` declaration to check the truth against.
|
|
24
|
+
|
|
25
|
+
All three are now `Carbon` everywhere, declared and at runtime. Code calling a `Date`
|
|
26
|
+
method on them was already failing at runtime and now fails to compile; `.toISOString()`
|
|
27
|
+
gives a string, `.valueOf()` epoch milliseconds, and `.toDate()` a native `Date`.
|
|
28
|
+
|
|
29
|
+
Compile and follow the errors — every one is a line that would have thrown against a
|
|
30
|
+
loaded row. See the [Upgrade Guide](/docs/upgrade#114-to-115).
|
|
31
|
+
|
|
11
32
|
## [1.12.0] — 2026-08-31
|
|
12
33
|
|
|
13
34
|
### Changed — **BREAKING**
|
package/api-surface.md
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
<!-- AUTO-GENERATED by scripts/api-surface.ts. Do not edit by hand.
|
|
4
4
|
Run `bun run api:surface` to regenerate after an intentional API change. -->
|
|
5
5
|
|
|
6
|
+
> Not a guide — this is a flat, alphabetical snapshot used to detect unintended
|
|
7
|
+
> API changes. For how to use these, read the [documentation](https://zerotal.dev/docs).
|
|
8
|
+
|
|
6
9
|
## . `(./src/index.ts)`
|
|
7
10
|
|
|
8
11
|
class ArrayCast = {
|
|
@@ -78,7 +81,7 @@ class BaseModel = {
|
|
|
78
81
|
$dirty: () => Record<string, unknown>
|
|
79
82
|
append: (...keys: string[]) => BaseModel
|
|
80
83
|
associate: (relation: string, model: BaseModel) => BaseModel
|
|
81
|
-
createdAt?:
|
|
84
|
+
createdAt?: Carbon
|
|
82
85
|
decrement: (column: 'id' | 'createdAt' | 'updatedAt' | '__isZerotalModel' | 'fill' | 'forceFill' | 'save' | 'delete' | 'load' | 'loadMissing' | 'fresh' | 'refresh' | 'replicate' | 'touch' | 'is' | 'isNot' | 'increment' | 'decrement' | 'loadCount' | 'loadSum' | 'loadAvg' | 'loadMin' | 'loadMax' | 'makeHidden' | 'makeVisible' | 'append' | 'associate' | 'dissociate' | 'isDirty' | '$dirty' | 'markDirty' | 'toJSON', amount?: number) => Promise<BaseModel>
|
|
83
86
|
delete: () => Promise<void>
|
|
84
87
|
dissociate: (relation: string) => BaseModel
|
|
@@ -106,7 +109,7 @@ class BaseModel = {
|
|
|
106
109
|
save: () => Promise<BaseModel>
|
|
107
110
|
toJSON: () => Record<string, unknown>
|
|
108
111
|
touch: () => Promise<BaseModel>
|
|
109
|
-
updatedAt?:
|
|
112
|
+
updatedAt?: Carbon
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
class Blueprint = {
|
|
@@ -392,7 +395,7 @@ class Model = {
|
|
|
392
395
|
$dirty: () => Record<string, unknown>
|
|
393
396
|
append: (...keys: string[]) => BaseModel
|
|
394
397
|
associate: (relation: string, model: BaseModel) => BaseModel
|
|
395
|
-
createdAt?:
|
|
398
|
+
createdAt?: Carbon
|
|
396
399
|
decrement: (column: 'id' | 'createdAt' | 'updatedAt' | '__isZerotalModel' | 'fill' | 'forceFill' | 'save' | 'delete' | 'load' | 'loadMissing' | 'fresh' | 'refresh' | 'replicate' | 'touch' | 'is' | 'isNot' | 'increment' | 'decrement' | 'loadCount' | 'loadSum' | 'loadAvg' | 'loadMin' | 'loadMax' | 'makeHidden' | 'makeVisible' | 'append' | 'associate' | 'dissociate' | 'isDirty' | '$dirty' | 'markDirty' | 'toJSON', amount?: number) => Promise<BaseModel>
|
|
397
400
|
delete: () => Promise<void>
|
|
398
401
|
dissociate: (relation: string) => BaseModel
|
|
@@ -420,7 +423,7 @@ class Model = {
|
|
|
420
423
|
save: () => Promise<BaseModel>
|
|
421
424
|
toJSON: () => Record<string, unknown>
|
|
422
425
|
touch: () => Promise<BaseModel>
|
|
423
|
-
updatedAt?:
|
|
426
|
+
updatedAt?: Carbon
|
|
424
427
|
}
|
|
425
428
|
|
|
426
429
|
class ModelChanged = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/orm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"typecheck": "tsc --noEmit"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@zerotal/core": "1.
|
|
35
|
-
"@zerotal/validator": "1.
|
|
34
|
+
"@zerotal/core": "1.15.0",
|
|
35
|
+
"@zerotal/validator": "1.15.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"typescript": "^5.8.0"
|
package/src/model/BaseModel.ts
CHANGED
|
@@ -930,16 +930,30 @@ export class BaseModel {
|
|
|
930
930
|
/**
|
|
931
931
|
* Creation timestamp, set on insert when {@link timestamps} is enabled.
|
|
932
932
|
*
|
|
933
|
+
* A {@link Carbon}, like every other `datetime` column — not a `Date`. These
|
|
934
|
+
* two were declared as `Date` while hydration produced a `Carbon`, so
|
|
935
|
+
* `order.createdAt.getTime()` type-checked and threw, and `.slice(0, 10)` on
|
|
936
|
+
* a value assumed to be an ISO string threw differently. Neither has a column
|
|
937
|
+
* declaration in the model to read the truth off, which is exactly why the
|
|
938
|
+
* declaration here has to be right.
|
|
939
|
+
*
|
|
940
|
+
* Use `.toISOString()` for a string, `.valueOf()` for epoch milliseconds, and
|
|
941
|
+
* `.toDate()` when something really needs a native `Date`.
|
|
942
|
+
*
|
|
933
943
|
* @category Timestamps
|
|
934
944
|
*/
|
|
935
|
-
createdAt?:
|
|
945
|
+
createdAt?: Carbon;
|
|
936
946
|
|
|
937
947
|
/**
|
|
938
948
|
* Last-update timestamp, bumped on every save when {@link timestamps} is enabled.
|
|
939
949
|
*
|
|
950
|
+
* A {@link Carbon} — see {@link createdAt}. It used to depend on how you got
|
|
951
|
+
* here: a `Carbon` on a freshly loaded row, a raw `Date` on one you had just
|
|
952
|
+
* saved, from the same property on the same model.
|
|
953
|
+
*
|
|
940
954
|
* @category Timestamps
|
|
941
955
|
*/
|
|
942
|
-
updatedAt?:
|
|
956
|
+
updatedAt?: Carbon;
|
|
943
957
|
|
|
944
958
|
/** @internal Snapshot of attribute values at load/last-save, for dirty tracking. */
|
|
945
959
|
private _original: Record<string, unknown> = {};
|
|
@@ -1849,7 +1863,10 @@ export class BaseModel {
|
|
|
1849
1863
|
await HookRegistry.run(ModelClass, "beforeUpdate", this);
|
|
1850
1864
|
|
|
1851
1865
|
const dirty = this.$dirty();
|
|
1852
|
-
|
|
1866
|
+
// Carbon, not `new Date()`. The entries below are written back onto the
|
|
1867
|
+
// instance, so a plain Date here left `updatedAt` holding a different class
|
|
1868
|
+
// after a save than after a load — with the declared type matching neither.
|
|
1869
|
+
if (ModelClass.timestamps) dirty["updatedAt"] = Carbon.now();
|
|
1853
1870
|
|
|
1854
1871
|
if (Object.keys(dirty).length > 0) {
|
|
1855
1872
|
const entries = _writeDialect.run(dialect, () =>
|
|
@@ -1900,16 +1917,17 @@ export class BaseModel {
|
|
|
1900
1917
|
await HookRegistry.run(ModelClass, "beforeDelete", this);
|
|
1901
1918
|
|
|
1902
1919
|
if (ModelClass.softDeletes) {
|
|
1903
|
-
const now =
|
|
1920
|
+
const now = Carbon.now();
|
|
1904
1921
|
await runSegs(conn, [
|
|
1905
1922
|
`UPDATE ${ModelClass.table} SET deleted_at = `,
|
|
1906
|
-
{ val: _writeDialect.run(dialect, () => _serializeDate(now)) },
|
|
1923
|
+
{ val: _writeDialect.run(dialect, () => _serializeDate(now.toDate())) },
|
|
1907
1924
|
` WHERE ${ModelClass.primaryKey} = `,
|
|
1908
1925
|
{ val: this.id },
|
|
1909
1926
|
]);
|
|
1910
1927
|
// `deletedAt` lives on the SoftDeletes mixin; this branch only runs for models
|
|
1911
|
-
// that compose it (softDeletes === true).
|
|
1912
|
-
|
|
1928
|
+
// that compose it (softDeletes === true). Carbon, to match what a reload of
|
|
1929
|
+
// this same row would put there.
|
|
1930
|
+
(this as { deletedAt?: Carbon | null }).deletedAt = now;
|
|
1913
1931
|
} else {
|
|
1914
1932
|
await runSegs(conn, [
|
|
1915
1933
|
`DELETE FROM ${ModelClass.table} WHERE ${ModelClass.primaryKey} = `,
|
|
@@ -2028,7 +2046,7 @@ export class BaseModel {
|
|
|
2028
2046
|
async touch(): Promise<this> {
|
|
2029
2047
|
const ModelClass = this.constructor as typeof BaseModel;
|
|
2030
2048
|
if (!ModelClass.timestamps) return this;
|
|
2031
|
-
this.updatedAt =
|
|
2049
|
+
this.updatedAt = Carbon.now();
|
|
2032
2050
|
this.markDirty("updatedAt" as keyof this);
|
|
2033
2051
|
return this.save();
|
|
2034
2052
|
}
|
package/src/model/SoftDeletes.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { _resolveConn, type BaseModel } from "./BaseModel.ts";
|
|
|
24
24
|
import { ModelQueryBuilder } from "./ModelQueryBuilder.ts";
|
|
25
25
|
import { QueryBuilder } from "../db/QueryBuilder.ts";
|
|
26
26
|
import type { Constructor } from "./mixins.ts";
|
|
27
|
+
import type { Carbon } from "@zerotal/core/carbon";
|
|
27
28
|
|
|
28
29
|
// Structural view of the concrete model class used by the static scopes.
|
|
29
30
|
interface SoftDeleteModelClass<T extends BaseModel> {
|
|
@@ -63,8 +64,13 @@ export function SoftDeletes<TBase extends Constructor>(Base: TBase) {
|
|
|
63
64
|
/** Engine switch — query scoping + schema `deleted_at` provisioning read this. */
|
|
64
65
|
static softDeletes = true;
|
|
65
66
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
67
|
+
/**
|
|
68
|
+
* When the row was soft-deleted, or null/undefined when live.
|
|
69
|
+
*
|
|
70
|
+
* A {@link Carbon}, like `createdAt`/`updatedAt` and every other `datetime`
|
|
71
|
+
* column — `.toISOString()` for a string, `.toDate()` for a native `Date`.
|
|
72
|
+
*/
|
|
73
|
+
declare deletedAt?: Carbon | null;
|
|
68
74
|
|
|
69
75
|
/** A query that INCLUDES soft-deleted rows (bypasses the default scope). */
|
|
70
76
|
static withTrashed<T extends BaseModel>(this: SoftDeleteModelClass<T>): ModelQueryBuilder<T> {
|