Skip to content

Commit 725c10c

Browse files
committed
fix(objectql)!: the number arm accepts only a finite JS number (#20309)
The arm judged Number(value) and the write carried value, so [500], [], true, '0x10', ' 12 ' and '12' passed and reached the driver as sent. Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx Co-authored-by: Claude <noreply@anthropic.com>
1 parent c74de10 commit 725c10c

1 file changed

Lines changed: 19 additions & 2 deletions

File tree

‎packages/objectql/src/validation/record-validator.ts‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
* - `valueDomain` a declared standard domain's membership, judged by the
2626
* spec's shared `isValueDomainMember` — the WRITTEN value
2727
* only (#14168, maintainer ruling 2026-09-02 option A)
28+
* - number types the value must be a finite JS number, the spec's stored
29+
* value; a string, array or boolean is `invalid_number`,
30+
* never coerced (#20309)
2831
* - `min` / `max` (number/currency/percent/rating/slider)
2932
* - `scale` more decimal places than the field's STORED allowance →
3033
* `max_scale` (#7501; rejection, NEVER rounding —
@@ -857,11 +860,25 @@ function validateOne(
857860
// failed with `ERR_SUMMARY_RECOMPUTE` on memory and SQLite. A blank on a
858861
// `summary` is still `null` at the door (`normalizeBlankTypedValues` reads the
859862
// whole numeric class).
863+
//
864+
// [#20309] ONLY a finite JS number passes: the spec's stored value for this
865+
// class, `valueSchemaFor`'s `z.number().finite()`. The arm judges the value
866+
// the driver receives, and the driver receives exactly what was sent, since
867+
// nothing between here and the driver rewrites a numeric value. The arm used
868+
// to judge `Number(value)` instead, so every value JS coerces to a finite
869+
// number passed and was then written as sent: `[500]` (SQLite stored the
870+
// TEXT `'[500]'`, memory the array), `[]`, `true` / `false`, `'0x10'`,
871+
// `' 12 '`, `'1e3'`, and a plain `'12'` (memory stored the string).
872+
// ⛔ No coercion here: refuse, never silently alter (the #7501 posture). A
873+
// write door that parses a string into a number is a second dialect of the
874+
// value contract. A producer that holds a string converts it itself, as the
875+
// import route does (`parseNumberCell` in `@objectstack/rest`) before the
876+
// engine sees the row.
860877
if (NUMERIC_VALUE_TYPES.has(t) && !COMPUTED_VALUE_TYPES.has(t)) {
861-
const n = typeof value === 'number' ? value : Number(value);
862-
if (!Number.isFinite(n)) {
878+
if (typeof value !== 'number' || !Number.isFinite(value)) {
863879
return fail('invalid_number');
864880
}
881+
const n = value;
865882
// [#20308] `progress` joined the TYPE check above, and only that. The
866883
// bounds and `scale` below keep the five types they always read: `scale`'s
867884
// own contract names the types it is enforced on (`number`, `percent`,

0 commit comments

Comments
 (0)