@zerotal/orm 1.11.2 → 1.13.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 +31 -0
- package/api-surface.md +7 -0
- package/package.json +3 -3
- package/src/errors/ColumnTypeError.ts +39 -0
- package/src/errors/index.ts +1 -0
- package/src/index.ts +1 -0
- package/src/model/BaseModel.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,37 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.12.0] — 2026-08-31
|
|
12
|
+
|
|
13
|
+
### Changed — **BREAKING**
|
|
14
|
+
|
|
15
|
+
- **A boolean written to a column declared to hold text is refused.** A bare
|
|
16
|
+
`@column()` resolves to `{ type: "string" }`, so a boolean property decorated with
|
|
17
|
+
one was stored as text — and a text column has text affinity, so `false` was stored
|
|
18
|
+
as `"0"`, which is truthy in JavaScript. Every `if (model.flag)` took the wrong
|
|
19
|
+
branch for a stored `false`, on every row, with nothing in the app or the database
|
|
20
|
+
registering a fault. An app found it when a feature flag read as enabled for every
|
|
21
|
+
record that had it turned off.
|
|
22
|
+
|
|
23
|
+
There is no correct coercion — `0` becomes `"0"` and `"false"` is truthy too — so
|
|
24
|
+
the value cannot survive the round trip and the write raises `ColumnTypeError`
|
|
25
|
+
instead, naming the property and the fix: `@column("boolean")`.
|
|
26
|
+
|
|
27
|
+
The decorator cannot infer it for you. `declare active: boolean` erases the
|
|
28
|
+
TypeScript type at runtime, so a bare `@column()` on a boolean is indistinguishable
|
|
29
|
+
from one on a string until a value arrives. Declaring the type is the only signal
|
|
30
|
+
there is.
|
|
31
|
+
|
|
32
|
+
An explicit `@column({ type: "string", cast: "boolean" })` is honoured — that is
|
|
33
|
+
someone saying what they meant. The guard is for the column that says nothing.
|
|
34
|
+
|
|
35
|
+
**This release stops new bad writes; it does not migrate old rows.** See
|
|
36
|
+
[the upgrade guide](https://zerotal.dev/docs/upgrade#1-11-to-1-12).
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **`ColumnTypeError`**, for the above.
|
|
41
|
+
|
|
11
42
|
## [1.11.0] — 2026-08-31
|
|
12
43
|
|
|
13
44
|
### Changed
|
package/api-surface.md
CHANGED
|
@@ -208,6 +208,13 @@ class ColumnBuilder = {
|
|
|
208
208
|
wantsIndex: boolean
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
class ColumnTypeError = {
|
|
212
|
+
new (column: string, declared: string): ColumnTypeError
|
|
213
|
+
readonly code: string
|
|
214
|
+
readonly context?: Record<string, unknown> | undefined
|
|
215
|
+
readonly status: number
|
|
216
|
+
}
|
|
217
|
+
|
|
211
218
|
class DatabaseProvider = {
|
|
212
219
|
new (app: Application): DatabaseProvider
|
|
213
220
|
static dependsOn?: (new (app: Application) => ServiceProvider)[]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/orm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.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.13.0",
|
|
35
|
+
"@zerotal/validator": "1.13.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"typescript": "^5.8.0"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ZerotalError } from "@zerotal/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thrown when a boolean is written to a column declared to hold text.
|
|
5
|
+
*
|
|
6
|
+
* The reason this is an error rather than a coercion is that there is no correct
|
|
7
|
+
* coercion. SQLite gives a `TEXT` column text affinity, so an integer `0` written
|
|
8
|
+
* there is stored as the string `"0"` — and `"0"` is truthy in JavaScript. A
|
|
9
|
+
* `false` written to a text column therefore reads back as something that passes
|
|
10
|
+
* `if (model.flag)`, silently, on every row, forever. Writing `"false"` has the
|
|
11
|
+
* same problem. The value cannot survive the round trip, so the write is refused
|
|
12
|
+
* instead of being quietly made wrong.
|
|
13
|
+
*
|
|
14
|
+
* **A bare `@column()` is a text column.** With no argument it resolves to
|
|
15
|
+
* `{ type: "string" }`, which is the right default for the common case and the
|
|
16
|
+
* trap here: a boolean property decorated with a bare `@column()` looks declared
|
|
17
|
+
* and is stored as text. The `declare` keyword erases the property's TypeScript
|
|
18
|
+
* type at runtime, so the decorator cannot infer `boolean` and pick for you —
|
|
19
|
+
* which is exactly why the mistake is worth naming loudly rather than guessing at.
|
|
20
|
+
*
|
|
21
|
+
* Reported from production: a feature flag read as enabled for every record that
|
|
22
|
+
* had it turned off, and nothing in the app or the database registered a fault.
|
|
23
|
+
*/
|
|
24
|
+
export class ColumnTypeError extends ZerotalError {
|
|
25
|
+
constructor(column: string, declared: string) {
|
|
26
|
+
super(
|
|
27
|
+
`[Zerotal ORM] ${column} is declared as a \`${declared}\` column and was given a boolean. ` +
|
|
28
|
+
`A text column stores that as "0"/"1", and "0" is truthy in JavaScript — so a stored ` +
|
|
29
|
+
`\`false\` would read back as true and every \`if (…)\` on it would take the wrong branch. ` +
|
|
30
|
+
`Declare the column's type instead: \`@column("boolean")\`. ` +
|
|
31
|
+
`A bare \`@column()\` defaults to \`string\`, which is the usual cause. ` +
|
|
32
|
+
`If you genuinely want the text "true"/"false", assign a string.`,
|
|
33
|
+
"E_COLUMN_TYPE",
|
|
34
|
+
500,
|
|
35
|
+
{ column, declared },
|
|
36
|
+
);
|
|
37
|
+
this.name = "ColumnTypeError";
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/errors/index.ts
CHANGED
|
@@ -3,5 +3,6 @@ export { RelationNotLoadedError } from "./RelationNotLoadedError.ts";
|
|
|
3
3
|
export { TransactionError } from "./TransactionError.ts";
|
|
4
4
|
export { MigrationError } from "./MigrationError.ts";
|
|
5
5
|
export { StateError } from "./StateError.ts";
|
|
6
|
+
export { ColumnTypeError } from "./ColumnTypeError.ts";
|
|
6
7
|
export { MassAssignmentError } from "./MassAssignmentError.ts";
|
|
7
8
|
export { UnsupportedDialectError } from "./UnsupportedDialectError.ts";
|
package/src/index.ts
CHANGED
package/src/model/BaseModel.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
ModelNotFoundError,
|
|
31
31
|
RelationNotLoadedError,
|
|
32
32
|
MassAssignmentError,
|
|
33
|
+
ColumnTypeError,
|
|
33
34
|
} from "../errors/index.ts";
|
|
34
35
|
import { type ManyToMany } from "./relations/RelationRegistry.ts";
|
|
35
36
|
import { installReactiveAccessors, type ColumnOptions } from "./decorators/column.ts";
|
|
@@ -396,6 +397,13 @@ function _serializeForWrite(
|
|
|
396
397
|
serializedVal = castOpt.set(val);
|
|
397
398
|
} else if (typeof castOpt === "string") {
|
|
398
399
|
serializedVal = applyCastSet(val, castOpt, model ? `${model}.${key}` : key);
|
|
400
|
+
} else if (typeof val === "boolean" && (colType === "string" || colType === "text")) {
|
|
401
|
+
// No coercion is correct here, so the write is refused rather than made wrong.
|
|
402
|
+
// A text column has text affinity: `0` is stored as "0", and "0" is truthy in
|
|
403
|
+
// JavaScript, so a stored `false` reads back as true on every row. "false" has
|
|
404
|
+
// the same problem. A bare `@column()` resolves to `{ type: "string" }`, which
|
|
405
|
+
// is how a boolean property ends up here looking perfectly declared.
|
|
406
|
+
throw new ColumnTypeError(model ? `${model}.${key}` : key, colType);
|
|
399
407
|
} else if (colType === "boolean" && val !== null && val !== undefined) {
|
|
400
408
|
serializedVal = val ? 1 : 0;
|
|
401
409
|
} else if (colType === "json" && val !== null) {
|