@evolu/common 6.0.1-preview.8 → 7.0.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/README.md +2 -2
- package/dist/src/Array.d.ts +256 -12
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +215 -9
- package/dist/src/Assert.d.ts +0 -13
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +0 -15
- 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 +56 -42
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +40 -53
- package/dist/src/Evolu/Db.d.ts +162 -74
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +284 -702
- 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 +214 -134
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +189 -180
- 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 +268 -240
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +600 -454
- 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.d.ts.map +1 -1
- package/dist/src/Evolu/PublicKysely.js +3 -4
- package/dist/src/Evolu/Query.d.ts +2 -1
- package/dist/src/Evolu/Query.d.ts.map +1 -1
- package/dist/src/Evolu/Query.js +1 -1
- package/dist/src/Evolu/Relay.d.ts +91 -8
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +214 -88
- package/dist/src/Evolu/Schema.d.ts +125 -47
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +175 -31
- package/dist/src/Evolu/Storage.d.ts +249 -27
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +198 -92
- package/dist/src/Evolu/Sync.d.ts +68 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +469 -20
- package/dist/src/Evolu/Timestamp.d.ts +83 -30
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +79 -34
- 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/Number.d.ts +4 -3
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +5 -4
- package/dist/src/Object.d.ts +10 -4
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +9 -3
- 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/Relation.d.ts +79 -0
- package/dist/src/Relation.d.ts.map +1 -0
- package/dist/src/Relation.js +127 -0
- 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 +89 -5
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +136 -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 +676 -343
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +718 -467
- 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 +8 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +8 -3
- package/package.json +15 -14
- package/src/Array.ts +301 -19
- package/src/Assert.ts +0 -21
- 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 +94 -90
- package/src/Evolu/Db.ts +519 -1026
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +477 -361
- 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 +842 -742
- package/src/Evolu/Public.ts +7 -14
- package/src/Evolu/PublicKysely.ts +4 -5
- package/src/Evolu/Query.ts +3 -2
- package/src/Evolu/Relay.ts +406 -103
- package/src/Evolu/Schema.ts +323 -91
- package/src/Evolu/Storage.ts +559 -137
- package/src/Evolu/Sync.ts +819 -36
- package/src/Evolu/Timestamp.ts +90 -58
- package/src/Identicon.ts +197 -0
- package/src/Instances.ts +90 -0
- package/src/Number.ts +6 -10
- package/src/Object.ts +13 -5
- package/src/Platform.ts +26 -0
- package/src/Random.ts +3 -2
- package/src/Relation.ts +234 -0
- package/src/Resources.ts +367 -0
- package/src/Result.ts +191 -54
- package/src/Skiplist.ts +1 -1
- package/src/Sqlite.ts +152 -17
- package/src/Task.ts +901 -0
- package/src/Time.ts +180 -5
- package/src/Type.ts +1135 -730
- package/src/Types.ts +1 -77
- package/src/WebSocket.ts +27 -25
- package/src/Worker.ts +72 -23
- package/src/index.ts +8 -3
- 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/ManyToManyMap.d.ts +0 -26
- package/dist/src/ManyToManyMap.d.ts.map +0 -1
- package/dist/src/ManyToManyMap.js +0 -92
- 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/ManyToManyMap.ts +0 -140
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/src/Type.ts
CHANGED
|
@@ -1,84 +1,219 @@
|
|
|
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
|
+
* composable typed error telling us exactly why validation failed.
|
|
5
7
|
*
|
|
6
|
-
*
|
|
8
|
+
* Why another validation library?
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* - **Result-based error handling** – no exceptions for normal control flow.
|
|
11
|
+
* - **Typed errors with decoupled formatters** – validation logic ≠ user
|
|
12
|
+
* messages.
|
|
13
|
+
* - **Consistent constraints via {@link Brand}** – every constraint becomes part
|
|
14
|
+
* of the type.
|
|
15
|
+
* - **Skippable validation** – parent validations can be skipped when already
|
|
16
|
+
* proved by types.
|
|
17
|
+
* - **Simple, top-down implementation** – readable source code from top to
|
|
18
|
+
* bottom.
|
|
19
|
+
* - **No user-land chaining DSL** – prepared for TC39 Hack pipes.
|
|
10
20
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* with validators.
|
|
16
|
-
* - **No user-land chaining**: Designed with ES pipe operator in mind.
|
|
17
|
-
* - **Selective validation/transformation**: Skipping parent Type validations and
|
|
18
|
-
* transformations when TypeScript's type system can be relied upon.
|
|
19
|
-
* - **Bidirectional transformations**: Supporting transformations in both
|
|
20
|
-
* directions.
|
|
21
|
-
* - **Minimal and transparent code**: No runtime dependencies or hidden magic.
|
|
21
|
+
* A distinctive feature of Evolu Type compared to other validation libraries is
|
|
22
|
+
* that it returns typed errors rather than string messages. This allows
|
|
23
|
+
* TypeScript to enforce that all validation errors are handled by type
|
|
24
|
+
* checking, significantly improving the developer experience.
|
|
22
25
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* practical demonstrations of the API. Or dang, just read the code. It's
|
|
26
|
-
* simple.
|
|
26
|
+
* Evolu Type supports [Standard Schema](https://standardschema.dev/) for
|
|
27
|
+
* interoperability with 40+ validation-compatible tools and frameworks.
|
|
27
28
|
*
|
|
28
|
-
*
|
|
29
|
+
* ### Base Types Quick Start
|
|
29
30
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* // Validate unknown values
|
|
33
|
+
* const value: unknown = "hello";
|
|
34
|
+
* const stringResult = String.fromUnknown(value);
|
|
35
|
+
* if (!stringResult.ok) {
|
|
36
|
+
* // console.error(formatStringError(stringResult.error));
|
|
37
|
+
* return stringResult; // inside a function returning Result<string, _>
|
|
38
|
+
* }
|
|
39
|
+
* // Safe branch: value is now string
|
|
40
|
+
* const upper = stringResult.value.toUpperCase();
|
|
41
|
+
*
|
|
42
|
+
* // Type guard style
|
|
43
|
+
* if (String.is(value)) {
|
|
44
|
+
* // narrowed to string
|
|
45
|
+
* }
|
|
46
|
+
*
|
|
47
|
+
* // Composing: arrays & objects
|
|
48
|
+
* const Numbers = array(Number); // ReadonlyArray<number>
|
|
49
|
+
* const Point = object({ x: Number, y: Number });
|
|
50
|
+
*
|
|
51
|
+
* Numbers.from([1, 2, 3]); // ok
|
|
52
|
+
* Point.from({ x: 1, y: 2 }); // ok
|
|
53
|
+
* Point.from({ x: 1, y: "2" }); // err -> nested Number error
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* ### Branding Basics
|
|
57
|
+
*
|
|
58
|
+
* Branding adds semantic meaning & constraints while preserving the runtime
|
|
59
|
+
* shape:
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* const CurrencyCode = brand("CurrencyCode", String, (value) =>
|
|
63
|
+
* /^[A-Z]{3}$/.test(value)
|
|
64
|
+
* ? ok(value)
|
|
65
|
+
* : err<CurrencyCodeError>({ type: "CurrencyCode", value }),
|
|
66
|
+
* );
|
|
67
|
+
* type CurrencyCode = typeof CurrencyCode.Type; // string & Brand<"CurrencyCode">
|
|
68
|
+
*
|
|
69
|
+
* interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
|
|
70
|
+
*
|
|
71
|
+
* const formatCurrencyCodeError =
|
|
72
|
+
* createTypeErrorFormatter<CurrencyCodeError>(
|
|
73
|
+
* (error) => `Invalid currency code: ${error.value}`,
|
|
74
|
+
* );
|
|
75
|
+
*
|
|
76
|
+
* const r = CurrencyCode.from("USD"); // ok("USD")
|
|
77
|
+
* const e = CurrencyCode.from("usd"); // err(...)
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* See also reusable brand factories like `minLength`, `maxLength`, `trimmed`,
|
|
81
|
+
* `positive`, `between`, etc.
|
|
82
|
+
*
|
|
83
|
+
* ### Objects & Optional Fields
|
|
84
|
+
*
|
|
85
|
+
* ```ts
|
|
86
|
+
* const User = object({
|
|
87
|
+
* name: NonEmptyTrimmedString100,
|
|
88
|
+
* age: optional(PositiveInt),
|
|
89
|
+
* });
|
|
90
|
+
* type User = typeof User.Type;
|
|
91
|
+
*
|
|
92
|
+
* User.from({ name: "Alice" }); // ok
|
|
93
|
+
* User.from({ name: "Alice", age: -1 }); // err(PositiveInt)
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* ### Deriving JSON String Types
|
|
97
|
+
*
|
|
98
|
+
* ```ts
|
|
99
|
+
* const Person = object({
|
|
100
|
+
* name: NonEmptyString50,
|
|
101
|
+
* // Did you know that JSON.stringify converts NaN (a number) into null?
|
|
102
|
+
* // To prevent this, use FiniteNumber.
|
|
103
|
+
* age: FiniteNumber,
|
|
104
|
+
* });
|
|
105
|
+
* type Person = typeof Person.Type;
|
|
106
|
+
*
|
|
107
|
+
* const [PersonJson, personToPersonJson, personJsonToPerson] = json(
|
|
108
|
+
* Person,
|
|
109
|
+
* "PersonJson",
|
|
110
|
+
* );
|
|
111
|
+
* // string & Brand<"PersonJson">
|
|
112
|
+
* type PersonJson = typeof PersonJson.Type;
|
|
113
|
+
*
|
|
114
|
+
* const person = Person.orThrow({
|
|
115
|
+
* name: "Alice",
|
|
116
|
+
* age: 30,
|
|
117
|
+
* });
|
|
118
|
+
*
|
|
119
|
+
* const personJson = personToPersonJson(person);
|
|
120
|
+
* expect(personJsonToPerson(personJson)).toEqual(person);
|
|
121
|
+
* ```
|
|
122
|
+
*
|
|
123
|
+
* ### Error Formatting
|
|
124
|
+
*
|
|
125
|
+
* Evolu separates validation logic from human-readable messages. There are two
|
|
126
|
+
* layers:
|
|
127
|
+
*
|
|
128
|
+
* 1. Per-type formatters (e.g. `formatStringError`) – simple, focused, already
|
|
129
|
+
* used earlier in the quick start example.
|
|
130
|
+
* 2. A unified formatter via `createFormatTypeError` – composes all built-in and
|
|
131
|
+
* custom errors (including nested composite types) and lets us override
|
|
132
|
+
* selected messages.
|
|
133
|
+
*
|
|
134
|
+
* #### 1. Per-Type Formatter (recap)
|
|
34
135
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
136
|
+
* ```ts
|
|
137
|
+
* const r = String.fromUnknown(42);
|
|
138
|
+
* if (!r.ok) console.error(formatStringError(r.error));
|
|
139
|
+
* ```
|
|
39
140
|
*
|
|
40
|
-
*
|
|
141
|
+
* #### 2. Unified Formatter with Overrides
|
|
41
142
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
143
|
+
* ```ts
|
|
144
|
+
* // Override only what we care about; fall back to built-ins for the rest.
|
|
145
|
+
* const formatTypeError = createFormatTypeError((error) => {
|
|
146
|
+
* if (error.type === "MinLength") return `Min length is ${error.min}`;
|
|
147
|
+
* });
|
|
44
148
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
149
|
+
* const User = object({ name: NonEmptyTrimmedString100 });
|
|
150
|
+
* const resultUser = User.from({ name: "" });
|
|
151
|
+
* if (!resultUser.ok) console.error(formatTypeError(resultUser.error));
|
|
48
152
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
153
|
+
* const badPoint = object({ x: Number, y: Number }).from({
|
|
154
|
+
* x: 1,
|
|
155
|
+
* y: "foo",
|
|
156
|
+
* });
|
|
157
|
+
* if (!badPoint.ok) console.error(formatTypeError(badPoint.error));
|
|
158
|
+
* ```
|
|
51
159
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* `to` will not restore the original representation. You can also use
|
|
56
|
-
* {@link assert}: `assert(false, "Untrim is not possible")`.
|
|
160
|
+
* The unified formatter walks nested structures (object / array / record /
|
|
161
|
+
* tuple / union) and applies overrides only where specified, greatly reducing
|
|
162
|
+
* boilerplate when formatting complex validation errors.
|
|
57
163
|
*
|
|
58
164
|
* ### Tip
|
|
59
165
|
*
|
|
60
166
|
* If necessary, write `globalThis.String` instead of `String` to avoid naming
|
|
61
|
-
* clashes with
|
|
167
|
+
* clashes with native types.
|
|
168
|
+
*
|
|
169
|
+
* ### Design Decision: No Bidirectional Transformations
|
|
62
170
|
*
|
|
63
|
-
*
|
|
171
|
+
* Evolu Type intentionally does not support bidirectional transformations. It
|
|
172
|
+
* previously did, but supporting that while keeping typed error fidelity added
|
|
173
|
+
* complexity that hurt readability & reliability. Most persistence pipelines
|
|
174
|
+
* (e.g. SQLite) already require explicit mapping of query results, so implicit
|
|
175
|
+
* reverse transforms would not buy much. We may revisit this if we can design a
|
|
176
|
+
* minimal, 100% safe API that preserves simplicity.
|
|
64
177
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
178
|
+
* ### Prepared for TC39 Hack Pipes
|
|
179
|
+
*
|
|
180
|
+
* Take a look how `SimplePassword` is defined:
|
|
181
|
+
*
|
|
182
|
+
* ```ts
|
|
183
|
+
* export const SimplePassword = brand(
|
|
184
|
+
* "SimplePassword",
|
|
185
|
+
* minLength(8)(maxLength(64)(TrimmedString)),
|
|
186
|
+
* );
|
|
187
|
+
* ```
|
|
188
|
+
*
|
|
189
|
+
* Nested functions are often OK (if not, make a helper) and read well, but with
|
|
190
|
+
* TC39 Hack pipes it would be clearer:
|
|
191
|
+
*
|
|
192
|
+
* ```ts
|
|
193
|
+
* // TrimmedString
|
|
194
|
+
* // |> minLength(8)(%)
|
|
195
|
+
* // |> maxLength(64)(%)
|
|
196
|
+
* // |> brand("SimplePassword", %)
|
|
197
|
+
* ```
|
|
198
|
+
*
|
|
199
|
+
* Note `minLength` and `maxLength` are curried because they are factories.
|
|
69
200
|
*
|
|
70
201
|
* @module
|
|
71
202
|
*/
|
|
72
203
|
|
|
204
|
+
import { utf8ToBytes } from "@noble/ciphers/utils.js";
|
|
205
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
73
206
|
import * as bip39 from "@scure/bip39";
|
|
74
|
-
import { wordlist } from "@scure/bip39/wordlists/english";
|
|
75
|
-
import {
|
|
76
|
-
import {
|
|
77
|
-
import {
|
|
207
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
208
|
+
import { pack } from "msgpackr";
|
|
209
|
+
import type { Brand } from "./Brand.js";
|
|
210
|
+
import { type RandomBytesDep } from "./Crypto.js";
|
|
78
211
|
import { isPlainObject } from "./Object.js";
|
|
79
|
-
import {
|
|
212
|
+
import { hasNodeBuffer } from "./Platform.js";
|
|
213
|
+
import { err, getOrNull, getOrThrow, ok, Result, trySync } from "./Result.js";
|
|
80
214
|
import { safelyStringifyUnknownValue } from "./String.js";
|
|
81
|
-
import type {
|
|
215
|
+
import type { TimeDep } from "./Time.js";
|
|
216
|
+
import type { Literal, Simplify, WidenLiteral } from "./Types.js";
|
|
82
217
|
import { IntentionalNever } from "./Types.js";
|
|
83
218
|
|
|
84
219
|
export interface Type<
|
|
@@ -93,7 +228,7 @@ export interface Type<
|
|
|
93
228
|
Parent = T,
|
|
94
229
|
/** The parent's error. */
|
|
95
230
|
ParentError extends TypeError = Error,
|
|
96
|
-
> {
|
|
231
|
+
> extends StandardSchemaV1<Input, T> {
|
|
97
232
|
readonly name: Name;
|
|
98
233
|
|
|
99
234
|
/**
|
|
@@ -106,41 +241,98 @@ export interface Type<
|
|
|
106
241
|
readonly from: (value: Input) => Result<T, ParentError | Error>;
|
|
107
242
|
|
|
108
243
|
/**
|
|
109
|
-
* Creates `T` from an
|
|
244
|
+
* Creates `T` from an `Input` value, throwing an error if validation fails.
|
|
110
245
|
*
|
|
111
|
-
*
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
*
|
|
246
|
+
* Throws an Error with the Type validation error in its `cause` property,
|
|
247
|
+
* making it debuggable while avoiding the need for custom error messages.
|
|
248
|
+
*
|
|
249
|
+
* This is a convenience method that combines `from` with `getOrThrow`.
|
|
250
|
+
*
|
|
251
|
+
* **When to use:**
|
|
117
252
|
*
|
|
118
|
-
*
|
|
253
|
+
* - Configuration values that are guaranteed to be valid (e.g., hardcoded
|
|
254
|
+
* constants)
|
|
255
|
+
* - Application startup where failure should crash the program
|
|
256
|
+
* - As an alternative to assertions when the Type error in the thrown Error's
|
|
257
|
+
* `cause` provides sufficient debugging information
|
|
258
|
+
* - Test code with known valid inputs (when error message clarity is not
|
|
259
|
+
* critical; for better test error messages, use Vitest `schemaMatching` +
|
|
260
|
+
* `assert` with `.is()`)
|
|
119
261
|
*
|
|
120
|
-
*
|
|
262
|
+
* ### Example
|
|
263
|
+
*
|
|
264
|
+
* ```ts
|
|
265
|
+
* // ✅ Good: Known valid constant
|
|
266
|
+
* const maxRetries = PositiveInt.orThrow(3);
|
|
267
|
+
*
|
|
268
|
+
* // ✅ Good: App configuration that should crash on invalid values
|
|
269
|
+
* const appName = SimpleName.orThrow("MyApp");
|
|
270
|
+
*
|
|
271
|
+
* // ✅ Good: Instead of assert when Type error is clear enough
|
|
272
|
+
* // Context makes it obvious: count increments from non-negative value
|
|
273
|
+
* const currentCount = counts.get(id) ?? 0;
|
|
274
|
+
* const newCount = PositiveInt.orThrow(currentCount + 1);
|
|
275
|
+
*
|
|
276
|
+
* // ✅ Good: Test setup with known valid values
|
|
277
|
+
* const testUser = User.orThrow({ name: "Alice", age: 30 });
|
|
278
|
+
*
|
|
279
|
+
* // ❌ Avoid: User input (use `from` instead)
|
|
280
|
+
* const userAge = PositiveInt.orThrow(userInput); // Could crash!
|
|
281
|
+
*
|
|
282
|
+
* // ✅ Better: Handle user input gracefully
|
|
283
|
+
* const ageResult = PositiveInt.from(userInput);
|
|
284
|
+
* if (!ageResult.ok) {
|
|
285
|
+
* // Handle validation error
|
|
286
|
+
* }
|
|
287
|
+
* ```
|
|
121
288
|
*/
|
|
122
|
-
readonly
|
|
289
|
+
readonly orThrow: (value: Input) => T;
|
|
123
290
|
|
|
124
291
|
/**
|
|
125
|
-
* Creates `T` from `
|
|
292
|
+
* Creates `T` from an `Input` value, returning `null` if validation fails.
|
|
126
293
|
*
|
|
127
|
-
* This
|
|
128
|
-
* already partially validated/transformed value.
|
|
294
|
+
* This is a convenience method that combines `from` with `getOrNull`.
|
|
129
295
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
296
|
+
* **When to use:**
|
|
297
|
+
*
|
|
298
|
+
* - When you need to convert a validation result to a nullable value
|
|
299
|
+
* - When the error is not important and you just want the value or nothing
|
|
132
300
|
*
|
|
133
301
|
* ### Example
|
|
134
302
|
*
|
|
135
303
|
* ```ts
|
|
136
|
-
* //
|
|
137
|
-
* const
|
|
304
|
+
* // ✅ Good: Optional user input
|
|
305
|
+
* const age = PositiveInt.orNull(userInput);
|
|
306
|
+
* if (age != null) {
|
|
307
|
+
* console.log("Valid age:", age);
|
|
308
|
+
* }
|
|
309
|
+
*
|
|
310
|
+
* // ✅ Good: Default fallback
|
|
311
|
+
* const maxRetries = PositiveInt.orNull(config.retries) ?? 3;
|
|
312
|
+
*
|
|
313
|
+
* // ❌ Avoid: When you need to know why validation failed (use `from` instead)
|
|
314
|
+
* const result = PositiveInt.from(userInput);
|
|
315
|
+
* if (!result.ok) {
|
|
316
|
+
* console.error(formatPositiveError(result.error));
|
|
317
|
+
* }
|
|
138
318
|
* ```
|
|
139
319
|
*/
|
|
140
|
-
readonly
|
|
320
|
+
readonly orNull: (value: Input) => T | null;
|
|
141
321
|
|
|
142
|
-
/**
|
|
143
|
-
|
|
322
|
+
/**
|
|
323
|
+
* Creates `T` from an unknown value.
|
|
324
|
+
*
|
|
325
|
+
* This is useful when a value is unknown.
|
|
326
|
+
*/
|
|
327
|
+
readonly fromUnknown: (value: unknown) => Result<T, ParentError | Error>;
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Creates `T` from `Parent` type.
|
|
331
|
+
*
|
|
332
|
+
* This function skips parent Types validations when we have already partially
|
|
333
|
+
* validated value.
|
|
334
|
+
*/
|
|
335
|
+
readonly fromParent: (value: Parent) => Result<T, Error>;
|
|
144
336
|
|
|
145
337
|
/**
|
|
146
338
|
* A **type guard** that checks whether an unknown value satisfies the
|
|
@@ -221,8 +413,6 @@ export interface Type<
|
|
|
221
413
|
readonly ParentError: ParentError;
|
|
222
414
|
|
|
223
415
|
/**
|
|
224
|
-
* Error | ParentError
|
|
225
|
-
*
|
|
226
416
|
* ### Example
|
|
227
417
|
*
|
|
228
418
|
* ```ts
|
|
@@ -261,26 +451,61 @@ export interface TypeErrorWithReason<
|
|
|
261
451
|
|
|
262
452
|
export type AnyType = Type<any, any, any, any, any, any>;
|
|
263
453
|
|
|
454
|
+
/**
|
|
455
|
+
* Extracts the name from a {@link Type}.
|
|
456
|
+
*
|
|
457
|
+
* @category Utilities
|
|
458
|
+
*/
|
|
264
459
|
export type InferName<A extends AnyType> =
|
|
265
460
|
A extends Type<infer Name, any, any, any, any, any> ? Name : never;
|
|
266
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Extracts the type from a {@link Type}.
|
|
464
|
+
*
|
|
465
|
+
* @category Utilities
|
|
466
|
+
*/
|
|
267
467
|
export type InferType<A extends AnyType> =
|
|
268
468
|
A extends Type<any, infer T, any, any, any, any> ? T : never;
|
|
269
469
|
|
|
470
|
+
/**
|
|
471
|
+
* Extracts the input type from a {@link Type}.
|
|
472
|
+
*
|
|
473
|
+
* @category Utilities
|
|
474
|
+
*/
|
|
270
475
|
export type InferInput<A extends AnyType> =
|
|
271
476
|
A extends Type<any, any, infer Input, any, any, any> ? Input : never;
|
|
272
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Extracts the specific error type from a {@link Type}.
|
|
480
|
+
*
|
|
481
|
+
* @category Utilities
|
|
482
|
+
*/
|
|
273
483
|
export type InferError<A extends AnyType> =
|
|
274
484
|
A extends Type<any, any, any, infer Error, any, any> ? Error : never;
|
|
275
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Extracts the parent type from a {@link Type}.
|
|
488
|
+
*
|
|
489
|
+
* @category Utilities
|
|
490
|
+
*/
|
|
276
491
|
export type InferParent<A extends AnyType> =
|
|
277
492
|
A extends Type<any, any, any, any, infer Parent, any> ? Parent : never;
|
|
278
493
|
|
|
494
|
+
/**
|
|
495
|
+
* Extracts the parent error type from a {@link Type}.
|
|
496
|
+
*
|
|
497
|
+
* @category Utilities
|
|
498
|
+
*/
|
|
279
499
|
export type InferParentError<A extends AnyType> =
|
|
280
500
|
A extends Type<any, any, any, any, any, infer ParentError>
|
|
281
501
|
? ParentError
|
|
282
502
|
: never;
|
|
283
503
|
|
|
504
|
+
/**
|
|
505
|
+
* Extracts all error types from a {@link Type}.
|
|
506
|
+
*
|
|
507
|
+
* @category Utilities
|
|
508
|
+
*/
|
|
284
509
|
export type InferErrors<T extends AnyType> =
|
|
285
510
|
T extends Type<any, any, any, infer Error, any, infer ParentError>
|
|
286
511
|
? Error | ParentError
|
|
@@ -310,6 +535,8 @@ const createType = <
|
|
|
310
535
|
| "name"
|
|
311
536
|
| "is"
|
|
312
537
|
| "from"
|
|
538
|
+
| "orThrow"
|
|
539
|
+
| "orNull"
|
|
313
540
|
| typeof EvoluTypeSymbol
|
|
314
541
|
| "Type"
|
|
315
542
|
| "Input"
|
|
@@ -317,12 +544,15 @@ const createType = <
|
|
|
317
544
|
| "Parent"
|
|
318
545
|
| "ParentError"
|
|
319
546
|
| "Errors"
|
|
547
|
+
| "~standard"
|
|
320
548
|
>,
|
|
321
549
|
): Type<Name, T, Input, Error, Parent, ParentError> => ({
|
|
322
550
|
...definition,
|
|
323
551
|
name,
|
|
324
552
|
is: (value: unknown): value is T => definition.fromUnknown(value).ok,
|
|
325
553
|
from: definition.fromUnknown,
|
|
554
|
+
orThrow: (value) => getOrThrow(definition.fromUnknown(value)),
|
|
555
|
+
orNull: (value) => getOrNull(definition.fromUnknown(value)),
|
|
326
556
|
[EvoluTypeSymbol]: true,
|
|
327
557
|
Type: undefined as unknown as T,
|
|
328
558
|
Input: undefined as unknown as Input,
|
|
@@ -330,6 +560,27 @@ const createType = <
|
|
|
330
560
|
Parent: undefined as unknown as Parent,
|
|
331
561
|
ParentError: undefined as unknown as ParentError,
|
|
332
562
|
Errors: undefined as unknown as Error | ParentError,
|
|
563
|
+
"~standard": {
|
|
564
|
+
version: 1,
|
|
565
|
+
vendor: "evolu",
|
|
566
|
+
validate: (value: unknown): StandardSchemaV1.Result<T> => {
|
|
567
|
+
const result = definition.fromUnknown(value);
|
|
568
|
+
if (result.ok) {
|
|
569
|
+
return { value: result.value };
|
|
570
|
+
}
|
|
571
|
+
cachedStandardSchemaFormatTypeError ??= createFormatTypeError();
|
|
572
|
+
return {
|
|
573
|
+
issues: typeErrorToStandardSchemaIssues(
|
|
574
|
+
result.error as TypeErrors<Error>,
|
|
575
|
+
cachedStandardSchemaFormatTypeError,
|
|
576
|
+
),
|
|
577
|
+
};
|
|
578
|
+
},
|
|
579
|
+
types: {
|
|
580
|
+
input: undefined as unknown as Input,
|
|
581
|
+
output: undefined as unknown as T,
|
|
582
|
+
},
|
|
583
|
+
},
|
|
333
584
|
});
|
|
334
585
|
|
|
335
586
|
/**
|
|
@@ -365,12 +616,6 @@ export type TypeErrorFormatter<Error extends TypeError> = (
|
|
|
365
616
|
* Base {@link Type}.
|
|
366
617
|
*
|
|
367
618
|
* A Base Type validates that a value conforms to a specific TypeScript type.
|
|
368
|
-
* Unlike refinements or transformations, Base Types establish the fundamental
|
|
369
|
-
* shape of a value before any branding or transformation occurs.
|
|
370
|
-
*
|
|
371
|
-
* - To **refine** a Base Type further, use the {@link brand} Type Factory.
|
|
372
|
-
* - To **transform** a Base Type into a different representation, use the
|
|
373
|
-
* {@link transform} Type Factory.
|
|
374
619
|
*
|
|
375
620
|
* ### Example
|
|
376
621
|
*
|
|
@@ -396,9 +641,7 @@ export const base = <Name extends TypeName, T, Error extends TypeError>(
|
|
|
396
641
|
): Type<Name, T, T, Error> =>
|
|
397
642
|
createType(name, {
|
|
398
643
|
fromUnknown,
|
|
399
|
-
to: identity,
|
|
400
644
|
fromParent: ok<T>, // `fromParent` relies on types, so it can't fail for the Base Type
|
|
401
|
-
toParent: identity,
|
|
402
645
|
});
|
|
403
646
|
|
|
404
647
|
/**
|
|
@@ -564,7 +807,7 @@ export interface InstanceOfType<T extends abstract new (...args: any) => any>
|
|
|
564
807
|
}
|
|
565
808
|
|
|
566
809
|
export const formatInstanceOfError = createTypeErrorFormatter<InstanceOfError>(
|
|
567
|
-
(error) => `
|
|
810
|
+
(error) => `The value ${error.value} is not an instance of ${error.ctor}.`,
|
|
568
811
|
);
|
|
569
812
|
|
|
570
813
|
/**
|
|
@@ -607,7 +850,7 @@ export const formatIsTypeError = createTypeErrorFormatter<EvoluTypeError>(
|
|
|
607
850
|
* The `brand` Type Factory takes the name of a new {@link Brand}, a parent Type
|
|
608
851
|
* to be branded, and the optional `refine` function for additional constraint.
|
|
609
852
|
*
|
|
610
|
-
*
|
|
853
|
+
* The `refine` function can be omitted if we only want to add a brand.
|
|
611
854
|
*
|
|
612
855
|
* ### Examples
|
|
613
856
|
*
|
|
@@ -686,7 +929,7 @@ export const formatIsTypeError = createTypeErrorFormatter<EvoluTypeError>(
|
|
|
686
929
|
* confirmPassword: SimplePassword,
|
|
687
930
|
* });
|
|
688
931
|
*
|
|
689
|
-
* const ValidForm = brand("
|
|
932
|
+
* const ValidForm = brand("ValidForm", Form, (value) => {
|
|
690
933
|
* if (value.password !== value.confirmPassword)
|
|
691
934
|
* return err<ValidFormError>({
|
|
692
935
|
* type: "ValidForm",
|
|
@@ -780,9 +1023,7 @@ export function brand<
|
|
|
780
1023
|
return {
|
|
781
1024
|
...createType("Brand", {
|
|
782
1025
|
fromUnknown,
|
|
783
|
-
to: identity,
|
|
784
1026
|
fromParent: (refine ?? ok) as IntentionalNever,
|
|
785
|
-
toParent: identity,
|
|
786
1027
|
}),
|
|
787
1028
|
brand: name,
|
|
788
1029
|
parentType: parent,
|
|
@@ -830,7 +1071,7 @@ export interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
|
|
|
830
1071
|
|
|
831
1072
|
export const formatCurrencyCodeError =
|
|
832
1073
|
createTypeErrorFormatter<CurrencyCodeError>(
|
|
833
|
-
(error) => `Invalid currency code: ${error.value}
|
|
1074
|
+
(error) => `Invalid currency code: ${error.value}.`,
|
|
834
1075
|
);
|
|
835
1076
|
|
|
836
1077
|
/**
|
|
@@ -848,30 +1089,41 @@ export const formatCurrencyCodeError =
|
|
|
848
1089
|
* ### Example
|
|
849
1090
|
*
|
|
850
1091
|
* ```ts
|
|
851
|
-
* const result =
|
|
852
|
-
* const error =
|
|
1092
|
+
* const result = DateIso.from("2023-01-01T12:00:00.000Z"); // ok
|
|
1093
|
+
* const error = DateIso.from("10000-01-01T00:00:00.000Z"); // err
|
|
853
1094
|
* ```
|
|
854
1095
|
*
|
|
855
1096
|
* @category String
|
|
856
1097
|
*/
|
|
857
|
-
export const
|
|
1098
|
+
export const DateIso = brand("DateIso", String, (value) => {
|
|
858
1099
|
if (value.length !== 24) {
|
|
859
|
-
return err<
|
|
1100
|
+
return err<DateIsoError>({ type: "DateIso", value });
|
|
1101
|
+
}
|
|
1102
|
+
const parsed = globalThis.Date.parse(value);
|
|
1103
|
+
if (isNaN(parsed)) {
|
|
1104
|
+
return err<DateIsoError>({ type: "DateIso", value });
|
|
860
1105
|
}
|
|
861
|
-
|
|
862
|
-
|
|
1106
|
+
// Round-trip test: ensure the string is actually a proper ISO format
|
|
1107
|
+
const roundTrip = new globalThis.Date(parsed).toISOString();
|
|
1108
|
+
if (roundTrip !== value) {
|
|
1109
|
+
return err<DateIsoError>({ type: "DateIso", value });
|
|
863
1110
|
}
|
|
864
1111
|
return ok(value);
|
|
865
1112
|
});
|
|
866
1113
|
|
|
867
|
-
export type
|
|
1114
|
+
export type DateIso = typeof DateIso.Type;
|
|
868
1115
|
|
|
869
|
-
export interface
|
|
1116
|
+
export interface DateIsoError extends TypeError<"DateIso"> {}
|
|
870
1117
|
|
|
871
|
-
export const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1118
|
+
export const formatDateIsoError = createTypeErrorFormatter<DateIsoError>(
|
|
1119
|
+
(error) => `The value ${error.value} is not a valid ISO 8601 date string.`,
|
|
1120
|
+
);
|
|
1121
|
+
|
|
1122
|
+
export const dateToDateIso = (value: Date): Result<DateIso, DateIsoError> =>
|
|
1123
|
+
DateIso.fromParent(value.toISOString());
|
|
1124
|
+
|
|
1125
|
+
export const dateIsoToDate = (value: DateIso): Date =>
|
|
1126
|
+
new globalThis.Date(value);
|
|
875
1127
|
|
|
876
1128
|
/**
|
|
877
1129
|
* Helper type for Type Factory that creates a branded Type.
|
|
@@ -914,18 +1166,12 @@ export type BrandFactory<
|
|
|
914
1166
|
/**
|
|
915
1167
|
* Trimmed string.
|
|
916
1168
|
*
|
|
917
|
-
* This Type Factory
|
|
918
|
-
*
|
|
919
|
-
* Factory.
|
|
1169
|
+
* This Type Factory validates whether a string has no leading or trailing
|
|
1170
|
+
* whitespaces.
|
|
920
1171
|
*
|
|
921
|
-
* ###
|
|
1172
|
+
* ### Example
|
|
922
1173
|
*
|
|
923
1174
|
* ```ts
|
|
924
|
-
* // this Type already exists
|
|
925
|
-
* const TrimmedString = trimmed(String);
|
|
926
|
-
* type TrimmedString = typeof TrimmedString.Type;
|
|
927
|
-
*
|
|
928
|
-
* // we can make any branded Type trimmed:
|
|
929
1175
|
* const TrimmedNonEmptyString = trimmed(minLength(1)(String));
|
|
930
1176
|
* // string & Brand<"MinLength1"> & Brand<"Trimmed">
|
|
931
1177
|
* type TrimmedNonEmptyString = typeof TrimmedNonEmptyString.Type;
|
|
@@ -945,67 +1191,9 @@ export const trimmed: BrandFactory<"Trimmed", string, TrimmedError> = (
|
|
|
945
1191
|
export interface TrimmedError extends TypeError<"Trimmed"> {}
|
|
946
1192
|
|
|
947
1193
|
export const formatTrimmedError = createTypeErrorFormatter<TrimmedError>(
|
|
948
|
-
(error) => `
|
|
1194
|
+
(error) => `The value ${error.value} must be trimmed.`,
|
|
949
1195
|
);
|
|
950
1196
|
|
|
951
|
-
export type TransformBrandFactory<
|
|
952
|
-
Name extends TypeName,
|
|
953
|
-
Input,
|
|
954
|
-
TransformError extends TypeError = never,
|
|
955
|
-
> = <
|
|
956
|
-
PName extends TypeName,
|
|
957
|
-
P extends Input,
|
|
958
|
-
PInput,
|
|
959
|
-
PParent,
|
|
960
|
-
PError extends TypeError = never,
|
|
961
|
-
PParentError extends TypeError = never,
|
|
962
|
-
>(
|
|
963
|
-
parent: Type<PName, P, PInput, PError, PParent, PParentError>,
|
|
964
|
-
) => TransformType<
|
|
965
|
-
Type<PName, P, PInput, PError, PParent, PParentError>,
|
|
966
|
-
BrandType<
|
|
967
|
-
Type<PName, P, PInput, PError, PParent, PParentError>,
|
|
968
|
-
Name,
|
|
969
|
-
never,
|
|
970
|
-
PError | PParentError
|
|
971
|
-
>,
|
|
972
|
-
TransformError
|
|
973
|
-
>;
|
|
974
|
-
|
|
975
|
-
/**
|
|
976
|
-
* Trims leading and trailing whitespace from a string.
|
|
977
|
-
*
|
|
978
|
-
* This Type Factory **transforms** the input string by removing whitespace from
|
|
979
|
-
* both ends. For validation only, use {@link trimmed} Type Factory.
|
|
980
|
-
*
|
|
981
|
-
* ### Example
|
|
982
|
-
*
|
|
983
|
-
* ```ts
|
|
984
|
-
* const TrimString = trim(String);
|
|
985
|
-
* expect(TrimString.from("a ")).toEqual(ok("a"));
|
|
986
|
-
* expect(TrimString.fromParent("a ").value).toEqual("a");
|
|
987
|
-
*
|
|
988
|
-
* const TrimNonEmptyString = trim(NonEmptyString);
|
|
989
|
-
* expect(TrimNonEmptyString.from("a " as NonEmptyString)).toEqual(ok("a"));
|
|
990
|
-
* expect(
|
|
991
|
-
* TrimNonEmptyString.fromParent("a " as NonEmptyString).value,
|
|
992
|
-
* ).toEqual("a");
|
|
993
|
-
* ```
|
|
994
|
-
*
|
|
995
|
-
* **Note:** This transformation is irreversible. Calling `toParent` will not
|
|
996
|
-
* restore the original representation.
|
|
997
|
-
*
|
|
998
|
-
* @category String
|
|
999
|
-
*/
|
|
1000
|
-
|
|
1001
|
-
export const trim: TransformBrandFactory<"Trimmed", string> = (parent) =>
|
|
1002
|
-
transform(
|
|
1003
|
-
parent,
|
|
1004
|
-
trimmed(parent) as BrandType<typeof parent, "Trimmed">,
|
|
1005
|
-
(value) => ok(value.trim() as InferType<typeof parent> & Brand<"Trimmed">),
|
|
1006
|
-
(value) => value,
|
|
1007
|
-
);
|
|
1008
|
-
|
|
1009
1197
|
/**
|
|
1010
1198
|
* Trimmed string
|
|
1011
1199
|
*
|
|
@@ -1017,6 +1205,9 @@ export const trim: TransformBrandFactory<"Trimmed", string> = (parent) =>
|
|
|
1017
1205
|
export const TrimmedString = trimmed(String);
|
|
1018
1206
|
export type TrimmedString = typeof TrimmedString.Type;
|
|
1019
1207
|
|
|
1208
|
+
export const trim = (value: string): TrimmedString =>
|
|
1209
|
+
value.trim() as TrimmedString;
|
|
1210
|
+
|
|
1020
1211
|
/**
|
|
1021
1212
|
* Minimum length.
|
|
1022
1213
|
*
|
|
@@ -1045,7 +1236,7 @@ export interface MinLengthError<Min extends number = number>
|
|
|
1045
1236
|
|
|
1046
1237
|
export const formatMinLengthError = createTypeErrorFormatter<MinLengthError>(
|
|
1047
1238
|
(error) =>
|
|
1048
|
-
`
|
|
1239
|
+
`The value ${error.value} does not meet the minimum length of ${error.min}.`,
|
|
1049
1240
|
);
|
|
1050
1241
|
|
|
1051
1242
|
/**
|
|
@@ -1075,7 +1266,8 @@ export interface MaxLengthError<Max extends number = number>
|
|
|
1075
1266
|
}
|
|
1076
1267
|
|
|
1077
1268
|
export const formatMaxLengthError = createTypeErrorFormatter<MaxLengthError>(
|
|
1078
|
-
(error) =>
|
|
1269
|
+
(error) =>
|
|
1270
|
+
`The value ${error.value} exceeds the maximum length of ${error.max}.`,
|
|
1079
1271
|
);
|
|
1080
1272
|
|
|
1081
1273
|
/**
|
|
@@ -1108,7 +1300,7 @@ export interface LengthError<Exact extends number = number>
|
|
|
1108
1300
|
|
|
1109
1301
|
export const formatLengthError = createTypeErrorFormatter<LengthError>(
|
|
1110
1302
|
(error) =>
|
|
1111
|
-
`
|
|
1303
|
+
`The value ${error.value} does not have the required length of ${error.exact}.`,
|
|
1112
1304
|
);
|
|
1113
1305
|
|
|
1114
1306
|
/** @category String */
|
|
@@ -1170,7 +1362,7 @@ export type Mnemonic = typeof Mnemonic.Type;
|
|
|
1170
1362
|
export interface MnemonicError extends TypeError<"Mnemonic"> {}
|
|
1171
1363
|
|
|
1172
1364
|
export const formatMnemonicError = createTypeErrorFormatter<MnemonicError>(
|
|
1173
|
-
(error) => `Invalid BIP39 mnemonic: ${error.value}
|
|
1365
|
+
(error) => `Invalid BIP39 mnemonic: ${error.value}.`,
|
|
1174
1366
|
);
|
|
1175
1367
|
|
|
1176
1368
|
/**
|
|
@@ -1209,13 +1401,13 @@ export interface RegexError<Name extends TypeName = TypeName>
|
|
|
1209
1401
|
|
|
1210
1402
|
export const formatRegexError = createTypeErrorFormatter<RegexError>(
|
|
1211
1403
|
(error) =>
|
|
1212
|
-
`
|
|
1404
|
+
`The value ${error.value} does not match the pattern for ${error.name}: ${error.pattern}.`,
|
|
1213
1405
|
);
|
|
1214
1406
|
|
|
1215
1407
|
/**
|
|
1216
|
-
* URL-safe
|
|
1408
|
+
* URL-safe string.
|
|
1217
1409
|
*
|
|
1218
|
-
* A `
|
|
1410
|
+
* A `UrlSafeString` uses a limited alphabet that is safe for URLs:
|
|
1219
1411
|
*
|
|
1220
1412
|
* - Uppercase letters (`A-Z`)
|
|
1221
1413
|
* - Lowercase letters (`a-z`)
|
|
@@ -1223,37 +1415,117 @@ export const formatRegexError = createTypeErrorFormatter<RegexError>(
|
|
|
1223
1415
|
* - Dash (`-`)
|
|
1224
1416
|
* - Underscore (`_`)
|
|
1225
1417
|
*
|
|
1418
|
+
* This is the same character set used by Base64Url encoding, but this type does
|
|
1419
|
+
* not validate that the string is actually Base64Url-encoded data.
|
|
1420
|
+
*
|
|
1226
1421
|
* ### Example
|
|
1227
1422
|
*
|
|
1228
1423
|
* ```ts
|
|
1229
|
-
* const result =
|
|
1424
|
+
* const result = UrlSafeString.from("abc123_-");
|
|
1230
1425
|
* if (result.ok) {
|
|
1231
|
-
* console.log("Valid
|
|
1426
|
+
* console.log("Valid URL-safe string:", result.value);
|
|
1232
1427
|
* } else {
|
|
1233
|
-
* console.error("Invalid
|
|
1428
|
+
* console.error("Invalid URL-safe string:", result.error);
|
|
1234
1429
|
* }
|
|
1235
1430
|
* ```
|
|
1236
1431
|
*
|
|
1237
1432
|
* @category String
|
|
1238
1433
|
*/
|
|
1239
|
-
export const
|
|
1434
|
+
export const UrlSafeString = regex("UrlSafeString", /^[A-Za-z0-9_-]+$/)(String);
|
|
1435
|
+
export type UrlSafeString = typeof UrlSafeString.Type;
|
|
1436
|
+
export type UrlSafeStringError = typeof UrlSafeString.Error;
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* Base64Url without padding.
|
|
1440
|
+
*
|
|
1441
|
+
* Encode with {@link uint8ArrayToBase64Url}, decode with
|
|
1442
|
+
* {@link base64UrlToUint8Array}.
|
|
1443
|
+
*
|
|
1444
|
+
* @category String
|
|
1445
|
+
*/
|
|
1446
|
+
export const Base64Url = brand(
|
|
1240
1447
|
"Base64Url",
|
|
1241
|
-
|
|
1242
|
-
|
|
1448
|
+
String,
|
|
1449
|
+
(value: string): Result<string, Base64UrlError> => {
|
|
1450
|
+
// Round-trip validation ensures consistency across different base64url
|
|
1451
|
+
// implementations (Node.js Buffer, native browser API, manual fallback).
|
|
1452
|
+
// Only strings that decode and encode identically are accepted.
|
|
1453
|
+
let roundTrip;
|
|
1454
|
+
try {
|
|
1455
|
+
roundTrip = uint8ArrayToBase64Url(
|
|
1456
|
+
base64UrlToUint8Array(value as Base64Url),
|
|
1457
|
+
);
|
|
1458
|
+
} catch {
|
|
1459
|
+
//
|
|
1460
|
+
}
|
|
1461
|
+
return roundTrip === value
|
|
1462
|
+
? ok(value)
|
|
1463
|
+
: err<Base64UrlError>({ type: "Base64Url", value });
|
|
1464
|
+
},
|
|
1465
|
+
);
|
|
1243
1466
|
export type Base64Url = typeof Base64Url.Type;
|
|
1244
|
-
export
|
|
1467
|
+
export interface Base64UrlError extends TypeError<"Base64Url"> {}
|
|
1468
|
+
|
|
1469
|
+
export const formatBase64UrlError = createTypeErrorFormatter<Base64UrlError>(
|
|
1470
|
+
(error) => `The value ${error.value} is not a valid Base64Url string.`,
|
|
1471
|
+
);
|
|
1472
|
+
|
|
1473
|
+
const base64UrlOptions = { alphabet: "base64url", omitPadding: true };
|
|
1474
|
+
|
|
1475
|
+
/** Encodes a Uint8Array to a {@link Base64Url} string. */
|
|
1476
|
+
export const uint8ArrayToBase64Url: (bytes: Uint8Array) => Base64Url =
|
|
1477
|
+
hasNodeBuffer
|
|
1478
|
+
? (bytes: Uint8Array) =>
|
|
1479
|
+
globalThis.Buffer.from(bytes).toString("base64url") as Base64Url
|
|
1480
|
+
: // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
1481
|
+
typeof (globalThis.Uint8Array.prototype as any)?.toBase64 !== "undefined"
|
|
1482
|
+
? (bytes: Uint8Array) =>
|
|
1483
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
1484
|
+
(bytes as any).toBase64(base64UrlOptions) as Base64Url
|
|
1485
|
+
: (bytes: Uint8Array) => {
|
|
1486
|
+
const binaryString = Array.from(bytes, (byte) =>
|
|
1487
|
+
globalThis.String.fromCodePoint(byte),
|
|
1488
|
+
).join("");
|
|
1489
|
+
const base64 = globalThis.btoa(binaryString);
|
|
1490
|
+
return base64
|
|
1491
|
+
.replace(/\+/g, "-")
|
|
1492
|
+
.replace(/\//g, "_")
|
|
1493
|
+
.replace(/=/g, "") as Base64Url;
|
|
1494
|
+
};
|
|
1495
|
+
|
|
1496
|
+
/** Decodes a {@link Base64Url} string to a Uint8Array. */
|
|
1497
|
+
export const base64UrlToUint8Array: (str: Base64Url) => Uint8Array =
|
|
1498
|
+
hasNodeBuffer
|
|
1499
|
+
? (str: Base64Url) => {
|
|
1500
|
+
const nodeBuffer = globalThis.Buffer.from(str, "base64url");
|
|
1501
|
+
return new globalThis.Uint8Array(nodeBuffer);
|
|
1502
|
+
}
|
|
1503
|
+
: // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
1504
|
+
typeof (globalThis.Uint8Array as any)?.fromBase64 !== "undefined"
|
|
1505
|
+
? (str: Base64Url) =>
|
|
1506
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
1507
|
+
(globalThis.Uint8Array as any).fromBase64(
|
|
1508
|
+
str,
|
|
1509
|
+
base64UrlOptions,
|
|
1510
|
+
) as Uint8Array
|
|
1511
|
+
: (str: Base64Url) => {
|
|
1512
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
1513
|
+
while (base64.length % 4 !== 0) {
|
|
1514
|
+
base64 += "=";
|
|
1515
|
+
}
|
|
1516
|
+
const binaryString = globalThis.atob(base64);
|
|
1517
|
+
return globalThis.Uint8Array.from(binaryString, (c) =>
|
|
1518
|
+
c.charCodeAt(0),
|
|
1519
|
+
);
|
|
1520
|
+
};
|
|
1245
1521
|
|
|
1246
1522
|
/**
|
|
1247
|
-
* Simple alphanumeric string for naming.
|
|
1523
|
+
* Simple alphanumeric string for naming in file systems, URLs, and identifiers.
|
|
1248
1524
|
*
|
|
1249
|
-
*
|
|
1525
|
+
* Uses the same safe alphabet as {@link UrlSafeString} (letters, digits, `-`,
|
|
1526
|
+
* `_`). See `UrlSafeString` for details.
|
|
1250
1527
|
*
|
|
1251
|
-
*
|
|
1252
|
-
* - Lowercase letters (`a-z`)
|
|
1253
|
-
* - Digits (`0-9`)
|
|
1254
|
-
* - Dash (`-`)
|
|
1255
|
-
*
|
|
1256
|
-
* The string must be between 1 and 42 characters.
|
|
1528
|
+
* The string must be between 1 and 64 characters.
|
|
1257
1529
|
*
|
|
1258
1530
|
* ### Example
|
|
1259
1531
|
*
|
|
@@ -1268,22 +1540,36 @@ export type Base64UrlError = typeof Base64Url.Error;
|
|
|
1268
1540
|
*
|
|
1269
1541
|
* @category String
|
|
1270
1542
|
*/
|
|
1271
|
-
export const SimpleName =
|
|
1543
|
+
export const SimpleName = brand("SimpleName", UrlSafeString, (value) =>
|
|
1544
|
+
value.length >= 1 && value.length <= 64
|
|
1545
|
+
? ok(value)
|
|
1546
|
+
: err<SimpleNameError>({ type: "SimpleName", value }),
|
|
1547
|
+
);
|
|
1272
1548
|
export type SimpleName = typeof SimpleName.Type;
|
|
1273
|
-
export
|
|
1274
|
-
|
|
1275
|
-
/**
|
|
1276
|
-
* Default NanoId.
|
|
1277
|
-
*
|
|
1278
|
-
* @category String
|
|
1279
|
-
*/
|
|
1280
|
-
export const NanoId = regex("NanoId", /^[A-Za-z0-9_-]{21}$/)(String);
|
|
1281
|
-
export type NanoId = typeof NanoId.Type;
|
|
1282
|
-
export type NanoIdError = typeof NanoId.Error;
|
|
1549
|
+
export interface SimpleNameError extends TypeError<"SimpleName"> {}
|
|
1283
1550
|
|
|
1284
1551
|
/**
|
|
1285
1552
|
* Trimmed string between 8 and 64 characters, branded as `SimplePassword`.
|
|
1286
1553
|
*
|
|
1554
|
+
* Take a look how `SimplePassword` is defined:
|
|
1555
|
+
*
|
|
1556
|
+
* ```ts
|
|
1557
|
+
* export const SimplePassword = brand(
|
|
1558
|
+
* "SimplePassword",
|
|
1559
|
+
* minLength(8)(maxLength(64)(TrimmedString)),
|
|
1560
|
+
* );
|
|
1561
|
+
* ```
|
|
1562
|
+
*
|
|
1563
|
+
* Nested functions are often OK (if not, make a helper), but with TC39 Hack
|
|
1564
|
+
* pipes it would be clearer:
|
|
1565
|
+
*
|
|
1566
|
+
* ```ts
|
|
1567
|
+
* // TrimmedString
|
|
1568
|
+
* // |> minLength(8)(%)
|
|
1569
|
+
* // |> maxLength(64)(%)
|
|
1570
|
+
* // |> brand("SimplePassword", %)
|
|
1571
|
+
* ```
|
|
1572
|
+
*
|
|
1287
1573
|
* @category String
|
|
1288
1574
|
*/
|
|
1289
1575
|
export const SimplePassword = brand(
|
|
@@ -1304,33 +1590,146 @@ export const formatSimplePasswordError = (
|
|
|
1304
1590
|
);
|
|
1305
1591
|
|
|
1306
1592
|
/**
|
|
1307
|
-
*
|
|
1593
|
+
* Evolu Id: 16 bytes encoded as a 22‑character Base64Url string.
|
|
1308
1594
|
*
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1595
|
+
* There are three ways to create an Evolu Id:
|
|
1596
|
+
*
|
|
1597
|
+
* - {@link createId} – default cryptographically secure random bytes
|
|
1598
|
+
* (privacy‑preserving)
|
|
1599
|
+
* - {@link createIdFromString} – deterministic: first 16 bytes of SHA‑256 of a
|
|
1600
|
+
* string
|
|
1601
|
+
* - {@link createIdAsUuidv7} – optional: embeds timestamp bits (UUID v7 layout)
|
|
1602
|
+
*
|
|
1603
|
+
* Privacy: the default random Id does not leak creation time and is safe to
|
|
1604
|
+
* share or log. The UUID v7 variant leaks creation time anywhere the Id is
|
|
1605
|
+
* copied (logs, URLs, exports); only use it when you explicitly want insertion
|
|
1606
|
+
* locality for very large write‑heavy tables and accept timestamp exposure.
|
|
1607
|
+
*
|
|
1608
|
+
* ### Future
|
|
1609
|
+
*
|
|
1610
|
+
* A possible hybrid masked‑time approach (`timestamp ^ H(cluster_id, timestamp
|
|
1611
|
+
*
|
|
1612
|
+
* > > N)`) could provide locality without exposing raw creation time. See
|
|
1613
|
+
* > > https://brooker.co.za/blog/2025/10/22/uuidv7.html
|
|
1311
1614
|
*
|
|
1312
1615
|
* @category String
|
|
1313
1616
|
*/
|
|
1314
|
-
export const Id =
|
|
1617
|
+
export const Id = brand("Id", String, (value) =>
|
|
1618
|
+
value.length === 22 && Base64Url.fromParent(value).ok
|
|
1619
|
+
? ok(value)
|
|
1620
|
+
: err<IdError>({ type: "Id", value }),
|
|
1621
|
+
);
|
|
1315
1622
|
export type Id = typeof Id.Type;
|
|
1316
1623
|
|
|
1317
|
-
export
|
|
1624
|
+
export interface IdError extends TypeError<"Id"> {}
|
|
1625
|
+
|
|
1626
|
+
export const formatIdError = createTypeErrorFormatter<IdError>(
|
|
1627
|
+
(error) => `The value ${error.value} is not a valid Id.`,
|
|
1628
|
+
);
|
|
1318
1629
|
|
|
1319
1630
|
/**
|
|
1320
|
-
* Creates
|
|
1631
|
+
* Creates a random {@link Id}. This is the recommended default.
|
|
1632
|
+
*
|
|
1633
|
+
* Use {@link createIdFromString} for deterministic mapping of external IDs or
|
|
1634
|
+
* {@link createIdAsUuidv7} when you accept timestamp leakage for index
|
|
1635
|
+
* locality.
|
|
1321
1636
|
*
|
|
1322
1637
|
* ### Example
|
|
1323
1638
|
*
|
|
1324
1639
|
* ```ts
|
|
1325
|
-
* // string & Brand<"Id">
|
|
1326
1640
|
* const id = createId(deps);
|
|
1641
|
+
* const todoId = createId<"Todo">(deps);
|
|
1642
|
+
* ```
|
|
1643
|
+
*/
|
|
1644
|
+
export const createId = <B extends string = never>(
|
|
1645
|
+
deps: RandomBytesDep,
|
|
1646
|
+
): [B] extends [never] ? Id : Id & Brand<B> => {
|
|
1647
|
+
const id = uint8ArrayToBase64Url(deps.randomBytes.create(16));
|
|
1648
|
+
return id as unknown as [B] extends [never] ? Id : Id & Brand<B>;
|
|
1649
|
+
};
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* Creates an {@link Id} from a string using SHA-256.
|
|
1653
|
+
*
|
|
1654
|
+
* When integrating with external systems that use different ID formats, use
|
|
1655
|
+
* this function to convert external IDs into valid Evolu IDs.
|
|
1656
|
+
*
|
|
1657
|
+
* In Evolu's CRDT, the ID serves as the unique identifier for conflict
|
|
1658
|
+
* resolution across distributed clients. When multiple clients create records
|
|
1659
|
+
* with the same external identifier, they must resolve to the same Evolu ID to
|
|
1660
|
+
* ensure data consistency.
|
|
1661
|
+
*
|
|
1662
|
+
* ### Example
|
|
1663
|
+
*
|
|
1664
|
+
* ```ts
|
|
1665
|
+
* // Both clients will generate the same ID
|
|
1666
|
+
* const id1 = createIdFromString("user-api-123");
|
|
1667
|
+
* const id2 = createIdFromString("user-api-123");
|
|
1668
|
+
* console.log(id1 === id2); // true
|
|
1669
|
+
*
|
|
1670
|
+
* upsert("todo", {
|
|
1671
|
+
* id: createIdFromString("external-todo-456"),
|
|
1672
|
+
* title: "Synced from external system",
|
|
1673
|
+
* });
|
|
1327
1674
|
* ```
|
|
1675
|
+
*
|
|
1676
|
+
* **Important**: This transformation uses the first 16 bytes of SHA-256 hash of
|
|
1677
|
+
* the string bytes, therefore it's not possible to recover the original
|
|
1678
|
+
* external string from the generated {@link Id}. If you need to preserve the
|
|
1679
|
+
* original external ID, store it in a separate column.
|
|
1680
|
+
*
|
|
1681
|
+
* @category String
|
|
1328
1682
|
*/
|
|
1329
|
-
export const
|
|
1330
|
-
|
|
1683
|
+
export const createIdFromString = <B extends string = never>(
|
|
1684
|
+
value: string,
|
|
1685
|
+
): [B] extends [never] ? Id : Id & Brand<B> => {
|
|
1686
|
+
const hash = sha256(utf8ToBytes(value));
|
|
1687
|
+
// Take first 16 bytes of hash and convert to Id
|
|
1688
|
+
const id = idBytesToId(hash.slice(0, 16) as IdBytes);
|
|
1689
|
+
|
|
1690
|
+
return id as [B] extends [never] ? Id : Id & Brand<B>;
|
|
1691
|
+
};
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* Creates an {@link Id} embedding timestamp bits (UUID v7 layout) before
|
|
1695
|
+
* Base64Url encoding.
|
|
1696
|
+
*
|
|
1697
|
+
* Tradeoff: better insertion locality / index performance for huge datasets vs
|
|
1698
|
+
* leaking creation time everywhere the Id appears. Evolu uses {@link createId}
|
|
1699
|
+
* by default to avoid activity leakage; choose this only if you explicitly
|
|
1700
|
+
* accept timestamp exposure.
|
|
1701
|
+
*
|
|
1702
|
+
* ### Example
|
|
1703
|
+
*
|
|
1704
|
+
* ```ts
|
|
1705
|
+
* const id = createIdAsUuidv7({ randomBytes, time });
|
|
1706
|
+
* const todoId = createIdAsUuidv7<"Todo">({ randomBytes, time });
|
|
1707
|
+
* ```
|
|
1708
|
+
*/
|
|
1709
|
+
export const createIdAsUuidv7 = <B extends string = never>(
|
|
1710
|
+
deps: RandomBytesDep & TimeDep,
|
|
1711
|
+
): [B] extends [never] ? Id : Id & Brand<B> => {
|
|
1712
|
+
const id = deps.randomBytes.create(16);
|
|
1713
|
+
|
|
1714
|
+
const timestamp = globalThis.BigInt(deps.time.now());
|
|
1715
|
+
|
|
1716
|
+
id[0] = globalThis.Number((timestamp >> 40n) & 0xffn);
|
|
1717
|
+
id[1] = globalThis.Number((timestamp >> 32n) & 0xffn);
|
|
1718
|
+
id[2] = globalThis.Number((timestamp >> 24n) & 0xffn);
|
|
1719
|
+
id[3] = globalThis.Number((timestamp >> 16n) & 0xffn);
|
|
1720
|
+
id[4] = globalThis.Number((timestamp >> 8n) & 0xffn);
|
|
1721
|
+
id[5] = globalThis.Number(timestamp & 0xffn);
|
|
1722
|
+
|
|
1723
|
+
id[6] = (id[6] & 0x0f) | 0x70;
|
|
1724
|
+
id[8] = (id[8] & 0x3f) | 0x80;
|
|
1725
|
+
|
|
1726
|
+
return id as unknown as [B] extends [never] ? Id : Id & Brand<B>;
|
|
1727
|
+
};
|
|
1331
1728
|
|
|
1332
1729
|
/**
|
|
1333
|
-
*
|
|
1730
|
+
* Creates a branded {@link Id} Type for a table's primary key.
|
|
1731
|
+
*
|
|
1732
|
+
* The table name becomes an additional brand for type safety.
|
|
1334
1733
|
*
|
|
1335
1734
|
* ### Example
|
|
1336
1735
|
*
|
|
@@ -1342,55 +1741,64 @@ export const createId = (deps: NanoIdLibDep): Id =>
|
|
|
1342
1741
|
*
|
|
1343
1742
|
* @category String
|
|
1344
1743
|
*/
|
|
1345
|
-
export const id = <Table extends TypeName>(table: Table):
|
|
1744
|
+
export const id = <Table extends TypeName>(table: Table): TableId<Table> => {
|
|
1745
|
+
const fromUnknown = (value: unknown) => {
|
|
1746
|
+
const parentResult = String.fromUnknown(value);
|
|
1747
|
+
if (!parentResult.ok) return parentResult;
|
|
1748
|
+
return fromParent(parentResult.value);
|
|
1749
|
+
};
|
|
1750
|
+
|
|
1346
1751
|
const fromParent = (value: string) => {
|
|
1347
1752
|
const idResult = Id.fromParent(value);
|
|
1348
1753
|
if (!idResult.ok) {
|
|
1349
|
-
return err<
|
|
1754
|
+
return err<TableIdError<Table>>({ type: "TableId", value, table });
|
|
1350
1755
|
}
|
|
1351
1756
|
return ok(idResult.value as Id & Brand<Table>);
|
|
1352
1757
|
};
|
|
1353
1758
|
|
|
1354
|
-
const fromUnknown = (value: unknown) => {
|
|
1355
|
-
const parentResult = String.fromUnknown(value);
|
|
1356
|
-
if (!parentResult.ok) return parentResult;
|
|
1357
|
-
return fromParent(parentResult.value);
|
|
1358
|
-
};
|
|
1359
|
-
|
|
1360
1759
|
return {
|
|
1361
|
-
...createType("Id", {
|
|
1362
|
-
fromUnknown,
|
|
1363
|
-
to: (value: Id & Brand<Table>) => value as string,
|
|
1364
|
-
fromParent,
|
|
1365
|
-
toParent: (value: Id & Brand<Table>) => value as string,
|
|
1366
|
-
}),
|
|
1760
|
+
...createType("Id", { fromUnknown, fromParent }),
|
|
1367
1761
|
table,
|
|
1368
1762
|
};
|
|
1369
1763
|
};
|
|
1370
1764
|
|
|
1371
|
-
export interface
|
|
1765
|
+
export interface TableId<Table extends TypeName>
|
|
1372
1766
|
extends Type<
|
|
1373
1767
|
"Id",
|
|
1374
1768
|
string & Brand<"Id"> & Brand<Table>,
|
|
1375
1769
|
string,
|
|
1376
|
-
|
|
1770
|
+
TableIdError<Table>,
|
|
1377
1771
|
string,
|
|
1378
1772
|
StringError
|
|
1379
1773
|
> {
|
|
1380
1774
|
table: Table;
|
|
1381
1775
|
}
|
|
1382
1776
|
|
|
1383
|
-
export interface
|
|
1384
|
-
extends TypeError<"
|
|
1777
|
+
export interface TableIdError<Table extends TypeName = TypeName>
|
|
1778
|
+
extends TypeError<"TableId"> {
|
|
1385
1779
|
readonly table: Table;
|
|
1386
1780
|
}
|
|
1387
1781
|
|
|
1388
|
-
export const
|
|
1389
|
-
(error) => `Invalid ${error.
|
|
1782
|
+
export const formatTableIdError = createTypeErrorFormatter<TableIdError>(
|
|
1783
|
+
(error) => `Invalid Id for table ${error.table}: ${error.value}.`,
|
|
1390
1784
|
);
|
|
1391
1785
|
|
|
1786
|
+
/** Binary representation of an {@link Id}. */
|
|
1787
|
+
export const IdBytes = brand("IdBytes", length(16)(Uint8Array));
|
|
1788
|
+
export type IdBytes = typeof IdBytes.Type;
|
|
1789
|
+
|
|
1790
|
+
export const idBytesTypeValueLength = 16 as NonNegativeInt;
|
|
1791
|
+
|
|
1792
|
+
export const idToIdBytes = (id: Id): IdBytes =>
|
|
1793
|
+
// Id is Base64Url (validated by Id.from), cast is safe
|
|
1794
|
+
base64UrlToUint8Array(id as unknown as Base64Url) as IdBytes;
|
|
1795
|
+
|
|
1796
|
+
export const idBytesToId = (idBytes: IdBytes): Id =>
|
|
1797
|
+
// Base64Url encoding of 16 bytes always produces valid Id (22 chars)
|
|
1798
|
+
uint8ArrayToBase64Url(idBytes) as unknown as Id;
|
|
1799
|
+
|
|
1392
1800
|
/**
|
|
1393
|
-
* Positive number.
|
|
1801
|
+
* Positive number (> 0).
|
|
1394
1802
|
*
|
|
1395
1803
|
* ### Example
|
|
1396
1804
|
*
|
|
@@ -1413,11 +1821,11 @@ export const positive: BrandFactory<"Positive", number, PositiveError> = (
|
|
|
1413
1821
|
export interface PositiveError extends TypeError<"Positive"> {}
|
|
1414
1822
|
|
|
1415
1823
|
export const formatPositiveError = createTypeErrorFormatter<PositiveError>(
|
|
1416
|
-
(error) => `The value ${error.value}
|
|
1824
|
+
(error) => `The value ${error.value} must be positive (> 0).`,
|
|
1417
1825
|
);
|
|
1418
1826
|
|
|
1419
1827
|
/**
|
|
1420
|
-
* Negative number.
|
|
1828
|
+
* Negative number (< 0).
|
|
1421
1829
|
*
|
|
1422
1830
|
* ### Example
|
|
1423
1831
|
*
|
|
@@ -1437,11 +1845,11 @@ export const negative: BrandFactory<"Negative", number, NegativeError> = (
|
|
|
1437
1845
|
export interface NegativeError extends TypeError<"Negative"> {}
|
|
1438
1846
|
|
|
1439
1847
|
export const formatNegativeError = createTypeErrorFormatter<NegativeError>(
|
|
1440
|
-
(error) => `The value ${error.value}
|
|
1848
|
+
(error) => `The value ${error.value} must be negative (< 0).`,
|
|
1441
1849
|
);
|
|
1442
1850
|
|
|
1443
1851
|
/**
|
|
1444
|
-
* Non-positive number.
|
|
1852
|
+
* Non-positive number (≤ 0).
|
|
1445
1853
|
*
|
|
1446
1854
|
* ### Example
|
|
1447
1855
|
*
|
|
@@ -1466,11 +1874,11 @@ export interface NonPositiveError extends TypeError<"NonPositive"> {}
|
|
|
1466
1874
|
|
|
1467
1875
|
export const formatNonPositiveError =
|
|
1468
1876
|
createTypeErrorFormatter<NonPositiveError>(
|
|
1469
|
-
(error) => `The value ${error.value}
|
|
1877
|
+
(error) => `The value ${error.value} must be non-positive (≤ 0).`,
|
|
1470
1878
|
);
|
|
1471
1879
|
|
|
1472
1880
|
/**
|
|
1473
|
-
* Non-negative number.
|
|
1881
|
+
* Non-negative number (≥ 0).
|
|
1474
1882
|
*
|
|
1475
1883
|
* ### Example
|
|
1476
1884
|
*
|
|
@@ -1495,22 +1903,38 @@ export interface NonNegativeError extends TypeError<"NonNegative"> {}
|
|
|
1495
1903
|
|
|
1496
1904
|
export const formatNonNegativeError =
|
|
1497
1905
|
createTypeErrorFormatter<NonNegativeError>(
|
|
1498
|
-
(error) => `The value ${error.value}
|
|
1906
|
+
(error) => `The value ${error.value} must be non-negative (≥ 0).`,
|
|
1499
1907
|
);
|
|
1500
1908
|
|
|
1501
|
-
/**
|
|
1909
|
+
/**
|
|
1910
|
+
* Non-negative number (≥ 0).
|
|
1911
|
+
*
|
|
1912
|
+
* @category Number
|
|
1913
|
+
*/
|
|
1502
1914
|
export const NonNegativeNumber = nonNegative(Number);
|
|
1503
1915
|
export type NonNegativeNumber = typeof NonNegativeNumber.Type;
|
|
1504
1916
|
|
|
1505
|
-
/**
|
|
1917
|
+
/**
|
|
1918
|
+
* Positive number (> 0).
|
|
1919
|
+
*
|
|
1920
|
+
* @category Number
|
|
1921
|
+
*/
|
|
1506
1922
|
export const PositiveNumber = positive(NonNegativeNumber);
|
|
1507
1923
|
export type PositiveNumber = typeof PositiveNumber.Type;
|
|
1508
1924
|
|
|
1509
|
-
/**
|
|
1925
|
+
/**
|
|
1926
|
+
* Non-positive number (≤ 0).
|
|
1927
|
+
*
|
|
1928
|
+
* @category Number
|
|
1929
|
+
*/
|
|
1510
1930
|
export const NonPositiveNumber = nonPositive(Number);
|
|
1511
1931
|
export type NonPositiveNumber = typeof NonPositiveNumber.Type;
|
|
1512
1932
|
|
|
1513
|
-
/**
|
|
1933
|
+
/**
|
|
1934
|
+
* Negative number (< 0).
|
|
1935
|
+
*
|
|
1936
|
+
* @category Number
|
|
1937
|
+
*/
|
|
1514
1938
|
export const NegativeNumber = negative(NonPositiveNumber);
|
|
1515
1939
|
export type NegativeNumber = typeof NegativeNumber.Type;
|
|
1516
1940
|
|
|
@@ -1535,7 +1959,7 @@ export const int: BrandFactory<"Int", number, IntError> = (parent) =>
|
|
|
1535
1959
|
export interface IntError extends TypeError<"Int"> {}
|
|
1536
1960
|
|
|
1537
1961
|
export const formatIntError = createTypeErrorFormatter<IntError>(
|
|
1538
|
-
(error) => `The value ${error.value}
|
|
1962
|
+
(error) => `The value ${error.value} must be an integer.`,
|
|
1539
1963
|
);
|
|
1540
1964
|
|
|
1541
1965
|
/**
|
|
@@ -1546,19 +1970,40 @@ export const formatIntError = createTypeErrorFormatter<IntError>(
|
|
|
1546
1970
|
export const Int = int(Number);
|
|
1547
1971
|
export type Int = typeof Int.Type;
|
|
1548
1972
|
|
|
1549
|
-
/**
|
|
1973
|
+
/**
|
|
1974
|
+
* Non-negative integer (≥ 0).
|
|
1975
|
+
*
|
|
1976
|
+
* @category Number
|
|
1977
|
+
*/
|
|
1550
1978
|
export const NonNegativeInt = nonNegative(Int);
|
|
1551
1979
|
export type NonNegativeInt = typeof NonNegativeInt.Type;
|
|
1552
1980
|
|
|
1553
|
-
/**
|
|
1981
|
+
/**
|
|
1982
|
+
* Positive integer (> 0).
|
|
1983
|
+
*
|
|
1984
|
+
* @category Number
|
|
1985
|
+
*/
|
|
1554
1986
|
export const PositiveInt = positive(NonNegativeInt);
|
|
1555
1987
|
export type PositiveInt = typeof PositiveInt.Type;
|
|
1556
1988
|
|
|
1557
|
-
/**
|
|
1989
|
+
/** Maximum safe positive integer value for practically infinite operations. */
|
|
1990
|
+
export const maxPositiveInt = PositiveInt.orThrow(
|
|
1991
|
+
globalThis.Number.MAX_SAFE_INTEGER,
|
|
1992
|
+
);
|
|
1993
|
+
|
|
1994
|
+
/**
|
|
1995
|
+
* Non-positive integer (≤ 0).
|
|
1996
|
+
*
|
|
1997
|
+
* @category Number
|
|
1998
|
+
*/
|
|
1558
1999
|
export const NonPositiveInt = nonPositive(Int);
|
|
1559
2000
|
export type NonPositiveInt = typeof NonPositiveInt.Type;
|
|
1560
2001
|
|
|
1561
|
-
/**
|
|
2002
|
+
/**
|
|
2003
|
+
* Negative integer (< 0).
|
|
2004
|
+
*
|
|
2005
|
+
* @category Number
|
|
2006
|
+
*/
|
|
1562
2007
|
export const NegativeInt = negative(NonPositiveInt);
|
|
1563
2008
|
export type NegativeInt = typeof NegativeInt.Type;
|
|
1564
2009
|
|
|
@@ -1676,7 +2121,7 @@ export const nonNaN: BrandFactory<"NonNaN", number, NonNaNError> = (parent) =>
|
|
|
1676
2121
|
export interface NonNaNError extends TypeError<"NonNaN"> {}
|
|
1677
2122
|
|
|
1678
2123
|
export const formatNonNaNError = createTypeErrorFormatter<NonNaNError>(
|
|
1679
|
-
(
|
|
2124
|
+
() => `The value must not be NaN.`,
|
|
1680
2125
|
);
|
|
1681
2126
|
|
|
1682
2127
|
/** @category Number */
|
|
@@ -1698,7 +2143,7 @@ export const finite: BrandFactory<"Finite", number, FiniteError> = (parent) =>
|
|
|
1698
2143
|
export interface FiniteError extends TypeError<"Finite"> {}
|
|
1699
2144
|
|
|
1700
2145
|
export const formatFiniteError = createTypeErrorFormatter<FiniteError>(
|
|
1701
|
-
(error) => `The value ${error.value}
|
|
2146
|
+
(error) => `The value ${error.value} must be finite.`,
|
|
1702
2147
|
);
|
|
1703
2148
|
|
|
1704
2149
|
/**
|
|
@@ -1778,195 +2223,54 @@ export const formatBetweenError = createTypeErrorFormatter<BetweenError>(
|
|
|
1778
2223
|
`The value ${error.value} is not between ${error.min} and ${error.max}, inclusive.`,
|
|
1779
2224
|
);
|
|
1780
2225
|
|
|
1781
|
-
/** @category Number */
|
|
1782
|
-
export const Between1And10 = between(1, 10)(Number);
|
|
1783
|
-
export type Between1And10 = typeof Between1And10.Type;
|
|
1784
|
-
|
|
1785
2226
|
/**
|
|
1786
2227
|
* Literal {@link Type}.
|
|
1787
2228
|
*
|
|
1788
|
-
* https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types
|
|
1789
|
-
*
|
|
1790
|
-
* ### Example
|
|
1791
|
-
*
|
|
1792
|
-
* ```ts
|
|
1793
|
-
* const LiteralHello = literal("Hello");
|
|
1794
|
-
* const result = LiteralHello.from("Hello"); // ok("Hello")
|
|
1795
|
-
* const errorResult = LiteralHello.from("World"); // err
|
|
1796
|
-
* ```
|
|
1797
|
-
*
|
|
1798
|
-
* TODO: Add JsonValue
|
|
1799
|
-
*
|
|
1800
|
-
* @category Base Factories
|
|
1801
|
-
*/
|
|
1802
|
-
export const literal = <T extends Literal>(expected: T): LiteralType<T> => {
|
|
1803
|
-
const fromUnknown = (value: unknown): Result<T, LiteralError<T>> =>
|
|
1804
|
-
value === expected
|
|
1805
|
-
? ok(expected)
|
|
1806
|
-
: err<LiteralError<T>>({ type: "Literal", value, expected });
|
|
1807
|
-
|
|
1808
|
-
return {
|
|
1809
|
-
...createType("Literal", {
|
|
1810
|
-
fromUnknown,
|
|
1811
|
-
to: identity as (value: T) => WidenLiteral<T>,
|
|
1812
|
-
fromParent: ok,
|
|
1813
|
-
toParent: identity,
|
|
1814
|
-
}),
|
|
1815
|
-
expected,
|
|
1816
|
-
};
|
|
1817
|
-
};
|
|
1818
|
-
|
|
1819
|
-
export interface LiteralType<T extends Literal>
|
|
1820
|
-
extends Type<"Literal", T, WidenLiteral<T>, LiteralError<T>> {
|
|
1821
|
-
expected: T;
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
export interface LiteralError<T extends Literal = Literal>
|
|
1825
|
-
extends TypeError<"Literal"> {
|
|
1826
|
-
readonly expected: T;
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
export const formatLiteralError = createTypeErrorFormatter<LiteralError>(
|
|
1830
|
-
(error) =>
|
|
1831
|
-
`The value ${error.value} is not strictly equal to the expected literal: ${globalThis.String(
|
|
1832
|
-
error.expected,
|
|
1833
|
-
)}.`,
|
|
1834
|
-
);
|
|
1835
|
-
|
|
1836
|
-
/**
|
|
1837
|
-
* {@link Type} that transforms values between `FromType` and `ToType`.
|
|
1838
|
-
*
|
|
1839
|
-
* - `fromParent`: Converts `FromType` to `ToType`, may fail.
|
|
1840
|
-
* - `toParent`: Converts `ToType` back to `FromType`, must not fail.
|
|
1841
|
-
*
|
|
1842
|
-
* ### Example
|
|
1843
|
-
*
|
|
1844
|
-
* // TODO: Examples
|
|
1845
|
-
*
|
|
1846
|
-
* @category Base Factories
|
|
1847
|
-
*/
|
|
1848
|
-
export const transform = <
|
|
1849
|
-
FromType extends AnyType,
|
|
1850
|
-
ToType extends AnyType,
|
|
1851
|
-
TransformError extends TypeError = never,
|
|
1852
|
-
>(
|
|
1853
|
-
fromType: FromType,
|
|
1854
|
-
toType: ToType,
|
|
1855
|
-
fromParent: (
|
|
1856
|
-
parentValue: InferType<FromType>,
|
|
1857
|
-
) => Result<InferType<ToType>, TransformError>,
|
|
1858
|
-
toParent: (value: InferType<ToType>) => InferType<FromType>,
|
|
1859
|
-
): TransformType<FromType, ToType, TransformError> => {
|
|
1860
|
-
const fromUnknown = (
|
|
1861
|
-
value: unknown,
|
|
1862
|
-
): Result<InferType<ToType>, InferErrors<FromType> | TransformError> => {
|
|
1863
|
-
const parentResult = fromType.fromUnknown(value);
|
|
1864
|
-
if (!parentResult.ok) return parentResult;
|
|
1865
|
-
return fromParent(parentResult.value as IntentionalNever);
|
|
1866
|
-
};
|
|
1867
|
-
|
|
1868
|
-
const to = (value: InferType<ToType>): InferInput<FromType> =>
|
|
1869
|
-
fromType.to(toParent(value)) as IntentionalNever;
|
|
1870
|
-
|
|
1871
|
-
return {
|
|
1872
|
-
...createType("Transform", {
|
|
1873
|
-
fromUnknown,
|
|
1874
|
-
to,
|
|
1875
|
-
fromParent,
|
|
1876
|
-
toParent,
|
|
1877
|
-
}),
|
|
1878
|
-
is: toType.is,
|
|
1879
|
-
fromType,
|
|
1880
|
-
toType,
|
|
1881
|
-
} as TransformType<FromType, ToType, TransformError>;
|
|
1882
|
-
};
|
|
1883
|
-
|
|
1884
|
-
/**
|
|
1885
|
-
* TransformType extends {@link Type} with additional `fromType` and `toType`
|
|
1886
|
-
* properties for reflection.
|
|
1887
|
-
*/
|
|
1888
|
-
export interface TransformType<
|
|
1889
|
-
FromType extends AnyType,
|
|
1890
|
-
ToType extends AnyType,
|
|
1891
|
-
TransformError extends TypeError = never,
|
|
1892
|
-
> extends Type<
|
|
1893
|
-
"Transform",
|
|
1894
|
-
InferType<ToType>,
|
|
1895
|
-
InferInput<FromType>,
|
|
1896
|
-
TransformError,
|
|
1897
|
-
InferType<FromType>,
|
|
1898
|
-
InferErrors<FromType>
|
|
1899
|
-
> {
|
|
1900
|
-
readonly fromType: FromType;
|
|
1901
|
-
readonly toType: ToType;
|
|
1902
|
-
|
|
1903
|
-
readonly fromParent: (
|
|
1904
|
-
value: InferType<FromType>,
|
|
1905
|
-
) => [TransformError] extends [never]
|
|
1906
|
-
? Ok<InferType<ToType>>
|
|
1907
|
-
: Result<InferType<ToType>, TransformError>;
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
/**
|
|
1911
|
-
* Trims leading and trailing whitespace from a string.
|
|
1912
|
-
*
|
|
1913
|
-
* ### Example
|
|
1914
|
-
*
|
|
1915
|
-
* ```ts
|
|
1916
|
-
* expect(TrimString.from("a ")).toEqual(ok("a"));
|
|
1917
|
-
* expect(TrimString.fromParent("a ").value).toEqual("a");
|
|
1918
|
-
* ```
|
|
1919
|
-
*
|
|
1920
|
-
* @category String
|
|
1921
|
-
*/
|
|
1922
|
-
export const TrimString = trim(String);
|
|
1923
|
-
|
|
1924
|
-
/**
|
|
1925
|
-
* Transforms a {@link Date} into a {@link DateIsoString} string and vice versa.
|
|
1926
|
-
*
|
|
1927
|
-
* ### Example
|
|
1928
|
-
*
|
|
1929
|
-
* TODO:
|
|
1930
|
-
*
|
|
1931
|
-
* @category String
|
|
1932
|
-
*/
|
|
1933
|
-
export const DateIso = transform(
|
|
1934
|
-
Date,
|
|
1935
|
-
DateIsoString,
|
|
1936
|
-
(value) => DateIsoString.fromParent(value.toISOString()),
|
|
1937
|
-
(value) => new globalThis.Date(value),
|
|
1938
|
-
);
|
|
1939
|
-
|
|
1940
|
-
/**
|
|
1941
|
-
* Transforms a {@link NonEmptyTrimmedString} into a {@link FiniteNumber}.
|
|
1942
|
-
*
|
|
2229
|
+
* https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types
|
|
2230
|
+
*
|
|
1943
2231
|
* ### Example
|
|
1944
2232
|
*
|
|
1945
2233
|
* ```ts
|
|
1946
|
-
*
|
|
1947
|
-
*
|
|
2234
|
+
* const LiteralHello = literal("Hello");
|
|
2235
|
+
* const result = LiteralHello.from("Hello"); // ok("Hello")
|
|
2236
|
+
* const errorResult = LiteralHello.from("World"); // err
|
|
1948
2237
|
* ```
|
|
1949
2238
|
*
|
|
1950
|
-
*
|
|
2239
|
+
* TODO: Add JsonValue
|
|
2240
|
+
*
|
|
2241
|
+
* @category Base Factories
|
|
1951
2242
|
*/
|
|
1952
|
-
export const
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
if (!result.ok)
|
|
1958
|
-
return err<NumberFromStringError>({ type: "NumberFromString", value });
|
|
1959
|
-
return result;
|
|
1960
|
-
},
|
|
1961
|
-
(num) => num.toString() as NonEmptyTrimmedString,
|
|
1962
|
-
);
|
|
2243
|
+
export const literal = <T extends Literal>(expected: T): LiteralType<T> => {
|
|
2244
|
+
const fromUnknown = (value: unknown): Result<T, LiteralError<T>> =>
|
|
2245
|
+
value === expected
|
|
2246
|
+
? ok(expected)
|
|
2247
|
+
: err<LiteralError<T>>({ type: "Literal", value, expected });
|
|
1963
2248
|
|
|
1964
|
-
|
|
2249
|
+
return {
|
|
2250
|
+
...createType("Literal", {
|
|
2251
|
+
fromUnknown,
|
|
2252
|
+
fromParent: ok,
|
|
2253
|
+
}),
|
|
2254
|
+
expected,
|
|
2255
|
+
} as LiteralType<T>;
|
|
2256
|
+
};
|
|
1965
2257
|
|
|
1966
|
-
export
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
2258
|
+
export interface LiteralType<T extends Literal>
|
|
2259
|
+
extends Type<"Literal", T, WidenLiteral<T>, LiteralError<T>> {
|
|
2260
|
+
expected: T;
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
export interface LiteralError<T extends Literal = Literal>
|
|
2264
|
+
extends TypeError<"Literal"> {
|
|
2265
|
+
readonly expected: T;
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
export const formatLiteralError = createTypeErrorFormatter<LiteralError>(
|
|
2269
|
+
(error) =>
|
|
2270
|
+
`The value ${error.value} is not strictly equal to the expected literal: ${globalThis.String(
|
|
2271
|
+
error.expected,
|
|
2272
|
+
)}.`,
|
|
2273
|
+
);
|
|
1970
2274
|
|
|
1971
2275
|
/**
|
|
1972
2276
|
* Array of a specific {@link Type}.
|
|
@@ -2020,9 +2324,6 @@ export const array = <ElementType extends AnyType>(
|
|
|
2020
2324
|
return ok(result);
|
|
2021
2325
|
};
|
|
2022
2326
|
|
|
2023
|
-
const to = (value: ReadonlyArray<InferType<ElementType>>) =>
|
|
2024
|
-
value.map(element.to) as ReadonlyArray<InferInput<ElementType>>;
|
|
2025
|
-
|
|
2026
2327
|
const fromParent = (
|
|
2027
2328
|
value: ReadonlyArray<InferParent<ElementType>>,
|
|
2028
2329
|
): Result<
|
|
@@ -2048,18 +2349,10 @@ export const array = <ElementType extends AnyType>(
|
|
|
2048
2349
|
return ok(result);
|
|
2049
2350
|
};
|
|
2050
2351
|
|
|
2051
|
-
const toParent = (values: ReadonlyArray<InferType<ElementType>>) =>
|
|
2052
|
-
values.map(element.toParent) as ReadonlyArray<InferParent<ElementType>>;
|
|
2053
|
-
|
|
2054
2352
|
return {
|
|
2055
|
-
...createType("Array", {
|
|
2056
|
-
fromUnknown,
|
|
2057
|
-
to,
|
|
2058
|
-
fromParent,
|
|
2059
|
-
toParent,
|
|
2060
|
-
}),
|
|
2353
|
+
...createType("Array", { fromUnknown, fromParent }),
|
|
2061
2354
|
element,
|
|
2062
|
-
}
|
|
2355
|
+
};
|
|
2063
2356
|
};
|
|
2064
2357
|
|
|
2065
2358
|
/** ArrayType extends Type with an additional `element` property for reflection. */
|
|
@@ -2192,16 +2485,6 @@ export const record = <
|
|
|
2192
2485
|
return ok(result);
|
|
2193
2486
|
};
|
|
2194
2487
|
|
|
2195
|
-
const to = (
|
|
2196
|
-
value: Readonly<Record<KeyT, InferType<Value>>>,
|
|
2197
|
-
): Readonly<Record<KeyInput, InferInput<Value>>> =>
|
|
2198
|
-
Object.fromEntries(
|
|
2199
|
-
Object.entries(value).map(([key, val]) => [
|
|
2200
|
-
keyType.to(key as KeyT),
|
|
2201
|
-
valueType.to(val),
|
|
2202
|
-
]),
|
|
2203
|
-
) as Readonly<Record<KeyInput, InferInput<Value>>>;
|
|
2204
|
-
|
|
2205
2488
|
const fromParent = (
|
|
2206
2489
|
value: Readonly<Record<KeyParent, InferParent<Value>>>,
|
|
2207
2490
|
): Result<
|
|
@@ -2246,22 +2529,10 @@ export const record = <
|
|
|
2246
2529
|
return ok(result);
|
|
2247
2530
|
};
|
|
2248
2531
|
|
|
2249
|
-
const toParent = (
|
|
2250
|
-
value: Readonly<Record<KeyT, InferType<Value>>>,
|
|
2251
|
-
): Readonly<Record<KeyParent, InferParent<Value>>> =>
|
|
2252
|
-
Object.fromEntries(
|
|
2253
|
-
Object.entries(value).map(([key, val]) => [
|
|
2254
|
-
keyType.toParent(key as KeyT),
|
|
2255
|
-
valueType.toParent(val),
|
|
2256
|
-
]),
|
|
2257
|
-
) as Readonly<Record<KeyParent, InferParent<Value>>>;
|
|
2258
|
-
|
|
2259
2532
|
return {
|
|
2260
2533
|
...createType("Record", {
|
|
2261
2534
|
fromUnknown,
|
|
2262
|
-
to,
|
|
2263
2535
|
fromParent,
|
|
2264
|
-
toParent,
|
|
2265
2536
|
}),
|
|
2266
2537
|
key: keyType,
|
|
2267
2538
|
value: valueType,
|
|
@@ -2561,25 +2832,6 @@ export function object(
|
|
|
2561
2832
|
return ok(result);
|
|
2562
2833
|
};
|
|
2563
2834
|
|
|
2564
|
-
const to = (value: any) => {
|
|
2565
|
-
const entries: [string, any][] = [];
|
|
2566
|
-
for (const key of propKeys) {
|
|
2567
|
-
if (!(key in value) && isOptionalType(props[key])) {
|
|
2568
|
-
continue;
|
|
2569
|
-
}
|
|
2570
|
-
entries.push([key, props[key].to(value[key])]);
|
|
2571
|
-
}
|
|
2572
|
-
if (record) {
|
|
2573
|
-
const recordEntries = Object.entries(value).filter(
|
|
2574
|
-
([key]) => !propKeys.includes(key),
|
|
2575
|
-
);
|
|
2576
|
-
for (const [key, val] of recordEntries) {
|
|
2577
|
-
entries.push([record.key.to(key), record.value.to(val)]);
|
|
2578
|
-
}
|
|
2579
|
-
}
|
|
2580
|
-
return Object.fromEntries(entries);
|
|
2581
|
-
};
|
|
2582
|
-
|
|
2583
2835
|
const fromParent = (value: any): Result<any, any> => {
|
|
2584
2836
|
const errors: Record<string, any> = {};
|
|
2585
2837
|
const result: Record<string, any> = {};
|
|
@@ -2631,31 +2883,10 @@ export function object(
|
|
|
2631
2883
|
return ok(result);
|
|
2632
2884
|
};
|
|
2633
2885
|
|
|
2634
|
-
const toParent = (value: any) => {
|
|
2635
|
-
const entries: [string, any][] = [];
|
|
2636
|
-
for (const key of propKeys) {
|
|
2637
|
-
if (!(key in value) && isOptionalType(props[key])) {
|
|
2638
|
-
continue;
|
|
2639
|
-
}
|
|
2640
|
-
entries.push([key, props[key].toParent(value[key])]);
|
|
2641
|
-
}
|
|
2642
|
-
if (record) {
|
|
2643
|
-
const recordEntries = Object.entries(value).filter(
|
|
2644
|
-
([key]) => !propKeys.includes(key),
|
|
2645
|
-
);
|
|
2646
|
-
for (const [key, val] of recordEntries) {
|
|
2647
|
-
entries.push([record.key.toParent(key), record.value.toParent(val)]);
|
|
2648
|
-
}
|
|
2649
|
-
}
|
|
2650
|
-
return Object.fromEntries(entries);
|
|
2651
|
-
};
|
|
2652
|
-
|
|
2653
2886
|
return {
|
|
2654
2887
|
...createType(record ? "ObjectWithRecord" : "Object", {
|
|
2655
2888
|
fromUnknown,
|
|
2656
|
-
to,
|
|
2657
2889
|
fromParent,
|
|
2658
|
-
toParent,
|
|
2659
2890
|
}),
|
|
2660
2891
|
props,
|
|
2661
2892
|
...(record ? { record } : {}),
|
|
@@ -2897,7 +3128,6 @@ export function union(...args: ReadonlyArray<any>): any {
|
|
|
2897
3128
|
* cases.
|
|
2898
3129
|
*/
|
|
2899
3130
|
|
|
2900
|
-
/* eslint-disable */
|
|
2901
3131
|
const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
|
|
2902
3132
|
|
|
2903
3133
|
const fromUnknown = (value: unknown) => {
|
|
@@ -2915,23 +3145,13 @@ export function union(...args: ReadonlyArray<any>): any {
|
|
|
2915
3145
|
});
|
|
2916
3146
|
};
|
|
2917
3147
|
|
|
2918
|
-
const to = (value: any) => {
|
|
2919
|
-
for (const member of members) {
|
|
2920
|
-
if (member.is(value)) return member.to(value);
|
|
2921
|
-
}
|
|
2922
|
-
assert(false, "No matching member found in Union Type `to` function");
|
|
2923
|
-
};
|
|
2924
|
-
|
|
2925
3148
|
return {
|
|
2926
3149
|
...createType("Union", {
|
|
2927
3150
|
fromUnknown,
|
|
2928
|
-
to,
|
|
2929
3151
|
fromParent: fromUnknown,
|
|
2930
|
-
toParent: to,
|
|
2931
3152
|
}),
|
|
2932
3153
|
members,
|
|
2933
3154
|
};
|
|
2934
|
-
/* eslint-enable */
|
|
2935
3155
|
}
|
|
2936
3156
|
|
|
2937
3157
|
/**
|
|
@@ -3025,20 +3245,10 @@ export const recursive = <ParentType extends AnyType>(
|
|
|
3025
3245
|
type ??= create();
|
|
3026
3246
|
return type.fromUnknown(value);
|
|
3027
3247
|
},
|
|
3028
|
-
to: (value: T) => {
|
|
3029
|
-
type ??= create();
|
|
3030
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
3031
|
-
return type.to(value);
|
|
3032
|
-
},
|
|
3033
3248
|
fromParent: (value: Parent) => {
|
|
3034
3249
|
type ??= create();
|
|
3035
3250
|
return type.fromParent(value);
|
|
3036
3251
|
},
|
|
3037
|
-
toParent: (value: T) => {
|
|
3038
|
-
type ??= create();
|
|
3039
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
3040
|
-
return type.toParent(value);
|
|
3041
|
-
},
|
|
3042
3252
|
is: (value: unknown): value is T => {
|
|
3043
3253
|
type ??= create();
|
|
3044
3254
|
return type.is(value);
|
|
@@ -3176,9 +3386,6 @@ export const tuple = <Elements extends [AnyType, ...ReadonlyArray<AnyType>]>(
|
|
|
3176
3386
|
return ok(result);
|
|
3177
3387
|
};
|
|
3178
3388
|
|
|
3179
|
-
const to = (value: { [K in keyof Elements]: InferType<Elements[K]> }) =>
|
|
3180
|
-
value.map((val, index) => elements[index].to(val) as IntentionalNever);
|
|
3181
|
-
|
|
3182
3389
|
const fromParent = (value: {
|
|
3183
3390
|
[K in keyof Elements]: InferParent<Elements[K]>;
|
|
3184
3391
|
}) => {
|
|
@@ -3210,21 +3417,10 @@ export const tuple = <Elements extends [AnyType, ...ReadonlyArray<AnyType>]>(
|
|
|
3210
3417
|
return ok(result as { [K in keyof Elements]: InferType<Elements[K]> });
|
|
3211
3418
|
};
|
|
3212
3419
|
|
|
3213
|
-
const toParent = (value: { [K in keyof Elements]: InferType<Elements[K]> }): {
|
|
3214
|
-
[K in keyof Elements]: InferParent<Elements[K]>;
|
|
3215
|
-
} =>
|
|
3216
|
-
value.map(
|
|
3217
|
-
(val, index) => elements[index].toParent(val) as IntentionalNever,
|
|
3218
|
-
) as {
|
|
3219
|
-
[K in keyof Elements]: InferParent<Elements[K]>;
|
|
3220
|
-
};
|
|
3221
|
-
|
|
3222
3420
|
return {
|
|
3223
3421
|
...createType("Tuple", {
|
|
3224
3422
|
fromUnknown,
|
|
3225
|
-
to,
|
|
3226
3423
|
fromParent,
|
|
3227
|
-
toParent,
|
|
3228
3424
|
}),
|
|
3229
3425
|
elements,
|
|
3230
3426
|
} as IntentionalNever;
|
|
@@ -3300,37 +3496,21 @@ export const formatInt64Error = createTypeErrorFormatter<Int64Error>(
|
|
|
3300
3496
|
`The value ${error.value} is not a valid 64-bit signed integer (Int64).`,
|
|
3301
3497
|
);
|
|
3302
3498
|
|
|
3303
|
-
export const BigIntFromString = transform(
|
|
3304
|
-
String,
|
|
3305
|
-
BigInt,
|
|
3306
|
-
(value) =>
|
|
3307
|
-
trySync(
|
|
3308
|
-
() => globalThis.BigInt(value),
|
|
3309
|
-
(): BigIntFromStringError => ({ type: "BigIntFromString", value }),
|
|
3310
|
-
),
|
|
3311
|
-
(value) => value.toString(),
|
|
3312
|
-
);
|
|
3313
|
-
|
|
3314
|
-
export interface BigIntFromStringError extends TypeError<"BigIntFromString"> {}
|
|
3315
|
-
|
|
3316
|
-
export const formatBigIntFromStringError =
|
|
3317
|
-
createTypeErrorFormatter<BigIntFromStringError>(
|
|
3318
|
-
(error) => `The value ${error.value} could not be converted to a BigInt.`,
|
|
3319
|
-
);
|
|
3320
|
-
|
|
3321
3499
|
/**
|
|
3322
3500
|
* Stringified {@link Int64}.
|
|
3323
3501
|
*
|
|
3324
|
-
* @category
|
|
3502
|
+
* @category String
|
|
3325
3503
|
*/
|
|
3326
|
-
export const Int64String = brand("Int64",
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
})
|
|
3504
|
+
export const Int64String = brand("Int64", NonEmptyTrimmedString, (value) =>
|
|
3505
|
+
trySync(
|
|
3506
|
+
() => {
|
|
3507
|
+
const maybeInt = globalThis.BigInt(value);
|
|
3508
|
+
Int64.orThrow(maybeInt);
|
|
3509
|
+
return value;
|
|
3510
|
+
},
|
|
3511
|
+
(): Int64StringError => ({ type: "Int64String", value }),
|
|
3512
|
+
),
|
|
3513
|
+
);
|
|
3334
3514
|
|
|
3335
3515
|
export type Int64String = typeof Int64String.Type;
|
|
3336
3516
|
|
|
@@ -3435,42 +3615,14 @@ export const JsonArray = array(JsonValue);
|
|
|
3435
3615
|
*/
|
|
3436
3616
|
export const JsonObject = record(String, JsonValue);
|
|
3437
3617
|
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
* JsonValueFromString.to({ key: "value" }); // -> '{"key":"value"}'
|
|
3447
|
-
* ```
|
|
3448
|
-
*
|
|
3449
|
-
* @category String
|
|
3450
|
-
*/
|
|
3451
|
-
export const JsonValueFromString = transform(
|
|
3452
|
-
String,
|
|
3453
|
-
JsonValue,
|
|
3454
|
-
(value) =>
|
|
3455
|
-
trySync(
|
|
3456
|
-
() => JSON.parse(value) as JsonValue,
|
|
3457
|
-
(error): JsonValueFromStringError => ({
|
|
3458
|
-
type: `JsonValueFromString`,
|
|
3459
|
-
value,
|
|
3460
|
-
message: globalThis.String(error),
|
|
3461
|
-
}),
|
|
3462
|
-
),
|
|
3463
|
-
(value) => JSON.stringify(value),
|
|
3464
|
-
);
|
|
3465
|
-
|
|
3466
|
-
export interface JsonValueFromStringError
|
|
3467
|
-
extends TypeError<"JsonValueFromString"> {
|
|
3468
|
-
readonly message: string;
|
|
3469
|
-
}
|
|
3470
|
-
|
|
3471
|
-
export const formatJsonValueFromStringError =
|
|
3472
|
-
createTypeErrorFormatter<JsonValueFromStringError>(
|
|
3473
|
-
(error) => `Invalid JSONValue: ${error.value}. Error: ${error.message}`,
|
|
3618
|
+
export const parseJson = (value: string): Result<JsonValue, JsonError> =>
|
|
3619
|
+
trySync(
|
|
3620
|
+
() => JSON.parse(value) as JsonValue,
|
|
3621
|
+
(error): JsonError => ({
|
|
3622
|
+
type: `Json`,
|
|
3623
|
+
value,
|
|
3624
|
+
message: globalThis.String(error),
|
|
3625
|
+
}),
|
|
3474
3626
|
);
|
|
3475
3627
|
|
|
3476
3628
|
/**
|
|
@@ -3479,20 +3631,15 @@ export const formatJsonValueFromStringError =
|
|
|
3479
3631
|
* ### Example
|
|
3480
3632
|
*
|
|
3481
3633
|
* ```ts
|
|
3482
|
-
* const result = Json.from('{"key":"value"}'); //
|
|
3483
|
-
* const error = Json.from("invalid json"); //
|
|
3634
|
+
* const result = Json.from('{"key":"value"}'); // ok
|
|
3635
|
+
* const error = Json.from("invalid json"); // err
|
|
3484
3636
|
* ```
|
|
3485
3637
|
*
|
|
3486
3638
|
* @category String
|
|
3487
3639
|
*/
|
|
3488
3640
|
export const Json = brand("Json", String, (value) => {
|
|
3489
|
-
const result =
|
|
3490
|
-
if (!result.ok)
|
|
3491
|
-
return err<JsonError>({
|
|
3492
|
-
type: "Json",
|
|
3493
|
-
value,
|
|
3494
|
-
message: result.error.message,
|
|
3495
|
-
});
|
|
3641
|
+
const result = parseJson(value);
|
|
3642
|
+
if (!result.ok) return result;
|
|
3496
3643
|
return ok(value);
|
|
3497
3644
|
});
|
|
3498
3645
|
|
|
@@ -3506,6 +3653,80 @@ export const formatJsonError = createTypeErrorFormatter<JsonError>(
|
|
|
3506
3653
|
(error) => `Invalid JSON: ${error.value}. Error: ${error.message}`,
|
|
3507
3654
|
);
|
|
3508
3655
|
|
|
3656
|
+
export const jsonValueToJson = (value: JsonValue): Json =>
|
|
3657
|
+
JSON.stringify(value) as Json;
|
|
3658
|
+
|
|
3659
|
+
export const jsonToJsonValue = (value: Json): JsonValue =>
|
|
3660
|
+
JSON.parse(value) as JsonValue;
|
|
3661
|
+
|
|
3662
|
+
/**
|
|
3663
|
+
* Creates a branded JSON string {@link Type} and type-safe conversion functions
|
|
3664
|
+
* for a given Type.
|
|
3665
|
+
*
|
|
3666
|
+
* This factory creates:
|
|
3667
|
+
*
|
|
3668
|
+
* 1. A branded string Type that validates JSON parsing and structural conformity
|
|
3669
|
+
* 2. A serialization function (Type → branded JSON string)
|
|
3670
|
+
* 3. A parsing function (branded JSON string → Type, skipping validation)
|
|
3671
|
+
*
|
|
3672
|
+
* Optimized for Evolu's SQLite workflow where we store typed JSON strings and
|
|
3673
|
+
* need type-safe conversions without double parsing.
|
|
3674
|
+
*
|
|
3675
|
+
* ### Example
|
|
3676
|
+
*
|
|
3677
|
+
* ```ts
|
|
3678
|
+
* const Person = object({
|
|
3679
|
+
* name: NonEmptyString100,
|
|
3680
|
+
* age: FiniteNumber,
|
|
3681
|
+
* });
|
|
3682
|
+
* type Person = typeof Person.Type;
|
|
3683
|
+
*
|
|
3684
|
+
* const [PersonJson, personToPersonJson, personJsonToPerson] = json(
|
|
3685
|
+
* Person,
|
|
3686
|
+
* "PersonJson",
|
|
3687
|
+
* );
|
|
3688
|
+
* // string & Brand<"PersonJson">
|
|
3689
|
+
* type PersonJson = typeof PersonJson.Type;
|
|
3690
|
+
*
|
|
3691
|
+
* // Usage:
|
|
3692
|
+
* const person: Person = { name: "Alice", age: 30 };
|
|
3693
|
+
* const jsonString = personToPersonJson(person); // PersonJson
|
|
3694
|
+
* const backToPerson = personJsonToPerson(jsonString); // Person
|
|
3695
|
+
* ```
|
|
3696
|
+
*/
|
|
3697
|
+
export const json = <T extends AnyType, Name extends TypeName>(
|
|
3698
|
+
type: T,
|
|
3699
|
+
name: Name,
|
|
3700
|
+
): [
|
|
3701
|
+
BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>,
|
|
3702
|
+
(
|
|
3703
|
+
value: InferType<T>,
|
|
3704
|
+
) => InferType<
|
|
3705
|
+
BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>
|
|
3706
|
+
>,
|
|
3707
|
+
(
|
|
3708
|
+
value: InferType<
|
|
3709
|
+
BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>
|
|
3710
|
+
>,
|
|
3711
|
+
) => InferType<T>,
|
|
3712
|
+
] => {
|
|
3713
|
+
const BrandedJsonType = brand(name, String, (value) => {
|
|
3714
|
+
const parseResult = parseJson(value);
|
|
3715
|
+
if (!parseResult.ok) return parseResult;
|
|
3716
|
+
|
|
3717
|
+
const validationResult = type.fromUnknown(parseResult.value);
|
|
3718
|
+
if (!validationResult.ok) return validationResult;
|
|
3719
|
+
|
|
3720
|
+
return ok(value);
|
|
3721
|
+
}) as BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>;
|
|
3722
|
+
|
|
3723
|
+
return [
|
|
3724
|
+
BrandedJsonType,
|
|
3725
|
+
jsonValueToJson as IntentionalNever,
|
|
3726
|
+
jsonToJsonValue as IntentionalNever,
|
|
3727
|
+
];
|
|
3728
|
+
};
|
|
3729
|
+
|
|
3509
3730
|
/**
|
|
3510
3731
|
* Optional {@link Type}.
|
|
3511
3732
|
*
|
|
@@ -3530,9 +3751,7 @@ export const formatJsonError = createTypeErrorFormatter<JsonError>(
|
|
|
3530
3751
|
export const optional = <T extends AnyType>(type: T): OptionalType<T> => ({
|
|
3531
3752
|
...createType("Optional", {
|
|
3532
3753
|
fromUnknown: type.fromUnknown,
|
|
3533
|
-
to: type.to,
|
|
3534
3754
|
fromParent: type.fromParent,
|
|
3535
|
-
toParent: type.toParent,
|
|
3536
3755
|
}),
|
|
3537
3756
|
parent: type,
|
|
3538
3757
|
});
|
|
@@ -3556,7 +3775,7 @@ export const isOptionalType = (x: unknown): x is OptionalType<any> =>
|
|
|
3556
3775
|
/**
|
|
3557
3776
|
* Creates a partial object type where all properties are optional.
|
|
3558
3777
|
*
|
|
3559
|
-
* This is useful when
|
|
3778
|
+
* This is useful when we want to validate an object in which none of the keys
|
|
3560
3779
|
* are required, but if they are present they must conform to their
|
|
3561
3780
|
* corresponding Types.
|
|
3562
3781
|
*
|
|
@@ -3661,68 +3880,39 @@ export function omit<T extends ObjectType<any>, Keys extends keyof T["props"]>(
|
|
|
3661
3880
|
return object(newProps);
|
|
3662
3881
|
}
|
|
3663
3882
|
|
|
3883
|
+
export const maxMutationSize = 655360;
|
|
3884
|
+
|
|
3664
3885
|
/**
|
|
3665
|
-
*
|
|
3666
|
-
*
|
|
3667
|
-
*
|
|
3668
|
-
*
|
|
3669
|
-
* ### Example
|
|
3670
|
-
*
|
|
3671
|
-
* ```ts
|
|
3672
|
-
* const Person = object({
|
|
3673
|
-
* name: NonEmptyString50,
|
|
3674
|
-
* age: FiniteNumber,
|
|
3675
|
-
* });
|
|
3676
|
-
* type Person = typeof Person.Type;
|
|
3677
|
-
*
|
|
3678
|
-
* const PersonJson = json(Person, "PersonJson");
|
|
3679
|
-
* // string & Brand<"PersonJson">
|
|
3680
|
-
* type PersonJson = typeof PersonJson.Type;
|
|
3681
|
-
*
|
|
3682
|
-
* // Person -> string & Brand<"PersonJson">
|
|
3683
|
-
* const personJson = PersonJson.from({ name: "Alice", age: 30 });
|
|
3684
|
-
* expect(personJson).toEqual(ok('{"name":"Alice","age":30}'));
|
|
3685
|
-
*
|
|
3686
|
-
* // string & Brand<"PersonJson"> -> Person
|
|
3687
|
-
* const person = PersonJson.to(personJson);
|
|
3688
|
-
*
|
|
3689
|
-
* // serialize/parse any JSON value
|
|
3690
|
-
* const AnyJson = json(JsonValue, "AnyJson");
|
|
3691
|
-
* ```
|
|
3886
|
+
* Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
|
|
3887
|
+
* the `maxProtocolMessageRangesSize`. The max size is 640KB in bytes, measured
|
|
3888
|
+
* via MessagePack. Evolu Protocol DbChange will be smaller thanks to various
|
|
3889
|
+
* optimizations.
|
|
3692
3890
|
*/
|
|
3693
|
-
export const
|
|
3891
|
+
export const validMutationSize = <T extends AnyType>(
|
|
3694
3892
|
type: T,
|
|
3695
|
-
|
|
3696
|
-
)
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
JsonValueFromStringError | T["Errors"],
|
|
3702
|
-
StringError
|
|
3703
|
-
>
|
|
3704
|
-
> => {
|
|
3705
|
-
type E = JsonValueFromStringError | T["Errors"];
|
|
3706
|
-
|
|
3707
|
-
const BrandedJsonString: BrandType<typeof String, Name, E, StringError> =
|
|
3708
|
-
brand(name, String, (value) => {
|
|
3709
|
-
const jsonValue = JsonValueFromString.fromParent(value);
|
|
3710
|
-
if (!jsonValue.ok) return jsonValue as Err<E>;
|
|
3711
|
-
|
|
3712
|
-
const parsed = type.fromUnknown(jsonValue.value);
|
|
3713
|
-
if (!parsed.ok) return parsed as Err<E>;
|
|
3893
|
+
): BrandType<T, "ValidMutationSize", ValidMutationSizeError, InferErrors<T>> =>
|
|
3894
|
+
brand("ValidMutationSize", type, (value) =>
|
|
3895
|
+
pack(value).byteLength <= maxMutationSize
|
|
3896
|
+
? ok(value)
|
|
3897
|
+
: err<ValidMutationSizeError>({ type: "ValidMutationSize", value }),
|
|
3898
|
+
);
|
|
3714
3899
|
|
|
3715
|
-
|
|
3716
|
-
|
|
3900
|
+
export interface ValidMutationSizeError
|
|
3901
|
+
extends TypeError<"ValidMutationSize"> {}
|
|
3717
3902
|
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
3723
|
-
(value) => JSON.parse(value) as InferType<T>,
|
|
3903
|
+
export const formatValidMutationSizeError =
|
|
3904
|
+
createTypeErrorFormatter<ValidMutationSizeError>(
|
|
3905
|
+
(error) =>
|
|
3906
|
+
`The mutation size exceeds the maximum limit of ${maxMutationSize} bytes. The provided mutation has a size of ${pack(error.value).byteLength} bytes.`,
|
|
3724
3907
|
);
|
|
3725
|
-
|
|
3908
|
+
|
|
3909
|
+
export type ValidMutationSize<Props extends Record<string, AnyType>> =
|
|
3910
|
+
BrandType<
|
|
3911
|
+
ObjectType<Props>,
|
|
3912
|
+
"ValidMutationSize",
|
|
3913
|
+
ValidMutationSizeError,
|
|
3914
|
+
InferErrors<ObjectType<Props>>
|
|
3915
|
+
>;
|
|
3726
3916
|
|
|
3727
3917
|
/**
|
|
3728
3918
|
* Union of all `TypeError`s defined in the `Type.ts` file, including base type
|
|
@@ -3749,16 +3939,16 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
|
|
|
3749
3939
|
| InstanceOfError
|
|
3750
3940
|
| EvoluTypeError
|
|
3751
3941
|
| CurrencyCodeError
|
|
3752
|
-
|
|
|
3942
|
+
| DateIsoError
|
|
3753
3943
|
| TrimmedError
|
|
3754
3944
|
| MinLengthError
|
|
3755
3945
|
| MaxLengthError
|
|
3756
3946
|
| LengthError
|
|
3757
3947
|
| MnemonicError
|
|
3758
3948
|
| RegexError
|
|
3759
|
-
| NanoIdError
|
|
3760
3949
|
| SimplePasswordError
|
|
3761
3950
|
| IdError
|
|
3951
|
+
| TableIdError
|
|
3762
3952
|
| PositiveError
|
|
3763
3953
|
| NegativeError
|
|
3764
3954
|
| NonPositiveError
|
|
@@ -3774,10 +3964,9 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
|
|
|
3774
3964
|
| BetweenError
|
|
3775
3965
|
| LiteralError
|
|
3776
3966
|
| Int64Error
|
|
3777
|
-
| BigIntFromStringError
|
|
3778
3967
|
| Int64StringError
|
|
3779
|
-
| JsonValueFromStringError
|
|
3780
3968
|
| JsonError
|
|
3969
|
+
| ValidMutationSizeError
|
|
3781
3970
|
| ExtraErrors
|
|
3782
3971
|
// Composite errors
|
|
3783
3972
|
| ArrayError<TypeErrors<ExtraErrors>>
|
|
@@ -3792,70 +3981,74 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
|
|
|
3792
3981
|
| TupleError<TypeErrors<ExtraErrors>>;
|
|
3793
3982
|
|
|
3794
3983
|
/**
|
|
3795
|
-
*
|
|
3796
|
-
* {@link TypeErrors} and custom errors. It also lets us override the default
|
|
3797
|
-
* formatting for specific errors.
|
|
3984
|
+
* Formats Evolu Type errors into user-friendly messages.
|
|
3798
3985
|
*
|
|
3799
|
-
*
|
|
3800
|
-
*
|
|
3986
|
+
* Evolu Type typed errors ensure every error type must have a formatter.
|
|
3987
|
+
* TypeScript enforces this at compile-time, preventing unhandled validation
|
|
3988
|
+
* errors from reaching users.
|
|
3801
3989
|
*
|
|
3802
|
-
*
|
|
3990
|
+
* The `createFormatTypeError` function handles both built-in {@link TypeErrors}
|
|
3991
|
+
* and custom errors, and lets us override default formatting for specific
|
|
3992
|
+
* errors.
|
|
3993
|
+
*
|
|
3994
|
+
* ### Example
|
|
3803
3995
|
*
|
|
3804
3996
|
* ```ts
|
|
3805
|
-
* const
|
|
3806
|
-
*
|
|
3807
|
-
*
|
|
3997
|
+
* const formatTypeError = createFormatTypeError<
|
|
3998
|
+
* MinLengthError | MaxLengthError
|
|
3999
|
+
* >((error): string => {
|
|
4000
|
+
* switch (error.type) {
|
|
4001
|
+
* case "MinLength":
|
|
4002
|
+
* return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
|
|
4003
|
+
* case "MaxLength":
|
|
4004
|
+
* return `Text is too long (maximum ${error.max} characters)`;
|
|
4005
|
+
* }
|
|
4006
|
+
* });
|
|
3808
4007
|
* ```
|
|
3809
4008
|
*
|
|
3810
|
-
*
|
|
4009
|
+
* Alternatively, write a custom formatter from scratch without using
|
|
4010
|
+
* `createFormatTypeError`. This gives us full control over error formatting:
|
|
3811
4011
|
*
|
|
3812
4012
|
* ```ts
|
|
3813
|
-
*
|
|
3814
|
-
*
|
|
4013
|
+
* const Person = object({
|
|
4014
|
+
* name: NonEmptyTrimmedString100,
|
|
4015
|
+
* age: optional(PositiveInt),
|
|
4016
|
+
* });
|
|
4017
|
+
*
|
|
4018
|
+
* // Define only the errors actually used by Person Type
|
|
4019
|
+
* type PersonErrors =
|
|
3815
4020
|
* | StringError
|
|
3816
|
-
* | MinLengthError
|
|
3817
4021
|
* | MaxLengthError
|
|
3818
|
-
* |
|
|
3819
|
-
* | IdError
|
|
4022
|
+
* | MinLengthError
|
|
3820
4023
|
* | TrimmedError
|
|
3821
|
-
* |
|
|
3822
|
-
* |
|
|
3823
|
-
*
|
|
3824
|
-
* |
|
|
3825
|
-
* |
|
|
3826
|
-
*
|
|
3827
|
-
* const formatTypeError: TypeErrorFormatter<
|
|
3828
|
-
* // In the real code, we would use the createTypeErrorFormatter helper
|
|
3829
|
-
* // that safely stringifies error value.
|
|
4024
|
+
* | PositiveError
|
|
4025
|
+
* | NonNegativeError
|
|
4026
|
+
* | IntError
|
|
4027
|
+
* | NumberError
|
|
4028
|
+
* | ObjectError<Record<string, PersonErrors>>;
|
|
4029
|
+
*
|
|
4030
|
+
* const formatTypeError: TypeErrorFormatter<PersonErrors> = (error) => {
|
|
3830
4031
|
* switch (error.type) {
|
|
3831
|
-
* case "Id":
|
|
3832
|
-
* return `Invalid Id on table: ${error.table}.`;
|
|
3833
|
-
* case "MaxLength":
|
|
3834
|
-
* return `Max length is ${error.max}.`;
|
|
3835
|
-
* case "MinLength":
|
|
3836
|
-
* return `Min length is ${error.min}.`;
|
|
3837
|
-
* case "Mnemonic":
|
|
3838
|
-
* return `Invalid mnemonic: ${String(error.value)}`;
|
|
3839
|
-
* case "Null":
|
|
3840
|
-
* return `Not null`;
|
|
3841
4032
|
* case "String":
|
|
3842
|
-
* // We can reuse existing formatter.
|
|
3843
4033
|
* return formatStringError(error);
|
|
4034
|
+
* case "Number":
|
|
4035
|
+
* return "Must be a number";
|
|
4036
|
+
* case "MinLength":
|
|
4037
|
+
* return `Must be at least ${error.min} characters`;
|
|
4038
|
+
* case "MaxLength":
|
|
4039
|
+
* return `Cannot exceed ${error.max} characters`;
|
|
3844
4040
|
* case "Trimmed":
|
|
3845
|
-
* return "
|
|
3846
|
-
* case "
|
|
3847
|
-
* return "
|
|
3848
|
-
* case "
|
|
3849
|
-
* return
|
|
3850
|
-
*
|
|
3851
|
-
*
|
|
3852
|
-
* return `Union errors: ${error.errors.map(formatTypeError).join(", ")}`;
|
|
4041
|
+
* return "Cannot have leading or trailing spaces";
|
|
4042
|
+
* case "Positive":
|
|
4043
|
+
* return "Must be a positive number";
|
|
4044
|
+
* case "NonNegative":
|
|
4045
|
+
* return "Must be zero or positive";
|
|
4046
|
+
* case "Int":
|
|
4047
|
+
* return "Must be an integer";
|
|
3853
4048
|
* case "Object": {
|
|
3854
|
-
* if (
|
|
3855
|
-
*
|
|
3856
|
-
*
|
|
3857
|
-
* )
|
|
3858
|
-
* return "A developer made an error, this should not happen.";
|
|
4049
|
+
* if (error.reason.kind === "NotObject") return "Must be an object";
|
|
4050
|
+
* if (error.reason.kind === "ExtraKeys")
|
|
4051
|
+
* return "Contains unexpected fields";
|
|
3859
4052
|
* const firstError = Object.values(error.reason.errors).find(
|
|
3860
4053
|
* (e) => e !== undefined,
|
|
3861
4054
|
* )!;
|
|
@@ -3902,8 +4095,8 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
|
|
|
3902
4095
|
return formatIsTypeError(error);
|
|
3903
4096
|
case "CurrencyCode":
|
|
3904
4097
|
return formatCurrencyCodeError(error);
|
|
3905
|
-
case "
|
|
3906
|
-
return
|
|
4098
|
+
case "DateIso":
|
|
4099
|
+
return formatDateIsoError(error);
|
|
3907
4100
|
case "Trimmed":
|
|
3908
4101
|
return formatTrimmedError(error);
|
|
3909
4102
|
case "MinLength":
|
|
@@ -3918,6 +4111,8 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
|
|
|
3918
4111
|
return formatRegexError(error);
|
|
3919
4112
|
case "Id":
|
|
3920
4113
|
return formatIdError(error);
|
|
4114
|
+
case "TableId":
|
|
4115
|
+
return formatTableIdError(error);
|
|
3921
4116
|
case "Positive":
|
|
3922
4117
|
return formatPositiveError(error);
|
|
3923
4118
|
case "Negative":
|
|
@@ -3948,14 +4143,12 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
|
|
|
3948
4143
|
return formatLiteralError(error);
|
|
3949
4144
|
case "Int64":
|
|
3950
4145
|
return formatInt64Error(error);
|
|
3951
|
-
case "BigIntFromString":
|
|
3952
|
-
return formatBigIntFromStringError(error);
|
|
3953
4146
|
case "Int64String":
|
|
3954
4147
|
return formatInt64StringError(error);
|
|
3955
|
-
case "JsonValueFromString":
|
|
3956
|
-
return formatJsonValueFromStringError(error);
|
|
3957
4148
|
case "Json":
|
|
3958
4149
|
return formatJsonError(error);
|
|
4150
|
+
case "ValidMutationSize":
|
|
4151
|
+
return formatValidMutationSizeError(error);
|
|
3959
4152
|
// Composite Types
|
|
3960
4153
|
case "SimplePassword":
|
|
3961
4154
|
return formatSimplePasswordError(formatTypeError)(error);
|
|
@@ -3971,8 +4164,220 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
|
|
|
3971
4164
|
return formatUnionError(formatTypeError)(error);
|
|
3972
4165
|
case "Tuple":
|
|
3973
4166
|
return formatTupleError(formatTypeError)(error);
|
|
4167
|
+
default: {
|
|
4168
|
+
// Fallback for unknown error types
|
|
4169
|
+
const unknownError = error as TypeError;
|
|
4170
|
+
return `A value ${safelyStringifyUnknownValue(unknownError.value)} is not valid for type ${unknownError.type}.`;
|
|
4171
|
+
}
|
|
3974
4172
|
}
|
|
3975
4173
|
};
|
|
3976
4174
|
|
|
3977
4175
|
return formatTypeError;
|
|
3978
4176
|
};
|
|
4177
|
+
|
|
4178
|
+
/**
|
|
4179
|
+
* Converts an Evolu {@link TypeError} to Standard Schema V1 issues format.
|
|
4180
|
+
*
|
|
4181
|
+
* This function recursively converts Evolu's typed errors into the Standard
|
|
4182
|
+
* Schema issue format with proper path tracking for nested structures.
|
|
4183
|
+
*
|
|
4184
|
+
* @category Utilities
|
|
4185
|
+
*/
|
|
4186
|
+
export const typeErrorToStandardSchemaIssues = <
|
|
4187
|
+
ExtraErrors extends TypeError = never,
|
|
4188
|
+
>(
|
|
4189
|
+
error: TypeErrors<ExtraErrors>,
|
|
4190
|
+
formatTypeError: TypeErrorFormatter<TypeErrors<ExtraErrors>>,
|
|
4191
|
+
path: ReadonlyArray<PropertyKey> = [],
|
|
4192
|
+
): ReadonlyArray<StandardSchemaV1.Issue> => {
|
|
4193
|
+
if (error.type === "Array") {
|
|
4194
|
+
const arrayError = error as ArrayError;
|
|
4195
|
+
if (arrayError.reason.kind === "NotArray") {
|
|
4196
|
+
return [{ message: formatTypeError(error), path }];
|
|
4197
|
+
}
|
|
4198
|
+
return typeErrorToStandardSchemaIssues(
|
|
4199
|
+
arrayError.reason.error as TypeErrors<ExtraErrors>,
|
|
4200
|
+
formatTypeError,
|
|
4201
|
+
[...path, arrayError.reason.index],
|
|
4202
|
+
);
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
if (error.type === "Object") {
|
|
4206
|
+
const objectError = error as ObjectError;
|
|
4207
|
+
if (
|
|
4208
|
+
objectError.reason.kind === "NotObject" ||
|
|
4209
|
+
objectError.reason.kind === "ExtraKeys"
|
|
4210
|
+
) {
|
|
4211
|
+
return [{ message: formatTypeError(error), path }];
|
|
4212
|
+
}
|
|
4213
|
+
const issues: Array<StandardSchemaV1.Issue> = [];
|
|
4214
|
+
for (const [key, propError] of Object.entries(objectError.reason.errors)) {
|
|
4215
|
+
issues.push(
|
|
4216
|
+
...typeErrorToStandardSchemaIssues(
|
|
4217
|
+
propError as TypeErrors<ExtraErrors>,
|
|
4218
|
+
formatTypeError,
|
|
4219
|
+
[...path, key],
|
|
4220
|
+
),
|
|
4221
|
+
);
|
|
4222
|
+
}
|
|
4223
|
+
return issues;
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
if (error.type === "ObjectWithRecord") {
|
|
4227
|
+
const objectWithRecordError = error as ObjectWithRecordError;
|
|
4228
|
+
if (objectWithRecordError.reason.kind === "NotObject") {
|
|
4229
|
+
return [{ message: formatTypeError(error), path }];
|
|
4230
|
+
}
|
|
4231
|
+
if (
|
|
4232
|
+
objectWithRecordError.reason.kind === "IndexKey" ||
|
|
4233
|
+
objectWithRecordError.reason.kind === "IndexValue"
|
|
4234
|
+
) {
|
|
4235
|
+
return typeErrorToStandardSchemaIssues(
|
|
4236
|
+
objectWithRecordError.reason.error as TypeErrors<ExtraErrors>,
|
|
4237
|
+
formatTypeError,
|
|
4238
|
+
[...path, objectWithRecordError.reason.key as PropertyKey],
|
|
4239
|
+
);
|
|
4240
|
+
}
|
|
4241
|
+
const issues: Array<StandardSchemaV1.Issue> = [];
|
|
4242
|
+
for (const [key, propError] of Object.entries(
|
|
4243
|
+
objectWithRecordError.reason.errors,
|
|
4244
|
+
)) {
|
|
4245
|
+
issues.push(
|
|
4246
|
+
...typeErrorToStandardSchemaIssues(
|
|
4247
|
+
propError as TypeErrors<ExtraErrors>,
|
|
4248
|
+
formatTypeError,
|
|
4249
|
+
[...path, key],
|
|
4250
|
+
),
|
|
4251
|
+
);
|
|
4252
|
+
}
|
|
4253
|
+
return issues;
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
if (error.type === "Record") {
|
|
4257
|
+
const recordError = error as RecordError;
|
|
4258
|
+
if (recordError.reason.kind === "NotRecord") {
|
|
4259
|
+
return [{ message: formatTypeError(error), path }];
|
|
4260
|
+
}
|
|
4261
|
+
return typeErrorToStandardSchemaIssues(
|
|
4262
|
+
recordError.reason.error as TypeErrors<ExtraErrors>,
|
|
4263
|
+
formatTypeError,
|
|
4264
|
+
[...path, recordError.reason.key as PropertyKey],
|
|
4265
|
+
);
|
|
4266
|
+
}
|
|
4267
|
+
|
|
4268
|
+
if (error.type === "Tuple") {
|
|
4269
|
+
const tupleError = error as TupleError;
|
|
4270
|
+
if (tupleError.reason.kind === "InvalidLength") {
|
|
4271
|
+
return [{ message: formatTypeError(error), path }];
|
|
4272
|
+
}
|
|
4273
|
+
return typeErrorToStandardSchemaIssues(
|
|
4274
|
+
tupleError.reason.error as TypeErrors<ExtraErrors>,
|
|
4275
|
+
formatTypeError,
|
|
4276
|
+
[...path, tupleError.reason.index],
|
|
4277
|
+
);
|
|
4278
|
+
}
|
|
4279
|
+
|
|
4280
|
+
if (error.type === "Union") {
|
|
4281
|
+
const unionError = error as UnionError;
|
|
4282
|
+
return unionError.errors.flatMap((err) =>
|
|
4283
|
+
typeErrorToStandardSchemaIssues(
|
|
4284
|
+
err as TypeErrors<ExtraErrors>,
|
|
4285
|
+
formatTypeError,
|
|
4286
|
+
path,
|
|
4287
|
+
),
|
|
4288
|
+
);
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
if (error.type === "Brand") {
|
|
4292
|
+
const brandError = error as BrandWithoutRefineError<TypeName, TypeError>;
|
|
4293
|
+
if ("parentError" in brandError) {
|
|
4294
|
+
return typeErrorToStandardSchemaIssues(
|
|
4295
|
+
brandError.parentError as TypeErrors<ExtraErrors>,
|
|
4296
|
+
formatTypeError,
|
|
4297
|
+
path,
|
|
4298
|
+
);
|
|
4299
|
+
}
|
|
4300
|
+
return [{ message: formatTypeError(error), path }];
|
|
4301
|
+
}
|
|
4302
|
+
|
|
4303
|
+
return [{ message: formatTypeError(error), path }];
|
|
4304
|
+
};
|
|
4305
|
+
|
|
4306
|
+
/** The Standard Schema interface. */
|
|
4307
|
+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
4308
|
+
/** The Standard Schema properties. */
|
|
4309
|
+
readonly "~standard": StandardSchemaV1.Props<Input, Output>;
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4312
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
4313
|
+
export declare namespace StandardSchemaV1 {
|
|
4314
|
+
/** The Standard Schema properties interface. */
|
|
4315
|
+
export interface Props<Input = unknown, Output = Input> {
|
|
4316
|
+
/** The version number of the standard. */
|
|
4317
|
+
readonly version: 1;
|
|
4318
|
+
/** The vendor name of the schema library. */
|
|
4319
|
+
readonly vendor: string;
|
|
4320
|
+
/** Validates unknown input values. */
|
|
4321
|
+
readonly validate: (
|
|
4322
|
+
value: unknown,
|
|
4323
|
+
) => Result<Output> | Promise<Result<Output>>;
|
|
4324
|
+
/** Inferred types associated with the schema. */
|
|
4325
|
+
readonly types?: Types<Input, Output> | undefined;
|
|
4326
|
+
}
|
|
4327
|
+
|
|
4328
|
+
/** The result interface of the validate function. */
|
|
4329
|
+
export type Result<Output> = SuccessResult<Output> | FailureResult;
|
|
4330
|
+
|
|
4331
|
+
/** The result interface if validation succeeds. */
|
|
4332
|
+
export interface SuccessResult<Output> {
|
|
4333
|
+
/** The typed output value. */
|
|
4334
|
+
readonly value: Output;
|
|
4335
|
+
/** The non-existent issues. */
|
|
4336
|
+
readonly issues?: undefined;
|
|
4337
|
+
}
|
|
4338
|
+
|
|
4339
|
+
/** The result interface if validation fails. */
|
|
4340
|
+
export interface FailureResult {
|
|
4341
|
+
/** The issues of failed validation. */
|
|
4342
|
+
readonly issues: ReadonlyArray<Issue>;
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4345
|
+
/** The issue interface of the failure output. */
|
|
4346
|
+
export interface Issue {
|
|
4347
|
+
/** The error message of the issue. */
|
|
4348
|
+
readonly message: string;
|
|
4349
|
+
/** The path of the issue, if any. */
|
|
4350
|
+
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
|
|
4351
|
+
}
|
|
4352
|
+
|
|
4353
|
+
/** The path segment interface of the issue. */
|
|
4354
|
+
export interface PathSegment {
|
|
4355
|
+
/** The key representing a path segment. */
|
|
4356
|
+
readonly key: PropertyKey;
|
|
4357
|
+
}
|
|
4358
|
+
|
|
4359
|
+
/** The Standard Schema types interface. */
|
|
4360
|
+
export interface Types<Input = unknown, Output = Input> {
|
|
4361
|
+
/** The input type of the schema. */
|
|
4362
|
+
readonly input: Input;
|
|
4363
|
+
/** The output type of the schema. */
|
|
4364
|
+
readonly output: Output;
|
|
4365
|
+
}
|
|
4366
|
+
|
|
4367
|
+
/** Infers the input type of a Standard Schema. */
|
|
4368
|
+
export type InferInput<Schema extends StandardSchemaV1> = NonNullable<
|
|
4369
|
+
Schema["~standard"]["types"]
|
|
4370
|
+
>["input"];
|
|
4371
|
+
|
|
4372
|
+
/** Infers the output type of a Standard Schema. */
|
|
4373
|
+
export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<
|
|
4374
|
+
Schema["~standard"]["types"]
|
|
4375
|
+
>["output"];
|
|
4376
|
+
}
|
|
4377
|
+
|
|
4378
|
+
/**
|
|
4379
|
+
* Shared formatter cache for Standard Schema integration - avoids circular
|
|
4380
|
+
* dependency by lazily creating the formatter on first use rather than during
|
|
4381
|
+
* module initialization.
|
|
4382
|
+
*/
|
|
4383
|
+
let cachedStandardSchemaFormatTypeError: TypeErrorFormatter<any> | undefined;
|