@evolu/common 6.0.1-preview.3 → 6.0.1-preview.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Array.d.ts +69 -5
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +64 -5
- package/dist/src/Assert.d.ts +6 -16
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +6 -18
- package/dist/src/Brand.d.ts +75 -0
- package/dist/src/Brand.d.ts.map +1 -0
- package/dist/src/Brand.js +1 -0
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +8 -7
- package/dist/src/Cache.d.ts +44 -0
- package/dist/src/Cache.d.ts.map +1 -0
- package/dist/src/Cache.js +52 -0
- package/dist/src/Callbacks.d.ts +45 -12
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +14 -7
- package/dist/src/Console.d.ts +31 -6
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +72 -9
- package/dist/src/Crypto.d.ts +61 -34
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +32 -45
- package/dist/src/Evolu/Db.d.ts +161 -65
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +286 -694
- package/dist/src/Evolu/Diff.d.ts +3 -3
- package/dist/src/Evolu/Diff.d.ts.map +1 -1
- package/dist/src/Evolu/Diff.js +7 -5
- package/dist/src/Evolu/Evolu.d.ts +208 -133
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +188 -183
- package/dist/src/Evolu/Internal.d.ts +0 -2
- package/dist/src/Evolu/Internal.d.ts.map +1 -1
- package/dist/src/Evolu/Internal.js +0 -2
- package/dist/src/Evolu/LocalAuth.d.ts +150 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +174 -0
- package/dist/src/Evolu/Owner.d.ts +273 -120
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +130 -104
- package/dist/src/Evolu/Platform.d.ts +9 -7
- package/dist/src/Evolu/Platform.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.d.ts +277 -232
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +603 -378
- package/dist/src/Evolu/Public.d.ts +6 -8
- package/dist/src/Evolu/Public.d.ts.map +1 -1
- package/dist/src/Evolu/Public.js +2 -3
- package/dist/src/Evolu/PublicKysely.js +3 -3
- package/dist/src/Evolu/Query.d.ts +2 -1
- package/dist/src/Evolu/Query.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.d.ts +92 -7
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +238 -76
- package/dist/src/Evolu/Schema.d.ts +129 -73
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +169 -89
- package/dist/src/Evolu/Storage.d.ts +240 -26
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +189 -91
- package/dist/src/Evolu/Sync.d.ts +67 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +441 -20
- package/dist/src/Evolu/Timestamp.d.ts +85 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +77 -18
- package/dist/src/Identicon.d.ts +35 -0
- package/dist/src/Identicon.d.ts.map +1 -0
- package/dist/src/Identicon.js +143 -0
- package/dist/src/Instances.d.ts +34 -0
- package/dist/src/Instances.d.ts.map +1 -0
- package/dist/src/Instances.js +44 -0
- package/dist/src/ManyToManyMap.d.ts +71 -10
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/ManyToManyMap.js +41 -6
- package/dist/src/Number.d.ts +4 -3
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +5 -4
- package/dist/src/Platform.d.ts +20 -0
- package/dist/src/Platform.d.ts.map +1 -0
- package/dist/src/Platform.js +22 -0
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Resources.d.ts +118 -0
- package/dist/src/Resources.d.ts.map +1 -0
- package/dist/src/Resources.js +197 -0
- package/dist/src/Result.d.ts +184 -52
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +30 -241
- package/dist/src/Skiplist.js +2 -1
- package/dist/src/Sqlite.d.ts +63 -5
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +110 -9
- package/dist/src/Task.d.ts +586 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +469 -0
- package/dist/src/Time.d.ts +66 -1
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +99 -5
- package/dist/src/Type.d.ts +622 -340
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +666 -464
- package/dist/src/Types.d.ts +1 -75
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -18
- package/dist/src/Worker.d.ts +39 -11
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +22 -4
- package/dist/src/index.d.ts +7 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -2
- package/package.json +14 -13
- package/src/Array.ts +90 -11
- package/src/Assert.ts +6 -24
- package/src/Brand.ts +75 -0
- package/src/Buffer.ts +7 -7
- package/src/Cache.ts +85 -0
- package/src/Callbacks.ts +62 -22
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +97 -82
- package/src/Evolu/Db.ts +517 -1020
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +464 -355
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +463 -0
- package/src/Evolu/Owner.ts +355 -228
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +859 -676
- package/src/Evolu/Public.ts +7 -14
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Query.ts +2 -1
- package/src/Evolu/Relay.ts +437 -93
- package/src/Evolu/Schema.ts +391 -191
- package/src/Evolu/Storage.ts +532 -135
- package/src/Evolu/Sync.ts +766 -37
- package/src/Evolu/Timestamp.ts +88 -35
- package/src/Identicon.ts +197 -0
- package/src/Instances.ts +90 -0
- package/src/ManyToManyMap.ts +124 -24
- package/src/Number.ts +6 -10
- package/src/Platform.ts +26 -0
- package/src/Random.ts +3 -2
- package/src/Resources.ts +367 -0
- package/src/Result.ts +191 -54
- package/src/Skiplist.ts +1 -1
- package/src/Sqlite.ts +122 -17
- package/src/Task.ts +901 -0
- package/src/Time.ts +180 -5
- package/src/Type.ts +1084 -727
- package/src/Types.ts +1 -77
- package/src/WebSocket.ts +27 -25
- package/src/Worker.ts +72 -23
- package/src/index.ts +7 -2
- package/dist/src/Evolu/Config.d.ts +0 -69
- package/dist/src/Evolu/Config.d.ts.map +0 -1
- package/dist/src/Evolu/Config.js +0 -9
- package/dist/src/Evolu/Kysely.d.ts +0 -6
- package/dist/src/Evolu/Kysely.d.ts.map +0 -1
- package/dist/src/Evolu/Kysely.js +0 -21
- package/dist/src/NanoId.d.ts +0 -27
- package/dist/src/NanoId.d.ts.map +0 -1
- package/dist/src/NanoId.js +0 -6
- package/dist/src/Promise.d.ts +0 -180
- package/dist/src/Promise.d.ts.map +0 -1
- package/dist/src/Promise.js +0 -176
- package/src/Evolu/Config.ts +0 -83
- package/src/Evolu/Kysely.ts +0 -38
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/src/Type.ts
CHANGED
|
@@ -1,84 +1,191 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 🧩
|
|
2
|
+
* 🧩 Type-safe runtime types
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Evolu {@link Type} is like a type guard that returns typed errors (via
|
|
5
|
+
* {@link Result}) instead of throwing. We either get a safely typed value or a
|
|
6
|
+
* precise, composable error value telling us exactly why validation failed.
|
|
5
7
|
*
|
|
6
|
-
*
|
|
8
|
+
* Evolu Type supports [Standard Schema](https://standardschema.dev/) for
|
|
9
|
+
* interoperability with 40+ validation-compatible tools and frameworks.
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
* library fully met our needs:
|
|
11
|
+
* Why another validation library?
|
|
10
12
|
*
|
|
11
|
-
* - **Result-based error handling
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - **
|
|
15
|
-
*
|
|
16
|
-
* - **No user-land chaining
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
13
|
+
* - **Result-based error handling** – no exceptions for normal control flow.
|
|
14
|
+
* - **Typed errors with decoupled formatters** – validation logic ≠ user
|
|
15
|
+
* messages.
|
|
16
|
+
* - **Consistent constraints via {@link Brand}** – every constraint becomes part
|
|
17
|
+
* of the type.
|
|
18
|
+
* - **No user-land chaining DSL** – designed with the upcoming ES pipe operator
|
|
19
|
+
* in mind.
|
|
20
|
+
* - **Selective validation** – parent validations are skipped when already proved
|
|
21
|
+
* by typing.
|
|
22
|
+
* - **Simple, top-down implementation** – readable source code from top to bottom
|
|
23
|
+
* with no hidden magic; just plain functions and composition.
|
|
22
24
|
*
|
|
23
|
-
*
|
|
24
|
-
* includes its own usage example, and you can (and should) check the tests for
|
|
25
|
-
* practical demonstrations of the API. Or dang, just read the code. It's
|
|
26
|
-
* simple.
|
|
25
|
+
* ### Base Types Quick Start
|
|
27
26
|
*
|
|
28
|
-
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* // Validate unknown values
|
|
29
|
+
* const value: unknown = "hello";
|
|
30
|
+
* const stringResult = String.fromUnknown(value);
|
|
31
|
+
* if (!stringResult.ok) {
|
|
32
|
+
* // console.error(formatStringError(stringResult.error));
|
|
33
|
+
* return stringResult; // inside a function returning Result<string, _>
|
|
34
|
+
* }
|
|
35
|
+
* // Safe branch: value is now string
|
|
36
|
+
* const upper = stringResult.value.toUpperCase();
|
|
37
|
+
*
|
|
38
|
+
* // Type guard style
|
|
39
|
+
* if (String.is(value)) {
|
|
40
|
+
* // narrowed to string
|
|
41
|
+
* }
|
|
42
|
+
*
|
|
43
|
+
* // Composing: arrays & objects
|
|
44
|
+
* const Numbers = array(Number); // ReadonlyArray<number>
|
|
45
|
+
* const Point = object({ x: Number, y: Number });
|
|
46
|
+
*
|
|
47
|
+
* Numbers.from([1, 2, 3]); // ok
|
|
48
|
+
* Point.from({ x: 1, y: 2 }); // ok
|
|
49
|
+
* Point.from({ x: 1, y: "2" }); // err -> nested Number error
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* ### Branding Basics
|
|
53
|
+
*
|
|
54
|
+
* Branding adds semantic meaning & constraints while preserving the runtime
|
|
55
|
+
* shape:
|
|
56
|
+
*
|
|
57
|
+
* ```ts
|
|
58
|
+
* const CurrencyCode = brand("CurrencyCode", String, (value) =>
|
|
59
|
+
* /^[A-Z]{3}$/.test(value)
|
|
60
|
+
* ? ok(value)
|
|
61
|
+
* : err<CurrencyCodeError>({ type: "CurrencyCode", value }),
|
|
62
|
+
* );
|
|
63
|
+
* type CurrencyCode = typeof CurrencyCode.Type; // string & Brand<"CurrencyCode">
|
|
64
|
+
*
|
|
65
|
+
* interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
|
|
66
|
+
*
|
|
67
|
+
* const formatCurrencyCodeError =
|
|
68
|
+
* createTypeErrorFormatter<CurrencyCodeError>(
|
|
69
|
+
* (error) => `Invalid currency code: ${error.value}`,
|
|
70
|
+
* );
|
|
71
|
+
*
|
|
72
|
+
* const r = CurrencyCode.from("USD"); // ok("USD")
|
|
73
|
+
* const e = CurrencyCode.from("usd"); // err(...)
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* See also reusable brand factories like `minLength`, `maxLength`, `trimmed`,
|
|
77
|
+
* `positive`, `between`, etc.
|
|
78
|
+
*
|
|
79
|
+
* ### Objects & Optional Fields
|
|
80
|
+
*
|
|
81
|
+
* ```ts
|
|
82
|
+
* const User = object({
|
|
83
|
+
* name: NonEmptyTrimmedString100,
|
|
84
|
+
* age: optional(PositiveInt),
|
|
85
|
+
* });
|
|
86
|
+
* type User = typeof User.Type;
|
|
87
|
+
*
|
|
88
|
+
* User.from({ name: "Alice" }); // ok
|
|
89
|
+
* User.from({ name: "Alice", age: -1 }); // err(PositiveInt)
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* ### Deriving JSON String Types
|
|
93
|
+
*
|
|
94
|
+
* ```ts
|
|
95
|
+
* const Person = object({
|
|
96
|
+
* name: NonEmptyString50,
|
|
97
|
+
* // Did you know that JSON.stringify converts NaN (a number) into null?
|
|
98
|
+
* // To prevent this, use FiniteNumber.
|
|
99
|
+
* age: FiniteNumber,
|
|
100
|
+
* });
|
|
101
|
+
* type Person = typeof Person.Type;
|
|
102
|
+
*
|
|
103
|
+
* const [PersonJson, personToPersonJson, personJsonToPerson] = json(
|
|
104
|
+
* Person,
|
|
105
|
+
* "PersonJson",
|
|
106
|
+
* );
|
|
107
|
+
* // string & Brand<"PersonJson">
|
|
108
|
+
* type PersonJson = typeof PersonJson.Type;
|
|
109
|
+
*
|
|
110
|
+
* const person = Person.orThrow({
|
|
111
|
+
* name: "Alice",
|
|
112
|
+
* age: 30,
|
|
113
|
+
* });
|
|
114
|
+
*
|
|
115
|
+
* const personJson = personToPersonJson(person);
|
|
116
|
+
* expect(personJsonToPerson(personJson)).toEqual(person);
|
|
117
|
+
* ```
|
|
29
118
|
*
|
|
30
|
-
*
|
|
31
|
-
* - A function to create a value of that type, which may fail.
|
|
32
|
-
* - A function to transform value back to its original representation, which
|
|
33
|
-
* cannot fail.
|
|
119
|
+
* ### Error Formatting
|
|
34
120
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* transformations. For example, `NonEmptyTrimmedString100` chain looks like
|
|
38
|
-
* this:
|
|
121
|
+
* Evolu separates validation logic from human-readable messages. There are two
|
|
122
|
+
* layers:
|
|
39
123
|
*
|
|
40
|
-
*
|
|
124
|
+
* 1. Per-type formatters (e.g. `formatStringError`) – simple, focused, already
|
|
125
|
+
* used earlier in the quick start example.
|
|
126
|
+
* 2. A unified formatter via `createFormatTypeError` – composes all built-in and
|
|
127
|
+
* custom errors (including nested composite types) and lets us override
|
|
128
|
+
* selected messages.
|
|
41
129
|
*
|
|
42
|
-
*
|
|
43
|
-
* `TrimmedString`, the parent Type is `String`.
|
|
130
|
+
* #### 1. Per-Type Formatter (recap)
|
|
44
131
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
132
|
+
* ```ts
|
|
133
|
+
* const r = String.fromUnknown(42);
|
|
134
|
+
* if (!r.ok) console.error(formatStringError(r.error));
|
|
135
|
+
* ```
|
|
48
136
|
*
|
|
49
|
-
*
|
|
50
|
-
* parent Types when we can rely on TypeScript types.
|
|
137
|
+
* #### 2. Unified Formatter with Overrides
|
|
51
138
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
139
|
+
* ```ts
|
|
140
|
+
* // Override only what we care about; fall back to built-ins for the rest.
|
|
141
|
+
* const formatTypeError = createFormatTypeError((error) => {
|
|
142
|
+
* if (error.type === "MinLength") return `Min length is ${error.min}`;
|
|
143
|
+
* });
|
|
144
|
+
*
|
|
145
|
+
* const User = object({ name: NonEmptyTrimmedString100 });
|
|
146
|
+
* const resultUser = User.from({ name: "" });
|
|
147
|
+
* if (!resultUser.ok) console.error(formatTypeError(resultUser.error));
|
|
148
|
+
*
|
|
149
|
+
* const badPoint = object({ x: Number, y: Number }).from({
|
|
150
|
+
* x: 1,
|
|
151
|
+
* y: "foo",
|
|
152
|
+
* });
|
|
153
|
+
* if (!badPoint.ok) console.error(formatTypeError(badPoint.error));
|
|
154
|
+
* ```
|
|
155
|
+
*
|
|
156
|
+
* The unified formatter walks nested structures (object / array / record /
|
|
157
|
+
* tuple / union) and applies overrides only where specified, greatly reducing
|
|
158
|
+
* boilerplate when formatting complex validation errors.
|
|
57
159
|
*
|
|
58
160
|
* ### Tip
|
|
59
161
|
*
|
|
60
162
|
* If necessary, write `globalThis.String` instead of `String` to avoid naming
|
|
61
|
-
* clashes with
|
|
163
|
+
* clashes with native types.
|
|
62
164
|
*
|
|
63
|
-
* ### Design Decision:
|
|
165
|
+
* ### Design Decision: No Bidirectional Transformations
|
|
64
166
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
167
|
+
* Evolu Type intentionally does not support bidirectional transformations. It
|
|
168
|
+
* previously did, but supporting that while keeping typed error fidelity added
|
|
169
|
+
* complexity that hurt readability & reliability. Most persistence pipelines
|
|
170
|
+
* (e.g. SQLite) already require explicit mapping of query results, so implicit
|
|
171
|
+
* reverse transforms would not buy much. We may revisit this if we can design a
|
|
172
|
+
* minimal, 100% safe API that preserves simplicity.
|
|
69
173
|
*
|
|
70
174
|
* @module
|
|
71
175
|
*/
|
|
72
176
|
|
|
177
|
+
import { utf8ToBytes } from "@noble/ciphers/utils.js";
|
|
178
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
73
179
|
import * as bip39 from "@scure/bip39";
|
|
74
|
-
import { wordlist } from "@scure/bip39/wordlists/english";
|
|
75
|
-
import {
|
|
76
|
-
import {
|
|
77
|
-
import {
|
|
180
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
181
|
+
import { pack } from "msgpackr";
|
|
182
|
+
import type { Brand } from "./Brand.js";
|
|
183
|
+
import { type RandomBytesDep } from "./Crypto.js";
|
|
78
184
|
import { isPlainObject } from "./Object.js";
|
|
79
|
-
import {
|
|
185
|
+
import { hasNodeBuffer } from "./Platform.js";
|
|
186
|
+
import { err, getOrNull, getOrThrow, ok, Result, trySync } from "./Result.js";
|
|
80
187
|
import { safelyStringifyUnknownValue } from "./String.js";
|
|
81
|
-
import type {
|
|
188
|
+
import type { Literal, Simplify, WidenLiteral } from "./Types.js";
|
|
82
189
|
import { IntentionalNever } from "./Types.js";
|
|
83
190
|
|
|
84
191
|
export interface Type<
|
|
@@ -93,7 +200,7 @@ export interface Type<
|
|
|
93
200
|
Parent = T,
|
|
94
201
|
/** The parent's error. */
|
|
95
202
|
ParentError extends TypeError = Error,
|
|
96
|
-
> {
|
|
203
|
+
> extends StandardSchemaV1<Input, T> {
|
|
97
204
|
readonly name: Name;
|
|
98
205
|
|
|
99
206
|
/**
|
|
@@ -106,41 +213,98 @@ export interface Type<
|
|
|
106
213
|
readonly from: (value: Input) => Result<T, ParentError | Error>;
|
|
107
214
|
|
|
108
215
|
/**
|
|
109
|
-
* Creates `T` from an
|
|
216
|
+
* Creates `T` from an `Input` value, throwing an error if validation fails.
|
|
110
217
|
*
|
|
111
|
-
*
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
*
|
|
218
|
+
* Throws an Error with the Type validation error in its `cause` property,
|
|
219
|
+
* making it debuggable while avoiding the need for custom error messages.
|
|
220
|
+
*
|
|
221
|
+
* This is a convenience method that combines `from` with `getOrThrow`.
|
|
222
|
+
*
|
|
223
|
+
* **When to use:**
|
|
224
|
+
*
|
|
225
|
+
* - Configuration values that are guaranteed to be valid (e.g., hardcoded
|
|
226
|
+
* constants)
|
|
227
|
+
* - Application startup where failure should crash the program
|
|
228
|
+
* - As an alternative to assertions when the Type error in the thrown Error's
|
|
229
|
+
* `cause` provides sufficient debugging information
|
|
230
|
+
* - Test code with known valid inputs (when error message clarity is not
|
|
231
|
+
* critical; for better test error messages, use Vitest `schemaMatching` +
|
|
232
|
+
* `assert` with `.is()`)
|
|
233
|
+
*
|
|
234
|
+
* ### Example
|
|
235
|
+
*
|
|
236
|
+
* ```ts
|
|
237
|
+
* // ✅ Good: Known valid constant
|
|
238
|
+
* const maxRetries = PositiveInt.orThrow(3);
|
|
239
|
+
*
|
|
240
|
+
* // ✅ Good: App configuration that should crash on invalid values
|
|
241
|
+
* const appName = SimpleName.orThrow("MyApp");
|
|
242
|
+
*
|
|
243
|
+
* // ✅ Good: Instead of assert when Type error is clear enough
|
|
244
|
+
* // Context makes it obvious: count increments from non-negative value
|
|
245
|
+
* const currentCount = counts.get(id) ?? 0;
|
|
246
|
+
* const newCount = PositiveInt.orThrow(currentCount + 1);
|
|
117
247
|
*
|
|
118
|
-
*
|
|
248
|
+
* // ✅ Good: Test setup with known valid values
|
|
249
|
+
* const testUser = User.orThrow({ name: "Alice", age: 30 });
|
|
119
250
|
*
|
|
120
|
-
*
|
|
251
|
+
* // ❌ Avoid: User input (use `from` instead)
|
|
252
|
+
* const userAge = PositiveInt.orThrow(userInput); // Could crash!
|
|
253
|
+
*
|
|
254
|
+
* // ✅ Better: Handle user input gracefully
|
|
255
|
+
* const ageResult = PositiveInt.from(userInput);
|
|
256
|
+
* if (!ageResult.ok) {
|
|
257
|
+
* // Handle validation error
|
|
258
|
+
* }
|
|
259
|
+
* ```
|
|
121
260
|
*/
|
|
122
|
-
readonly
|
|
261
|
+
readonly orThrow: (value: Input) => T;
|
|
123
262
|
|
|
124
263
|
/**
|
|
125
|
-
* Creates `T` from `
|
|
264
|
+
* Creates `T` from an `Input` value, returning `null` if validation fails.
|
|
265
|
+
*
|
|
266
|
+
* This is a convenience method that combines `from` with `getOrNull`.
|
|
126
267
|
*
|
|
127
|
-
*
|
|
128
|
-
* already partially validated/transformed value.
|
|
268
|
+
* **When to use:**
|
|
129
269
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
270
|
+
* - When you need to convert a validation result to a nullable value
|
|
271
|
+
* - When the error is not important and you just want the value or nothing
|
|
132
272
|
*
|
|
133
273
|
* ### Example
|
|
134
274
|
*
|
|
135
275
|
* ```ts
|
|
136
|
-
* //
|
|
137
|
-
* const
|
|
276
|
+
* // ✅ Good: Optional user input
|
|
277
|
+
* const age = PositiveInt.orNull(userInput);
|
|
278
|
+
* if (age != null) {
|
|
279
|
+
* console.log("Valid age:", age);
|
|
280
|
+
* }
|
|
281
|
+
*
|
|
282
|
+
* // ✅ Good: Default fallback
|
|
283
|
+
* const maxRetries = PositiveInt.orNull(config.retries) ?? 3;
|
|
284
|
+
*
|
|
285
|
+
* // ❌ Avoid: When you need to know why validation failed (use `from` instead)
|
|
286
|
+
* const result = PositiveInt.from(userInput);
|
|
287
|
+
* if (!result.ok) {
|
|
288
|
+
* console.error(formatPositiveError(result.error));
|
|
289
|
+
* }
|
|
138
290
|
* ```
|
|
139
291
|
*/
|
|
140
|
-
readonly
|
|
292
|
+
readonly orNull: (value: Input) => T | null;
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Creates `T` from an unknown value.
|
|
296
|
+
*
|
|
297
|
+
* This is useful when a value is unknown.
|
|
298
|
+
*/
|
|
299
|
+
readonly fromUnknown: (value: unknown) => Result<T, ParentError | Error>;
|
|
141
300
|
|
|
142
|
-
/**
|
|
143
|
-
|
|
301
|
+
/**
|
|
302
|
+
* Creates `T` from `Parent` type.
|
|
303
|
+
*
|
|
304
|
+
* This function skips parent Types validations when we have already partially
|
|
305
|
+
* validated value.
|
|
306
|
+
*/
|
|
307
|
+
readonly fromParent: (value: Parent) => Result<T, Error>;
|
|
144
308
|
|
|
145
309
|
/**
|
|
146
310
|
* A **type guard** that checks whether an unknown value satisfies the
|
|
@@ -221,8 +385,6 @@ export interface Type<
|
|
|
221
385
|
readonly ParentError: ParentError;
|
|
222
386
|
|
|
223
387
|
/**
|
|
224
|
-
* Error | ParentError
|
|
225
|
-
*
|
|
226
388
|
* ### Example
|
|
227
389
|
*
|
|
228
390
|
* ```ts
|
|
@@ -261,26 +423,61 @@ export interface TypeErrorWithReason<
|
|
|
261
423
|
|
|
262
424
|
export type AnyType = Type<any, any, any, any, any, any>;
|
|
263
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Extracts the name from a {@link Type}.
|
|
428
|
+
*
|
|
429
|
+
* @category Utilities
|
|
430
|
+
*/
|
|
264
431
|
export type InferName<A extends AnyType> =
|
|
265
432
|
A extends Type<infer Name, any, any, any, any, any> ? Name : never;
|
|
266
433
|
|
|
434
|
+
/**
|
|
435
|
+
* Extracts the type from a {@link Type}.
|
|
436
|
+
*
|
|
437
|
+
* @category Utilities
|
|
438
|
+
*/
|
|
267
439
|
export type InferType<A extends AnyType> =
|
|
268
440
|
A extends Type<any, infer T, any, any, any, any> ? T : never;
|
|
269
441
|
|
|
442
|
+
/**
|
|
443
|
+
* Extracts the input type from a {@link Type}.
|
|
444
|
+
*
|
|
445
|
+
* @category Utilities
|
|
446
|
+
*/
|
|
270
447
|
export type InferInput<A extends AnyType> =
|
|
271
448
|
A extends Type<any, any, infer Input, any, any, any> ? Input : never;
|
|
272
449
|
|
|
450
|
+
/**
|
|
451
|
+
* Extracts the specific error type from a {@link Type}.
|
|
452
|
+
*
|
|
453
|
+
* @category Utilities
|
|
454
|
+
*/
|
|
273
455
|
export type InferError<A extends AnyType> =
|
|
274
456
|
A extends Type<any, any, any, infer Error, any, any> ? Error : never;
|
|
275
457
|
|
|
458
|
+
/**
|
|
459
|
+
* Extracts the parent type from a {@link Type}.
|
|
460
|
+
*
|
|
461
|
+
* @category Utilities
|
|
462
|
+
*/
|
|
276
463
|
export type InferParent<A extends AnyType> =
|
|
277
464
|
A extends Type<any, any, any, any, infer Parent, any> ? Parent : never;
|
|
278
465
|
|
|
466
|
+
/**
|
|
467
|
+
* Extracts the parent error type from a {@link Type}.
|
|
468
|
+
*
|
|
469
|
+
* @category Utilities
|
|
470
|
+
*/
|
|
279
471
|
export type InferParentError<A extends AnyType> =
|
|
280
472
|
A extends Type<any, any, any, any, any, infer ParentError>
|
|
281
473
|
? ParentError
|
|
282
474
|
: never;
|
|
283
475
|
|
|
476
|
+
/**
|
|
477
|
+
* Extracts all error types from a {@link Type}.
|
|
478
|
+
*
|
|
479
|
+
* @category Utilities
|
|
480
|
+
*/
|
|
284
481
|
export type InferErrors<T extends AnyType> =
|
|
285
482
|
T extends Type<any, any, any, infer Error, any, infer ParentError>
|
|
286
483
|
? Error | ParentError
|
|
@@ -310,6 +507,8 @@ const createType = <
|
|
|
310
507
|
| "name"
|
|
311
508
|
| "is"
|
|
312
509
|
| "from"
|
|
510
|
+
| "orThrow"
|
|
511
|
+
| "orNull"
|
|
313
512
|
| typeof EvoluTypeSymbol
|
|
314
513
|
| "Type"
|
|
315
514
|
| "Input"
|
|
@@ -317,12 +516,15 @@ const createType = <
|
|
|
317
516
|
| "Parent"
|
|
318
517
|
| "ParentError"
|
|
319
518
|
| "Errors"
|
|
519
|
+
| "~standard"
|
|
320
520
|
>,
|
|
321
521
|
): Type<Name, T, Input, Error, Parent, ParentError> => ({
|
|
322
522
|
...definition,
|
|
323
523
|
name,
|
|
324
524
|
is: (value: unknown): value is T => definition.fromUnknown(value).ok,
|
|
325
525
|
from: definition.fromUnknown,
|
|
526
|
+
orThrow: (value) => getOrThrow(definition.fromUnknown(value)),
|
|
527
|
+
orNull: (value) => getOrNull(definition.fromUnknown(value)),
|
|
326
528
|
[EvoluTypeSymbol]: true,
|
|
327
529
|
Type: undefined as unknown as T,
|
|
328
530
|
Input: undefined as unknown as Input,
|
|
@@ -330,6 +532,27 @@ const createType = <
|
|
|
330
532
|
Parent: undefined as unknown as Parent,
|
|
331
533
|
ParentError: undefined as unknown as ParentError,
|
|
332
534
|
Errors: undefined as unknown as Error | ParentError,
|
|
535
|
+
"~standard": {
|
|
536
|
+
version: 1,
|
|
537
|
+
vendor: "evolu",
|
|
538
|
+
validate: (value: unknown): StandardSchemaV1.Result<T> => {
|
|
539
|
+
const result = definition.fromUnknown(value);
|
|
540
|
+
if (result.ok) {
|
|
541
|
+
return { value: result.value };
|
|
542
|
+
}
|
|
543
|
+
cachedStandardSchemaFormatTypeError ??= createFormatTypeError();
|
|
544
|
+
return {
|
|
545
|
+
issues: typeErrorToStandardSchemaIssues(
|
|
546
|
+
result.error as TypeErrors<Error>,
|
|
547
|
+
cachedStandardSchemaFormatTypeError,
|
|
548
|
+
),
|
|
549
|
+
};
|
|
550
|
+
},
|
|
551
|
+
types: {
|
|
552
|
+
input: undefined as unknown as Input,
|
|
553
|
+
output: undefined as unknown as T,
|
|
554
|
+
},
|
|
555
|
+
},
|
|
333
556
|
});
|
|
334
557
|
|
|
335
558
|
/**
|
|
@@ -365,12 +588,6 @@ export type TypeErrorFormatter<Error extends TypeError> = (
|
|
|
365
588
|
* Base {@link Type}.
|
|
366
589
|
*
|
|
367
590
|
* 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
591
|
*
|
|
375
592
|
* ### Example
|
|
376
593
|
*
|
|
@@ -396,9 +613,7 @@ export const base = <Name extends TypeName, T, Error extends TypeError>(
|
|
|
396
613
|
): Type<Name, T, T, Error> =>
|
|
397
614
|
createType(name, {
|
|
398
615
|
fromUnknown,
|
|
399
|
-
to: identity,
|
|
400
616
|
fromParent: ok<T>, // `fromParent` relies on types, so it can't fail for the Base Type
|
|
401
|
-
toParent: identity,
|
|
402
617
|
});
|
|
403
618
|
|
|
404
619
|
/**
|
|
@@ -564,7 +779,7 @@ export interface InstanceOfType<T extends abstract new (...args: any) => any>
|
|
|
564
779
|
}
|
|
565
780
|
|
|
566
781
|
export const formatInstanceOfError = createTypeErrorFormatter<InstanceOfError>(
|
|
567
|
-
(error) => `
|
|
782
|
+
(error) => `The value ${error.value} is not an instance of ${error.ctor}.`,
|
|
568
783
|
);
|
|
569
784
|
|
|
570
785
|
/**
|
|
@@ -607,7 +822,7 @@ export const formatIsTypeError = createTypeErrorFormatter<EvoluTypeError>(
|
|
|
607
822
|
* The `brand` Type Factory takes the name of a new {@link Brand}, a parent Type
|
|
608
823
|
* to be branded, and the optional `refine` function for additional constraint.
|
|
609
824
|
*
|
|
610
|
-
*
|
|
825
|
+
* The `refine` function can be omitted if we only want to add a brand.
|
|
611
826
|
*
|
|
612
827
|
* ### Examples
|
|
613
828
|
*
|
|
@@ -686,7 +901,7 @@ export const formatIsTypeError = createTypeErrorFormatter<EvoluTypeError>(
|
|
|
686
901
|
* confirmPassword: SimplePassword,
|
|
687
902
|
* });
|
|
688
903
|
*
|
|
689
|
-
* const ValidForm = brand("
|
|
904
|
+
* const ValidForm = brand("ValidForm", Form, (value) => {
|
|
690
905
|
* if (value.password !== value.confirmPassword)
|
|
691
906
|
* return err<ValidFormError>({
|
|
692
907
|
* type: "ValidForm",
|
|
@@ -780,9 +995,7 @@ export function brand<
|
|
|
780
995
|
return {
|
|
781
996
|
...createType("Brand", {
|
|
782
997
|
fromUnknown,
|
|
783
|
-
to: identity,
|
|
784
998
|
fromParent: (refine ?? ok) as IntentionalNever,
|
|
785
|
-
toParent: identity,
|
|
786
999
|
}),
|
|
787
1000
|
brand: name,
|
|
788
1001
|
parentType: parent,
|
|
@@ -830,7 +1043,7 @@ export interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
|
|
|
830
1043
|
|
|
831
1044
|
export const formatCurrencyCodeError =
|
|
832
1045
|
createTypeErrorFormatter<CurrencyCodeError>(
|
|
833
|
-
(error) => `Invalid currency code: ${error.value}
|
|
1046
|
+
(error) => `Invalid currency code: ${error.value}.`,
|
|
834
1047
|
);
|
|
835
1048
|
|
|
836
1049
|
/**
|
|
@@ -848,30 +1061,41 @@ export const formatCurrencyCodeError =
|
|
|
848
1061
|
* ### Example
|
|
849
1062
|
*
|
|
850
1063
|
* ```ts
|
|
851
|
-
* const result =
|
|
852
|
-
* const error =
|
|
1064
|
+
* const result = DateIso.from("2023-01-01T12:00:00.000Z"); // ok
|
|
1065
|
+
* const error = DateIso.from("10000-01-01T00:00:00.000Z"); // err
|
|
853
1066
|
* ```
|
|
854
1067
|
*
|
|
855
1068
|
* @category String
|
|
856
1069
|
*/
|
|
857
|
-
export const
|
|
1070
|
+
export const DateIso = brand("DateIso", String, (value) => {
|
|
858
1071
|
if (value.length !== 24) {
|
|
859
|
-
return err<
|
|
1072
|
+
return err<DateIsoError>({ type: "DateIso", value });
|
|
1073
|
+
}
|
|
1074
|
+
const parsed = globalThis.Date.parse(value);
|
|
1075
|
+
if (isNaN(parsed)) {
|
|
1076
|
+
return err<DateIsoError>({ type: "DateIso", value });
|
|
860
1077
|
}
|
|
861
|
-
|
|
862
|
-
|
|
1078
|
+
// Round-trip test: ensure the string is actually a proper ISO format
|
|
1079
|
+
const roundTrip = new globalThis.Date(parsed).toISOString();
|
|
1080
|
+
if (roundTrip !== value) {
|
|
1081
|
+
return err<DateIsoError>({ type: "DateIso", value });
|
|
863
1082
|
}
|
|
864
1083
|
return ok(value);
|
|
865
1084
|
});
|
|
866
1085
|
|
|
867
|
-
export type
|
|
1086
|
+
export type DateIso = typeof DateIso.Type;
|
|
868
1087
|
|
|
869
|
-
export interface
|
|
1088
|
+
export interface DateIsoError extends TypeError<"DateIso"> {}
|
|
870
1089
|
|
|
871
|
-
export const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1090
|
+
export const formatDateIsoError = createTypeErrorFormatter<DateIsoError>(
|
|
1091
|
+
(error) => `The value ${error.value} is not a valid ISO 8601 date string.`,
|
|
1092
|
+
);
|
|
1093
|
+
|
|
1094
|
+
export const dateToDateIso = (value: Date): Result<DateIso, DateIsoError> =>
|
|
1095
|
+
DateIso.fromParent(value.toISOString());
|
|
1096
|
+
|
|
1097
|
+
export const dateIsoToDate = (value: DateIso): Date =>
|
|
1098
|
+
new globalThis.Date(value);
|
|
875
1099
|
|
|
876
1100
|
/**
|
|
877
1101
|
* Helper type for Type Factory that creates a branded Type.
|
|
@@ -914,18 +1138,12 @@ export type BrandFactory<
|
|
|
914
1138
|
/**
|
|
915
1139
|
* Trimmed string.
|
|
916
1140
|
*
|
|
917
|
-
* This Type Factory
|
|
918
|
-
*
|
|
919
|
-
* Factory.
|
|
1141
|
+
* This Type Factory validates whether a string has no leading or trailing
|
|
1142
|
+
* whitespaces.
|
|
920
1143
|
*
|
|
921
|
-
* ###
|
|
1144
|
+
* ### Example
|
|
922
1145
|
*
|
|
923
1146
|
* ```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
1147
|
* const TrimmedNonEmptyString = trimmed(minLength(1)(String));
|
|
930
1148
|
* // string & Brand<"MinLength1"> & Brand<"Trimmed">
|
|
931
1149
|
* type TrimmedNonEmptyString = typeof TrimmedNonEmptyString.Type;
|
|
@@ -945,67 +1163,9 @@ export const trimmed: BrandFactory<"Trimmed", string, TrimmedError> = (
|
|
|
945
1163
|
export interface TrimmedError extends TypeError<"Trimmed"> {}
|
|
946
1164
|
|
|
947
1165
|
export const formatTrimmedError = createTypeErrorFormatter<TrimmedError>(
|
|
948
|
-
(error) => `
|
|
1166
|
+
(error) => `The value ${error.value} must be trimmed.`,
|
|
949
1167
|
);
|
|
950
1168
|
|
|
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
1169
|
/**
|
|
1010
1170
|
* Trimmed string
|
|
1011
1171
|
*
|
|
@@ -1017,6 +1177,9 @@ export const trim: TransformBrandFactory<"Trimmed", string> = (parent) =>
|
|
|
1017
1177
|
export const TrimmedString = trimmed(String);
|
|
1018
1178
|
export type TrimmedString = typeof TrimmedString.Type;
|
|
1019
1179
|
|
|
1180
|
+
export const trim = (value: string): TrimmedString =>
|
|
1181
|
+
value.trim() as TrimmedString;
|
|
1182
|
+
|
|
1020
1183
|
/**
|
|
1021
1184
|
* Minimum length.
|
|
1022
1185
|
*
|
|
@@ -1045,7 +1208,7 @@ export interface MinLengthError<Min extends number = number>
|
|
|
1045
1208
|
|
|
1046
1209
|
export const formatMinLengthError = createTypeErrorFormatter<MinLengthError>(
|
|
1047
1210
|
(error) =>
|
|
1048
|
-
`
|
|
1211
|
+
`The value ${error.value} does not meet the minimum length of ${error.min}.`,
|
|
1049
1212
|
);
|
|
1050
1213
|
|
|
1051
1214
|
/**
|
|
@@ -1075,7 +1238,8 @@ export interface MaxLengthError<Max extends number = number>
|
|
|
1075
1238
|
}
|
|
1076
1239
|
|
|
1077
1240
|
export const formatMaxLengthError = createTypeErrorFormatter<MaxLengthError>(
|
|
1078
|
-
(error) =>
|
|
1241
|
+
(error) =>
|
|
1242
|
+
`The value ${error.value} exceeds the maximum length of ${error.max}.`,
|
|
1079
1243
|
);
|
|
1080
1244
|
|
|
1081
1245
|
/**
|
|
@@ -1108,7 +1272,7 @@ export interface LengthError<Exact extends number = number>
|
|
|
1108
1272
|
|
|
1109
1273
|
export const formatLengthError = createTypeErrorFormatter<LengthError>(
|
|
1110
1274
|
(error) =>
|
|
1111
|
-
`
|
|
1275
|
+
`The value ${error.value} does not have the required length of ${error.exact}.`,
|
|
1112
1276
|
);
|
|
1113
1277
|
|
|
1114
1278
|
/** @category String */
|
|
@@ -1170,7 +1334,7 @@ export type Mnemonic = typeof Mnemonic.Type;
|
|
|
1170
1334
|
export interface MnemonicError extends TypeError<"Mnemonic"> {}
|
|
1171
1335
|
|
|
1172
1336
|
export const formatMnemonicError = createTypeErrorFormatter<MnemonicError>(
|
|
1173
|
-
(error) => `Invalid BIP39 mnemonic: ${error.value}
|
|
1337
|
+
(error) => `Invalid BIP39 mnemonic: ${error.value}.`,
|
|
1174
1338
|
);
|
|
1175
1339
|
|
|
1176
1340
|
/**
|
|
@@ -1209,13 +1373,13 @@ export interface RegexError<Name extends TypeName = TypeName>
|
|
|
1209
1373
|
|
|
1210
1374
|
export const formatRegexError = createTypeErrorFormatter<RegexError>(
|
|
1211
1375
|
(error) =>
|
|
1212
|
-
`
|
|
1376
|
+
`The value ${error.value} does not match the pattern for ${error.name}: ${error.pattern}.`,
|
|
1213
1377
|
);
|
|
1214
1378
|
|
|
1215
1379
|
/**
|
|
1216
|
-
* URL-safe
|
|
1380
|
+
* URL-safe string.
|
|
1217
1381
|
*
|
|
1218
|
-
* A `
|
|
1382
|
+
* A `UrlSafeString` uses a limited alphabet that is safe for URLs:
|
|
1219
1383
|
*
|
|
1220
1384
|
* - Uppercase letters (`A-Z`)
|
|
1221
1385
|
* - Lowercase letters (`a-z`)
|
|
@@ -1223,35 +1387,115 @@ export const formatRegexError = createTypeErrorFormatter<RegexError>(
|
|
|
1223
1387
|
* - Dash (`-`)
|
|
1224
1388
|
* - Underscore (`_`)
|
|
1225
1389
|
*
|
|
1390
|
+
* This is the same character set used by Base64Url encoding, but this type does
|
|
1391
|
+
* not validate that the string is actually Base64Url-encoded data.
|
|
1392
|
+
*
|
|
1226
1393
|
* ### Example
|
|
1227
1394
|
*
|
|
1228
1395
|
* ```ts
|
|
1229
|
-
* const result =
|
|
1396
|
+
* const result = UrlSafeString.from("abc123_-");
|
|
1230
1397
|
* if (result.ok) {
|
|
1231
|
-
* console.log("Valid
|
|
1398
|
+
* console.log("Valid URL-safe string:", result.value);
|
|
1232
1399
|
* } else {
|
|
1233
|
-
* console.error("Invalid
|
|
1400
|
+
* console.error("Invalid URL-safe string:", result.error);
|
|
1234
1401
|
* }
|
|
1235
1402
|
* ```
|
|
1236
1403
|
*
|
|
1237
1404
|
* @category String
|
|
1238
1405
|
*/
|
|
1239
|
-
export const
|
|
1406
|
+
export const UrlSafeString = regex("UrlSafeString", /^[A-Za-z0-9_-]+$/)(String);
|
|
1407
|
+
export type UrlSafeString = typeof UrlSafeString.Type;
|
|
1408
|
+
export type UrlSafeStringError = typeof UrlSafeString.Error;
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* Base64Url without padding.
|
|
1412
|
+
*
|
|
1413
|
+
* Encode with {@link uint8ArrayToBase64Url}, decode with
|
|
1414
|
+
* {@link base64UrlToUint8Array}.
|
|
1415
|
+
*
|
|
1416
|
+
* @category String
|
|
1417
|
+
*/
|
|
1418
|
+
export const Base64Url = brand(
|
|
1240
1419
|
"Base64Url",
|
|
1241
|
-
|
|
1242
|
-
|
|
1420
|
+
String,
|
|
1421
|
+
(value: string): Result<string, Base64UrlError> => {
|
|
1422
|
+
// Round-trip validation ensures consistency across different base64url
|
|
1423
|
+
// implementations (Node.js Buffer, native browser API, manual fallback).
|
|
1424
|
+
// Only strings that decode and encode identically are accepted.
|
|
1425
|
+
let roundTrip;
|
|
1426
|
+
try {
|
|
1427
|
+
roundTrip = uint8ArrayToBase64Url(
|
|
1428
|
+
base64UrlToUint8Array(value as Base64Url),
|
|
1429
|
+
);
|
|
1430
|
+
} catch {
|
|
1431
|
+
//
|
|
1432
|
+
}
|
|
1433
|
+
return roundTrip === value
|
|
1434
|
+
? ok(value)
|
|
1435
|
+
: err<Base64UrlError>({ type: "Base64Url", value });
|
|
1436
|
+
},
|
|
1437
|
+
);
|
|
1243
1438
|
export type Base64Url = typeof Base64Url.Type;
|
|
1244
|
-
export
|
|
1439
|
+
export interface Base64UrlError extends TypeError<"Base64Url"> {}
|
|
1440
|
+
|
|
1441
|
+
export const formatBase64UrlError = createTypeErrorFormatter<Base64UrlError>(
|
|
1442
|
+
(error) => `The value ${error.value} is not a valid Base64Url string.`,
|
|
1443
|
+
);
|
|
1444
|
+
|
|
1445
|
+
const base64UrlOptions = { alphabet: "base64url", omitPadding: true };
|
|
1446
|
+
|
|
1447
|
+
/** Encodes a Uint8Array to a {@link Base64Url} string. */
|
|
1448
|
+
export const uint8ArrayToBase64Url: (bytes: Uint8Array) => Base64Url =
|
|
1449
|
+
hasNodeBuffer
|
|
1450
|
+
? (bytes: Uint8Array) =>
|
|
1451
|
+
globalThis.Buffer.from(bytes).toString("base64url") as Base64Url
|
|
1452
|
+
: // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
1453
|
+
typeof (globalThis.Uint8Array.prototype as any)?.toBase64 !== "undefined"
|
|
1454
|
+
? (bytes: Uint8Array) =>
|
|
1455
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
1456
|
+
(bytes as any).toBase64(base64UrlOptions) as Base64Url
|
|
1457
|
+
: (bytes: Uint8Array) => {
|
|
1458
|
+
const binaryString = Array.from(bytes, (byte) =>
|
|
1459
|
+
globalThis.String.fromCodePoint(byte),
|
|
1460
|
+
).join("");
|
|
1461
|
+
const base64 = globalThis.btoa(binaryString);
|
|
1462
|
+
return base64
|
|
1463
|
+
.replace(/\+/g, "-")
|
|
1464
|
+
.replace(/\//g, "_")
|
|
1465
|
+
.replace(/=/g, "") as Base64Url;
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
/** Decodes a {@link Base64Url} string to a Uint8Array. */
|
|
1469
|
+
export const base64UrlToUint8Array: (str: Base64Url) => Uint8Array =
|
|
1470
|
+
hasNodeBuffer
|
|
1471
|
+
? (str: Base64Url) => {
|
|
1472
|
+
const nodeBuffer = globalThis.Buffer.from(str, "base64url");
|
|
1473
|
+
return new globalThis.Uint8Array(nodeBuffer);
|
|
1474
|
+
}
|
|
1475
|
+
: // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
1476
|
+
typeof (globalThis.Uint8Array as any)?.fromBase64 !== "undefined"
|
|
1477
|
+
? (str: Base64Url) =>
|
|
1478
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
1479
|
+
(globalThis.Uint8Array as any).fromBase64(
|
|
1480
|
+
str,
|
|
1481
|
+
base64UrlOptions,
|
|
1482
|
+
) as Uint8Array
|
|
1483
|
+
: (str: Base64Url) => {
|
|
1484
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
1485
|
+
while (base64.length % 4 !== 0) {
|
|
1486
|
+
base64 += "=";
|
|
1487
|
+
}
|
|
1488
|
+
const binaryString = globalThis.atob(base64);
|
|
1489
|
+
return globalThis.Uint8Array.from(binaryString, (c) =>
|
|
1490
|
+
c.charCodeAt(0),
|
|
1491
|
+
);
|
|
1492
|
+
};
|
|
1245
1493
|
|
|
1246
1494
|
/**
|
|
1247
|
-
* Simple alphanumeric string for naming.
|
|
1248
|
-
*
|
|
1249
|
-
* A `SimpleName` string uses a limited, safe alphabet for naming purposes:
|
|
1495
|
+
* Simple alphanumeric string for naming in file systems, URLs, and identifiers.
|
|
1250
1496
|
*
|
|
1251
|
-
*
|
|
1252
|
-
*
|
|
1253
|
-
* - Digits (`0-9`)
|
|
1254
|
-
* - Dash (`-`)
|
|
1497
|
+
* Uses the same safe alphabet as {@link UrlSafeString} (letters, digits, `-`,
|
|
1498
|
+
* `_`). See `UrlSafeString` for details.
|
|
1255
1499
|
*
|
|
1256
1500
|
* The string must be between 1 and 42 characters.
|
|
1257
1501
|
*
|
|
@@ -1268,18 +1512,13 @@ export type Base64UrlError = typeof Base64Url.Error;
|
|
|
1268
1512
|
*
|
|
1269
1513
|
* @category String
|
|
1270
1514
|
*/
|
|
1271
|
-
export const SimpleName =
|
|
1515
|
+
export const SimpleName = brand("SimpleName", UrlSafeString, (value) =>
|
|
1516
|
+
value.length >= 1 && value.length <= 42
|
|
1517
|
+
? ok(value)
|
|
1518
|
+
: err<SimpleNameError>({ type: "SimpleName", value }),
|
|
1519
|
+
);
|
|
1272
1520
|
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;
|
|
1521
|
+
export interface SimpleNameError extends TypeError<"SimpleName"> {}
|
|
1283
1522
|
|
|
1284
1523
|
/**
|
|
1285
1524
|
* Trimmed string between 8 and 64 characters, branded as `SimplePassword`.
|
|
@@ -1304,17 +1543,56 @@ export const formatSimplePasswordError = (
|
|
|
1304
1543
|
);
|
|
1305
1544
|
|
|
1306
1545
|
/**
|
|
1307
|
-
*
|
|
1308
|
-
*
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1546
|
+
* Globally unique identifier.
|
|
1547
|
+
*
|
|
1548
|
+
* **Evolu Id** is 16 random bytes from a cryptographically secure random
|
|
1549
|
+
* generator, encoded as 22-character Base64Url string. This provides strong
|
|
1550
|
+
* collision resistance for distributed ID generation.
|
|
1551
|
+
*
|
|
1552
|
+
* ### Design Rationale
|
|
1553
|
+
*
|
|
1554
|
+
* Why Evolu Id over alternatives:
|
|
1555
|
+
*
|
|
1556
|
+
* - **NanoID**: No standard binary serialization format, and uses only ~126 bits
|
|
1557
|
+
* of entropy (21 characters from 64-symbol alphabet) compared to Evolu Id's
|
|
1558
|
+
* 128 bits.
|
|
1559
|
+
* - **UUID (v4)**: String format is 36 characters (with hyphens) compared to
|
|
1560
|
+
* Evolu Id's 22 characters. While UUIDs can be stored as 16 bytes, their
|
|
1561
|
+
* standard string representation is verbose.
|
|
1562
|
+
* - **UUID v7**: Includes timestamp in the ID, which leaks information about when
|
|
1563
|
+
* data was created. This is a privacy concern for local-first applications
|
|
1564
|
+
* where creation time must remain private.
|
|
1565
|
+
*
|
|
1566
|
+
* Evolu Id provides 128 bits of entropy, compact string representation (22
|
|
1567
|
+
* characters), standard and native string serialization (Base64Url), and no
|
|
1568
|
+
* privacy leaks.
|
|
1569
|
+
*
|
|
1570
|
+
* ### Future Consideration
|
|
1571
|
+
*
|
|
1572
|
+
* For database-heavy workloads where insert performance is critical, a hybrid
|
|
1573
|
+
* approach could be considered: `timestamp ^ H(cluster_id, timestamp >> N)`
|
|
1574
|
+
* where H is a keyed hash function and N is a configurable parameter. This
|
|
1575
|
+
* would maintain spatial locality for database caches (improving insert
|
|
1576
|
+
* performance by an order of magnitude) while adding entropy to prevent
|
|
1577
|
+
* timestamp leakage and correlation across systems. The parameter N would allow
|
|
1578
|
+
* trading off cache locality (larger N = better locality) versus entropy
|
|
1579
|
+
* distribution. See https://brooker.co.za/blog/2025/10/22/uuidv7.html for
|
|
1580
|
+
* details on this approach.
|
|
1311
1581
|
*
|
|
1312
1582
|
* @category String
|
|
1313
1583
|
*/
|
|
1314
|
-
export const Id =
|
|
1584
|
+
export const Id = brand("Id", String, (value) =>
|
|
1585
|
+
value.length === 22 && Base64Url.fromParent(value).ok
|
|
1586
|
+
? ok(value)
|
|
1587
|
+
: err<IdError>({ type: "Id", value }),
|
|
1588
|
+
);
|
|
1315
1589
|
export type Id = typeof Id.Type;
|
|
1316
1590
|
|
|
1317
|
-
export
|
|
1591
|
+
export interface IdError extends TypeError<"Id"> {}
|
|
1592
|
+
|
|
1593
|
+
export const formatIdError = createTypeErrorFormatter<IdError>(
|
|
1594
|
+
(error) => `The value ${error.value} is not a valid Id.`,
|
|
1595
|
+
);
|
|
1318
1596
|
|
|
1319
1597
|
/**
|
|
1320
1598
|
* Creates an {@link Id}.
|
|
@@ -1324,13 +1602,66 @@ export const idTypeValueLength = 21;
|
|
|
1324
1602
|
* ```ts
|
|
1325
1603
|
* // string & Brand<"Id">
|
|
1326
1604
|
* const id = createId(deps);
|
|
1605
|
+
*
|
|
1606
|
+
* // string & Brand<"Id"> & Brand<"Todo">
|
|
1607
|
+
* const todoId = createId<"Todo">(deps);
|
|
1327
1608
|
* ```
|
|
1328
1609
|
*/
|
|
1329
|
-
export const createId =
|
|
1330
|
-
deps
|
|
1610
|
+
export const createId = <B extends string = never>(
|
|
1611
|
+
deps: RandomBytesDep,
|
|
1612
|
+
): [B] extends [never] ? Id : Id & Brand<B> =>
|
|
1613
|
+
uint8ArrayToBase64Url(deps.randomBytes.create(16)) as unknown as [B] extends [
|
|
1614
|
+
never,
|
|
1615
|
+
]
|
|
1616
|
+
? Id
|
|
1617
|
+
: Id & Brand<B>;
|
|
1331
1618
|
|
|
1332
1619
|
/**
|
|
1333
|
-
*
|
|
1620
|
+
* Creates an {@link Id} from a string using SHA-256.
|
|
1621
|
+
*
|
|
1622
|
+
* When integrating with external systems that use different ID formats, use
|
|
1623
|
+
* this function to convert external IDs into valid Evolu IDs.
|
|
1624
|
+
*
|
|
1625
|
+
* In Evolu's CRDT, the ID serves as the unique identifier for conflict
|
|
1626
|
+
* resolution across distributed clients. When multiple clients create records
|
|
1627
|
+
* with the same external identifier, they must resolve to the same Evolu ID to
|
|
1628
|
+
* ensure data consistency.
|
|
1629
|
+
*
|
|
1630
|
+
* ### Example
|
|
1631
|
+
*
|
|
1632
|
+
* ```ts
|
|
1633
|
+
* // Both clients will generate the same ID
|
|
1634
|
+
* const id1 = createIdFromString("user-api-123");
|
|
1635
|
+
* const id2 = createIdFromString("user-api-123");
|
|
1636
|
+
* console.log(id1 === id2); // true
|
|
1637
|
+
*
|
|
1638
|
+
* upsert("todo", {
|
|
1639
|
+
* id: createIdFromString("external-todo-456"),
|
|
1640
|
+
* title: "Synced from external system",
|
|
1641
|
+
* });
|
|
1642
|
+
* ```
|
|
1643
|
+
*
|
|
1644
|
+
* **Important**: This transformation uses the first 16 bytes of SHA-256 hash of
|
|
1645
|
+
* the string bytes, therefore it's not possible to recover the original
|
|
1646
|
+
* external string from the generated {@link Id}. If you need to preserve the
|
|
1647
|
+
* original external ID, store it in a separate column.
|
|
1648
|
+
*
|
|
1649
|
+
* @category String
|
|
1650
|
+
*/
|
|
1651
|
+
export const createIdFromString = <B extends string = never>(
|
|
1652
|
+
value: string,
|
|
1653
|
+
): [B] extends [never] ? Id : Id & Brand<B> => {
|
|
1654
|
+
const hash = sha256(utf8ToBytes(value));
|
|
1655
|
+
// Take first 16 bytes of hash and convert to Id
|
|
1656
|
+
const id = idBytesToId(hash.slice(0, 16) as IdBytes);
|
|
1657
|
+
|
|
1658
|
+
return id as [B] extends [never] ? Id : Id & Brand<B>;
|
|
1659
|
+
};
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* Creates a branded {@link Id} Type for a table's primary key.
|
|
1663
|
+
*
|
|
1664
|
+
* The table name becomes an additional brand for type safety.
|
|
1334
1665
|
*
|
|
1335
1666
|
* ### Example
|
|
1336
1667
|
*
|
|
@@ -1342,55 +1673,64 @@ export const createId = (deps: NanoIdLibDep): Id =>
|
|
|
1342
1673
|
*
|
|
1343
1674
|
* @category String
|
|
1344
1675
|
*/
|
|
1345
|
-
export const id = <Table extends TypeName>(table: Table):
|
|
1676
|
+
export const id = <Table extends TypeName>(table: Table): TableId<Table> => {
|
|
1677
|
+
const fromUnknown = (value: unknown) => {
|
|
1678
|
+
const parentResult = String.fromUnknown(value);
|
|
1679
|
+
if (!parentResult.ok) return parentResult;
|
|
1680
|
+
return fromParent(parentResult.value);
|
|
1681
|
+
};
|
|
1682
|
+
|
|
1346
1683
|
const fromParent = (value: string) => {
|
|
1347
1684
|
const idResult = Id.fromParent(value);
|
|
1348
1685
|
if (!idResult.ok) {
|
|
1349
|
-
return err<
|
|
1686
|
+
return err<TableIdError<Table>>({ type: "TableId", value, table });
|
|
1350
1687
|
}
|
|
1351
1688
|
return ok(idResult.value as Id & Brand<Table>);
|
|
1352
1689
|
};
|
|
1353
1690
|
|
|
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
1691
|
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
|
-
}),
|
|
1692
|
+
...createType("Id", { fromUnknown, fromParent }),
|
|
1367
1693
|
table,
|
|
1368
1694
|
};
|
|
1369
1695
|
};
|
|
1370
1696
|
|
|
1371
|
-
export interface
|
|
1697
|
+
export interface TableId<Table extends TypeName>
|
|
1372
1698
|
extends Type<
|
|
1373
1699
|
"Id",
|
|
1374
1700
|
string & Brand<"Id"> & Brand<Table>,
|
|
1375
1701
|
string,
|
|
1376
|
-
|
|
1702
|
+
TableIdError<Table>,
|
|
1377
1703
|
string,
|
|
1378
1704
|
StringError
|
|
1379
1705
|
> {
|
|
1380
1706
|
table: Table;
|
|
1381
1707
|
}
|
|
1382
1708
|
|
|
1383
|
-
export interface
|
|
1384
|
-
extends TypeError<"
|
|
1709
|
+
export interface TableIdError<Table extends TypeName = TypeName>
|
|
1710
|
+
extends TypeError<"TableId"> {
|
|
1385
1711
|
readonly table: Table;
|
|
1386
1712
|
}
|
|
1387
1713
|
|
|
1388
|
-
export const
|
|
1389
|
-
(error) => `Invalid ${error.
|
|
1714
|
+
export const formatTableIdError = createTypeErrorFormatter<TableIdError>(
|
|
1715
|
+
(error) => `Invalid Id for table ${error.table}: ${error.value}.`,
|
|
1390
1716
|
);
|
|
1391
1717
|
|
|
1718
|
+
/** Binary representation of an {@link Id}. */
|
|
1719
|
+
export const IdBytes = brand("IdBytes", length(16)(Uint8Array));
|
|
1720
|
+
export type IdBytes = typeof IdBytes.Type;
|
|
1721
|
+
|
|
1722
|
+
export const idBytesTypeValueLength = 16 as NonNegativeInt;
|
|
1723
|
+
|
|
1724
|
+
export const idToIdBytes = (id: Id): IdBytes =>
|
|
1725
|
+
// Id is Base64Url (validated by Id.from), cast is safe
|
|
1726
|
+
base64UrlToUint8Array(id as unknown as Base64Url) as IdBytes;
|
|
1727
|
+
|
|
1728
|
+
export const idBytesToId = (idBytes: IdBytes): Id =>
|
|
1729
|
+
// Base64Url encoding of 16 bytes always produces valid Id (22 chars)
|
|
1730
|
+
uint8ArrayToBase64Url(idBytes) as unknown as Id;
|
|
1731
|
+
|
|
1392
1732
|
/**
|
|
1393
|
-
* Positive number.
|
|
1733
|
+
* Positive number (> 0).
|
|
1394
1734
|
*
|
|
1395
1735
|
* ### Example
|
|
1396
1736
|
*
|
|
@@ -1413,11 +1753,11 @@ export const positive: BrandFactory<"Positive", number, PositiveError> = (
|
|
|
1413
1753
|
export interface PositiveError extends TypeError<"Positive"> {}
|
|
1414
1754
|
|
|
1415
1755
|
export const formatPositiveError = createTypeErrorFormatter<PositiveError>(
|
|
1416
|
-
(error) => `The value ${error.value}
|
|
1756
|
+
(error) => `The value ${error.value} must be positive (> 0).`,
|
|
1417
1757
|
);
|
|
1418
1758
|
|
|
1419
1759
|
/**
|
|
1420
|
-
* Negative number.
|
|
1760
|
+
* Negative number (< 0).
|
|
1421
1761
|
*
|
|
1422
1762
|
* ### Example
|
|
1423
1763
|
*
|
|
@@ -1437,11 +1777,11 @@ export const negative: BrandFactory<"Negative", number, NegativeError> = (
|
|
|
1437
1777
|
export interface NegativeError extends TypeError<"Negative"> {}
|
|
1438
1778
|
|
|
1439
1779
|
export const formatNegativeError = createTypeErrorFormatter<NegativeError>(
|
|
1440
|
-
(error) => `The value ${error.value}
|
|
1780
|
+
(error) => `The value ${error.value} must be negative (< 0).`,
|
|
1441
1781
|
);
|
|
1442
1782
|
|
|
1443
1783
|
/**
|
|
1444
|
-
* Non-positive number.
|
|
1784
|
+
* Non-positive number (≤ 0).
|
|
1445
1785
|
*
|
|
1446
1786
|
* ### Example
|
|
1447
1787
|
*
|
|
@@ -1466,11 +1806,11 @@ export interface NonPositiveError extends TypeError<"NonPositive"> {}
|
|
|
1466
1806
|
|
|
1467
1807
|
export const formatNonPositiveError =
|
|
1468
1808
|
createTypeErrorFormatter<NonPositiveError>(
|
|
1469
|
-
(error) => `The value ${error.value}
|
|
1809
|
+
(error) => `The value ${error.value} must be non-positive (≤ 0).`,
|
|
1470
1810
|
);
|
|
1471
1811
|
|
|
1472
1812
|
/**
|
|
1473
|
-
* Non-negative number.
|
|
1813
|
+
* Non-negative number (≥ 0).
|
|
1474
1814
|
*
|
|
1475
1815
|
* ### Example
|
|
1476
1816
|
*
|
|
@@ -1495,22 +1835,38 @@ export interface NonNegativeError extends TypeError<"NonNegative"> {}
|
|
|
1495
1835
|
|
|
1496
1836
|
export const formatNonNegativeError =
|
|
1497
1837
|
createTypeErrorFormatter<NonNegativeError>(
|
|
1498
|
-
(error) => `The value ${error.value}
|
|
1838
|
+
(error) => `The value ${error.value} must be non-negative (≥ 0).`,
|
|
1499
1839
|
);
|
|
1500
1840
|
|
|
1501
|
-
/**
|
|
1841
|
+
/**
|
|
1842
|
+
* Non-negative number (≥ 0).
|
|
1843
|
+
*
|
|
1844
|
+
* @category Number
|
|
1845
|
+
*/
|
|
1502
1846
|
export const NonNegativeNumber = nonNegative(Number);
|
|
1503
1847
|
export type NonNegativeNumber = typeof NonNegativeNumber.Type;
|
|
1504
1848
|
|
|
1505
|
-
/**
|
|
1849
|
+
/**
|
|
1850
|
+
* Positive number (> 0).
|
|
1851
|
+
*
|
|
1852
|
+
* @category Number
|
|
1853
|
+
*/
|
|
1506
1854
|
export const PositiveNumber = positive(NonNegativeNumber);
|
|
1507
1855
|
export type PositiveNumber = typeof PositiveNumber.Type;
|
|
1508
1856
|
|
|
1509
|
-
/**
|
|
1857
|
+
/**
|
|
1858
|
+
* Non-positive number (≤ 0).
|
|
1859
|
+
*
|
|
1860
|
+
* @category Number
|
|
1861
|
+
*/
|
|
1510
1862
|
export const NonPositiveNumber = nonPositive(Number);
|
|
1511
1863
|
export type NonPositiveNumber = typeof NonPositiveNumber.Type;
|
|
1512
1864
|
|
|
1513
|
-
/**
|
|
1865
|
+
/**
|
|
1866
|
+
* Negative number (< 0).
|
|
1867
|
+
*
|
|
1868
|
+
* @category Number
|
|
1869
|
+
*/
|
|
1514
1870
|
export const NegativeNumber = negative(NonPositiveNumber);
|
|
1515
1871
|
export type NegativeNumber = typeof NegativeNumber.Type;
|
|
1516
1872
|
|
|
@@ -1535,7 +1891,7 @@ export const int: BrandFactory<"Int", number, IntError> = (parent) =>
|
|
|
1535
1891
|
export interface IntError extends TypeError<"Int"> {}
|
|
1536
1892
|
|
|
1537
1893
|
export const formatIntError = createTypeErrorFormatter<IntError>(
|
|
1538
|
-
(error) => `The value ${error.value}
|
|
1894
|
+
(error) => `The value ${error.value} must be an integer.`,
|
|
1539
1895
|
);
|
|
1540
1896
|
|
|
1541
1897
|
/**
|
|
@@ -1546,19 +1902,40 @@ export const formatIntError = createTypeErrorFormatter<IntError>(
|
|
|
1546
1902
|
export const Int = int(Number);
|
|
1547
1903
|
export type Int = typeof Int.Type;
|
|
1548
1904
|
|
|
1549
|
-
/**
|
|
1905
|
+
/**
|
|
1906
|
+
* Non-negative integer (≥ 0).
|
|
1907
|
+
*
|
|
1908
|
+
* @category Number
|
|
1909
|
+
*/
|
|
1550
1910
|
export const NonNegativeInt = nonNegative(Int);
|
|
1551
1911
|
export type NonNegativeInt = typeof NonNegativeInt.Type;
|
|
1552
1912
|
|
|
1553
|
-
/**
|
|
1913
|
+
/**
|
|
1914
|
+
* Positive integer (> 0).
|
|
1915
|
+
*
|
|
1916
|
+
* @category Number
|
|
1917
|
+
*/
|
|
1554
1918
|
export const PositiveInt = positive(NonNegativeInt);
|
|
1555
1919
|
export type PositiveInt = typeof PositiveInt.Type;
|
|
1556
1920
|
|
|
1557
|
-
/**
|
|
1921
|
+
/** Maximum safe positive integer value for practically infinite operations. */
|
|
1922
|
+
export const maxPositiveInt = PositiveInt.orThrow(
|
|
1923
|
+
globalThis.Number.MAX_SAFE_INTEGER,
|
|
1924
|
+
);
|
|
1925
|
+
|
|
1926
|
+
/**
|
|
1927
|
+
* Non-positive integer (≤ 0).
|
|
1928
|
+
*
|
|
1929
|
+
* @category Number
|
|
1930
|
+
*/
|
|
1558
1931
|
export const NonPositiveInt = nonPositive(Int);
|
|
1559
1932
|
export type NonPositiveInt = typeof NonPositiveInt.Type;
|
|
1560
1933
|
|
|
1561
|
-
/**
|
|
1934
|
+
/**
|
|
1935
|
+
* Negative integer (< 0).
|
|
1936
|
+
*
|
|
1937
|
+
* @category Number
|
|
1938
|
+
*/
|
|
1562
1939
|
export const NegativeInt = negative(NonPositiveInt);
|
|
1563
1940
|
export type NegativeInt = typeof NegativeInt.Type;
|
|
1564
1941
|
|
|
@@ -1676,7 +2053,7 @@ export const nonNaN: BrandFactory<"NonNaN", number, NonNaNError> = (parent) =>
|
|
|
1676
2053
|
export interface NonNaNError extends TypeError<"NonNaN"> {}
|
|
1677
2054
|
|
|
1678
2055
|
export const formatNonNaNError = createTypeErrorFormatter<NonNaNError>(
|
|
1679
|
-
(
|
|
2056
|
+
() => `The value must not be NaN.`,
|
|
1680
2057
|
);
|
|
1681
2058
|
|
|
1682
2059
|
/** @category Number */
|
|
@@ -1698,7 +2075,7 @@ export const finite: BrandFactory<"Finite", number, FiniteError> = (parent) =>
|
|
|
1698
2075
|
export interface FiniteError extends TypeError<"Finite"> {}
|
|
1699
2076
|
|
|
1700
2077
|
export const formatFiniteError = createTypeErrorFormatter<FiniteError>(
|
|
1701
|
-
(error) => `The value ${error.value}
|
|
2078
|
+
(error) => `The value ${error.value} must be finite.`,
|
|
1702
2079
|
);
|
|
1703
2080
|
|
|
1704
2081
|
/**
|
|
@@ -1775,198 +2152,57 @@ export interface BetweenError<
|
|
|
1775
2152
|
|
|
1776
2153
|
export const formatBetweenError = createTypeErrorFormatter<BetweenError>(
|
|
1777
2154
|
(error) =>
|
|
1778
|
-
`The value ${error.value} is not between ${error.min} and ${error.max}, inclusive.`,
|
|
1779
|
-
);
|
|
1780
|
-
|
|
1781
|
-
/** @category Number */
|
|
1782
|
-
export const Between1And10 = between(1, 10)(Number);
|
|
1783
|
-
export type Between1And10 = typeof Between1And10.Type;
|
|
1784
|
-
|
|
1785
|
-
/**
|
|
1786
|
-
* Literal {@link Type}.
|
|
1787
|
-
*
|
|
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),
|
|
2155
|
+
`The value ${error.value} is not between ${error.min} and ${error.max}, inclusive.`,
|
|
1938
2156
|
);
|
|
1939
2157
|
|
|
1940
2158
|
/**
|
|
1941
|
-
*
|
|
2159
|
+
* Literal {@link Type}.
|
|
2160
|
+
*
|
|
2161
|
+
* https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types
|
|
1942
2162
|
*
|
|
1943
2163
|
* ### Example
|
|
1944
2164
|
*
|
|
1945
2165
|
* ```ts
|
|
1946
|
-
*
|
|
1947
|
-
*
|
|
2166
|
+
* const LiteralHello = literal("Hello");
|
|
2167
|
+
* const result = LiteralHello.from("Hello"); // ok("Hello")
|
|
2168
|
+
* const errorResult = LiteralHello.from("World"); // err
|
|
1948
2169
|
* ```
|
|
1949
2170
|
*
|
|
1950
|
-
*
|
|
2171
|
+
* TODO: Add JsonValue
|
|
2172
|
+
*
|
|
2173
|
+
* @category Base Factories
|
|
1951
2174
|
*/
|
|
1952
|
-
export const
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
)
|
|
2175
|
+
export const literal = <T extends Literal>(expected: T): LiteralType<T> => {
|
|
2176
|
+
const fromUnknown = (value: unknown): Result<T, LiteralError<T>> =>
|
|
2177
|
+
value === expected
|
|
2178
|
+
? ok(expected)
|
|
2179
|
+
: err<LiteralError<T>>({ type: "Literal", value, expected });
|
|
2180
|
+
|
|
2181
|
+
return {
|
|
2182
|
+
...createType("Literal", {
|
|
2183
|
+
fromUnknown,
|
|
2184
|
+
fromParent: ok,
|
|
2185
|
+
}),
|
|
2186
|
+
expected,
|
|
2187
|
+
} as LiteralType<T>;
|
|
2188
|
+
};
|
|
1963
2189
|
|
|
1964
|
-
export interface
|
|
2190
|
+
export interface LiteralType<T extends Literal>
|
|
2191
|
+
extends Type<"Literal", T, WidenLiteral<T>, LiteralError<T>> {
|
|
2192
|
+
expected: T;
|
|
2193
|
+
}
|
|
1965
2194
|
|
|
1966
|
-
export
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
2195
|
+
export interface LiteralError<T extends Literal = Literal>
|
|
2196
|
+
extends TypeError<"Literal"> {
|
|
2197
|
+
readonly expected: T;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
export const formatLiteralError = createTypeErrorFormatter<LiteralError>(
|
|
2201
|
+
(error) =>
|
|
2202
|
+
`The value ${error.value} is not strictly equal to the expected literal: ${globalThis.String(
|
|
2203
|
+
error.expected,
|
|
2204
|
+
)}.`,
|
|
2205
|
+
);
|
|
1970
2206
|
|
|
1971
2207
|
/**
|
|
1972
2208
|
* Array of a specific {@link Type}.
|
|
@@ -2020,9 +2256,6 @@ export const array = <ElementType extends AnyType>(
|
|
|
2020
2256
|
return ok(result);
|
|
2021
2257
|
};
|
|
2022
2258
|
|
|
2023
|
-
const to = (value: ReadonlyArray<InferType<ElementType>>) =>
|
|
2024
|
-
value.map(element.to) as ReadonlyArray<InferInput<ElementType>>;
|
|
2025
|
-
|
|
2026
2259
|
const fromParent = (
|
|
2027
2260
|
value: ReadonlyArray<InferParent<ElementType>>,
|
|
2028
2261
|
): Result<
|
|
@@ -2048,18 +2281,10 @@ export const array = <ElementType extends AnyType>(
|
|
|
2048
2281
|
return ok(result);
|
|
2049
2282
|
};
|
|
2050
2283
|
|
|
2051
|
-
const toParent = (values: ReadonlyArray<InferType<ElementType>>) =>
|
|
2052
|
-
values.map(element.toParent) as ReadonlyArray<InferParent<ElementType>>;
|
|
2053
|
-
|
|
2054
2284
|
return {
|
|
2055
|
-
...createType("Array", {
|
|
2056
|
-
fromUnknown,
|
|
2057
|
-
to,
|
|
2058
|
-
fromParent,
|
|
2059
|
-
toParent,
|
|
2060
|
-
}),
|
|
2285
|
+
...createType("Array", { fromUnknown, fromParent }),
|
|
2061
2286
|
element,
|
|
2062
|
-
}
|
|
2287
|
+
};
|
|
2063
2288
|
};
|
|
2064
2289
|
|
|
2065
2290
|
/** ArrayType extends Type with an additional `element` property for reflection. */
|
|
@@ -2192,16 +2417,6 @@ export const record = <
|
|
|
2192
2417
|
return ok(result);
|
|
2193
2418
|
};
|
|
2194
2419
|
|
|
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
2420
|
const fromParent = (
|
|
2206
2421
|
value: Readonly<Record<KeyParent, InferParent<Value>>>,
|
|
2207
2422
|
): Result<
|
|
@@ -2246,22 +2461,10 @@ export const record = <
|
|
|
2246
2461
|
return ok(result);
|
|
2247
2462
|
};
|
|
2248
2463
|
|
|
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
2464
|
return {
|
|
2260
2465
|
...createType("Record", {
|
|
2261
2466
|
fromUnknown,
|
|
2262
|
-
to,
|
|
2263
2467
|
fromParent,
|
|
2264
|
-
toParent,
|
|
2265
2468
|
}),
|
|
2266
2469
|
key: keyType,
|
|
2267
2470
|
value: valueType,
|
|
@@ -2561,25 +2764,6 @@ export function object(
|
|
|
2561
2764
|
return ok(result);
|
|
2562
2765
|
};
|
|
2563
2766
|
|
|
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
2767
|
const fromParent = (value: any): Result<any, any> => {
|
|
2584
2768
|
const errors: Record<string, any> = {};
|
|
2585
2769
|
const result: Record<string, any> = {};
|
|
@@ -2631,31 +2815,10 @@ export function object(
|
|
|
2631
2815
|
return ok(result);
|
|
2632
2816
|
};
|
|
2633
2817
|
|
|
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
2818
|
return {
|
|
2654
2819
|
...createType(record ? "ObjectWithRecord" : "Object", {
|
|
2655
2820
|
fromUnknown,
|
|
2656
|
-
to,
|
|
2657
2821
|
fromParent,
|
|
2658
|
-
toParent,
|
|
2659
2822
|
}),
|
|
2660
2823
|
props,
|
|
2661
2824
|
...(record ? { record } : {}),
|
|
@@ -2897,7 +3060,6 @@ export function union(...args: ReadonlyArray<any>): any {
|
|
|
2897
3060
|
* cases.
|
|
2898
3061
|
*/
|
|
2899
3062
|
|
|
2900
|
-
/* eslint-disable */
|
|
2901
3063
|
const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
|
|
2902
3064
|
|
|
2903
3065
|
const fromUnknown = (value: unknown) => {
|
|
@@ -2915,23 +3077,13 @@ export function union(...args: ReadonlyArray<any>): any {
|
|
|
2915
3077
|
});
|
|
2916
3078
|
};
|
|
2917
3079
|
|
|
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
3080
|
return {
|
|
2926
3081
|
...createType("Union", {
|
|
2927
3082
|
fromUnknown,
|
|
2928
|
-
to,
|
|
2929
3083
|
fromParent: fromUnknown,
|
|
2930
|
-
toParent: to,
|
|
2931
3084
|
}),
|
|
2932
3085
|
members,
|
|
2933
3086
|
};
|
|
2934
|
-
/* eslint-enable */
|
|
2935
3087
|
}
|
|
2936
3088
|
|
|
2937
3089
|
/**
|
|
@@ -3025,20 +3177,10 @@ export const recursive = <ParentType extends AnyType>(
|
|
|
3025
3177
|
type ??= create();
|
|
3026
3178
|
return type.fromUnknown(value);
|
|
3027
3179
|
},
|
|
3028
|
-
to: (value: T) => {
|
|
3029
|
-
type ??= create();
|
|
3030
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
3031
|
-
return type.to(value);
|
|
3032
|
-
},
|
|
3033
3180
|
fromParent: (value: Parent) => {
|
|
3034
3181
|
type ??= create();
|
|
3035
3182
|
return type.fromParent(value);
|
|
3036
3183
|
},
|
|
3037
|
-
toParent: (value: T) => {
|
|
3038
|
-
type ??= create();
|
|
3039
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
3040
|
-
return type.toParent(value);
|
|
3041
|
-
},
|
|
3042
3184
|
is: (value: unknown): value is T => {
|
|
3043
3185
|
type ??= create();
|
|
3044
3186
|
return type.is(value);
|
|
@@ -3176,9 +3318,6 @@ export const tuple = <Elements extends [AnyType, ...ReadonlyArray<AnyType>]>(
|
|
|
3176
3318
|
return ok(result);
|
|
3177
3319
|
};
|
|
3178
3320
|
|
|
3179
|
-
const to = (value: { [K in keyof Elements]: InferType<Elements[K]> }) =>
|
|
3180
|
-
value.map((val, index) => elements[index].to(val) as IntentionalNever);
|
|
3181
|
-
|
|
3182
3321
|
const fromParent = (value: {
|
|
3183
3322
|
[K in keyof Elements]: InferParent<Elements[K]>;
|
|
3184
3323
|
}) => {
|
|
@@ -3210,21 +3349,10 @@ export const tuple = <Elements extends [AnyType, ...ReadonlyArray<AnyType>]>(
|
|
|
3210
3349
|
return ok(result as { [K in keyof Elements]: InferType<Elements[K]> });
|
|
3211
3350
|
};
|
|
3212
3351
|
|
|
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
3352
|
return {
|
|
3223
3353
|
...createType("Tuple", {
|
|
3224
3354
|
fromUnknown,
|
|
3225
|
-
to,
|
|
3226
3355
|
fromParent,
|
|
3227
|
-
toParent,
|
|
3228
3356
|
}),
|
|
3229
3357
|
elements,
|
|
3230
3358
|
} as IntentionalNever;
|
|
@@ -3300,37 +3428,41 @@ export const formatInt64Error = createTypeErrorFormatter<Int64Error>(
|
|
|
3300
3428
|
`The value ${error.value} is not a valid 64-bit signed integer (Int64).`,
|
|
3301
3429
|
);
|
|
3302
3430
|
|
|
3303
|
-
|
|
3304
|
-
String,
|
|
3305
|
-
BigInt,
|
|
3306
|
-
(value) =>
|
|
3307
|
-
trySync(
|
|
3308
|
-
() => globalThis.BigInt(value),
|
|
3309
|
-
(): BigIntFromStringError => ({ type: "BigIntFromString", value }),
|
|
3310
|
-
),
|
|
3311
|
-
(value) => value.toString(),
|
|
3312
|
-
);
|
|
3431
|
+
// // co s timhle? je to string, ze ktereho lze udelat bigint
|
|
3313
3432
|
|
|
3314
|
-
export
|
|
3433
|
+
// export const BigIntFromString = transform(
|
|
3434
|
+
// String,
|
|
3435
|
+
// BigInt,
|
|
3436
|
+
// (value) =>
|
|
3437
|
+
// trySync(
|
|
3438
|
+
// () => globalThis.BigInt(value),
|
|
3439
|
+
// (): BigIntFromStringError => ({ type: "BigIntFromString", value }),
|
|
3440
|
+
// ),
|
|
3441
|
+
// (value) => value.toString(),
|
|
3442
|
+
// );
|
|
3315
3443
|
|
|
3316
|
-
export
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3444
|
+
// export interface BigIntFromStringError extends TypeError<"BigIntFromString"> {}
|
|
3445
|
+
|
|
3446
|
+
// export const formatBigIntFromStringError =
|
|
3447
|
+
// createTypeErrorFormatter<BigIntFromStringError>(
|
|
3448
|
+
// (error) => `The value ${error.value} could not be converted to a BigInt.`,
|
|
3449
|
+
// );
|
|
3320
3450
|
|
|
3321
3451
|
/**
|
|
3322
3452
|
* Stringified {@link Int64}.
|
|
3323
3453
|
*
|
|
3324
|
-
* @category
|
|
3454
|
+
* @category String
|
|
3325
3455
|
*/
|
|
3326
|
-
export const Int64String = brand("Int64",
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
})
|
|
3456
|
+
export const Int64String = brand("Int64", NonEmptyTrimmedString, (value) =>
|
|
3457
|
+
trySync(
|
|
3458
|
+
() => {
|
|
3459
|
+
const maybeInt = globalThis.BigInt(value);
|
|
3460
|
+
Int64.orThrow(maybeInt);
|
|
3461
|
+
return value;
|
|
3462
|
+
},
|
|
3463
|
+
(): Int64StringError => ({ type: "Int64String", value }),
|
|
3464
|
+
),
|
|
3465
|
+
);
|
|
3334
3466
|
|
|
3335
3467
|
export type Int64String = typeof Int64String.Type;
|
|
3336
3468
|
|
|
@@ -3435,42 +3567,14 @@ export const JsonArray = array(JsonValue);
|
|
|
3435
3567
|
*/
|
|
3436
3568
|
export const JsonObject = record(String, JsonValue);
|
|
3437
3569
|
|
|
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}`,
|
|
3570
|
+
export const parseJson = (value: string): Result<JsonValue, JsonError> =>
|
|
3571
|
+
trySync(
|
|
3572
|
+
() => JSON.parse(value) as JsonValue,
|
|
3573
|
+
(error): JsonError => ({
|
|
3574
|
+
type: `Json`,
|
|
3575
|
+
value,
|
|
3576
|
+
message: globalThis.String(error),
|
|
3577
|
+
}),
|
|
3474
3578
|
);
|
|
3475
3579
|
|
|
3476
3580
|
/**
|
|
@@ -3479,20 +3583,15 @@ export const formatJsonValueFromStringError =
|
|
|
3479
3583
|
* ### Example
|
|
3480
3584
|
*
|
|
3481
3585
|
* ```ts
|
|
3482
|
-
* const result = Json.from('{"key":"value"}'); //
|
|
3483
|
-
* const error = Json.from("invalid json"); //
|
|
3586
|
+
* const result = Json.from('{"key":"value"}'); // ok
|
|
3587
|
+
* const error = Json.from("invalid json"); // err
|
|
3484
3588
|
* ```
|
|
3485
3589
|
*
|
|
3486
3590
|
* @category String
|
|
3487
3591
|
*/
|
|
3488
3592
|
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
|
-
});
|
|
3593
|
+
const result = parseJson(value);
|
|
3594
|
+
if (!result.ok) return result;
|
|
3496
3595
|
return ok(value);
|
|
3497
3596
|
});
|
|
3498
3597
|
|
|
@@ -3506,6 +3605,80 @@ export const formatJsonError = createTypeErrorFormatter<JsonError>(
|
|
|
3506
3605
|
(error) => `Invalid JSON: ${error.value}. Error: ${error.message}`,
|
|
3507
3606
|
);
|
|
3508
3607
|
|
|
3608
|
+
export const jsonValueToJson = (value: JsonValue): Json =>
|
|
3609
|
+
JSON.stringify(value) as Json;
|
|
3610
|
+
|
|
3611
|
+
export const jsonToJsonValue = (value: Json): JsonValue =>
|
|
3612
|
+
JSON.parse(value) as JsonValue;
|
|
3613
|
+
|
|
3614
|
+
/**
|
|
3615
|
+
* Creates a branded JSON string {@link Type} and type-safe conversion functions
|
|
3616
|
+
* for a given Type.
|
|
3617
|
+
*
|
|
3618
|
+
* This factory creates:
|
|
3619
|
+
*
|
|
3620
|
+
* 1. A branded string Type that validates JSON parsing and structural conformity
|
|
3621
|
+
* 2. A serialization function (Type → branded JSON string)
|
|
3622
|
+
* 3. A parsing function (branded JSON string → Type, skipping validation)
|
|
3623
|
+
*
|
|
3624
|
+
* Optimized for Evolu's SQLite workflow where we store typed JSON strings and
|
|
3625
|
+
* need type-safe conversions without double parsing.
|
|
3626
|
+
*
|
|
3627
|
+
* ### Example
|
|
3628
|
+
*
|
|
3629
|
+
* ```ts
|
|
3630
|
+
* const Person = object({
|
|
3631
|
+
* name: NonEmptyString100,
|
|
3632
|
+
* age: FiniteNumber,
|
|
3633
|
+
* });
|
|
3634
|
+
* type Person = typeof Person.Type;
|
|
3635
|
+
*
|
|
3636
|
+
* const [PersonJson, personToPersonJson, personJsonToPerson] = json(
|
|
3637
|
+
* Person,
|
|
3638
|
+
* "PersonJson",
|
|
3639
|
+
* );
|
|
3640
|
+
* // string & Brand<"PersonJson">
|
|
3641
|
+
* type PersonJson = typeof PersonJson.Type;
|
|
3642
|
+
*
|
|
3643
|
+
* // Usage:
|
|
3644
|
+
* const person: Person = { name: "Alice", age: 30 };
|
|
3645
|
+
* const jsonString = personToPersonJson(person); // PersonJson
|
|
3646
|
+
* const backToPerson = personJsonToPerson(jsonString); // Person
|
|
3647
|
+
* ```
|
|
3648
|
+
*/
|
|
3649
|
+
export const json = <T extends AnyType, Name extends TypeName>(
|
|
3650
|
+
type: T,
|
|
3651
|
+
name: Name,
|
|
3652
|
+
): [
|
|
3653
|
+
BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>,
|
|
3654
|
+
(
|
|
3655
|
+
value: InferType<T>,
|
|
3656
|
+
) => InferType<
|
|
3657
|
+
BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>
|
|
3658
|
+
>,
|
|
3659
|
+
(
|
|
3660
|
+
value: InferType<
|
|
3661
|
+
BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>
|
|
3662
|
+
>,
|
|
3663
|
+
) => InferType<T>,
|
|
3664
|
+
] => {
|
|
3665
|
+
const BrandedJsonType = brand(name, String, (value) => {
|
|
3666
|
+
const parseResult = parseJson(value);
|
|
3667
|
+
if (!parseResult.ok) return parseResult;
|
|
3668
|
+
|
|
3669
|
+
const validationResult = type.fromUnknown(parseResult.value);
|
|
3670
|
+
if (!validationResult.ok) return validationResult;
|
|
3671
|
+
|
|
3672
|
+
return ok(value);
|
|
3673
|
+
}) as BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>;
|
|
3674
|
+
|
|
3675
|
+
return [
|
|
3676
|
+
BrandedJsonType,
|
|
3677
|
+
jsonValueToJson as IntentionalNever,
|
|
3678
|
+
jsonToJsonValue as IntentionalNever,
|
|
3679
|
+
];
|
|
3680
|
+
};
|
|
3681
|
+
|
|
3509
3682
|
/**
|
|
3510
3683
|
* Optional {@link Type}.
|
|
3511
3684
|
*
|
|
@@ -3530,9 +3703,7 @@ export const formatJsonError = createTypeErrorFormatter<JsonError>(
|
|
|
3530
3703
|
export const optional = <T extends AnyType>(type: T): OptionalType<T> => ({
|
|
3531
3704
|
...createType("Optional", {
|
|
3532
3705
|
fromUnknown: type.fromUnknown,
|
|
3533
|
-
to: type.to,
|
|
3534
3706
|
fromParent: type.fromParent,
|
|
3535
|
-
toParent: type.toParent,
|
|
3536
3707
|
}),
|
|
3537
3708
|
parent: type,
|
|
3538
3709
|
});
|
|
@@ -3556,7 +3727,7 @@ export const isOptionalType = (x: unknown): x is OptionalType<any> =>
|
|
|
3556
3727
|
/**
|
|
3557
3728
|
* Creates a partial object type where all properties are optional.
|
|
3558
3729
|
*
|
|
3559
|
-
* This is useful when
|
|
3730
|
+
* This is useful when we want to validate an object in which none of the keys
|
|
3560
3731
|
* are required, but if they are present they must conform to their
|
|
3561
3732
|
* corresponding Types.
|
|
3562
3733
|
*
|
|
@@ -3661,68 +3832,39 @@ export function omit<T extends ObjectType<any>, Keys extends keyof T["props"]>(
|
|
|
3661
3832
|
return object(newProps);
|
|
3662
3833
|
}
|
|
3663
3834
|
|
|
3835
|
+
export const maxMutationSize = 655360;
|
|
3836
|
+
|
|
3664
3837
|
/**
|
|
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
|
-
* ```
|
|
3838
|
+
* Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
|
|
3839
|
+
* the `maxProtocolMessageRangesSize`. The max size is 640KB in bytes, measured
|
|
3840
|
+
* via MessagePack. Evolu Protocol DbChange will be smaller thanks to various
|
|
3841
|
+
* optimizations.
|
|
3692
3842
|
*/
|
|
3693
|
-
export const
|
|
3843
|
+
export const validMutationSize = <T extends AnyType>(
|
|
3694
3844
|
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>;
|
|
3845
|
+
): BrandType<T, "ValidMutationSize", ValidMutationSizeError, InferErrors<T>> =>
|
|
3846
|
+
brand("ValidMutationSize", type, (value) =>
|
|
3847
|
+
pack(value).byteLength <= maxMutationSize
|
|
3848
|
+
? ok(value)
|
|
3849
|
+
: err<ValidMutationSizeError>({ type: "ValidMutationSize", value }),
|
|
3850
|
+
);
|
|
3714
3851
|
|
|
3715
|
-
|
|
3716
|
-
|
|
3852
|
+
export interface ValidMutationSizeError
|
|
3853
|
+
extends TypeError<"ValidMutationSize"> {}
|
|
3717
3854
|
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
3723
|
-
(value) => JSON.parse(value) as InferType<T>,
|
|
3855
|
+
export const formatValidMutationSizeError =
|
|
3856
|
+
createTypeErrorFormatter<ValidMutationSizeError>(
|
|
3857
|
+
(error) =>
|
|
3858
|
+
`The mutation size exceeds the maximum limit of ${maxMutationSize} bytes. The provided mutation has a size of ${pack(error.value).byteLength} bytes.`,
|
|
3724
3859
|
);
|
|
3725
|
-
|
|
3860
|
+
|
|
3861
|
+
export type ValidMutationSize<Props extends Record<string, AnyType>> =
|
|
3862
|
+
BrandType<
|
|
3863
|
+
ObjectType<Props>,
|
|
3864
|
+
"ValidMutationSize",
|
|
3865
|
+
ValidMutationSizeError,
|
|
3866
|
+
InferErrors<ObjectType<Props>>
|
|
3867
|
+
>;
|
|
3726
3868
|
|
|
3727
3869
|
/**
|
|
3728
3870
|
* Union of all `TypeError`s defined in the `Type.ts` file, including base type
|
|
@@ -3749,16 +3891,16 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
|
|
|
3749
3891
|
| InstanceOfError
|
|
3750
3892
|
| EvoluTypeError
|
|
3751
3893
|
| CurrencyCodeError
|
|
3752
|
-
|
|
|
3894
|
+
| DateIsoError
|
|
3753
3895
|
| TrimmedError
|
|
3754
3896
|
| MinLengthError
|
|
3755
3897
|
| MaxLengthError
|
|
3756
3898
|
| LengthError
|
|
3757
3899
|
| MnemonicError
|
|
3758
3900
|
| RegexError
|
|
3759
|
-
| NanoIdError
|
|
3760
3901
|
| SimplePasswordError
|
|
3761
3902
|
| IdError
|
|
3903
|
+
| TableIdError
|
|
3762
3904
|
| PositiveError
|
|
3763
3905
|
| NegativeError
|
|
3764
3906
|
| NonPositiveError
|
|
@@ -3774,10 +3916,9 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
|
|
|
3774
3916
|
| BetweenError
|
|
3775
3917
|
| LiteralError
|
|
3776
3918
|
| Int64Error
|
|
3777
|
-
| BigIntFromStringError
|
|
3778
3919
|
| Int64StringError
|
|
3779
|
-
| JsonValueFromStringError
|
|
3780
3920
|
| JsonError
|
|
3921
|
+
| ValidMutationSizeError
|
|
3781
3922
|
| ExtraErrors
|
|
3782
3923
|
// Composite errors
|
|
3783
3924
|
| ArrayError<TypeErrors<ExtraErrors>>
|
|
@@ -3792,70 +3933,74 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
|
|
|
3792
3933
|
| TupleError<TypeErrors<ExtraErrors>>;
|
|
3793
3934
|
|
|
3794
3935
|
/**
|
|
3795
|
-
*
|
|
3796
|
-
* {@link TypeErrors} and custom errors. It also lets us override the default
|
|
3797
|
-
* formatting for specific errors.
|
|
3936
|
+
* Formats Evolu Type errors into user-friendly messages.
|
|
3798
3937
|
*
|
|
3799
|
-
*
|
|
3800
|
-
*
|
|
3938
|
+
* Evolu Type typed errors ensure every error type must have a formatter.
|
|
3939
|
+
* TypeScript enforces this at compile-time, preventing unhandled validation
|
|
3940
|
+
* errors from reaching users.
|
|
3801
3941
|
*
|
|
3802
|
-
*
|
|
3942
|
+
* The `createFormatTypeError` function handles both built-in {@link TypeErrors}
|
|
3943
|
+
* and custom errors, and lets us override default formatting for specific
|
|
3944
|
+
* errors.
|
|
3945
|
+
*
|
|
3946
|
+
* ### Example
|
|
3803
3947
|
*
|
|
3804
3948
|
* ```ts
|
|
3805
|
-
* const
|
|
3806
|
-
*
|
|
3807
|
-
*
|
|
3949
|
+
* const formatTypeError = createFormatTypeError<
|
|
3950
|
+
* MinLengthError | MaxLengthError
|
|
3951
|
+
* >((error): string => {
|
|
3952
|
+
* switch (error.type) {
|
|
3953
|
+
* case "MinLength":
|
|
3954
|
+
* return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
|
|
3955
|
+
* case "MaxLength":
|
|
3956
|
+
* return `Text is too long (maximum ${error.max} characters)`;
|
|
3957
|
+
* }
|
|
3958
|
+
* });
|
|
3808
3959
|
* ```
|
|
3809
3960
|
*
|
|
3810
|
-
*
|
|
3961
|
+
* Alternatively, write a custom formatter from scratch without using
|
|
3962
|
+
* `createFormatTypeError`. This gives us full control over error formatting:
|
|
3811
3963
|
*
|
|
3812
3964
|
* ```ts
|
|
3813
|
-
*
|
|
3814
|
-
*
|
|
3965
|
+
* const Person = object({
|
|
3966
|
+
* name: NonEmptyTrimmedString100,
|
|
3967
|
+
* age: optional(PositiveInt),
|
|
3968
|
+
* });
|
|
3969
|
+
*
|
|
3970
|
+
* // Define only the errors actually used by Person Type
|
|
3971
|
+
* type PersonErrors =
|
|
3815
3972
|
* | StringError
|
|
3816
|
-
* | MinLengthError
|
|
3817
3973
|
* | MaxLengthError
|
|
3818
|
-
* |
|
|
3819
|
-
* | IdError
|
|
3974
|
+
* | MinLengthError
|
|
3820
3975
|
* | 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.
|
|
3976
|
+
* | PositiveError
|
|
3977
|
+
* | NonNegativeError
|
|
3978
|
+
* | IntError
|
|
3979
|
+
* | NumberError
|
|
3980
|
+
* | ObjectError<Record<string, PersonErrors>>;
|
|
3981
|
+
*
|
|
3982
|
+
* const formatTypeError: TypeErrorFormatter<PersonErrors> = (error) => {
|
|
3830
3983
|
* 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
3984
|
* case "String":
|
|
3842
|
-
* // We can reuse existing formatter.
|
|
3843
3985
|
* return formatStringError(error);
|
|
3986
|
+
* case "Number":
|
|
3987
|
+
* return "Must be a number";
|
|
3988
|
+
* case "MinLength":
|
|
3989
|
+
* return `Must be at least ${error.min} characters`;
|
|
3990
|
+
* case "MaxLength":
|
|
3991
|
+
* return `Cannot exceed ${error.max} characters`;
|
|
3844
3992
|
* case "Trimmed":
|
|
3845
|
-
* return "
|
|
3846
|
-
* case "
|
|
3847
|
-
* return "
|
|
3848
|
-
* case "
|
|
3849
|
-
* return
|
|
3850
|
-
*
|
|
3851
|
-
*
|
|
3852
|
-
* return `Union errors: ${error.errors.map(formatTypeError).join(", ")}`;
|
|
3993
|
+
* return "Cannot have leading or trailing spaces";
|
|
3994
|
+
* case "Positive":
|
|
3995
|
+
* return "Must be a positive number";
|
|
3996
|
+
* case "NonNegative":
|
|
3997
|
+
* return "Must be zero or positive";
|
|
3998
|
+
* case "Int":
|
|
3999
|
+
* return "Must be an integer";
|
|
3853
4000
|
* case "Object": {
|
|
3854
|
-
* if (
|
|
3855
|
-
*
|
|
3856
|
-
*
|
|
3857
|
-
* )
|
|
3858
|
-
* return "A developer made an error, this should not happen.";
|
|
4001
|
+
* if (error.reason.kind === "NotObject") return "Must be an object";
|
|
4002
|
+
* if (error.reason.kind === "ExtraKeys")
|
|
4003
|
+
* return "Contains unexpected fields";
|
|
3859
4004
|
* const firstError = Object.values(error.reason.errors).find(
|
|
3860
4005
|
* (e) => e !== undefined,
|
|
3861
4006
|
* )!;
|
|
@@ -3902,8 +4047,8 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
|
|
|
3902
4047
|
return formatIsTypeError(error);
|
|
3903
4048
|
case "CurrencyCode":
|
|
3904
4049
|
return formatCurrencyCodeError(error);
|
|
3905
|
-
case "
|
|
3906
|
-
return
|
|
4050
|
+
case "DateIso":
|
|
4051
|
+
return formatDateIsoError(error);
|
|
3907
4052
|
case "Trimmed":
|
|
3908
4053
|
return formatTrimmedError(error);
|
|
3909
4054
|
case "MinLength":
|
|
@@ -3918,6 +4063,8 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
|
|
|
3918
4063
|
return formatRegexError(error);
|
|
3919
4064
|
case "Id":
|
|
3920
4065
|
return formatIdError(error);
|
|
4066
|
+
case "TableId":
|
|
4067
|
+
return formatTableIdError(error);
|
|
3921
4068
|
case "Positive":
|
|
3922
4069
|
return formatPositiveError(error);
|
|
3923
4070
|
case "Negative":
|
|
@@ -3948,14 +4095,12 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
|
|
|
3948
4095
|
return formatLiteralError(error);
|
|
3949
4096
|
case "Int64":
|
|
3950
4097
|
return formatInt64Error(error);
|
|
3951
|
-
case "BigIntFromString":
|
|
3952
|
-
return formatBigIntFromStringError(error);
|
|
3953
4098
|
case "Int64String":
|
|
3954
4099
|
return formatInt64StringError(error);
|
|
3955
|
-
case "JsonValueFromString":
|
|
3956
|
-
return formatJsonValueFromStringError(error);
|
|
3957
4100
|
case "Json":
|
|
3958
4101
|
return formatJsonError(error);
|
|
4102
|
+
case "ValidMutationSize":
|
|
4103
|
+
return formatValidMutationSizeError(error);
|
|
3959
4104
|
// Composite Types
|
|
3960
4105
|
case "SimplePassword":
|
|
3961
4106
|
return formatSimplePasswordError(formatTypeError)(error);
|
|
@@ -3971,8 +4116,220 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
|
|
|
3971
4116
|
return formatUnionError(formatTypeError)(error);
|
|
3972
4117
|
case "Tuple":
|
|
3973
4118
|
return formatTupleError(formatTypeError)(error);
|
|
4119
|
+
default: {
|
|
4120
|
+
// Fallback for unknown error types
|
|
4121
|
+
const unknownError = error as TypeError;
|
|
4122
|
+
return `A value ${safelyStringifyUnknownValue(unknownError.value)} is not valid for type ${unknownError.type}.`;
|
|
4123
|
+
}
|
|
3974
4124
|
}
|
|
3975
4125
|
};
|
|
3976
4126
|
|
|
3977
4127
|
return formatTypeError;
|
|
3978
4128
|
};
|
|
4129
|
+
|
|
4130
|
+
/**
|
|
4131
|
+
* Converts an Evolu {@link TypeError} to Standard Schema V1 issues format.
|
|
4132
|
+
*
|
|
4133
|
+
* This function recursively converts Evolu's typed errors into the Standard
|
|
4134
|
+
* Schema issue format with proper path tracking for nested structures.
|
|
4135
|
+
*
|
|
4136
|
+
* @category Utilities
|
|
4137
|
+
*/
|
|
4138
|
+
export const typeErrorToStandardSchemaIssues = <
|
|
4139
|
+
ExtraErrors extends TypeError = never,
|
|
4140
|
+
>(
|
|
4141
|
+
error: TypeErrors<ExtraErrors>,
|
|
4142
|
+
formatTypeError: TypeErrorFormatter<TypeErrors<ExtraErrors>>,
|
|
4143
|
+
path: ReadonlyArray<PropertyKey> = [],
|
|
4144
|
+
): ReadonlyArray<StandardSchemaV1.Issue> => {
|
|
4145
|
+
if (error.type === "Array") {
|
|
4146
|
+
const arrayError = error as ArrayError;
|
|
4147
|
+
if (arrayError.reason.kind === "NotArray") {
|
|
4148
|
+
return [{ message: formatTypeError(error), path }];
|
|
4149
|
+
}
|
|
4150
|
+
return typeErrorToStandardSchemaIssues(
|
|
4151
|
+
arrayError.reason.error as TypeErrors<ExtraErrors>,
|
|
4152
|
+
formatTypeError,
|
|
4153
|
+
[...path, arrayError.reason.index],
|
|
4154
|
+
);
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4157
|
+
if (error.type === "Object") {
|
|
4158
|
+
const objectError = error as ObjectError;
|
|
4159
|
+
if (
|
|
4160
|
+
objectError.reason.kind === "NotObject" ||
|
|
4161
|
+
objectError.reason.kind === "ExtraKeys"
|
|
4162
|
+
) {
|
|
4163
|
+
return [{ message: formatTypeError(error), path }];
|
|
4164
|
+
}
|
|
4165
|
+
const issues: Array<StandardSchemaV1.Issue> = [];
|
|
4166
|
+
for (const [key, propError] of Object.entries(objectError.reason.errors)) {
|
|
4167
|
+
issues.push(
|
|
4168
|
+
...typeErrorToStandardSchemaIssues(
|
|
4169
|
+
propError as TypeErrors<ExtraErrors>,
|
|
4170
|
+
formatTypeError,
|
|
4171
|
+
[...path, key],
|
|
4172
|
+
),
|
|
4173
|
+
);
|
|
4174
|
+
}
|
|
4175
|
+
return issues;
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
if (error.type === "ObjectWithRecord") {
|
|
4179
|
+
const objectWithRecordError = error as ObjectWithRecordError;
|
|
4180
|
+
if (objectWithRecordError.reason.kind === "NotObject") {
|
|
4181
|
+
return [{ message: formatTypeError(error), path }];
|
|
4182
|
+
}
|
|
4183
|
+
if (
|
|
4184
|
+
objectWithRecordError.reason.kind === "IndexKey" ||
|
|
4185
|
+
objectWithRecordError.reason.kind === "IndexValue"
|
|
4186
|
+
) {
|
|
4187
|
+
return typeErrorToStandardSchemaIssues(
|
|
4188
|
+
objectWithRecordError.reason.error as TypeErrors<ExtraErrors>,
|
|
4189
|
+
formatTypeError,
|
|
4190
|
+
[...path, objectWithRecordError.reason.key as PropertyKey],
|
|
4191
|
+
);
|
|
4192
|
+
}
|
|
4193
|
+
const issues: Array<StandardSchemaV1.Issue> = [];
|
|
4194
|
+
for (const [key, propError] of Object.entries(
|
|
4195
|
+
objectWithRecordError.reason.errors,
|
|
4196
|
+
)) {
|
|
4197
|
+
issues.push(
|
|
4198
|
+
...typeErrorToStandardSchemaIssues(
|
|
4199
|
+
propError as TypeErrors<ExtraErrors>,
|
|
4200
|
+
formatTypeError,
|
|
4201
|
+
[...path, key],
|
|
4202
|
+
),
|
|
4203
|
+
);
|
|
4204
|
+
}
|
|
4205
|
+
return issues;
|
|
4206
|
+
}
|
|
4207
|
+
|
|
4208
|
+
if (error.type === "Record") {
|
|
4209
|
+
const recordError = error as RecordError;
|
|
4210
|
+
if (recordError.reason.kind === "NotRecord") {
|
|
4211
|
+
return [{ message: formatTypeError(error), path }];
|
|
4212
|
+
}
|
|
4213
|
+
return typeErrorToStandardSchemaIssues(
|
|
4214
|
+
recordError.reason.error as TypeErrors<ExtraErrors>,
|
|
4215
|
+
formatTypeError,
|
|
4216
|
+
[...path, recordError.reason.key as PropertyKey],
|
|
4217
|
+
);
|
|
4218
|
+
}
|
|
4219
|
+
|
|
4220
|
+
if (error.type === "Tuple") {
|
|
4221
|
+
const tupleError = error as TupleError;
|
|
4222
|
+
if (tupleError.reason.kind === "InvalidLength") {
|
|
4223
|
+
return [{ message: formatTypeError(error), path }];
|
|
4224
|
+
}
|
|
4225
|
+
return typeErrorToStandardSchemaIssues(
|
|
4226
|
+
tupleError.reason.error as TypeErrors<ExtraErrors>,
|
|
4227
|
+
formatTypeError,
|
|
4228
|
+
[...path, tupleError.reason.index],
|
|
4229
|
+
);
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
if (error.type === "Union") {
|
|
4233
|
+
const unionError = error as UnionError;
|
|
4234
|
+
return unionError.errors.flatMap((err) =>
|
|
4235
|
+
typeErrorToStandardSchemaIssues(
|
|
4236
|
+
err as TypeErrors<ExtraErrors>,
|
|
4237
|
+
formatTypeError,
|
|
4238
|
+
path,
|
|
4239
|
+
),
|
|
4240
|
+
);
|
|
4241
|
+
}
|
|
4242
|
+
|
|
4243
|
+
if (error.type === "Brand") {
|
|
4244
|
+
const brandError = error as BrandWithoutRefineError<TypeName, TypeError>;
|
|
4245
|
+
if ("parentError" in brandError) {
|
|
4246
|
+
return typeErrorToStandardSchemaIssues(
|
|
4247
|
+
brandError.parentError as TypeErrors<ExtraErrors>,
|
|
4248
|
+
formatTypeError,
|
|
4249
|
+
path,
|
|
4250
|
+
);
|
|
4251
|
+
}
|
|
4252
|
+
return [{ message: formatTypeError(error), path }];
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
return [{ message: formatTypeError(error), path }];
|
|
4256
|
+
};
|
|
4257
|
+
|
|
4258
|
+
/** The Standard Schema interface. */
|
|
4259
|
+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
4260
|
+
/** The Standard Schema properties. */
|
|
4261
|
+
readonly "~standard": StandardSchemaV1.Props<Input, Output>;
|
|
4262
|
+
}
|
|
4263
|
+
|
|
4264
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
4265
|
+
export declare namespace StandardSchemaV1 {
|
|
4266
|
+
/** The Standard Schema properties interface. */
|
|
4267
|
+
export interface Props<Input = unknown, Output = Input> {
|
|
4268
|
+
/** The version number of the standard. */
|
|
4269
|
+
readonly version: 1;
|
|
4270
|
+
/** The vendor name of the schema library. */
|
|
4271
|
+
readonly vendor: string;
|
|
4272
|
+
/** Validates unknown input values. */
|
|
4273
|
+
readonly validate: (
|
|
4274
|
+
value: unknown,
|
|
4275
|
+
) => Result<Output> | Promise<Result<Output>>;
|
|
4276
|
+
/** Inferred types associated with the schema. */
|
|
4277
|
+
readonly types?: Types<Input, Output> | undefined;
|
|
4278
|
+
}
|
|
4279
|
+
|
|
4280
|
+
/** The result interface of the validate function. */
|
|
4281
|
+
export type Result<Output> = SuccessResult<Output> | FailureResult;
|
|
4282
|
+
|
|
4283
|
+
/** The result interface if validation succeeds. */
|
|
4284
|
+
export interface SuccessResult<Output> {
|
|
4285
|
+
/** The typed output value. */
|
|
4286
|
+
readonly value: Output;
|
|
4287
|
+
/** The non-existent issues. */
|
|
4288
|
+
readonly issues?: undefined;
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
/** The result interface if validation fails. */
|
|
4292
|
+
export interface FailureResult {
|
|
4293
|
+
/** The issues of failed validation. */
|
|
4294
|
+
readonly issues: ReadonlyArray<Issue>;
|
|
4295
|
+
}
|
|
4296
|
+
|
|
4297
|
+
/** The issue interface of the failure output. */
|
|
4298
|
+
export interface Issue {
|
|
4299
|
+
/** The error message of the issue. */
|
|
4300
|
+
readonly message: string;
|
|
4301
|
+
/** The path of the issue, if any. */
|
|
4302
|
+
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
|
|
4303
|
+
}
|
|
4304
|
+
|
|
4305
|
+
/** The path segment interface of the issue. */
|
|
4306
|
+
export interface PathSegment {
|
|
4307
|
+
/** The key representing a path segment. */
|
|
4308
|
+
readonly key: PropertyKey;
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
/** The Standard Schema types interface. */
|
|
4312
|
+
export interface Types<Input = unknown, Output = Input> {
|
|
4313
|
+
/** The input type of the schema. */
|
|
4314
|
+
readonly input: Input;
|
|
4315
|
+
/** The output type of the schema. */
|
|
4316
|
+
readonly output: Output;
|
|
4317
|
+
}
|
|
4318
|
+
|
|
4319
|
+
/** Infers the input type of a Standard Schema. */
|
|
4320
|
+
export type InferInput<Schema extends StandardSchemaV1> = NonNullable<
|
|
4321
|
+
Schema["~standard"]["types"]
|
|
4322
|
+
>["input"];
|
|
4323
|
+
|
|
4324
|
+
/** Infers the output type of a Standard Schema. */
|
|
4325
|
+
export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<
|
|
4326
|
+
Schema["~standard"]["types"]
|
|
4327
|
+
>["output"];
|
|
4328
|
+
}
|
|
4329
|
+
|
|
4330
|
+
/**
|
|
4331
|
+
* Shared formatter cache for Standard Schema integration - avoids circular
|
|
4332
|
+
* dependency by lazily creating the formatter on first use rather than during
|
|
4333
|
+
* module initialization.
|
|
4334
|
+
*/
|
|
4335
|
+
let cachedStandardSchemaFormatTypeError: TypeErrorFormatter<any> | undefined;
|