@evolu/common 6.0.1-preview.3 → 6.0.1-preview.31
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/dist/src/Array.d.ts +69 -5
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +64 -5
- package/dist/src/Assert.d.ts +6 -16
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +6 -18
- package/dist/src/Brand.d.ts +75 -0
- package/dist/src/Brand.d.ts.map +1 -0
- package/dist/src/Brand.js +1 -0
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +8 -7
- package/dist/src/Cache.d.ts +44 -0
- package/dist/src/Cache.d.ts.map +1 -0
- package/dist/src/Cache.js +52 -0
- package/dist/src/Callbacks.d.ts +45 -12
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +14 -7
- package/dist/src/Console.d.ts +31 -6
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +72 -9
- package/dist/src/Crypto.d.ts +61 -34
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +32 -45
- package/dist/src/Evolu/Db.d.ts +161 -65
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +286 -694
- package/dist/src/Evolu/Diff.d.ts +3 -3
- package/dist/src/Evolu/Diff.d.ts.map +1 -1
- package/dist/src/Evolu/Diff.js +7 -5
- package/dist/src/Evolu/Evolu.d.ts +208 -133
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +188 -183
- package/dist/src/Evolu/Internal.d.ts +0 -2
- package/dist/src/Evolu/Internal.d.ts.map +1 -1
- package/dist/src/Evolu/Internal.js +0 -2
- package/dist/src/Evolu/LocalAuth.d.ts +150 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +174 -0
- package/dist/src/Evolu/Owner.d.ts +273 -120
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +130 -104
- package/dist/src/Evolu/Platform.d.ts +9 -7
- package/dist/src/Evolu/Platform.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.d.ts +277 -232
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +603 -378
- package/dist/src/Evolu/Public.d.ts +6 -8
- package/dist/src/Evolu/Public.d.ts.map +1 -1
- package/dist/src/Evolu/Public.js +2 -3
- package/dist/src/Evolu/PublicKysely.js +3 -3
- package/dist/src/Evolu/Query.d.ts +2 -1
- package/dist/src/Evolu/Query.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.d.ts +92 -7
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +238 -76
- package/dist/src/Evolu/Schema.d.ts +129 -73
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +169 -89
- package/dist/src/Evolu/Storage.d.ts +240 -26
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +189 -91
- package/dist/src/Evolu/Sync.d.ts +67 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +441 -20
- package/dist/src/Evolu/Timestamp.d.ts +85 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +77 -18
- package/dist/src/Identicon.d.ts +35 -0
- package/dist/src/Identicon.d.ts.map +1 -0
- package/dist/src/Identicon.js +143 -0
- package/dist/src/Instances.d.ts +34 -0
- package/dist/src/Instances.d.ts.map +1 -0
- package/dist/src/Instances.js +44 -0
- package/dist/src/ManyToManyMap.d.ts +71 -10
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/ManyToManyMap.js +41 -6
- package/dist/src/Number.d.ts +4 -3
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +5 -4
- package/dist/src/Platform.d.ts +20 -0
- package/dist/src/Platform.d.ts.map +1 -0
- package/dist/src/Platform.js +22 -0
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Resources.d.ts +118 -0
- package/dist/src/Resources.d.ts.map +1 -0
- package/dist/src/Resources.js +197 -0
- package/dist/src/Result.d.ts +184 -52
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +30 -241
- package/dist/src/Skiplist.js +2 -1
- package/dist/src/Sqlite.d.ts +63 -5
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +110 -9
- package/dist/src/Task.d.ts +586 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +469 -0
- package/dist/src/Time.d.ts +66 -1
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +99 -5
- package/dist/src/Type.d.ts +622 -340
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +666 -464
- package/dist/src/Types.d.ts +1 -75
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -18
- package/dist/src/Worker.d.ts +39 -11
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +22 -4
- package/dist/src/index.d.ts +7 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -2
- package/package.json +14 -13
- package/src/Array.ts +90 -11
- package/src/Assert.ts +6 -24
- package/src/Brand.ts +75 -0
- package/src/Buffer.ts +7 -7
- package/src/Cache.ts +85 -0
- package/src/Callbacks.ts +62 -22
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +97 -82
- package/src/Evolu/Db.ts +517 -1020
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +464 -355
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +463 -0
- package/src/Evolu/Owner.ts +355 -228
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +859 -676
- package/src/Evolu/Public.ts +7 -14
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Query.ts +2 -1
- package/src/Evolu/Relay.ts +437 -93
- package/src/Evolu/Schema.ts +391 -191
- package/src/Evolu/Storage.ts +532 -135
- package/src/Evolu/Sync.ts +766 -37
- package/src/Evolu/Timestamp.ts +88 -35
- package/src/Identicon.ts +197 -0
- package/src/Instances.ts +90 -0
- package/src/ManyToManyMap.ts +124 -24
- package/src/Number.ts +6 -10
- package/src/Platform.ts +26 -0
- package/src/Random.ts +3 -2
- package/src/Resources.ts +367 -0
- package/src/Result.ts +191 -54
- package/src/Skiplist.ts +1 -1
- package/src/Sqlite.ts +122 -17
- package/src/Task.ts +901 -0
- package/src/Time.ts +180 -5
- package/src/Type.ts +1084 -727
- package/src/Types.ts +1 -77
- package/src/WebSocket.ts +27 -25
- package/src/Worker.ts +72 -23
- package/src/index.ts +7 -2
- package/dist/src/Evolu/Config.d.ts +0 -69
- package/dist/src/Evolu/Config.d.ts.map +0 -1
- package/dist/src/Evolu/Config.js +0 -9
- package/dist/src/Evolu/Kysely.d.ts +0 -6
- package/dist/src/Evolu/Kysely.d.ts.map +0 -1
- package/dist/src/Evolu/Kysely.js +0 -21
- package/dist/src/NanoId.d.ts +0 -27
- package/dist/src/NanoId.d.ts.map +0 -1
- package/dist/src/NanoId.js +0 -6
- package/dist/src/Promise.d.ts +0 -180
- package/dist/src/Promise.d.ts.map +0 -1
- package/dist/src/Promise.js +0 -176
- package/src/Evolu/Config.ts +0 -83
- package/src/Evolu/Kysely.ts +0 -38
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/dist/src/Type.js
CHANGED
|
@@ -1,80 +1,186 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 🧩
|
|
2
|
+
* 🧩 Type-safe runtime types
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Evolu {@link Type} is like a type guard that returns typed errors (via
|
|
5
|
+
* {@link Result}) instead of throwing. We either get a safely typed value or a
|
|
6
|
+
* precise, composable error value telling us exactly why validation failed.
|
|
5
7
|
*
|
|
6
|
-
*
|
|
8
|
+
* Evolu Type supports [Standard Schema](https://standardschema.dev/) for
|
|
9
|
+
* interoperability with 40+ validation-compatible tools and frameworks.
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
* library fully met our needs:
|
|
11
|
+
* Why another validation library?
|
|
10
12
|
*
|
|
11
|
-
* - **Result-based error handling
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - **
|
|
15
|
-
*
|
|
16
|
-
* - **No user-land chaining
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
13
|
+
* - **Result-based error handling** – no exceptions for normal control flow.
|
|
14
|
+
* - **Typed errors with decoupled formatters** – validation logic ≠ user
|
|
15
|
+
* messages.
|
|
16
|
+
* - **Consistent constraints via {@link Brand}** – every constraint becomes part
|
|
17
|
+
* of the type.
|
|
18
|
+
* - **No user-land chaining DSL** – designed with the upcoming ES pipe operator
|
|
19
|
+
* in mind.
|
|
20
|
+
* - **Selective validation** – parent validations are skipped when already proved
|
|
21
|
+
* by typing.
|
|
22
|
+
* - **Simple, top-down implementation** – readable source code from top to bottom
|
|
23
|
+
* with no hidden magic; just plain functions and composition.
|
|
22
24
|
*
|
|
23
|
-
*
|
|
24
|
-
* includes its own usage example, and you can (and should) check the tests for
|
|
25
|
-
* practical demonstrations of the API. Or dang, just read the code. It's
|
|
26
|
-
* simple.
|
|
25
|
+
* ### Base Types Quick Start
|
|
27
26
|
*
|
|
28
|
-
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* // Validate unknown values
|
|
29
|
+
* const value: unknown = "hello";
|
|
30
|
+
* const stringResult = String.fromUnknown(value);
|
|
31
|
+
* if (!stringResult.ok) {
|
|
32
|
+
* // console.error(formatStringError(stringResult.error));
|
|
33
|
+
* return stringResult; // inside a function returning Result<string, _>
|
|
34
|
+
* }
|
|
35
|
+
* // Safe branch: value is now string
|
|
36
|
+
* const upper = stringResult.value.toUpperCase();
|
|
37
|
+
*
|
|
38
|
+
* // Type guard style
|
|
39
|
+
* if (String.is(value)) {
|
|
40
|
+
* // narrowed to string
|
|
41
|
+
* }
|
|
42
|
+
*
|
|
43
|
+
* // Composing: arrays & objects
|
|
44
|
+
* const Numbers = array(Number); // ReadonlyArray<number>
|
|
45
|
+
* const Point = object({ x: Number, y: Number });
|
|
46
|
+
*
|
|
47
|
+
* Numbers.from([1, 2, 3]); // ok
|
|
48
|
+
* Point.from({ x: 1, y: 2 }); // ok
|
|
49
|
+
* Point.from({ x: 1, y: "2" }); // err -> nested Number error
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* ### Branding Basics
|
|
53
|
+
*
|
|
54
|
+
* Branding adds semantic meaning & constraints while preserving the runtime
|
|
55
|
+
* shape:
|
|
56
|
+
*
|
|
57
|
+
* ```ts
|
|
58
|
+
* const CurrencyCode = brand("CurrencyCode", String, (value) =>
|
|
59
|
+
* /^[A-Z]{3}$/.test(value)
|
|
60
|
+
* ? ok(value)
|
|
61
|
+
* : err<CurrencyCodeError>({ type: "CurrencyCode", value }),
|
|
62
|
+
* );
|
|
63
|
+
* type CurrencyCode = typeof CurrencyCode.Type; // string & Brand<"CurrencyCode">
|
|
64
|
+
*
|
|
65
|
+
* interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
|
|
66
|
+
*
|
|
67
|
+
* const formatCurrencyCodeError =
|
|
68
|
+
* createTypeErrorFormatter<CurrencyCodeError>(
|
|
69
|
+
* (error) => `Invalid currency code: ${error.value}`,
|
|
70
|
+
* );
|
|
71
|
+
*
|
|
72
|
+
* const r = CurrencyCode.from("USD"); // ok("USD")
|
|
73
|
+
* const e = CurrencyCode.from("usd"); // err(...)
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* See also reusable brand factories like `minLength`, `maxLength`, `trimmed`,
|
|
77
|
+
* `positive`, `between`, etc.
|
|
78
|
+
*
|
|
79
|
+
* ### Objects & Optional Fields
|
|
80
|
+
*
|
|
81
|
+
* ```ts
|
|
82
|
+
* const User = object({
|
|
83
|
+
* name: NonEmptyTrimmedString100,
|
|
84
|
+
* age: optional(PositiveInt),
|
|
85
|
+
* });
|
|
86
|
+
* type User = typeof User.Type;
|
|
29
87
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
88
|
+
* User.from({ name: "Alice" }); // ok
|
|
89
|
+
* User.from({ name: "Alice", age: -1 }); // err(PositiveInt)
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* ### Deriving JSON String Types
|
|
93
|
+
*
|
|
94
|
+
* ```ts
|
|
95
|
+
* const Person = object({
|
|
96
|
+
* name: NonEmptyString50,
|
|
97
|
+
* // Did you know that JSON.stringify converts NaN (a number) into null?
|
|
98
|
+
* // To prevent this, use FiniteNumber.
|
|
99
|
+
* age: FiniteNumber,
|
|
100
|
+
* });
|
|
101
|
+
* type Person = typeof Person.Type;
|
|
102
|
+
*
|
|
103
|
+
* const [PersonJson, personToPersonJson, personJsonToPerson] = json(
|
|
104
|
+
* Person,
|
|
105
|
+
* "PersonJson",
|
|
106
|
+
* );
|
|
107
|
+
* // string & Brand<"PersonJson">
|
|
108
|
+
* type PersonJson = typeof PersonJson.Type;
|
|
109
|
+
*
|
|
110
|
+
* const person = Person.orThrow({
|
|
111
|
+
* name: "Alice",
|
|
112
|
+
* age: 30,
|
|
113
|
+
* });
|
|
34
114
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* this:
|
|
115
|
+
* const personJson = personToPersonJson(person);
|
|
116
|
+
* expect(personJsonToPerson(personJson)).toEqual(person);
|
|
117
|
+
* ```
|
|
39
118
|
*
|
|
40
|
-
*
|
|
119
|
+
* ### Error Formatting
|
|
41
120
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
121
|
+
* Evolu separates validation logic from human-readable messages. There are two
|
|
122
|
+
* layers:
|
|
44
123
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
124
|
+
* 1. Per-type formatters (e.g. `formatStringError`) – simple, focused, already
|
|
125
|
+
* used earlier in the quick start example.
|
|
126
|
+
* 2. A unified formatter via `createFormatTypeError` – composes all built-in and
|
|
127
|
+
* custom errors (including nested composite types) and lets us override
|
|
128
|
+
* selected messages.
|
|
48
129
|
*
|
|
49
|
-
*
|
|
50
|
-
* parent Types when we can rely on TypeScript types.
|
|
130
|
+
* #### 1. Per-Type Formatter (recap)
|
|
51
131
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
132
|
+
* ```ts
|
|
133
|
+
* const r = String.fromUnknown(42);
|
|
134
|
+
* if (!r.ok) console.error(formatStringError(r.error));
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* #### 2. Unified Formatter with Overrides
|
|
138
|
+
*
|
|
139
|
+
* ```ts
|
|
140
|
+
* // Override only what we care about; fall back to built-ins for the rest.
|
|
141
|
+
* const formatTypeError = createFormatTypeError((error) => {
|
|
142
|
+
* if (error.type === "MinLength") return `Min length is ${error.min}`;
|
|
143
|
+
* });
|
|
144
|
+
*
|
|
145
|
+
* const User = object({ name: NonEmptyTrimmedString100 });
|
|
146
|
+
* const resultUser = User.from({ name: "" });
|
|
147
|
+
* if (!resultUser.ok) console.error(formatTypeError(resultUser.error));
|
|
148
|
+
*
|
|
149
|
+
* const badPoint = object({ x: Number, y: Number }).from({
|
|
150
|
+
* x: 1,
|
|
151
|
+
* y: "foo",
|
|
152
|
+
* });
|
|
153
|
+
* if (!badPoint.ok) console.error(formatTypeError(badPoint.error));
|
|
154
|
+
* ```
|
|
155
|
+
*
|
|
156
|
+
* The unified formatter walks nested structures (object / array / record /
|
|
157
|
+
* tuple / union) and applies overrides only where specified, greatly reducing
|
|
158
|
+
* boilerplate when formatting complex validation errors.
|
|
57
159
|
*
|
|
58
160
|
* ### Tip
|
|
59
161
|
*
|
|
60
162
|
* If necessary, write `globalThis.String` instead of `String` to avoid naming
|
|
61
|
-
* clashes with
|
|
163
|
+
* clashes with native types.
|
|
62
164
|
*
|
|
63
|
-
* ### Design Decision:
|
|
165
|
+
* ### Design Decision: No Bidirectional Transformations
|
|
64
166
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
167
|
+
* Evolu Type intentionally does not support bidirectional transformations. It
|
|
168
|
+
* previously did, but supporting that while keeping typed error fidelity added
|
|
169
|
+
* complexity that hurt readability & reliability. Most persistence pipelines
|
|
170
|
+
* (e.g. SQLite) already require explicit mapping of query results, so implicit
|
|
171
|
+
* reverse transforms would not buy much. We may revisit this if we can design a
|
|
172
|
+
* minimal, 100% safe API that preserves simplicity.
|
|
69
173
|
*
|
|
70
174
|
* @module
|
|
71
175
|
*/
|
|
176
|
+
import { utf8ToBytes } from "@noble/ciphers/utils.js";
|
|
177
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
72
178
|
import * as bip39 from "@scure/bip39";
|
|
73
|
-
import { wordlist } from "@scure/bip39/wordlists/english";
|
|
74
|
-
import {
|
|
75
|
-
import { identity } from "./Function.js";
|
|
179
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
180
|
+
import { pack } from "msgpackr";
|
|
76
181
|
import { isPlainObject } from "./Object.js";
|
|
77
|
-
import {
|
|
182
|
+
import { hasNodeBuffer } from "./Platform.js";
|
|
183
|
+
import { err, getOrNull, getOrThrow, ok, trySync } from "./Result.js";
|
|
78
184
|
import { safelyStringifyUnknownValue } from "./String.js";
|
|
79
185
|
const EvoluTypeSymbol = Symbol("evolu.Type");
|
|
80
186
|
/**
|
|
@@ -88,6 +194,8 @@ const createType = (name, definition) => ({
|
|
|
88
194
|
name,
|
|
89
195
|
is: (value) => definition.fromUnknown(value).ok,
|
|
90
196
|
from: definition.fromUnknown,
|
|
197
|
+
orThrow: (value) => getOrThrow(definition.fromUnknown(value)),
|
|
198
|
+
orNull: (value) => getOrNull(definition.fromUnknown(value)),
|
|
91
199
|
[EvoluTypeSymbol]: true,
|
|
92
200
|
Type: undefined,
|
|
93
201
|
Input: undefined,
|
|
@@ -95,6 +203,24 @@ const createType = (name, definition) => ({
|
|
|
95
203
|
Parent: undefined,
|
|
96
204
|
ParentError: undefined,
|
|
97
205
|
Errors: undefined,
|
|
206
|
+
"~standard": {
|
|
207
|
+
version: 1,
|
|
208
|
+
vendor: "evolu",
|
|
209
|
+
validate: (value) => {
|
|
210
|
+
const result = definition.fromUnknown(value);
|
|
211
|
+
if (result.ok) {
|
|
212
|
+
return { value: result.value };
|
|
213
|
+
}
|
|
214
|
+
cachedStandardSchemaFormatTypeError ??= createFormatTypeError();
|
|
215
|
+
return {
|
|
216
|
+
issues: typeErrorToStandardSchemaIssues(result.error, cachedStandardSchemaFormatTypeError),
|
|
217
|
+
};
|
|
218
|
+
},
|
|
219
|
+
types: {
|
|
220
|
+
input: undefined,
|
|
221
|
+
output: undefined,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
98
224
|
});
|
|
99
225
|
/**
|
|
100
226
|
* Creates a formatter function for {@link TypeError}.
|
|
@@ -117,12 +243,6 @@ export const createTypeErrorFormatter = (format) => (error) => format({ ...error
|
|
|
117
243
|
* Base {@link Type}.
|
|
118
244
|
*
|
|
119
245
|
* A Base Type validates that a value conforms to a specific TypeScript type.
|
|
120
|
-
* Unlike refinements or transformations, Base Types establish the fundamental
|
|
121
|
-
* shape of a value before any branding or transformation occurs.
|
|
122
|
-
*
|
|
123
|
-
* - To **refine** a Base Type further, use the {@link brand} Type Factory.
|
|
124
|
-
* - To **transform** a Base Type into a different representation, use the
|
|
125
|
-
* {@link transform} Type Factory.
|
|
126
246
|
*
|
|
127
247
|
* ### Example
|
|
128
248
|
*
|
|
@@ -144,9 +264,7 @@ export const createTypeErrorFormatter = (format) => (error) => format({ ...error
|
|
|
144
264
|
*/
|
|
145
265
|
export const base = (name, fromUnknown) => createType(name, {
|
|
146
266
|
fromUnknown,
|
|
147
|
-
to: identity,
|
|
148
267
|
fromParent: (ok), // `fromParent` relies on types, so it can't fail for the Base Type
|
|
149
|
-
toParent: identity,
|
|
150
268
|
});
|
|
151
269
|
/**
|
|
152
270
|
* Creates a formatter function for a base {@link TypeError}.
|
|
@@ -233,7 +351,7 @@ export const instanceOf = (ctor) => ({
|
|
|
233
351
|
: err({ type: "InstanceOf", value, ctor: ctor.name })),
|
|
234
352
|
ctor,
|
|
235
353
|
});
|
|
236
|
-
export const formatInstanceOfError = createTypeErrorFormatter((error) => `
|
|
354
|
+
export const formatInstanceOfError = createTypeErrorFormatter((error) => `The value ${error.value} is not an instance of ${error.ctor}.`);
|
|
237
355
|
/**
|
|
238
356
|
* JavaScript Date.
|
|
239
357
|
*
|
|
@@ -280,9 +398,7 @@ export function brand(name, parent, refine) {
|
|
|
280
398
|
return {
|
|
281
399
|
...createType("Brand", {
|
|
282
400
|
fromUnknown,
|
|
283
|
-
to: identity,
|
|
284
401
|
fromParent: (refine ?? ok),
|
|
285
|
-
toParent: identity,
|
|
286
402
|
}),
|
|
287
403
|
brand: name,
|
|
288
404
|
parentType: parent,
|
|
@@ -296,7 +412,7 @@ export function brand(name, parent, refine) {
|
|
|
296
412
|
export const CurrencyCode = brand("CurrencyCode", String, (value) => /^[A-Z]{3}$/.test(value)
|
|
297
413
|
? ok(value)
|
|
298
414
|
: err({ type: "CurrencyCode", value }));
|
|
299
|
-
export const formatCurrencyCodeError = createTypeErrorFormatter((error) => `Invalid currency code: ${error.value}
|
|
415
|
+
export const formatCurrencyCodeError = createTypeErrorFormatter((error) => `Invalid currency code: ${error.value}.`);
|
|
300
416
|
/**
|
|
301
417
|
* ISO 8601 date-time string.
|
|
302
418
|
*
|
|
@@ -312,37 +428,39 @@ export const formatCurrencyCodeError = createTypeErrorFormatter((error) => `Inva
|
|
|
312
428
|
* ### Example
|
|
313
429
|
*
|
|
314
430
|
* ```ts
|
|
315
|
-
* const result =
|
|
316
|
-
* const error =
|
|
431
|
+
* const result = DateIso.from("2023-01-01T12:00:00.000Z"); // ok
|
|
432
|
+
* const error = DateIso.from("10000-01-01T00:00:00.000Z"); // err
|
|
317
433
|
* ```
|
|
318
434
|
*
|
|
319
435
|
* @category String
|
|
320
436
|
*/
|
|
321
|
-
export const
|
|
437
|
+
export const DateIso = brand("DateIso", String, (value) => {
|
|
322
438
|
if (value.length !== 24) {
|
|
323
|
-
return err({ type: "
|
|
439
|
+
return err({ type: "DateIso", value });
|
|
324
440
|
}
|
|
325
|
-
|
|
326
|
-
|
|
441
|
+
const parsed = globalThis.Date.parse(value);
|
|
442
|
+
if (isNaN(parsed)) {
|
|
443
|
+
return err({ type: "DateIso", value });
|
|
444
|
+
}
|
|
445
|
+
// Round-trip test: ensure the string is actually a proper ISO format
|
|
446
|
+
const roundTrip = new globalThis.Date(parsed).toISOString();
|
|
447
|
+
if (roundTrip !== value) {
|
|
448
|
+
return err({ type: "DateIso", value });
|
|
327
449
|
}
|
|
328
450
|
return ok(value);
|
|
329
451
|
});
|
|
330
|
-
export const
|
|
452
|
+
export const formatDateIsoError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid ISO 8601 date string.`);
|
|
453
|
+
export const dateToDateIso = (value) => DateIso.fromParent(value.toISOString());
|
|
454
|
+
export const dateIsoToDate = (value) => new globalThis.Date(value);
|
|
331
455
|
/**
|
|
332
456
|
* Trimmed string.
|
|
333
457
|
*
|
|
334
|
-
* This Type Factory
|
|
335
|
-
*
|
|
336
|
-
* Factory.
|
|
458
|
+
* This Type Factory validates whether a string has no leading or trailing
|
|
459
|
+
* whitespaces.
|
|
337
460
|
*
|
|
338
|
-
* ###
|
|
461
|
+
* ### Example
|
|
339
462
|
*
|
|
340
463
|
* ```ts
|
|
341
|
-
* // this Type already exists
|
|
342
|
-
* const TrimmedString = trimmed(String);
|
|
343
|
-
* type TrimmedString = typeof TrimmedString.Type;
|
|
344
|
-
*
|
|
345
|
-
* // we can make any branded Type trimmed:
|
|
346
464
|
* const TrimmedNonEmptyString = trimmed(minLength(1)(String));
|
|
347
465
|
* // string & Brand<"MinLength1"> & Brand<"Trimmed">
|
|
348
466
|
* type TrimmedNonEmptyString = typeof TrimmedNonEmptyString.Type;
|
|
@@ -353,33 +471,7 @@ export const formatDateIsoStringError = createTypeErrorFormatter((error) => `The
|
|
|
353
471
|
export const trimmed = (parent) => brand("Trimmed", parent, (value) => value.trim().length === value.length
|
|
354
472
|
? ok(value)
|
|
355
473
|
: err({ type: "Trimmed", value }));
|
|
356
|
-
export const formatTrimmedError = createTypeErrorFormatter((error) => `
|
|
357
|
-
/**
|
|
358
|
-
* Trims leading and trailing whitespace from a string.
|
|
359
|
-
*
|
|
360
|
-
* This Type Factory **transforms** the input string by removing whitespace from
|
|
361
|
-
* both ends. For validation only, use {@link trimmed} Type Factory.
|
|
362
|
-
*
|
|
363
|
-
* ### Example
|
|
364
|
-
*
|
|
365
|
-
* ```ts
|
|
366
|
-
* const TrimString = trim(String);
|
|
367
|
-
* expect(TrimString.from("a ")).toEqual(ok("a"));
|
|
368
|
-
* expect(TrimString.fromParent("a ").value).toEqual("a");
|
|
369
|
-
*
|
|
370
|
-
* const TrimNonEmptyString = trim(NonEmptyString);
|
|
371
|
-
* expect(TrimNonEmptyString.from("a " as NonEmptyString)).toEqual(ok("a"));
|
|
372
|
-
* expect(
|
|
373
|
-
* TrimNonEmptyString.fromParent("a " as NonEmptyString).value,
|
|
374
|
-
* ).toEqual("a");
|
|
375
|
-
* ```
|
|
376
|
-
*
|
|
377
|
-
* **Note:** This transformation is irreversible. Calling `toParent` will not
|
|
378
|
-
* restore the original representation.
|
|
379
|
-
*
|
|
380
|
-
* @category String
|
|
381
|
-
*/
|
|
382
|
-
export const trim = (parent) => transform(parent, trimmed(parent), (value) => ok(value.trim()), (value) => value);
|
|
474
|
+
export const formatTrimmedError = createTypeErrorFormatter((error) => `The value ${error.value} must be trimmed.`);
|
|
383
475
|
/**
|
|
384
476
|
* Trimmed string
|
|
385
477
|
*
|
|
@@ -389,6 +481,7 @@ export const trim = (parent) => transform(parent, trimmed(parent), (value) => ok
|
|
|
389
481
|
* @category String
|
|
390
482
|
*/
|
|
391
483
|
export const TrimmedString = trimmed(String);
|
|
484
|
+
export const trim = (value) => value.trim();
|
|
392
485
|
/**
|
|
393
486
|
* Minimum length.
|
|
394
487
|
*
|
|
@@ -403,7 +496,7 @@ export const TrimmedString = trimmed(String);
|
|
|
403
496
|
* @category Array
|
|
404
497
|
*/
|
|
405
498
|
export const minLength = (min) => (parent) => brand(`MinLength${min}`, parent, (value) => value.length >= min ? ok(value) : err({ type: "MinLength", value, min }));
|
|
406
|
-
export const formatMinLengthError = createTypeErrorFormatter((error) => `
|
|
499
|
+
export const formatMinLengthError = createTypeErrorFormatter((error) => `The value ${error.value} does not meet the minimum length of ${error.min}.`);
|
|
407
500
|
/**
|
|
408
501
|
* Maximum length.
|
|
409
502
|
*
|
|
@@ -418,7 +511,7 @@ export const formatMinLengthError = createTypeErrorFormatter((error) => `Value $
|
|
|
418
511
|
* @category Array
|
|
419
512
|
*/
|
|
420
513
|
export const maxLength = (max) => (parent) => brand(`MaxLength${max}`, parent, (value) => value.length <= max ? ok(value) : err({ type: "MaxLength", value, max }));
|
|
421
|
-
export const formatMaxLengthError = createTypeErrorFormatter((error) => `
|
|
514
|
+
export const formatMaxLengthError = createTypeErrorFormatter((error) => `The value ${error.value} exceeds the maximum length of ${error.max}.`);
|
|
422
515
|
/**
|
|
423
516
|
* Exact length.
|
|
424
517
|
*
|
|
@@ -435,7 +528,7 @@ export const formatMaxLengthError = createTypeErrorFormatter((error) => `Value $
|
|
|
435
528
|
export const length = (exact) => (parent) => brand(`Length${exact}`, parent, (value) => value.length === exact
|
|
436
529
|
? ok(value)
|
|
437
530
|
: err({ type: "Length", value, exact }));
|
|
438
|
-
export const formatLengthError = createTypeErrorFormatter((error) => `
|
|
531
|
+
export const formatLengthError = createTypeErrorFormatter((error) => `The value ${error.value} does not have the required length of ${error.exact}.`);
|
|
439
532
|
/** @category String */
|
|
440
533
|
export const NonEmptyString = minLength(1)(String);
|
|
441
534
|
/** @category String */
|
|
@@ -468,7 +561,7 @@ export const NonEmptyTrimmedString1000 = minLength(1)(TrimmedString1000);
|
|
|
468
561
|
export const Mnemonic = brand("Mnemonic", NonEmptyTrimmedString, (value) => bip39.validateMnemonic(value, wordlist)
|
|
469
562
|
? ok(value)
|
|
470
563
|
: err({ type: "Mnemonic", value }));
|
|
471
|
-
export const formatMnemonicError = createTypeErrorFormatter((error) => `Invalid BIP39 mnemonic: ${error.value}
|
|
564
|
+
export const formatMnemonicError = createTypeErrorFormatter((error) => `Invalid BIP39 mnemonic: ${error.value}.`);
|
|
472
565
|
/**
|
|
473
566
|
* String matching a regular expression.
|
|
474
567
|
*
|
|
@@ -492,11 +585,11 @@ export const regex = (name, pattern) => {
|
|
|
492
585
|
: err({ type: "Regex", name, value, pattern });
|
|
493
586
|
});
|
|
494
587
|
};
|
|
495
|
-
export const formatRegexError = createTypeErrorFormatter((error) => `
|
|
588
|
+
export const formatRegexError = createTypeErrorFormatter((error) => `The value ${error.value} does not match the pattern for ${error.name}: ${error.pattern}.`);
|
|
496
589
|
/**
|
|
497
|
-
* URL-safe
|
|
590
|
+
* URL-safe string.
|
|
498
591
|
*
|
|
499
|
-
* A `
|
|
592
|
+
* A `UrlSafeString` uses a limited alphabet that is safe for URLs:
|
|
500
593
|
*
|
|
501
594
|
* - Uppercase letters (`A-Z`)
|
|
502
595
|
* - Lowercase letters (`a-z`)
|
|
@@ -504,29 +597,88 @@ export const formatRegexError = createTypeErrorFormatter((error) => `Value ${err
|
|
|
504
597
|
* - Dash (`-`)
|
|
505
598
|
* - Underscore (`_`)
|
|
506
599
|
*
|
|
600
|
+
* This is the same character set used by Base64Url encoding, but this type does
|
|
601
|
+
* not validate that the string is actually Base64Url-encoded data.
|
|
602
|
+
*
|
|
507
603
|
* ### Example
|
|
508
604
|
*
|
|
509
605
|
* ```ts
|
|
510
|
-
* const result =
|
|
606
|
+
* const result = UrlSafeString.from("abc123_-");
|
|
511
607
|
* if (result.ok) {
|
|
512
|
-
* console.log("Valid
|
|
608
|
+
* console.log("Valid URL-safe string:", result.value);
|
|
513
609
|
* } else {
|
|
514
|
-
* console.error("Invalid
|
|
610
|
+
* console.error("Invalid URL-safe string:", result.error);
|
|
515
611
|
* }
|
|
516
612
|
* ```
|
|
517
613
|
*
|
|
518
614
|
* @category String
|
|
519
615
|
*/
|
|
520
|
-
export const
|
|
616
|
+
export const UrlSafeString = regex("UrlSafeString", /^[A-Za-z0-9_-]+$/)(String);
|
|
521
617
|
/**
|
|
522
|
-
*
|
|
618
|
+
* Base64Url without padding.
|
|
523
619
|
*
|
|
524
|
-
*
|
|
620
|
+
* Encode with {@link uint8ArrayToBase64Url}, decode with
|
|
621
|
+
* {@link base64UrlToUint8Array}.
|
|
525
622
|
*
|
|
526
|
-
*
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
623
|
+
* @category String
|
|
624
|
+
*/
|
|
625
|
+
export const Base64Url = brand("Base64Url", String, (value) => {
|
|
626
|
+
// Round-trip validation ensures consistency across different base64url
|
|
627
|
+
// implementations (Node.js Buffer, native browser API, manual fallback).
|
|
628
|
+
// Only strings that decode and encode identically are accepted.
|
|
629
|
+
let roundTrip;
|
|
630
|
+
try {
|
|
631
|
+
roundTrip = uint8ArrayToBase64Url(base64UrlToUint8Array(value));
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
//
|
|
635
|
+
}
|
|
636
|
+
return roundTrip === value
|
|
637
|
+
? ok(value)
|
|
638
|
+
: err({ type: "Base64Url", value });
|
|
639
|
+
});
|
|
640
|
+
export const formatBase64UrlError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Base64Url string.`);
|
|
641
|
+
const base64UrlOptions = { alphabet: "base64url", omitPadding: true };
|
|
642
|
+
/** Encodes a Uint8Array to a {@link Base64Url} string. */
|
|
643
|
+
export const uint8ArrayToBase64Url = hasNodeBuffer
|
|
644
|
+
? (bytes) => globalThis.Buffer.from(bytes).toString("base64url")
|
|
645
|
+
: // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
646
|
+
typeof globalThis.Uint8Array.prototype?.toBase64 !== "undefined"
|
|
647
|
+
? (bytes) =>
|
|
648
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
649
|
+
bytes.toBase64(base64UrlOptions)
|
|
650
|
+
: (bytes) => {
|
|
651
|
+
const binaryString = Array.from(bytes, (byte) => globalThis.String.fromCodePoint(byte)).join("");
|
|
652
|
+
const base64 = globalThis.btoa(binaryString);
|
|
653
|
+
return base64
|
|
654
|
+
.replace(/\+/g, "-")
|
|
655
|
+
.replace(/\//g, "_")
|
|
656
|
+
.replace(/=/g, "");
|
|
657
|
+
};
|
|
658
|
+
/** Decodes a {@link Base64Url} string to a Uint8Array. */
|
|
659
|
+
export const base64UrlToUint8Array = hasNodeBuffer
|
|
660
|
+
? (str) => {
|
|
661
|
+
const nodeBuffer = globalThis.Buffer.from(str, "base64url");
|
|
662
|
+
return new globalThis.Uint8Array(nodeBuffer);
|
|
663
|
+
}
|
|
664
|
+
: // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
665
|
+
typeof globalThis.Uint8Array?.fromBase64 !== "undefined"
|
|
666
|
+
? (str) =>
|
|
667
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
668
|
+
globalThis.Uint8Array.fromBase64(str, base64UrlOptions)
|
|
669
|
+
: (str) => {
|
|
670
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
671
|
+
while (base64.length % 4 !== 0) {
|
|
672
|
+
base64 += "=";
|
|
673
|
+
}
|
|
674
|
+
const binaryString = globalThis.atob(base64);
|
|
675
|
+
return globalThis.Uint8Array.from(binaryString, (c) => c.charCodeAt(0));
|
|
676
|
+
};
|
|
677
|
+
/**
|
|
678
|
+
* Simple alphanumeric string for naming in file systems, URLs, and identifiers.
|
|
679
|
+
*
|
|
680
|
+
* Uses the same safe alphabet as {@link UrlSafeString} (letters, digits, `-`,
|
|
681
|
+
* `_`). See `UrlSafeString` for details.
|
|
530
682
|
*
|
|
531
683
|
* The string must be between 1 and 42 characters.
|
|
532
684
|
*
|
|
@@ -543,13 +695,9 @@ export const Base64Url = regex("Base64Url", /^[A-Za-z0-9_-]+$/)(String);
|
|
|
543
695
|
*
|
|
544
696
|
* @category String
|
|
545
697
|
*/
|
|
546
|
-
export const SimpleName =
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
*
|
|
550
|
-
* @category String
|
|
551
|
-
*/
|
|
552
|
-
export const NanoId = regex("NanoId", /^[A-Za-z0-9_-]{21}$/)(String);
|
|
698
|
+
export const SimpleName = brand("SimpleName", UrlSafeString, (value) => value.length >= 1 && value.length <= 42
|
|
699
|
+
? ok(value)
|
|
700
|
+
: err({ type: "SimpleName", value }));
|
|
553
701
|
/**
|
|
554
702
|
* Trimmed string between 8 and 64 characters, branded as `SimplePassword`.
|
|
555
703
|
*
|
|
@@ -558,15 +706,48 @@ export const NanoId = regex("NanoId", /^[A-Za-z0-9_-]{21}$/)(String);
|
|
|
558
706
|
export const SimplePassword = brand("SimplePassword", minLength(8)(maxLength(64)(TrimmedString)));
|
|
559
707
|
export const formatSimplePasswordError = (formatTypeError) => createTypeErrorFormatter((error) => `Invalid password: ${formatTypeError(error.parentError)}`);
|
|
560
708
|
/**
|
|
561
|
-
*
|
|
709
|
+
* Globally unique identifier.
|
|
562
710
|
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
711
|
+
* **Evolu Id** is 16 random bytes from a cryptographically secure random
|
|
712
|
+
* generator, encoded as 22-character Base64Url string. This provides strong
|
|
713
|
+
* collision resistance for distributed ID generation.
|
|
714
|
+
*
|
|
715
|
+
* ### Design Rationale
|
|
716
|
+
*
|
|
717
|
+
* Why Evolu Id over alternatives:
|
|
718
|
+
*
|
|
719
|
+
* - **NanoID**: No standard binary serialization format, and uses only ~126 bits
|
|
720
|
+
* of entropy (21 characters from 64-symbol alphabet) compared to Evolu Id's
|
|
721
|
+
* 128 bits.
|
|
722
|
+
* - **UUID (v4)**: String format is 36 characters (with hyphens) compared to
|
|
723
|
+
* Evolu Id's 22 characters. While UUIDs can be stored as 16 bytes, their
|
|
724
|
+
* standard string representation is verbose.
|
|
725
|
+
* - **UUID v7**: Includes timestamp in the ID, which leaks information about when
|
|
726
|
+
* data was created. This is a privacy concern for local-first applications
|
|
727
|
+
* where creation time must remain private.
|
|
728
|
+
*
|
|
729
|
+
* Evolu Id provides 128 bits of entropy, compact string representation (22
|
|
730
|
+
* characters), standard and native string serialization (Base64Url), and no
|
|
731
|
+
* privacy leaks.
|
|
732
|
+
*
|
|
733
|
+
* ### Future Consideration
|
|
734
|
+
*
|
|
735
|
+
* For database-heavy workloads where insert performance is critical, a hybrid
|
|
736
|
+
* approach could be considered: `timestamp ^ H(cluster_id, timestamp >> N)`
|
|
737
|
+
* where H is a keyed hash function and N is a configurable parameter. This
|
|
738
|
+
* would maintain spatial locality for database caches (improving insert
|
|
739
|
+
* performance by an order of magnitude) while adding entropy to prevent
|
|
740
|
+
* timestamp leakage and correlation across systems. The parameter N would allow
|
|
741
|
+
* trading off cache locality (larger N = better locality) versus entropy
|
|
742
|
+
* distribution. See https://brooker.co.za/blog/2025/10/22/uuidv7.html for
|
|
743
|
+
* details on this approach.
|
|
565
744
|
*
|
|
566
745
|
* @category String
|
|
567
746
|
*/
|
|
568
|
-
export const Id =
|
|
569
|
-
|
|
747
|
+
export const Id = brand("Id", String, (value) => value.length === 22 && Base64Url.fromParent(value).ok
|
|
748
|
+
? ok(value)
|
|
749
|
+
: err({ type: "Id", value }));
|
|
750
|
+
export const formatIdError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Id.`);
|
|
570
751
|
/**
|
|
571
752
|
* Creates an {@link Id}.
|
|
572
753
|
*
|
|
@@ -575,11 +756,54 @@ export const idTypeValueLength = 21;
|
|
|
575
756
|
* ```ts
|
|
576
757
|
* // string & Brand<"Id">
|
|
577
758
|
* const id = createId(deps);
|
|
759
|
+
*
|
|
760
|
+
* // string & Brand<"Id"> & Brand<"Todo">
|
|
761
|
+
* const todoId = createId<"Todo">(deps);
|
|
578
762
|
* ```
|
|
579
763
|
*/
|
|
580
|
-
export const createId = (deps) => deps.
|
|
764
|
+
export const createId = (deps) => uint8ArrayToBase64Url(deps.randomBytes.create(16));
|
|
581
765
|
/**
|
|
582
|
-
*
|
|
766
|
+
* Creates an {@link Id} from a string using SHA-256.
|
|
767
|
+
*
|
|
768
|
+
* When integrating with external systems that use different ID formats, use
|
|
769
|
+
* this function to convert external IDs into valid Evolu IDs.
|
|
770
|
+
*
|
|
771
|
+
* In Evolu's CRDT, the ID serves as the unique identifier for conflict
|
|
772
|
+
* resolution across distributed clients. When multiple clients create records
|
|
773
|
+
* with the same external identifier, they must resolve to the same Evolu ID to
|
|
774
|
+
* ensure data consistency.
|
|
775
|
+
*
|
|
776
|
+
* ### Example
|
|
777
|
+
*
|
|
778
|
+
* ```ts
|
|
779
|
+
* // Both clients will generate the same ID
|
|
780
|
+
* const id1 = createIdFromString("user-api-123");
|
|
781
|
+
* const id2 = createIdFromString("user-api-123");
|
|
782
|
+
* console.log(id1 === id2); // true
|
|
783
|
+
*
|
|
784
|
+
* upsert("todo", {
|
|
785
|
+
* id: createIdFromString("external-todo-456"),
|
|
786
|
+
* title: "Synced from external system",
|
|
787
|
+
* });
|
|
788
|
+
* ```
|
|
789
|
+
*
|
|
790
|
+
* **Important**: This transformation uses the first 16 bytes of SHA-256 hash of
|
|
791
|
+
* the string bytes, therefore it's not possible to recover the original
|
|
792
|
+
* external string from the generated {@link Id}. If you need to preserve the
|
|
793
|
+
* original external ID, store it in a separate column.
|
|
794
|
+
*
|
|
795
|
+
* @category String
|
|
796
|
+
*/
|
|
797
|
+
export const createIdFromString = (value) => {
|
|
798
|
+
const hash = sha256(utf8ToBytes(value));
|
|
799
|
+
// Take first 16 bytes of hash and convert to Id
|
|
800
|
+
const id = idBytesToId(hash.slice(0, 16));
|
|
801
|
+
return id;
|
|
802
|
+
};
|
|
803
|
+
/**
|
|
804
|
+
* Creates a branded {@link Id} Type for a table's primary key.
|
|
805
|
+
*
|
|
806
|
+
* The table name becomes an additional brand for type safety.
|
|
583
807
|
*
|
|
584
808
|
* ### Example
|
|
585
809
|
*
|
|
@@ -592,32 +816,36 @@ export const createId = (deps) => deps.nanoIdLib.nanoid();
|
|
|
592
816
|
* @category String
|
|
593
817
|
*/
|
|
594
818
|
export const id = (table) => {
|
|
595
|
-
const fromParent = (value) => {
|
|
596
|
-
const idResult = Id.fromParent(value);
|
|
597
|
-
if (!idResult.ok) {
|
|
598
|
-
return err({ type: "Id", value, table });
|
|
599
|
-
}
|
|
600
|
-
return ok(idResult.value);
|
|
601
|
-
};
|
|
602
819
|
const fromUnknown = (value) => {
|
|
603
820
|
const parentResult = String.fromUnknown(value);
|
|
604
821
|
if (!parentResult.ok)
|
|
605
822
|
return parentResult;
|
|
606
823
|
return fromParent(parentResult.value);
|
|
607
824
|
};
|
|
825
|
+
const fromParent = (value) => {
|
|
826
|
+
const idResult = Id.fromParent(value);
|
|
827
|
+
if (!idResult.ok) {
|
|
828
|
+
return err({ type: "TableId", value, table });
|
|
829
|
+
}
|
|
830
|
+
return ok(idResult.value);
|
|
831
|
+
};
|
|
608
832
|
return {
|
|
609
|
-
...createType("Id", {
|
|
610
|
-
fromUnknown,
|
|
611
|
-
to: (value) => value,
|
|
612
|
-
fromParent,
|
|
613
|
-
toParent: (value) => value,
|
|
614
|
-
}),
|
|
833
|
+
...createType("Id", { fromUnknown, fromParent }),
|
|
615
834
|
table,
|
|
616
835
|
};
|
|
617
836
|
};
|
|
618
|
-
export const
|
|
619
|
-
/**
|
|
620
|
-
|
|
837
|
+
export const formatTableIdError = createTypeErrorFormatter((error) => `Invalid Id for table ${error.table}: ${error.value}.`);
|
|
838
|
+
/** Binary representation of an {@link Id}. */
|
|
839
|
+
export const IdBytes = brand("IdBytes", length(16)(Uint8Array));
|
|
840
|
+
export const idBytesTypeValueLength = 16;
|
|
841
|
+
export const idToIdBytes = (id) =>
|
|
842
|
+
// Id is Base64Url (validated by Id.from), cast is safe
|
|
843
|
+
base64UrlToUint8Array(id);
|
|
844
|
+
export const idBytesToId = (idBytes) =>
|
|
845
|
+
// Base64Url encoding of 16 bytes always produces valid Id (22 chars)
|
|
846
|
+
uint8ArrayToBase64Url(idBytes);
|
|
847
|
+
/**
|
|
848
|
+
* Positive number (> 0).
|
|
621
849
|
*
|
|
622
850
|
* ### Example
|
|
623
851
|
*
|
|
@@ -631,9 +859,9 @@ export const formatIdError = createTypeErrorFormatter((error) => `Invalid ${erro
|
|
|
631
859
|
* @category Number
|
|
632
860
|
*/
|
|
633
861
|
export const positive = (parent) => brand("Positive", parent, (value) => value > 0 ? ok(value) : err({ type: "Positive", value }));
|
|
634
|
-
export const formatPositiveError = createTypeErrorFormatter((error) => `The value ${error.value}
|
|
862
|
+
export const formatPositiveError = createTypeErrorFormatter((error) => `The value ${error.value} must be positive (> 0).`);
|
|
635
863
|
/**
|
|
636
|
-
* Negative number.
|
|
864
|
+
* Negative number (< 0).
|
|
637
865
|
*
|
|
638
866
|
* ### Example
|
|
639
867
|
*
|
|
@@ -644,9 +872,9 @@ export const formatPositiveError = createTypeErrorFormatter((error) => `The valu
|
|
|
644
872
|
* @category Number
|
|
645
873
|
*/
|
|
646
874
|
export const negative = (parent) => brand("Negative", parent, (value) => value < 0 ? ok(value) : err({ type: "Negative", value }));
|
|
647
|
-
export const formatNegativeError = createTypeErrorFormatter((error) => `The value ${error.value}
|
|
875
|
+
export const formatNegativeError = createTypeErrorFormatter((error) => `The value ${error.value} must be negative (< 0).`);
|
|
648
876
|
/**
|
|
649
|
-
* Non-positive number.
|
|
877
|
+
* Non-positive number (≤ 0).
|
|
650
878
|
*
|
|
651
879
|
* ### Example
|
|
652
880
|
*
|
|
@@ -659,9 +887,9 @@ export const formatNegativeError = createTypeErrorFormatter((error) => `The valu
|
|
|
659
887
|
export const nonPositive = (parent) => brand("NonPositive", parent, (value) => value <= 0
|
|
660
888
|
? ok(value)
|
|
661
889
|
: err({ type: "NonPositive", value }));
|
|
662
|
-
export const formatNonPositiveError = createTypeErrorFormatter((error) => `The value ${error.value}
|
|
890
|
+
export const formatNonPositiveError = createTypeErrorFormatter((error) => `The value ${error.value} must be non-positive (≤ 0).`);
|
|
663
891
|
/**
|
|
664
|
-
* Non-negative number.
|
|
892
|
+
* Non-negative number (≥ 0).
|
|
665
893
|
*
|
|
666
894
|
* ### Example
|
|
667
895
|
*
|
|
@@ -674,14 +902,30 @@ export const formatNonPositiveError = createTypeErrorFormatter((error) => `The v
|
|
|
674
902
|
export const nonNegative = (parent) => brand("NonNegative", parent, (value) => value >= 0
|
|
675
903
|
? ok(value)
|
|
676
904
|
: err({ type: "NonNegative", value }));
|
|
677
|
-
export const formatNonNegativeError = createTypeErrorFormatter((error) => `The value ${error.value}
|
|
678
|
-
/**
|
|
905
|
+
export const formatNonNegativeError = createTypeErrorFormatter((error) => `The value ${error.value} must be non-negative (≥ 0).`);
|
|
906
|
+
/**
|
|
907
|
+
* Non-negative number (≥ 0).
|
|
908
|
+
*
|
|
909
|
+
* @category Number
|
|
910
|
+
*/
|
|
679
911
|
export const NonNegativeNumber = nonNegative(Number);
|
|
680
|
-
/**
|
|
912
|
+
/**
|
|
913
|
+
* Positive number (> 0).
|
|
914
|
+
*
|
|
915
|
+
* @category Number
|
|
916
|
+
*/
|
|
681
917
|
export const PositiveNumber = positive(NonNegativeNumber);
|
|
682
|
-
/**
|
|
918
|
+
/**
|
|
919
|
+
* Non-positive number (≤ 0).
|
|
920
|
+
*
|
|
921
|
+
* @category Number
|
|
922
|
+
*/
|
|
683
923
|
export const NonPositiveNumber = nonPositive(Number);
|
|
684
|
-
/**
|
|
924
|
+
/**
|
|
925
|
+
* Negative number (< 0).
|
|
926
|
+
*
|
|
927
|
+
* @category Number
|
|
928
|
+
*/
|
|
685
929
|
export const NegativeNumber = negative(NonPositiveNumber);
|
|
686
930
|
/**
|
|
687
931
|
* Integer within the safe range of JavaScript numbers.
|
|
@@ -697,20 +941,38 @@ export const NegativeNumber = negative(NonPositiveNumber);
|
|
|
697
941
|
export const int = (parent) => brand("Int", parent, (value) => globalThis.Number.isSafeInteger(value)
|
|
698
942
|
? ok(value)
|
|
699
943
|
: err({ type: "Int", value }));
|
|
700
|
-
export const formatIntError = createTypeErrorFormatter((error) => `The value ${error.value}
|
|
944
|
+
export const formatIntError = createTypeErrorFormatter((error) => `The value ${error.value} must be an integer.`);
|
|
701
945
|
/**
|
|
702
946
|
* Integer within the safe range of JavaScript numbers.
|
|
703
947
|
*
|
|
704
948
|
* @category Number
|
|
705
949
|
*/
|
|
706
950
|
export const Int = int(Number);
|
|
707
|
-
/**
|
|
951
|
+
/**
|
|
952
|
+
* Non-negative integer (≥ 0).
|
|
953
|
+
*
|
|
954
|
+
* @category Number
|
|
955
|
+
*/
|
|
708
956
|
export const NonNegativeInt = nonNegative(Int);
|
|
709
|
-
/**
|
|
957
|
+
/**
|
|
958
|
+
* Positive integer (> 0).
|
|
959
|
+
*
|
|
960
|
+
* @category Number
|
|
961
|
+
*/
|
|
710
962
|
export const PositiveInt = positive(NonNegativeInt);
|
|
711
|
-
/**
|
|
963
|
+
/** Maximum safe positive integer value for practically infinite operations. */
|
|
964
|
+
export const maxPositiveInt = PositiveInt.orThrow(globalThis.Number.MAX_SAFE_INTEGER);
|
|
965
|
+
/**
|
|
966
|
+
* Non-positive integer (≤ 0).
|
|
967
|
+
*
|
|
968
|
+
* @category Number
|
|
969
|
+
*/
|
|
712
970
|
export const NonPositiveInt = nonPositive(Int);
|
|
713
|
-
/**
|
|
971
|
+
/**
|
|
972
|
+
* Negative integer (< 0).
|
|
973
|
+
*
|
|
974
|
+
* @category Number
|
|
975
|
+
*/
|
|
714
976
|
export const NegativeInt = negative(NonPositiveInt);
|
|
715
977
|
/**
|
|
716
978
|
* Number greater than a specified value.
|
|
@@ -750,7 +1012,7 @@ export const formatLessThanOrEqualToError = createTypeErrorFormatter((error) =>
|
|
|
750
1012
|
export const nonNaN = (parent) => brand("NonNaN", parent, (value) => !globalThis.Number.isNaN(value)
|
|
751
1013
|
? ok(value)
|
|
752
1014
|
: err({ type: "NonNaN", value }));
|
|
753
|
-
export const formatNonNaNError = createTypeErrorFormatter((
|
|
1015
|
+
export const formatNonNaNError = createTypeErrorFormatter(() => `The value must not be NaN.`);
|
|
754
1016
|
/** @category Number */
|
|
755
1017
|
export const NonNaNNumber = nonNaN(Number);
|
|
756
1018
|
/**
|
|
@@ -761,7 +1023,7 @@ export const NonNaNNumber = nonNaN(Number);
|
|
|
761
1023
|
export const finite = (parent) => brand("Finite", parent, (value) => globalThis.Number.isFinite(value)
|
|
762
1024
|
? ok(value)
|
|
763
1025
|
: err({ type: "Finite", value }));
|
|
764
|
-
export const formatFiniteError = createTypeErrorFormatter((error) => `The value ${error.value}
|
|
1026
|
+
export const formatFiniteError = createTypeErrorFormatter((error) => `The value ${error.value} must be finite.`);
|
|
765
1027
|
/**
|
|
766
1028
|
* Finite number.
|
|
767
1029
|
*
|
|
@@ -802,8 +1064,6 @@ export const between = (min, max) => (parent) => brand(`Between${min}-${max}`, p
|
|
|
802
1064
|
? ok(value)
|
|
803
1065
|
: err({ type: "Between", value, min, max }));
|
|
804
1066
|
export const formatBetweenError = createTypeErrorFormatter((error) => `The value ${error.value} is not between ${error.min} and ${error.max}, inclusive.`);
|
|
805
|
-
/** @category Number */
|
|
806
|
-
export const Between1And10 = between(1, 10)(Number);
|
|
807
1067
|
/**
|
|
808
1068
|
* Literal {@link Type}.
|
|
809
1069
|
*
|
|
@@ -828,90 +1088,12 @@ export const literal = (expected) => {
|
|
|
828
1088
|
return {
|
|
829
1089
|
...createType("Literal", {
|
|
830
1090
|
fromUnknown,
|
|
831
|
-
to: identity,
|
|
832
1091
|
fromParent: ok,
|
|
833
|
-
toParent: identity,
|
|
834
1092
|
}),
|
|
835
1093
|
expected,
|
|
836
1094
|
};
|
|
837
1095
|
};
|
|
838
1096
|
export const formatLiteralError = createTypeErrorFormatter((error) => `The value ${error.value} is not strictly equal to the expected literal: ${globalThis.String(error.expected)}.`);
|
|
839
|
-
/**
|
|
840
|
-
* {@link Type} that transforms values between `FromType` and `ToType`.
|
|
841
|
-
*
|
|
842
|
-
* - `fromParent`: Converts `FromType` to `ToType`, may fail.
|
|
843
|
-
* - `toParent`: Converts `ToType` back to `FromType`, must not fail.
|
|
844
|
-
*
|
|
845
|
-
* ### Example
|
|
846
|
-
*
|
|
847
|
-
* // TODO: Examples
|
|
848
|
-
*
|
|
849
|
-
* @category Base Factories
|
|
850
|
-
*/
|
|
851
|
-
export const transform = (fromType, toType, fromParent, toParent) => {
|
|
852
|
-
const fromUnknown = (value) => {
|
|
853
|
-
const parentResult = fromType.fromUnknown(value);
|
|
854
|
-
if (!parentResult.ok)
|
|
855
|
-
return parentResult;
|
|
856
|
-
return fromParent(parentResult.value);
|
|
857
|
-
};
|
|
858
|
-
const to = (value) => fromType.to(toParent(value));
|
|
859
|
-
return {
|
|
860
|
-
...createType("Transform", {
|
|
861
|
-
fromUnknown,
|
|
862
|
-
to,
|
|
863
|
-
fromParent,
|
|
864
|
-
toParent,
|
|
865
|
-
}),
|
|
866
|
-
is: toType.is,
|
|
867
|
-
fromType,
|
|
868
|
-
toType,
|
|
869
|
-
};
|
|
870
|
-
};
|
|
871
|
-
/**
|
|
872
|
-
* Trims leading and trailing whitespace from a string.
|
|
873
|
-
*
|
|
874
|
-
* ### Example
|
|
875
|
-
*
|
|
876
|
-
* ```ts
|
|
877
|
-
* expect(TrimString.from("a ")).toEqual(ok("a"));
|
|
878
|
-
* expect(TrimString.fromParent("a ").value).toEqual("a");
|
|
879
|
-
* ```
|
|
880
|
-
*
|
|
881
|
-
* @category String
|
|
882
|
-
*/
|
|
883
|
-
export const TrimString = trim(String);
|
|
884
|
-
/**
|
|
885
|
-
* Transforms a {@link Date} into a {@link DateIsoString} string and vice versa.
|
|
886
|
-
*
|
|
887
|
-
* ### Example
|
|
888
|
-
*
|
|
889
|
-
* TODO:
|
|
890
|
-
*
|
|
891
|
-
* @category String
|
|
892
|
-
*/
|
|
893
|
-
export const DateIso = transform(Date, DateIsoString, (value) => DateIsoString.fromParent(value.toISOString()), (value) => new globalThis.Date(value));
|
|
894
|
-
/**
|
|
895
|
-
* Transforms a {@link NonEmptyTrimmedString} into a {@link FiniteNumber}.
|
|
896
|
-
*
|
|
897
|
-
* ### Example
|
|
898
|
-
*
|
|
899
|
-
* ```ts
|
|
900
|
-
* NumberFromString.from("42"); // ok(42)
|
|
901
|
-
* NumberFromString.from("abc"); // err({ type: "NumberFromString", value: "abc" })
|
|
902
|
-
* ```
|
|
903
|
-
*
|
|
904
|
-
* @category Number
|
|
905
|
-
*/
|
|
906
|
-
export const NumberFromString = transform(NonEmptyTrimmedString, FiniteNumber, (value) => {
|
|
907
|
-
const result = FiniteNumber.fromParent(globalThis.Number(value));
|
|
908
|
-
if (!result.ok)
|
|
909
|
-
return err({ type: "NumberFromString", value });
|
|
910
|
-
return result;
|
|
911
|
-
}, (num) => num.toString());
|
|
912
|
-
export const formatNumberFromStringError = createTypeErrorFormatter((error) => {
|
|
913
|
-
return `The value ${error.value} could not be converted to a finite number.`;
|
|
914
|
-
});
|
|
915
1097
|
/**
|
|
916
1098
|
* Array of a specific {@link Type}.
|
|
917
1099
|
*
|
|
@@ -954,7 +1136,6 @@ export const array = (element) => {
|
|
|
954
1136
|
}
|
|
955
1137
|
return ok(result);
|
|
956
1138
|
};
|
|
957
|
-
const to = (value) => value.map(element.to);
|
|
958
1139
|
const fromParent = (value) => {
|
|
959
1140
|
const result = [];
|
|
960
1141
|
for (let i = 0; i < value.length; i++) {
|
|
@@ -974,14 +1155,8 @@ export const array = (element) => {
|
|
|
974
1155
|
}
|
|
975
1156
|
return ok(result);
|
|
976
1157
|
};
|
|
977
|
-
const toParent = (values) => values.map(element.toParent);
|
|
978
1158
|
return {
|
|
979
|
-
...createType("Array", {
|
|
980
|
-
fromUnknown,
|
|
981
|
-
to,
|
|
982
|
-
fromParent,
|
|
983
|
-
toParent,
|
|
984
|
-
}),
|
|
1159
|
+
...createType("Array", { fromUnknown, fromParent }),
|
|
985
1160
|
element,
|
|
986
1161
|
};
|
|
987
1162
|
};
|
|
@@ -1055,10 +1230,6 @@ export const record = (keyType, valueType) => {
|
|
|
1055
1230
|
}
|
|
1056
1231
|
return ok(result);
|
|
1057
1232
|
};
|
|
1058
|
-
const to = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
|
|
1059
|
-
keyType.to(key),
|
|
1060
|
-
valueType.to(val),
|
|
1061
|
-
]));
|
|
1062
1233
|
const fromParent = (value) => {
|
|
1063
1234
|
const result = {};
|
|
1064
1235
|
for (const [rawKey, rawValue] of Object.entries(value)) {
|
|
@@ -1090,16 +1261,10 @@ export const record = (keyType, valueType) => {
|
|
|
1090
1261
|
}
|
|
1091
1262
|
return ok(result);
|
|
1092
1263
|
};
|
|
1093
|
-
const toParent = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
|
|
1094
|
-
keyType.toParent(key),
|
|
1095
|
-
valueType.toParent(val),
|
|
1096
|
-
]));
|
|
1097
1264
|
return {
|
|
1098
1265
|
...createType("Record", {
|
|
1099
1266
|
fromUnknown,
|
|
1100
|
-
to,
|
|
1101
1267
|
fromParent,
|
|
1102
|
-
toParent,
|
|
1103
1268
|
}),
|
|
1104
1269
|
key: keyType,
|
|
1105
1270
|
value: valueType,
|
|
@@ -1186,22 +1351,6 @@ export function object(props, record) {
|
|
|
1186
1351
|
}
|
|
1187
1352
|
return ok(result);
|
|
1188
1353
|
};
|
|
1189
|
-
const to = (value) => {
|
|
1190
|
-
const entries = [];
|
|
1191
|
-
for (const key of propKeys) {
|
|
1192
|
-
if (!(key in value) && isOptionalType(props[key])) {
|
|
1193
|
-
continue;
|
|
1194
|
-
}
|
|
1195
|
-
entries.push([key, props[key].to(value[key])]);
|
|
1196
|
-
}
|
|
1197
|
-
if (record) {
|
|
1198
|
-
const recordEntries = Object.entries(value).filter(([key]) => !propKeys.includes(key));
|
|
1199
|
-
for (const [key, val] of recordEntries) {
|
|
1200
|
-
entries.push([record.key.to(key), record.value.to(val)]);
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
return Object.fromEntries(entries);
|
|
1204
|
-
};
|
|
1205
1354
|
const fromParent = (value) => {
|
|
1206
1355
|
const errors = {};
|
|
1207
1356
|
const result = {};
|
|
@@ -1249,28 +1398,10 @@ export function object(props, record) {
|
|
|
1249
1398
|
}
|
|
1250
1399
|
return ok(result);
|
|
1251
1400
|
};
|
|
1252
|
-
const toParent = (value) => {
|
|
1253
|
-
const entries = [];
|
|
1254
|
-
for (const key of propKeys) {
|
|
1255
|
-
if (!(key in value) && isOptionalType(props[key])) {
|
|
1256
|
-
continue;
|
|
1257
|
-
}
|
|
1258
|
-
entries.push([key, props[key].toParent(value[key])]);
|
|
1259
|
-
}
|
|
1260
|
-
if (record) {
|
|
1261
|
-
const recordEntries = Object.entries(value).filter(([key]) => !propKeys.includes(key));
|
|
1262
|
-
for (const [key, val] of recordEntries) {
|
|
1263
|
-
entries.push([record.key.toParent(key), record.value.toParent(val)]);
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
return Object.fromEntries(entries);
|
|
1267
|
-
};
|
|
1268
1401
|
return {
|
|
1269
1402
|
...createType(record ? "ObjectWithRecord" : "Object", {
|
|
1270
1403
|
fromUnknown,
|
|
1271
|
-
to,
|
|
1272
1404
|
fromParent,
|
|
1273
|
-
toParent,
|
|
1274
1405
|
}),
|
|
1275
1406
|
props,
|
|
1276
1407
|
...(record ? { record } : {}),
|
|
@@ -1323,7 +1454,6 @@ export function union(...args) {
|
|
|
1323
1454
|
* - Generate a specialized function to improve validation performance for such
|
|
1324
1455
|
* cases.
|
|
1325
1456
|
*/
|
|
1326
|
-
/* eslint-disable */
|
|
1327
1457
|
const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
|
|
1328
1458
|
const fromUnknown = (value) => {
|
|
1329
1459
|
const errors = [];
|
|
@@ -1339,23 +1469,13 @@ export function union(...args) {
|
|
|
1339
1469
|
errors,
|
|
1340
1470
|
});
|
|
1341
1471
|
};
|
|
1342
|
-
const to = (value) => {
|
|
1343
|
-
for (const member of members) {
|
|
1344
|
-
if (member.is(value))
|
|
1345
|
-
return member.to(value);
|
|
1346
|
-
}
|
|
1347
|
-
assert(false, "No matching member found in Union Type `to` function");
|
|
1348
|
-
};
|
|
1349
1472
|
return {
|
|
1350
1473
|
...createType("Union", {
|
|
1351
1474
|
fromUnknown,
|
|
1352
|
-
to,
|
|
1353
1475
|
fromParent: fromUnknown,
|
|
1354
|
-
toParent: to,
|
|
1355
1476
|
}),
|
|
1356
1477
|
members,
|
|
1357
1478
|
};
|
|
1358
|
-
/* eslint-enable */
|
|
1359
1479
|
}
|
|
1360
1480
|
export const formatUnionError = (formatTypeError) => createTypeErrorFormatter((error) => {
|
|
1361
1481
|
const formattedErrors = error.errors
|
|
@@ -1411,20 +1531,10 @@ export const recursive = (create) => {
|
|
|
1411
1531
|
type ??= create();
|
|
1412
1532
|
return type.fromUnknown(value);
|
|
1413
1533
|
},
|
|
1414
|
-
to: (value) => {
|
|
1415
|
-
type ??= create();
|
|
1416
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
1417
|
-
return type.to(value);
|
|
1418
|
-
},
|
|
1419
1534
|
fromParent: (value) => {
|
|
1420
1535
|
type ??= create();
|
|
1421
1536
|
return type.fromParent(value);
|
|
1422
1537
|
},
|
|
1423
|
-
toParent: (value) => {
|
|
1424
|
-
type ??= create();
|
|
1425
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
1426
|
-
return type.toParent(value);
|
|
1427
|
-
},
|
|
1428
1538
|
is: (value) => {
|
|
1429
1539
|
type ??= create();
|
|
1430
1540
|
return type.is(value);
|
|
@@ -1528,7 +1638,6 @@ export const tuple = (...elements) => {
|
|
|
1528
1638
|
}
|
|
1529
1639
|
return ok(result);
|
|
1530
1640
|
};
|
|
1531
|
-
const to = (value) => value.map((val, index) => elements[index].to(val));
|
|
1532
1641
|
const fromParent = (value) => {
|
|
1533
1642
|
if (!Array.isArray(value) || value.length !== elements.length) {
|
|
1534
1643
|
return err({
|
|
@@ -1555,13 +1664,10 @@ export const tuple = (...elements) => {
|
|
|
1555
1664
|
}
|
|
1556
1665
|
return ok(result);
|
|
1557
1666
|
};
|
|
1558
|
-
const toParent = (value) => value.map((val, index) => elements[index].toParent(val));
|
|
1559
1667
|
return {
|
|
1560
1668
|
...createType("Tuple", {
|
|
1561
1669
|
fromUnknown,
|
|
1562
|
-
to,
|
|
1563
1670
|
fromParent,
|
|
1564
|
-
toParent,
|
|
1565
1671
|
}),
|
|
1566
1672
|
elements,
|
|
1567
1673
|
};
|
|
@@ -1595,22 +1701,32 @@ export const Int64 = brand("Int64", BigInt, (value) => value >= -922337203685477
|
|
|
1595
1701
|
? ok(value)
|
|
1596
1702
|
: err({ type: "Int64", value }));
|
|
1597
1703
|
export const formatInt64Error = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid 64-bit signed integer (Int64).`);
|
|
1598
|
-
|
|
1599
|
-
export const
|
|
1704
|
+
// // co s timhle? je to string, ze ktereho lze udelat bigint
|
|
1705
|
+
// export const BigIntFromString = transform(
|
|
1706
|
+
// String,
|
|
1707
|
+
// BigInt,
|
|
1708
|
+
// (value) =>
|
|
1709
|
+
// trySync(
|
|
1710
|
+
// () => globalThis.BigInt(value),
|
|
1711
|
+
// (): BigIntFromStringError => ({ type: "BigIntFromString", value }),
|
|
1712
|
+
// ),
|
|
1713
|
+
// (value) => value.toString(),
|
|
1714
|
+
// );
|
|
1715
|
+
// export interface BigIntFromStringError extends TypeError<"BigIntFromString"> {}
|
|
1716
|
+
// export const formatBigIntFromStringError =
|
|
1717
|
+
// createTypeErrorFormatter<BigIntFromStringError>(
|
|
1718
|
+
// (error) => `The value ${error.value} could not be converted to a BigInt.`,
|
|
1719
|
+
// );
|
|
1600
1720
|
/**
|
|
1601
1721
|
* Stringified {@link Int64}.
|
|
1602
1722
|
*
|
|
1603
|
-
* @category
|
|
1723
|
+
* @category String
|
|
1604
1724
|
*/
|
|
1605
|
-
export const Int64String = brand("Int64",
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
return ok(value);
|
|
1611
|
-
}
|
|
1612
|
-
return err({ type: "Int64String", value });
|
|
1613
|
-
});
|
|
1725
|
+
export const Int64String = brand("Int64", NonEmptyTrimmedString, (value) => trySync(() => {
|
|
1726
|
+
const maybeInt = globalThis.BigInt(value);
|
|
1727
|
+
Int64.orThrow(maybeInt);
|
|
1728
|
+
return value;
|
|
1729
|
+
}, () => ({ type: "Int64String", value })));
|
|
1614
1730
|
export const formatInt64StringError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Int64 string.`);
|
|
1615
1731
|
/**
|
|
1616
1732
|
* JSON-compatible value: string, {@link FiniteNumber}, boolean, null,
|
|
@@ -1631,48 +1747,83 @@ export const JsonArray = array(JsonValue);
|
|
|
1631
1747
|
* @category Object
|
|
1632
1748
|
*/
|
|
1633
1749
|
export const JsonObject = record(String, JsonValue);
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
* JsonValue back into a JSON string.
|
|
1637
|
-
*
|
|
1638
|
-
* ### Example
|
|
1639
|
-
*
|
|
1640
|
-
* ```ts
|
|
1641
|
-
* JsonValueFromString.from(`{"key":"value"}`); // -> ok({ key: "value" })
|
|
1642
|
-
* JsonValueFromString.to({ key: "value" }); // -> '{"key":"value"}'
|
|
1643
|
-
* ```
|
|
1644
|
-
*
|
|
1645
|
-
* @category String
|
|
1646
|
-
*/
|
|
1647
|
-
export const JsonValueFromString = transform(String, JsonValue, (value) => trySync(() => JSON.parse(value), (error) => ({
|
|
1648
|
-
type: `JsonValueFromString`,
|
|
1750
|
+
export const parseJson = (value) => trySync(() => JSON.parse(value), (error) => ({
|
|
1751
|
+
type: `Json`,
|
|
1649
1752
|
value,
|
|
1650
1753
|
message: globalThis.String(error),
|
|
1651
|
-
}))
|
|
1652
|
-
export const formatJsonValueFromStringError = createTypeErrorFormatter((error) => `Invalid JSONValue: ${error.value}. Error: ${error.message}`);
|
|
1754
|
+
}));
|
|
1653
1755
|
/**
|
|
1654
1756
|
* JSON-string {@link Type}.
|
|
1655
1757
|
*
|
|
1656
1758
|
* ### Example
|
|
1657
1759
|
*
|
|
1658
1760
|
* ```ts
|
|
1659
|
-
* const result = Json.from('{"key":"value"}'); //
|
|
1660
|
-
* const error = Json.from("invalid json"); //
|
|
1761
|
+
* const result = Json.from('{"key":"value"}'); // ok
|
|
1762
|
+
* const error = Json.from("invalid json"); // err
|
|
1661
1763
|
* ```
|
|
1662
1764
|
*
|
|
1663
1765
|
* @category String
|
|
1664
1766
|
*/
|
|
1665
1767
|
export const Json = brand("Json", String, (value) => {
|
|
1666
|
-
const result =
|
|
1768
|
+
const result = parseJson(value);
|
|
1667
1769
|
if (!result.ok)
|
|
1668
|
-
return
|
|
1669
|
-
type: "Json",
|
|
1670
|
-
value,
|
|
1671
|
-
message: result.error.message,
|
|
1672
|
-
});
|
|
1770
|
+
return result;
|
|
1673
1771
|
return ok(value);
|
|
1674
1772
|
});
|
|
1675
1773
|
export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON: ${error.value}. Error: ${error.message}`);
|
|
1774
|
+
export const jsonValueToJson = (value) => JSON.stringify(value);
|
|
1775
|
+
export const jsonToJsonValue = (value) => JSON.parse(value);
|
|
1776
|
+
/**
|
|
1777
|
+
* Creates a branded JSON string {@link Type} and type-safe conversion functions
|
|
1778
|
+
* for a given Type.
|
|
1779
|
+
*
|
|
1780
|
+
* This factory creates:
|
|
1781
|
+
*
|
|
1782
|
+
* 1. A branded string Type that validates JSON parsing and structural conformity
|
|
1783
|
+
* 2. A serialization function (Type → branded JSON string)
|
|
1784
|
+
* 3. A parsing function (branded JSON string → Type, skipping validation)
|
|
1785
|
+
*
|
|
1786
|
+
* Optimized for Evolu's SQLite workflow where we store typed JSON strings and
|
|
1787
|
+
* need type-safe conversions without double parsing.
|
|
1788
|
+
*
|
|
1789
|
+
* ### Example
|
|
1790
|
+
*
|
|
1791
|
+
* ```ts
|
|
1792
|
+
* const Person = object({
|
|
1793
|
+
* name: NonEmptyString100,
|
|
1794
|
+
* age: FiniteNumber,
|
|
1795
|
+
* });
|
|
1796
|
+
* type Person = typeof Person.Type;
|
|
1797
|
+
*
|
|
1798
|
+
* const [PersonJson, personToPersonJson, personJsonToPerson] = json(
|
|
1799
|
+
* Person,
|
|
1800
|
+
* "PersonJson",
|
|
1801
|
+
* );
|
|
1802
|
+
* // string & Brand<"PersonJson">
|
|
1803
|
+
* type PersonJson = typeof PersonJson.Type;
|
|
1804
|
+
*
|
|
1805
|
+
* // Usage:
|
|
1806
|
+
* const person: Person = { name: "Alice", age: 30 };
|
|
1807
|
+
* const jsonString = personToPersonJson(person); // PersonJson
|
|
1808
|
+
* const backToPerson = personJsonToPerson(jsonString); // Person
|
|
1809
|
+
* ```
|
|
1810
|
+
*/
|
|
1811
|
+
export const json = (type, name) => {
|
|
1812
|
+
const BrandedJsonType = brand(name, String, (value) => {
|
|
1813
|
+
const parseResult = parseJson(value);
|
|
1814
|
+
if (!parseResult.ok)
|
|
1815
|
+
return parseResult;
|
|
1816
|
+
const validationResult = type.fromUnknown(parseResult.value);
|
|
1817
|
+
if (!validationResult.ok)
|
|
1818
|
+
return validationResult;
|
|
1819
|
+
return ok(value);
|
|
1820
|
+
});
|
|
1821
|
+
return [
|
|
1822
|
+
BrandedJsonType,
|
|
1823
|
+
jsonValueToJson,
|
|
1824
|
+
jsonToJsonValue,
|
|
1825
|
+
];
|
|
1826
|
+
};
|
|
1676
1827
|
/**
|
|
1677
1828
|
* Optional {@link Type}.
|
|
1678
1829
|
*
|
|
@@ -1697,9 +1848,7 @@ export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON
|
|
|
1697
1848
|
export const optional = (type) => ({
|
|
1698
1849
|
...createType("Optional", {
|
|
1699
1850
|
fromUnknown: type.fromUnknown,
|
|
1700
|
-
to: type.to,
|
|
1701
1851
|
fromParent: type.fromParent,
|
|
1702
|
-
toParent: type.toParent,
|
|
1703
1852
|
}),
|
|
1704
1853
|
parent: type,
|
|
1705
1854
|
});
|
|
@@ -1708,7 +1857,7 @@ export const isOptionalType = (x) => typeof x === "object" && x != null && "name
|
|
|
1708
1857
|
/**
|
|
1709
1858
|
* Creates a partial object type where all properties are optional.
|
|
1710
1859
|
*
|
|
1711
|
-
* This is useful when
|
|
1860
|
+
* This is useful when we want to validate an object in which none of the keys
|
|
1712
1861
|
* are required, but if they are present they must conform to their
|
|
1713
1862
|
* corresponding Types.
|
|
1714
1863
|
*
|
|
@@ -1779,114 +1928,86 @@ export function omit(objectType, ...keys) {
|
|
|
1779
1928
|
}
|
|
1780
1929
|
return object(newProps);
|
|
1781
1930
|
}
|
|
1931
|
+
export const maxMutationSize = 655360;
|
|
1782
1932
|
/**
|
|
1783
|
-
*
|
|
1784
|
-
*
|
|
1785
|
-
*
|
|
1786
|
-
*
|
|
1787
|
-
* ### Example
|
|
1788
|
-
*
|
|
1789
|
-
* ```ts
|
|
1790
|
-
* const Person = object({
|
|
1791
|
-
* name: NonEmptyString50,
|
|
1792
|
-
* age: FiniteNumber,
|
|
1793
|
-
* });
|
|
1794
|
-
* type Person = typeof Person.Type;
|
|
1795
|
-
*
|
|
1796
|
-
* const PersonJson = json(Person, "PersonJson");
|
|
1797
|
-
* // string & Brand<"PersonJson">
|
|
1798
|
-
* type PersonJson = typeof PersonJson.Type;
|
|
1799
|
-
*
|
|
1800
|
-
* // Person -> string & Brand<"PersonJson">
|
|
1801
|
-
* const personJson = PersonJson.from({ name: "Alice", age: 30 });
|
|
1802
|
-
* expect(personJson).toEqual(ok('{"name":"Alice","age":30}'));
|
|
1803
|
-
*
|
|
1804
|
-
* // string & Brand<"PersonJson"> -> Person
|
|
1805
|
-
* const person = PersonJson.to(personJson);
|
|
1806
|
-
*
|
|
1807
|
-
* // serialize/parse any JSON value
|
|
1808
|
-
* const AnyJson = json(JsonValue, "AnyJson");
|
|
1809
|
-
* ```
|
|
1933
|
+
* Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
|
|
1934
|
+
* the `maxProtocolMessageRangesSize`. The max size is 640KB in bytes, measured
|
|
1935
|
+
* via MessagePack. Evolu Protocol DbChange will be smaller thanks to various
|
|
1936
|
+
* optimizations.
|
|
1810
1937
|
*/
|
|
1811
|
-
export const
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
return jsonValue;
|
|
1816
|
-
const parsed = type.fromUnknown(jsonValue.value);
|
|
1817
|
-
if (!parsed.ok)
|
|
1818
|
-
return parsed;
|
|
1819
|
-
return ok(value);
|
|
1820
|
-
});
|
|
1821
|
-
return transform(type, BrandedJsonString, (value) => ok(JSON.stringify(value)),
|
|
1822
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
1823
|
-
(value) => JSON.parse(value));
|
|
1824
|
-
};
|
|
1938
|
+
export const validMutationSize = (type) => brand("ValidMutationSize", type, (value) => pack(value).byteLength <= maxMutationSize
|
|
1939
|
+
? ok(value)
|
|
1940
|
+
: err({ type: "ValidMutationSize", value }));
|
|
1941
|
+
export const formatValidMutationSizeError = createTypeErrorFormatter((error) => `The mutation size exceeds the maximum limit of ${maxMutationSize} bytes. The provided mutation has a size of ${pack(error.value).byteLength} bytes.`);
|
|
1825
1942
|
/**
|
|
1826
|
-
*
|
|
1827
|
-
*
|
|
1828
|
-
*
|
|
1943
|
+
* Formats Evolu Type errors into user-friendly messages.
|
|
1944
|
+
*
|
|
1945
|
+
* Evolu Type typed errors ensure every error type must have a formatter.
|
|
1946
|
+
* TypeScript enforces this at compile-time, preventing unhandled validation
|
|
1947
|
+
* errors from reaching users.
|
|
1829
1948
|
*
|
|
1830
|
-
*
|
|
1831
|
-
*
|
|
1949
|
+
* The `createFormatTypeError` function handles both built-in {@link TypeErrors}
|
|
1950
|
+
* and custom errors, and lets us override default formatting for specific
|
|
1951
|
+
* errors.
|
|
1832
1952
|
*
|
|
1833
|
-
* ###
|
|
1953
|
+
* ### Example
|
|
1834
1954
|
*
|
|
1835
1955
|
* ```ts
|
|
1836
|
-
* const
|
|
1837
|
-
*
|
|
1838
|
-
*
|
|
1956
|
+
* const formatTypeError = createFormatTypeError<
|
|
1957
|
+
* MinLengthError | MaxLengthError
|
|
1958
|
+
* >((error): string => {
|
|
1959
|
+
* switch (error.type) {
|
|
1960
|
+
* case "MinLength":
|
|
1961
|
+
* return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
|
|
1962
|
+
* case "MaxLength":
|
|
1963
|
+
* return `Text is too long (maximum ${error.max} characters)`;
|
|
1964
|
+
* }
|
|
1965
|
+
* });
|
|
1839
1966
|
* ```
|
|
1840
1967
|
*
|
|
1841
|
-
*
|
|
1968
|
+
* Alternatively, write a custom formatter from scratch without using
|
|
1969
|
+
* `createFormatTypeError`. This gives us full control over error formatting:
|
|
1842
1970
|
*
|
|
1843
1971
|
* ```ts
|
|
1844
|
-
*
|
|
1845
|
-
*
|
|
1972
|
+
* const Person = object({
|
|
1973
|
+
* name: NonEmptyTrimmedString100,
|
|
1974
|
+
* age: optional(PositiveInt),
|
|
1975
|
+
* });
|
|
1976
|
+
*
|
|
1977
|
+
* // Define only the errors actually used by Person Type
|
|
1978
|
+
* type PersonErrors =
|
|
1846
1979
|
* | StringError
|
|
1847
|
-
* | MinLengthError
|
|
1848
1980
|
* | MaxLengthError
|
|
1849
|
-
* |
|
|
1850
|
-
* | IdError
|
|
1981
|
+
* | MinLengthError
|
|
1851
1982
|
* | TrimmedError
|
|
1852
|
-
* |
|
|
1853
|
-
* |
|
|
1854
|
-
*
|
|
1855
|
-
* |
|
|
1856
|
-
* |
|
|
1857
|
-
*
|
|
1858
|
-
* const formatTypeError: TypeErrorFormatter<
|
|
1859
|
-
* // In the real code, we would use the createTypeErrorFormatter helper
|
|
1860
|
-
* // that safely stringifies error value.
|
|
1983
|
+
* | PositiveError
|
|
1984
|
+
* | NonNegativeError
|
|
1985
|
+
* | IntError
|
|
1986
|
+
* | NumberError
|
|
1987
|
+
* | ObjectError<Record<string, PersonErrors>>;
|
|
1988
|
+
*
|
|
1989
|
+
* const formatTypeError: TypeErrorFormatter<PersonErrors> = (error) => {
|
|
1861
1990
|
* switch (error.type) {
|
|
1862
|
-
* case "Id":
|
|
1863
|
-
* return `Invalid Id on table: ${error.table}.`;
|
|
1864
|
-
* case "MaxLength":
|
|
1865
|
-
* return `Max length is ${error.max}.`;
|
|
1866
|
-
* case "MinLength":
|
|
1867
|
-
* return `Min length is ${error.min}.`;
|
|
1868
|
-
* case "Mnemonic":
|
|
1869
|
-
* return `Invalid mnemonic: ${String(error.value)}`;
|
|
1870
|
-
* case "Null":
|
|
1871
|
-
* return `Not null`;
|
|
1872
1991
|
* case "String":
|
|
1873
|
-
* // We can reuse existing formatter.
|
|
1874
1992
|
* return formatStringError(error);
|
|
1993
|
+
* case "Number":
|
|
1994
|
+
* return "Must be a number";
|
|
1995
|
+
* case "MinLength":
|
|
1996
|
+
* return `Must be at least ${error.min} characters`;
|
|
1997
|
+
* case "MaxLength":
|
|
1998
|
+
* return `Cannot exceed ${error.max} characters`;
|
|
1875
1999
|
* case "Trimmed":
|
|
1876
|
-
* return "
|
|
1877
|
-
* case "
|
|
1878
|
-
* return "
|
|
1879
|
-
* case "
|
|
1880
|
-
* return
|
|
1881
|
-
*
|
|
1882
|
-
*
|
|
1883
|
-
* return `Union errors: ${error.errors.map(formatTypeError).join(", ")}`;
|
|
2000
|
+
* return "Cannot have leading or trailing spaces";
|
|
2001
|
+
* case "Positive":
|
|
2002
|
+
* return "Must be a positive number";
|
|
2003
|
+
* case "NonNegative":
|
|
2004
|
+
* return "Must be zero or positive";
|
|
2005
|
+
* case "Int":
|
|
2006
|
+
* return "Must be an integer";
|
|
1884
2007
|
* case "Object": {
|
|
1885
|
-
* if (
|
|
1886
|
-
*
|
|
1887
|
-
*
|
|
1888
|
-
* )
|
|
1889
|
-
* return "A developer made an error, this should not happen.";
|
|
2008
|
+
* if (error.reason.kind === "NotObject") return "Must be an object";
|
|
2009
|
+
* if (error.reason.kind === "ExtraKeys")
|
|
2010
|
+
* return "Contains unexpected fields";
|
|
1890
2011
|
* const firstError = Object.values(error.reason.errors).find(
|
|
1891
2012
|
* (e) => e !== undefined,
|
|
1892
2013
|
* )!;
|
|
@@ -1927,8 +2048,8 @@ export const createFormatTypeError = (extraFormatter) => {
|
|
|
1927
2048
|
return formatIsTypeError(error);
|
|
1928
2049
|
case "CurrencyCode":
|
|
1929
2050
|
return formatCurrencyCodeError(error);
|
|
1930
|
-
case "
|
|
1931
|
-
return
|
|
2051
|
+
case "DateIso":
|
|
2052
|
+
return formatDateIsoError(error);
|
|
1932
2053
|
case "Trimmed":
|
|
1933
2054
|
return formatTrimmedError(error);
|
|
1934
2055
|
case "MinLength":
|
|
@@ -1943,6 +2064,8 @@ export const createFormatTypeError = (extraFormatter) => {
|
|
|
1943
2064
|
return formatRegexError(error);
|
|
1944
2065
|
case "Id":
|
|
1945
2066
|
return formatIdError(error);
|
|
2067
|
+
case "TableId":
|
|
2068
|
+
return formatTableIdError(error);
|
|
1946
2069
|
case "Positive":
|
|
1947
2070
|
return formatPositiveError(error);
|
|
1948
2071
|
case "Negative":
|
|
@@ -1973,14 +2096,12 @@ export const createFormatTypeError = (extraFormatter) => {
|
|
|
1973
2096
|
return formatLiteralError(error);
|
|
1974
2097
|
case "Int64":
|
|
1975
2098
|
return formatInt64Error(error);
|
|
1976
|
-
case "BigIntFromString":
|
|
1977
|
-
return formatBigIntFromStringError(error);
|
|
1978
2099
|
case "Int64String":
|
|
1979
2100
|
return formatInt64StringError(error);
|
|
1980
|
-
case "JsonValueFromString":
|
|
1981
|
-
return formatJsonValueFromStringError(error);
|
|
1982
2101
|
case "Json":
|
|
1983
2102
|
return formatJsonError(error);
|
|
2103
|
+
case "ValidMutationSize":
|
|
2104
|
+
return formatValidMutationSizeError(error);
|
|
1984
2105
|
// Composite Types
|
|
1985
2106
|
case "SimplePassword":
|
|
1986
2107
|
return formatSimplePasswordError(formatTypeError)(error);
|
|
@@ -1996,7 +2117,88 @@ export const createFormatTypeError = (extraFormatter) => {
|
|
|
1996
2117
|
return formatUnionError(formatTypeError)(error);
|
|
1997
2118
|
case "Tuple":
|
|
1998
2119
|
return formatTupleError(formatTypeError)(error);
|
|
2120
|
+
default: {
|
|
2121
|
+
// Fallback for unknown error types
|
|
2122
|
+
const unknownError = error;
|
|
2123
|
+
return `A value ${safelyStringifyUnknownValue(unknownError.value)} is not valid for type ${unknownError.type}.`;
|
|
2124
|
+
}
|
|
1999
2125
|
}
|
|
2000
2126
|
};
|
|
2001
2127
|
return formatTypeError;
|
|
2002
2128
|
};
|
|
2129
|
+
/**
|
|
2130
|
+
* Converts an Evolu {@link TypeError} to Standard Schema V1 issues format.
|
|
2131
|
+
*
|
|
2132
|
+
* This function recursively converts Evolu's typed errors into the Standard
|
|
2133
|
+
* Schema issue format with proper path tracking for nested structures.
|
|
2134
|
+
*
|
|
2135
|
+
* @category Utilities
|
|
2136
|
+
*/
|
|
2137
|
+
export const typeErrorToStandardSchemaIssues = (error, formatTypeError, path = []) => {
|
|
2138
|
+
if (error.type === "Array") {
|
|
2139
|
+
const arrayError = error;
|
|
2140
|
+
if (arrayError.reason.kind === "NotArray") {
|
|
2141
|
+
return [{ message: formatTypeError(error), path }];
|
|
2142
|
+
}
|
|
2143
|
+
return typeErrorToStandardSchemaIssues(arrayError.reason.error, formatTypeError, [...path, arrayError.reason.index]);
|
|
2144
|
+
}
|
|
2145
|
+
if (error.type === "Object") {
|
|
2146
|
+
const objectError = error;
|
|
2147
|
+
if (objectError.reason.kind === "NotObject" ||
|
|
2148
|
+
objectError.reason.kind === "ExtraKeys") {
|
|
2149
|
+
return [{ message: formatTypeError(error), path }];
|
|
2150
|
+
}
|
|
2151
|
+
const issues = [];
|
|
2152
|
+
for (const [key, propError] of Object.entries(objectError.reason.errors)) {
|
|
2153
|
+
issues.push(...typeErrorToStandardSchemaIssues(propError, formatTypeError, [...path, key]));
|
|
2154
|
+
}
|
|
2155
|
+
return issues;
|
|
2156
|
+
}
|
|
2157
|
+
if (error.type === "ObjectWithRecord") {
|
|
2158
|
+
const objectWithRecordError = error;
|
|
2159
|
+
if (objectWithRecordError.reason.kind === "NotObject") {
|
|
2160
|
+
return [{ message: formatTypeError(error), path }];
|
|
2161
|
+
}
|
|
2162
|
+
if (objectWithRecordError.reason.kind === "IndexKey" ||
|
|
2163
|
+
objectWithRecordError.reason.kind === "IndexValue") {
|
|
2164
|
+
return typeErrorToStandardSchemaIssues(objectWithRecordError.reason.error, formatTypeError, [...path, objectWithRecordError.reason.key]);
|
|
2165
|
+
}
|
|
2166
|
+
const issues = [];
|
|
2167
|
+
for (const [key, propError] of Object.entries(objectWithRecordError.reason.errors)) {
|
|
2168
|
+
issues.push(...typeErrorToStandardSchemaIssues(propError, formatTypeError, [...path, key]));
|
|
2169
|
+
}
|
|
2170
|
+
return issues;
|
|
2171
|
+
}
|
|
2172
|
+
if (error.type === "Record") {
|
|
2173
|
+
const recordError = error;
|
|
2174
|
+
if (recordError.reason.kind === "NotRecord") {
|
|
2175
|
+
return [{ message: formatTypeError(error), path }];
|
|
2176
|
+
}
|
|
2177
|
+
return typeErrorToStandardSchemaIssues(recordError.reason.error, formatTypeError, [...path, recordError.reason.key]);
|
|
2178
|
+
}
|
|
2179
|
+
if (error.type === "Tuple") {
|
|
2180
|
+
const tupleError = error;
|
|
2181
|
+
if (tupleError.reason.kind === "InvalidLength") {
|
|
2182
|
+
return [{ message: formatTypeError(error), path }];
|
|
2183
|
+
}
|
|
2184
|
+
return typeErrorToStandardSchemaIssues(tupleError.reason.error, formatTypeError, [...path, tupleError.reason.index]);
|
|
2185
|
+
}
|
|
2186
|
+
if (error.type === "Union") {
|
|
2187
|
+
const unionError = error;
|
|
2188
|
+
return unionError.errors.flatMap((err) => typeErrorToStandardSchemaIssues(err, formatTypeError, path));
|
|
2189
|
+
}
|
|
2190
|
+
if (error.type === "Brand") {
|
|
2191
|
+
const brandError = error;
|
|
2192
|
+
if ("parentError" in brandError) {
|
|
2193
|
+
return typeErrorToStandardSchemaIssues(brandError.parentError, formatTypeError, path);
|
|
2194
|
+
}
|
|
2195
|
+
return [{ message: formatTypeError(error), path }];
|
|
2196
|
+
}
|
|
2197
|
+
return [{ message: formatTypeError(error), path }];
|
|
2198
|
+
};
|
|
2199
|
+
/**
|
|
2200
|
+
* Shared formatter cache for Standard Schema integration - avoids circular
|
|
2201
|
+
* dependency by lazily creating the formatter on first use rather than during
|
|
2202
|
+
* module initialization.
|
|
2203
|
+
*/
|
|
2204
|
+
let cachedStandardSchemaFormatTypeError;
|