@evolu/common 6.0.1-preview.2 → 6.0.1-preview.21
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/Assert.d.ts +6 -3
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +7 -4
- 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 +1 -1
- package/dist/src/CallbackRegistry.d.ts +53 -0
- package/dist/src/CallbackRegistry.d.ts.map +1 -0
- package/dist/src/CallbackRegistry.js +25 -0
- 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 +29 -42
- package/dist/src/Evolu/Db.d.ts +159 -66
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +300 -705
- 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 +200 -115
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +287 -157
- 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 +144 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +171 -0
- package/dist/src/Evolu/Owner.d.ts +209 -124
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +118 -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 +256 -211
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +558 -359
- 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 +72 -4
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +124 -13
- 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 +159 -14
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +42 -32
- package/dist/src/Evolu/Sync.d.ts +68 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +414 -20
- package/dist/src/Evolu/Timestamp.d.ts +35 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +27 -18
- package/dist/src/ManyToManyMap.d.ts +74 -10
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/ManyToManyMap.js +41 -6
- package/dist/src/Number.d.ts +2 -1
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/RefCountedResourceManager.d.ts +119 -0
- package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
- package/dist/src/RefCountedResourceManager.js +197 -0
- package/dist/src/Result.d.ts +176 -51
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +30 -241
- package/dist/src/Sqlite.d.ts +21 -4
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +50 -8
- package/dist/src/Task.d.ts +511 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +410 -0
- package/dist/src/Time.d.ts +59 -0
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +87 -4
- package/dist/src/Type.d.ts +474 -327
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +502 -436
- package/dist/src/Types.d.ts +0 -74
- 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 -13
- 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 +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -3
- package/package.json +14 -13
- package/src/Assert.ts +8 -7
- package/src/Brand.ts +75 -0
- package/src/Buffer.ts +1 -1
- package/src/CallbackRegistry.ts +84 -0
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +94 -79
- package/src/Evolu/Db.ts +528 -1027
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +682 -329
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +422 -0
- package/src/Evolu/Owner.ts +282 -227
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +788 -640
- 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 +238 -18
- package/src/Evolu/Schema.ts +391 -191
- package/src/Evolu/Storage.ts +275 -55
- package/src/Evolu/Sync.ts +708 -37
- package/src/Evolu/Timestamp.ts +38 -35
- package/src/ManyToManyMap.ts +127 -24
- package/src/Number.ts +2 -6
- package/src/Random.ts +3 -2
- package/src/RefCountedResourceManager.ts +368 -0
- package/src/Result.ts +183 -53
- package/src/Sqlite.ts +59 -23
- package/src/Task.ts +779 -0
- package/src/Time.ts +168 -4
- package/src/Type.ts +708 -660
- package/src/Types.ts +0 -76
- package/src/WebSocket.ts +23 -17
- package/src/Worker.ts +72 -23
- package/src/index.ts +3 -3
- package/dist/src/Callbacks.d.ts +0 -20
- package/dist/src/Callbacks.d.ts.map +0 -1
- package/dist/src/Callbacks.js +0 -18
- 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/Callbacks.ts +0 -43
- 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,187 @@
|
|
|
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
|
+
* Why another validation library?
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* - **Result-based error handling** – no exceptions for normal control flow.
|
|
11
|
+
* - **Typed errors with decoupled formatters** – validation logic ≠ user
|
|
12
|
+
* messages.
|
|
13
|
+
* - **Consistent constraints via {@link Brand}** – every constraint becomes part
|
|
14
|
+
* of the type.
|
|
15
|
+
* - **No user-land chaining DSL** – designed with the upcoming ES pipe operator
|
|
16
|
+
* in mind.
|
|
17
|
+
* - **Selective validation** – parent validations are skipped when already proved
|
|
18
|
+
* by typing.
|
|
19
|
+
* - **Simple, top-down implementation** – readable source code from top to bottom
|
|
20
|
+
* with no hidden magic; just plain functions and composition.
|
|
10
21
|
*
|
|
11
|
-
*
|
|
12
|
-
* exceptions.
|
|
13
|
-
* - **Consistent constraints**: Enforcing {@link Brand} for all constraints.
|
|
14
|
-
* - **Typed errors with decoupled formatters**: Avoiding coupling error messages
|
|
15
|
-
* with validators.
|
|
16
|
-
* - **No user-land chaining**: Designed with ES pipe operator in mind.
|
|
17
|
-
* - **Selective validation/transformation**: Skipping parent Type validations and
|
|
18
|
-
* transformations when TypeScript's type system can be relied upon.
|
|
19
|
-
* - **Bidirectional transformations**: Supporting transformations in both
|
|
20
|
-
* directions.
|
|
21
|
-
* - **Minimal and transparent code**: No runtime dependencies or hidden magic.
|
|
22
|
+
* ### Base Types Quick Start
|
|
22
23
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* // Validate unknown values
|
|
26
|
+
* const value: unknown = "hello";
|
|
27
|
+
* const stringResult = String.fromUnknown(value);
|
|
28
|
+
* if (!stringResult.ok) {
|
|
29
|
+
* // console.error(formatStringError(stringResult.error));
|
|
30
|
+
* return stringResult; // inside a function returning Result<string, _>
|
|
31
|
+
* }
|
|
32
|
+
* // Safe branch: value is now string
|
|
33
|
+
* const upper = stringResult.value.toUpperCase();
|
|
34
|
+
*
|
|
35
|
+
* // Type guard style
|
|
36
|
+
* if (String.is(value)) {
|
|
37
|
+
* // narrowed to string
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* // Composing: arrays & objects
|
|
41
|
+
* const Numbers = array(Number); // ReadonlyArray<number>
|
|
42
|
+
* const Point = object({ x: Number, y: Number });
|
|
43
|
+
*
|
|
44
|
+
* Numbers.from([1, 2, 3]); // ok
|
|
45
|
+
* Point.from({ x: 1, y: 2 }); // ok
|
|
46
|
+
* Point.from({ x: 1, y: "2" }); // err -> nested Number error
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* ### Branding Basics
|
|
50
|
+
*
|
|
51
|
+
* Branding adds semantic meaning & constraints while preserving the runtime
|
|
52
|
+
* shape:
|
|
53
|
+
*
|
|
54
|
+
* ```ts
|
|
55
|
+
* const CurrencyCode = brand("CurrencyCode", String, (value) =>
|
|
56
|
+
* /^[A-Z]{3}$/.test(value)
|
|
57
|
+
* ? ok(value)
|
|
58
|
+
* : err<CurrencyCodeError>({ type: "CurrencyCode", value }),
|
|
59
|
+
* );
|
|
60
|
+
* type CurrencyCode = typeof CurrencyCode.Type; // string & Brand<"CurrencyCode">
|
|
61
|
+
*
|
|
62
|
+
* interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
|
|
63
|
+
*
|
|
64
|
+
* const formatCurrencyCodeError =
|
|
65
|
+
* createTypeErrorFormatter<CurrencyCodeError>(
|
|
66
|
+
* (error) => `Invalid currency code: ${error.value}`,
|
|
67
|
+
* );
|
|
68
|
+
*
|
|
69
|
+
* const r = CurrencyCode.from("USD"); // ok("USD")
|
|
70
|
+
* const e = CurrencyCode.from("usd"); // err(...)
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* See also reusable brand factories like `minLength`, `maxLength`, `trimmed`,
|
|
74
|
+
* `positive`, `between`, etc.
|
|
75
|
+
*
|
|
76
|
+
* ### Objects & Optional Fields
|
|
77
|
+
*
|
|
78
|
+
* ```ts
|
|
79
|
+
* const User = object({
|
|
80
|
+
* name: NonEmptyTrimmedString100,
|
|
81
|
+
* age: optional(PositiveInt),
|
|
82
|
+
* });
|
|
83
|
+
* type User = typeof User.Type;
|
|
27
84
|
*
|
|
28
|
-
*
|
|
85
|
+
* User.from({ name: "Alice" }); // ok
|
|
86
|
+
* User.from({ name: "Alice", age: -1 }); // err(PositiveInt)
|
|
87
|
+
* ```
|
|
29
88
|
*
|
|
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.
|
|
89
|
+
* ### Deriving JSON String Types
|
|
34
90
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
91
|
+
* ```ts
|
|
92
|
+
* const Person = object({
|
|
93
|
+
* name: NonEmptyString50,
|
|
94
|
+
* // Did you know that JSON.stringify converts NaN (a number) into null?
|
|
95
|
+
* // To prevent this, use FiniteNumber.
|
|
96
|
+
* age: FiniteNumber,
|
|
97
|
+
* });
|
|
98
|
+
* type Person = typeof Person.Type;
|
|
39
99
|
*
|
|
40
|
-
*
|
|
100
|
+
* const [PersonJson, personToPersonJson, personJsonToPerson] = json(
|
|
101
|
+
* Person,
|
|
102
|
+
* "PersonJson",
|
|
103
|
+
* );
|
|
104
|
+
* // string & Brand<"PersonJson">
|
|
105
|
+
* type PersonJson = typeof PersonJson.Type;
|
|
41
106
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
107
|
+
* const person = Person.orThrow({
|
|
108
|
+
* name: "Alice",
|
|
109
|
+
* age: 30,
|
|
110
|
+
* });
|
|
44
111
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
112
|
+
* const personJson = personToPersonJson(person);
|
|
113
|
+
* expect(personJsonToPerson(personJson)).toEqual(person);
|
|
114
|
+
* ```
|
|
48
115
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
116
|
+
* ### Error Formatting
|
|
117
|
+
*
|
|
118
|
+
* Evolu separates validation logic from human-readable messages. There are two
|
|
119
|
+
* layers:
|
|
120
|
+
*
|
|
121
|
+
* 1. Per-type formatters (e.g. `formatStringError`) – simple, focused, already
|
|
122
|
+
* used earlier in the quick start example.
|
|
123
|
+
* 2. A unified formatter via `createFormatTypeError` – composes all built-in and
|
|
124
|
+
* custom errors (including nested composite types) and lets us override
|
|
125
|
+
* selected messages.
|
|
126
|
+
*
|
|
127
|
+
* #### 1. Per-Type Formatter (recap)
|
|
128
|
+
*
|
|
129
|
+
* ```ts
|
|
130
|
+
* const r = String.fromUnknown(42);
|
|
131
|
+
* if (!r.ok) console.error(formatStringError(r.error));
|
|
132
|
+
* ```
|
|
133
|
+
*
|
|
134
|
+
* #### 2. Unified Formatter with Overrides
|
|
135
|
+
*
|
|
136
|
+
* ```ts
|
|
137
|
+
* // Override only what we care about; fall back to built-ins for the rest.
|
|
138
|
+
* const formatTypeError = createFormatTypeError((error) => {
|
|
139
|
+
* if (error.type === "MinLength") return `Min length is ${error.min}`;
|
|
140
|
+
* });
|
|
141
|
+
*
|
|
142
|
+
* const User = object({ name: NonEmptyTrimmedString100 });
|
|
143
|
+
* const resultUser = User.from({ name: "" });
|
|
144
|
+
* if (!resultUser.ok) console.error(formatTypeError(resultUser.error));
|
|
145
|
+
*
|
|
146
|
+
* const badPoint = object({ x: Number, y: Number }).from({
|
|
147
|
+
* x: 1,
|
|
148
|
+
* y: "foo",
|
|
149
|
+
* });
|
|
150
|
+
* if (!badPoint.ok) console.error(formatTypeError(badPoint.error));
|
|
151
|
+
* ```
|
|
51
152
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* `to` will not restore the original representation. You can also use
|
|
56
|
-
* {@link assert}: `assert(false, "Untrim is not possible")`.
|
|
153
|
+
* The unified formatter walks nested structures (object / array / record /
|
|
154
|
+
* tuple / union) and applies overrides only where specified, greatly reducing
|
|
155
|
+
* boilerplate when formatting complex validation errors.
|
|
57
156
|
*
|
|
58
157
|
* ### Tip
|
|
59
158
|
*
|
|
60
159
|
* If necessary, write `globalThis.String` instead of `String` to avoid naming
|
|
61
|
-
* clashes with
|
|
160
|
+
* clashes with native types.
|
|
62
161
|
*
|
|
63
|
-
* ### Design Decision:
|
|
162
|
+
* ### Design Decision: No Bidirectional Transformations
|
|
64
163
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
164
|
+
* Evolu Type intentionally does not support bidirectional transformations. It
|
|
165
|
+
* previously did, but supporting that while keeping typed error fidelity added
|
|
166
|
+
* complexity that hurt readability & reliability. Most persistence pipelines
|
|
167
|
+
* (e.g. SQLite) already require explicit mapping of query results, so implicit
|
|
168
|
+
* reverse transforms would not buy much. We may revisit this if we can design a
|
|
169
|
+
* minimal, 100% safe API that preserves simplicity.
|
|
69
170
|
*
|
|
70
171
|
* @module
|
|
71
172
|
*/
|
|
72
173
|
|
|
174
|
+
import { utf8ToBytes } from "@noble/ciphers/utils.js";
|
|
175
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
73
176
|
import * as bip39 from "@scure/bip39";
|
|
74
|
-
import { wordlist } from "@scure/bip39/wordlists/english";
|
|
75
|
-
import {
|
|
76
|
-
import {
|
|
77
|
-
import {
|
|
177
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
178
|
+
import { pack } from "msgpackr";
|
|
179
|
+
import type { Brand } from "./Brand.js";
|
|
180
|
+
import { type RandomBytesDep } from "./Crypto.js";
|
|
78
181
|
import { isPlainObject } from "./Object.js";
|
|
79
|
-
import {
|
|
182
|
+
import { err, getOrThrow, ok, Result, trySync } from "./Result.js";
|
|
80
183
|
import { safelyStringifyUnknownValue } from "./String.js";
|
|
81
|
-
import type {
|
|
184
|
+
import type { Literal, Simplify, WidenLiteral } from "./Types.js";
|
|
82
185
|
import { IntentionalNever } from "./Types.js";
|
|
83
186
|
|
|
84
187
|
export interface Type<
|
|
@@ -106,41 +209,60 @@ export interface Type<
|
|
|
106
209
|
readonly from: (value: Input) => Result<T, ParentError | Error>;
|
|
107
210
|
|
|
108
211
|
/**
|
|
109
|
-
* Creates `T` from an
|
|
212
|
+
* Creates `T` from an `Input` value, throwing an error if validation fails.
|
|
110
213
|
*
|
|
111
|
-
* This is
|
|
112
|
-
*/
|
|
113
|
-
readonly fromUnknown: (value: unknown) => Result<T, ParentError | Error>;
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* The opposite of `from` and `fromUnknown`.
|
|
214
|
+
* This is a convenience method that combines `from` with `getOrThrow`.
|
|
117
215
|
*
|
|
118
|
-
*
|
|
216
|
+
* **When to use:**
|
|
119
217
|
*
|
|
120
|
-
*
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
*
|
|
218
|
+
* - Configuration values that are guaranteed to be valid (e.g., hardcoded
|
|
219
|
+
* constants)
|
|
220
|
+
* - Application startup where failure should crash the program
|
|
221
|
+
* - Test code with known valid inputs
|
|
222
|
+
* - Converting from trusted sources where validation failure indicates a
|
|
223
|
+
* programming error
|
|
126
224
|
*
|
|
127
|
-
*
|
|
128
|
-
* already partially validated/transformed value.
|
|
225
|
+
* **When NOT to use:**
|
|
129
226
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
227
|
+
* - User input validation - use `from` and handle errors gracefully
|
|
228
|
+
* - Data from external APIs or files - use `from` for proper error handling
|
|
229
|
+
* - Library code that should return Results rather than throw
|
|
132
230
|
*
|
|
133
231
|
* ### Example
|
|
134
232
|
*
|
|
135
233
|
* ```ts
|
|
136
|
-
* //
|
|
137
|
-
* const
|
|
234
|
+
* // ✅ Good: Known valid constant
|
|
235
|
+
* const maxRetries = PositiveInt.orThrow(3);
|
|
236
|
+
*
|
|
237
|
+
* // ✅ Good: App configuration that should crash on invalid values
|
|
238
|
+
* const appName = SimpleName.orThrow("MyApp");
|
|
239
|
+
*
|
|
240
|
+
* // ❌ Avoid: User input (use `from` instead)
|
|
241
|
+
* const userAge = PositiveInt.orThrow(userInput); // Could crash!
|
|
242
|
+
*
|
|
243
|
+
* // ✅ Better: Handle user input gracefully
|
|
244
|
+
* const ageResult = PositiveInt.from(userInput);
|
|
245
|
+
* if (!ageResult.ok) {
|
|
246
|
+
* // Handle validation error
|
|
247
|
+
* }
|
|
138
248
|
* ```
|
|
139
249
|
*/
|
|
140
|
-
readonly
|
|
250
|
+
readonly orThrow: (value: Input) => T;
|
|
141
251
|
|
|
142
|
-
/**
|
|
143
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Creates `T` from an unknown value.
|
|
254
|
+
*
|
|
255
|
+
* This is useful when a value is unknown.
|
|
256
|
+
*/
|
|
257
|
+
readonly fromUnknown: (value: unknown) => Result<T, ParentError | Error>;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Creates `T` from `Parent` type.
|
|
261
|
+
*
|
|
262
|
+
* This function skips parent Types validations when we have already partially
|
|
263
|
+
* validated value.
|
|
264
|
+
*/
|
|
265
|
+
readonly fromParent: (value: Parent) => Result<T, Error>;
|
|
144
266
|
|
|
145
267
|
/**
|
|
146
268
|
* A **type guard** that checks whether an unknown value satisfies the
|
|
@@ -261,26 +383,61 @@ export interface TypeErrorWithReason<
|
|
|
261
383
|
|
|
262
384
|
export type AnyType = Type<any, any, any, any, any, any>;
|
|
263
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Extracts the name from a {@link Type}.
|
|
388
|
+
*
|
|
389
|
+
* @category Utilities
|
|
390
|
+
*/
|
|
264
391
|
export type InferName<A extends AnyType> =
|
|
265
392
|
A extends Type<infer Name, any, any, any, any, any> ? Name : never;
|
|
266
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Extracts the type from a {@link Type}.
|
|
396
|
+
*
|
|
397
|
+
* @category Utilities
|
|
398
|
+
*/
|
|
267
399
|
export type InferType<A extends AnyType> =
|
|
268
400
|
A extends Type<any, infer T, any, any, any, any> ? T : never;
|
|
269
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Extracts the input type from a {@link Type}.
|
|
404
|
+
*
|
|
405
|
+
* @category Utilities
|
|
406
|
+
*/
|
|
270
407
|
export type InferInput<A extends AnyType> =
|
|
271
408
|
A extends Type<any, any, infer Input, any, any, any> ? Input : never;
|
|
272
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Extracts the specific error type from a {@link Type}.
|
|
412
|
+
*
|
|
413
|
+
* @category Utilities
|
|
414
|
+
*/
|
|
273
415
|
export type InferError<A extends AnyType> =
|
|
274
416
|
A extends Type<any, any, any, infer Error, any, any> ? Error : never;
|
|
275
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Extracts the parent type from a {@link Type}.
|
|
420
|
+
*
|
|
421
|
+
* @category Utilities
|
|
422
|
+
*/
|
|
276
423
|
export type InferParent<A extends AnyType> =
|
|
277
424
|
A extends Type<any, any, any, any, infer Parent, any> ? Parent : never;
|
|
278
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Extracts the parent error type from a {@link Type}.
|
|
428
|
+
*
|
|
429
|
+
* @category Utilities
|
|
430
|
+
*/
|
|
279
431
|
export type InferParentError<A extends AnyType> =
|
|
280
432
|
A extends Type<any, any, any, any, any, infer ParentError>
|
|
281
433
|
? ParentError
|
|
282
434
|
: never;
|
|
283
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Extracts all error types (Error | ParentError) from a {@link Type}.
|
|
438
|
+
*
|
|
439
|
+
* @category Utilities
|
|
440
|
+
*/
|
|
284
441
|
export type InferErrors<T extends AnyType> =
|
|
285
442
|
T extends Type<any, any, any, infer Error, any, infer ParentError>
|
|
286
443
|
? Error | ParentError
|
|
@@ -310,6 +467,7 @@ const createType = <
|
|
|
310
467
|
| "name"
|
|
311
468
|
| "is"
|
|
312
469
|
| "from"
|
|
470
|
+
| "orThrow"
|
|
313
471
|
| typeof EvoluTypeSymbol
|
|
314
472
|
| "Type"
|
|
315
473
|
| "Input"
|
|
@@ -323,6 +481,7 @@ const createType = <
|
|
|
323
481
|
name,
|
|
324
482
|
is: (value: unknown): value is T => definition.fromUnknown(value).ok,
|
|
325
483
|
from: definition.fromUnknown,
|
|
484
|
+
orThrow: (value: Input): T => getOrThrow(definition.fromUnknown(value)),
|
|
326
485
|
[EvoluTypeSymbol]: true,
|
|
327
486
|
Type: undefined as unknown as T,
|
|
328
487
|
Input: undefined as unknown as Input,
|
|
@@ -365,12 +524,6 @@ export type TypeErrorFormatter<Error extends TypeError> = (
|
|
|
365
524
|
* Base {@link Type}.
|
|
366
525
|
*
|
|
367
526
|
* 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
527
|
*
|
|
375
528
|
* ### Example
|
|
376
529
|
*
|
|
@@ -396,9 +549,7 @@ export const base = <Name extends TypeName, T, Error extends TypeError>(
|
|
|
396
549
|
): Type<Name, T, T, Error> =>
|
|
397
550
|
createType(name, {
|
|
398
551
|
fromUnknown,
|
|
399
|
-
to: identity,
|
|
400
552
|
fromParent: ok<T>, // `fromParent` relies on types, so it can't fail for the Base Type
|
|
401
|
-
toParent: identity,
|
|
402
553
|
});
|
|
403
554
|
|
|
404
555
|
/**
|
|
@@ -607,7 +758,7 @@ export const formatIsTypeError = createTypeErrorFormatter<EvoluTypeError>(
|
|
|
607
758
|
* The `brand` Type Factory takes the name of a new {@link Brand}, a parent Type
|
|
608
759
|
* to be branded, and the optional `refine` function for additional constraint.
|
|
609
760
|
*
|
|
610
|
-
*
|
|
761
|
+
* The `refine` function can be omitted if we only want to add a brand.
|
|
611
762
|
*
|
|
612
763
|
* ### Examples
|
|
613
764
|
*
|
|
@@ -686,7 +837,7 @@ export const formatIsTypeError = createTypeErrorFormatter<EvoluTypeError>(
|
|
|
686
837
|
* confirmPassword: SimplePassword,
|
|
687
838
|
* });
|
|
688
839
|
*
|
|
689
|
-
* const ValidForm = brand("
|
|
840
|
+
* const ValidForm = brand("ValidForm", Form, (value) => {
|
|
690
841
|
* if (value.password !== value.confirmPassword)
|
|
691
842
|
* return err<ValidFormError>({
|
|
692
843
|
* type: "ValidForm",
|
|
@@ -780,9 +931,7 @@ export function brand<
|
|
|
780
931
|
return {
|
|
781
932
|
...createType("Brand", {
|
|
782
933
|
fromUnknown,
|
|
783
|
-
to: identity,
|
|
784
934
|
fromParent: (refine ?? ok) as IntentionalNever,
|
|
785
|
-
toParent: identity,
|
|
786
935
|
}),
|
|
787
936
|
brand: name,
|
|
788
937
|
parentType: parent,
|
|
@@ -848,30 +997,41 @@ export const formatCurrencyCodeError =
|
|
|
848
997
|
* ### Example
|
|
849
998
|
*
|
|
850
999
|
* ```ts
|
|
851
|
-
* const result =
|
|
852
|
-
* const error =
|
|
1000
|
+
* const result = DateIso.from("2023-01-01T12:00:00.000Z"); // ok
|
|
1001
|
+
* const error = DateIso.from("10000-01-01T00:00:00.000Z"); // err
|
|
853
1002
|
* ```
|
|
854
1003
|
*
|
|
855
1004
|
* @category String
|
|
856
1005
|
*/
|
|
857
|
-
export const
|
|
1006
|
+
export const DateIso = brand("DateIso", String, (value) => {
|
|
858
1007
|
if (value.length !== 24) {
|
|
859
|
-
return err<
|
|
1008
|
+
return err<DateIsoError>({ type: "DateIso", value });
|
|
1009
|
+
}
|
|
1010
|
+
const parsed = globalThis.Date.parse(value);
|
|
1011
|
+
if (isNaN(parsed)) {
|
|
1012
|
+
return err<DateIsoError>({ type: "DateIso", value });
|
|
860
1013
|
}
|
|
861
|
-
|
|
862
|
-
|
|
1014
|
+
// Round-trip test: ensure the string is actually a proper ISO format
|
|
1015
|
+
const roundTrip = new globalThis.Date(parsed).toISOString();
|
|
1016
|
+
if (roundTrip !== value) {
|
|
1017
|
+
return err<DateIsoError>({ type: "DateIso", value });
|
|
863
1018
|
}
|
|
864
1019
|
return ok(value);
|
|
865
1020
|
});
|
|
866
1021
|
|
|
867
|
-
export type
|
|
1022
|
+
export type DateIso = typeof DateIso.Type;
|
|
868
1023
|
|
|
869
|
-
export interface
|
|
1024
|
+
export interface DateIsoError extends TypeError<"DateIso"> {}
|
|
870
1025
|
|
|
871
|
-
export const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1026
|
+
export const formatDateIsoError = createTypeErrorFormatter<DateIsoError>(
|
|
1027
|
+
(error) => `The value ${error.value} is not a valid ISO 8601 date string.`,
|
|
1028
|
+
);
|
|
1029
|
+
|
|
1030
|
+
export const dateToDateIso = (value: Date): Result<DateIso, DateIsoError> =>
|
|
1031
|
+
DateIso.fromParent(value.toISOString());
|
|
1032
|
+
|
|
1033
|
+
export const dateIsoToDate = (value: DateIso): Date =>
|
|
1034
|
+
new globalThis.Date(value);
|
|
875
1035
|
|
|
876
1036
|
/**
|
|
877
1037
|
* Helper type for Type Factory that creates a branded Type.
|
|
@@ -914,18 +1074,12 @@ export type BrandFactory<
|
|
|
914
1074
|
/**
|
|
915
1075
|
* Trimmed string.
|
|
916
1076
|
*
|
|
917
|
-
* This Type Factory
|
|
918
|
-
*
|
|
919
|
-
* Factory.
|
|
1077
|
+
* This Type Factory validates whether a string has no leading or trailing
|
|
1078
|
+
* whitespaces.
|
|
920
1079
|
*
|
|
921
|
-
* ###
|
|
1080
|
+
* ### Example
|
|
922
1081
|
*
|
|
923
1082
|
* ```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
1083
|
* const TrimmedNonEmptyString = trimmed(minLength(1)(String));
|
|
930
1084
|
* // string & Brand<"MinLength1"> & Brand<"Trimmed">
|
|
931
1085
|
* type TrimmedNonEmptyString = typeof TrimmedNonEmptyString.Type;
|
|
@@ -948,64 +1102,6 @@ export const formatTrimmedError = createTypeErrorFormatter<TrimmedError>(
|
|
|
948
1102
|
(error) => `A value ${error.value} is not trimmed`,
|
|
949
1103
|
);
|
|
950
1104
|
|
|
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
1105
|
/**
|
|
1010
1106
|
* Trimmed string
|
|
1011
1107
|
*
|
|
@@ -1017,6 +1113,9 @@ export const trim: TransformBrandFactory<"Trimmed", string> = (parent) =>
|
|
|
1017
1113
|
export const TrimmedString = trimmed(String);
|
|
1018
1114
|
export type TrimmedString = typeof TrimmedString.Type;
|
|
1019
1115
|
|
|
1116
|
+
export const trim = (value: string): TrimmedString =>
|
|
1117
|
+
value.trim() as TrimmedString;
|
|
1118
|
+
|
|
1020
1119
|
/**
|
|
1021
1120
|
* Minimum length.
|
|
1022
1121
|
*
|
|
@@ -1213,9 +1312,9 @@ export const formatRegexError = createTypeErrorFormatter<RegexError>(
|
|
|
1213
1312
|
);
|
|
1214
1313
|
|
|
1215
1314
|
/**
|
|
1216
|
-
* URL-safe
|
|
1315
|
+
* URL-safe string.
|
|
1217
1316
|
*
|
|
1218
|
-
* A `
|
|
1317
|
+
* A `UrlSafeString` uses a limited alphabet that is safe for URLs:
|
|
1219
1318
|
*
|
|
1220
1319
|
* - Uppercase letters (`A-Z`)
|
|
1221
1320
|
* - Lowercase letters (`a-z`)
|
|
@@ -1223,35 +1322,116 @@ export const formatRegexError = createTypeErrorFormatter<RegexError>(
|
|
|
1223
1322
|
* - Dash (`-`)
|
|
1224
1323
|
* - Underscore (`_`)
|
|
1225
1324
|
*
|
|
1325
|
+
* This is the same character set used by Base64Url encoding, but this type does
|
|
1326
|
+
* not validate that the string is actually Base64Url-encoded data.
|
|
1327
|
+
*
|
|
1226
1328
|
* ### Example
|
|
1227
1329
|
*
|
|
1228
1330
|
* ```ts
|
|
1229
|
-
* const result =
|
|
1331
|
+
* const result = UrlSafeString.from("abc123_-");
|
|
1230
1332
|
* if (result.ok) {
|
|
1231
|
-
* console.log("Valid
|
|
1333
|
+
* console.log("Valid URL-safe string:", result.value);
|
|
1232
1334
|
* } else {
|
|
1233
|
-
* console.error("Invalid
|
|
1335
|
+
* console.error("Invalid URL-safe string:", result.error);
|
|
1234
1336
|
* }
|
|
1235
1337
|
* ```
|
|
1236
1338
|
*
|
|
1237
1339
|
* @category String
|
|
1238
1340
|
*/
|
|
1239
|
-
export const
|
|
1341
|
+
export const UrlSafeString = regex("UrlSafeString", /^[A-Za-z0-9_-]+$/)(String);
|
|
1342
|
+
export type UrlSafeString = typeof UrlSafeString.Type;
|
|
1343
|
+
export type UrlSafeStringError = typeof UrlSafeString.Error;
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* Base64Url without padding.
|
|
1347
|
+
*
|
|
1348
|
+
* Encode with {@link uint8ArrayToBase64Url}, decode with
|
|
1349
|
+
* {@link base64UrlToUint8Array}.
|
|
1350
|
+
*
|
|
1351
|
+
* @category String
|
|
1352
|
+
*/
|
|
1353
|
+
export const Base64Url = brand(
|
|
1240
1354
|
"Base64Url",
|
|
1241
|
-
|
|
1242
|
-
|
|
1355
|
+
String,
|
|
1356
|
+
(value: string): Result<string, Base64UrlError> => {
|
|
1357
|
+
// Round-trip validation ensures consistency across different base64url
|
|
1358
|
+
// implementations (Node.js Buffer, native browser API, manual fallback).
|
|
1359
|
+
// Only strings that decode and encode identically are accepted.
|
|
1360
|
+
let roundTrip;
|
|
1361
|
+
try {
|
|
1362
|
+
roundTrip = uint8ArrayToBase64Url(
|
|
1363
|
+
base64UrlToUint8Array(value as Base64Url),
|
|
1364
|
+
);
|
|
1365
|
+
} catch {
|
|
1366
|
+
//
|
|
1367
|
+
}
|
|
1368
|
+
return roundTrip === value
|
|
1369
|
+
? ok(value)
|
|
1370
|
+
: err<Base64UrlError>({ type: "Base64Url", value });
|
|
1371
|
+
},
|
|
1372
|
+
);
|
|
1243
1373
|
export type Base64Url = typeof Base64Url.Type;
|
|
1244
|
-
export
|
|
1374
|
+
export interface Base64UrlError extends TypeError<"Base64Url"> {}
|
|
1375
|
+
|
|
1376
|
+
export const formatBase64UrlError = createTypeErrorFormatter<Base64UrlError>(
|
|
1377
|
+
(error) => `Value ${error.value} is not a valid Base64Url string.`,
|
|
1378
|
+
);
|
|
1379
|
+
|
|
1380
|
+
const hasNodeBuffer = typeof globalThis.Buffer !== "undefined";
|
|
1381
|
+
const base64UrlOptions = { alphabet: "base64url", omitPadding: true };
|
|
1382
|
+
|
|
1383
|
+
/** Encodes a Uint8Array to a {@link Base64Url} string. */
|
|
1384
|
+
export const uint8ArrayToBase64Url: (bytes: Uint8Array) => Base64Url =
|
|
1385
|
+
hasNodeBuffer
|
|
1386
|
+
? (bytes: Uint8Array) =>
|
|
1387
|
+
globalThis.Buffer.from(bytes).toString("base64url") as Base64Url
|
|
1388
|
+
: // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
1389
|
+
typeof (globalThis.Uint8Array.prototype as any)?.toBase64 !== "undefined"
|
|
1390
|
+
? (bytes: Uint8Array) =>
|
|
1391
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
1392
|
+
(bytes as any).toBase64(base64UrlOptions) as Base64Url
|
|
1393
|
+
: (bytes: Uint8Array) => {
|
|
1394
|
+
const binaryString = Array.from(bytes, (byte) =>
|
|
1395
|
+
globalThis.String.fromCodePoint(byte),
|
|
1396
|
+
).join("");
|
|
1397
|
+
const base64 = globalThis.btoa(binaryString);
|
|
1398
|
+
return base64
|
|
1399
|
+
.replace(/\+/g, "-")
|
|
1400
|
+
.replace(/\//g, "_")
|
|
1401
|
+
.replace(/=/g, "") as Base64Url;
|
|
1402
|
+
};
|
|
1403
|
+
|
|
1404
|
+
/** Decodes a {@link Base64Url} string to a Uint8Array. */
|
|
1405
|
+
export const base64UrlToUint8Array: (str: Base64Url) => Uint8Array =
|
|
1406
|
+
hasNodeBuffer
|
|
1407
|
+
? (str: Base64Url) => {
|
|
1408
|
+
const nodeBuffer = globalThis.Buffer.from(str, "base64url");
|
|
1409
|
+
return new globalThis.Uint8Array(nodeBuffer);
|
|
1410
|
+
}
|
|
1411
|
+
: // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
1412
|
+
typeof (globalThis.Uint8Array as any)?.fromBase64 !== "undefined"
|
|
1413
|
+
? (str: Base64Url) =>
|
|
1414
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
1415
|
+
(globalThis.Uint8Array as any).fromBase64(
|
|
1416
|
+
str,
|
|
1417
|
+
base64UrlOptions,
|
|
1418
|
+
) as Uint8Array
|
|
1419
|
+
: (str: Base64Url) => {
|
|
1420
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
1421
|
+
while (base64.length % 4 !== 0) {
|
|
1422
|
+
base64 += "=";
|
|
1423
|
+
}
|
|
1424
|
+
const binaryString = globalThis.atob(base64);
|
|
1425
|
+
return globalThis.Uint8Array.from(binaryString, (c) =>
|
|
1426
|
+
c.charCodeAt(0),
|
|
1427
|
+
);
|
|
1428
|
+
};
|
|
1245
1429
|
|
|
1246
1430
|
/**
|
|
1247
|
-
* Simple alphanumeric string for naming.
|
|
1248
|
-
*
|
|
1249
|
-
* A `SimpleName` string uses a limited, safe alphabet for naming purposes:
|
|
1431
|
+
* Simple alphanumeric string for naming in file systems, URLs, and identifiers.
|
|
1250
1432
|
*
|
|
1251
|
-
*
|
|
1252
|
-
*
|
|
1253
|
-
* - Digits (`0-9`)
|
|
1254
|
-
* - Dash (`-`)
|
|
1433
|
+
* Uses the same safe alphabet as {@link UrlSafeString} (letters, digits, `-`,
|
|
1434
|
+
* `_`). See `UrlSafeString` for details.
|
|
1255
1435
|
*
|
|
1256
1436
|
* The string must be between 1 and 42 characters.
|
|
1257
1437
|
*
|
|
@@ -1268,18 +1448,13 @@ export type Base64UrlError = typeof Base64Url.Error;
|
|
|
1268
1448
|
*
|
|
1269
1449
|
* @category String
|
|
1270
1450
|
*/
|
|
1271
|
-
export const SimpleName =
|
|
1451
|
+
export const SimpleName = brand("SimpleName", UrlSafeString, (value) =>
|
|
1452
|
+
value.length >= 1 && value.length <= 42
|
|
1453
|
+
? ok(value)
|
|
1454
|
+
: err<SimpleNameError>({ type: "SimpleName", value }),
|
|
1455
|
+
);
|
|
1272
1456
|
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;
|
|
1457
|
+
export interface SimpleNameError extends TypeError<"SimpleName"> {}
|
|
1283
1458
|
|
|
1284
1459
|
/**
|
|
1285
1460
|
* Trimmed string between 8 and 64 characters, branded as `SimplePassword`.
|
|
@@ -1304,17 +1479,56 @@ export const formatSimplePasswordError = (
|
|
|
1304
1479
|
);
|
|
1305
1480
|
|
|
1306
1481
|
/**
|
|
1307
|
-
*
|
|
1482
|
+
* Globally unique identifier.
|
|
1308
1483
|
*
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1484
|
+
* **Evolu Id** is 16 random bytes from a cryptographically secure random
|
|
1485
|
+
* generator, encoded as 22-character Base64Url string. This provides strong
|
|
1486
|
+
* collision resistance for distributed ID generation.
|
|
1487
|
+
*
|
|
1488
|
+
* ### Design Rationale
|
|
1489
|
+
*
|
|
1490
|
+
* Why Evolu Id over alternatives:
|
|
1491
|
+
*
|
|
1492
|
+
* - **NanoID**: No standard binary serialization format, and uses only ~126 bits
|
|
1493
|
+
* of entropy (21 characters from 64-symbol alphabet) compared to Evolu Id's
|
|
1494
|
+
* 128 bits.
|
|
1495
|
+
* - **UUID (v4)**: String format is 36 characters (with hyphens) compared to
|
|
1496
|
+
* Evolu Id's 22 characters. While UUIDs can be stored as 16 bytes, their
|
|
1497
|
+
* standard string representation is verbose.
|
|
1498
|
+
* - **UUID v7**: Includes timestamp in the ID, which leaks information about when
|
|
1499
|
+
* data was created. This is a privacy concern for local-first applications
|
|
1500
|
+
* where creation time must remain private.
|
|
1501
|
+
*
|
|
1502
|
+
* Evolu Id provides 128 bits of entropy, compact string representation (22
|
|
1503
|
+
* characters), standard and native string serialization (Base64Url), and no
|
|
1504
|
+
* privacy leaks.
|
|
1505
|
+
*
|
|
1506
|
+
* ### Future Consideration
|
|
1507
|
+
*
|
|
1508
|
+
* For database-heavy workloads where insert performance is critical, a hybrid
|
|
1509
|
+
* approach could be considered: `timestamp ^ H(cluster_id, timestamp >> N)`
|
|
1510
|
+
* where H is a keyed hash function and N is a configurable parameter. This
|
|
1511
|
+
* would maintain spatial locality for database caches (improving insert
|
|
1512
|
+
* performance by an order of magnitude) while adding entropy to prevent
|
|
1513
|
+
* timestamp leakage and correlation across systems. The parameter N would allow
|
|
1514
|
+
* trading off cache locality (larger N = better locality) versus entropy
|
|
1515
|
+
* distribution. See https://brooker.co.za/blog/2025/10/22/uuidv7.html for
|
|
1516
|
+
* details on this approach.
|
|
1311
1517
|
*
|
|
1312
1518
|
* @category String
|
|
1313
1519
|
*/
|
|
1314
|
-
export const Id =
|
|
1520
|
+
export const Id = brand("Id", String, (value) =>
|
|
1521
|
+
value.length === 22 && Base64Url.fromParent(value).ok
|
|
1522
|
+
? ok(value)
|
|
1523
|
+
: err<IdError>({ type: "Id", value }),
|
|
1524
|
+
);
|
|
1315
1525
|
export type Id = typeof Id.Type;
|
|
1316
1526
|
|
|
1317
|
-
export
|
|
1527
|
+
export interface IdError extends TypeError<"Id"> {}
|
|
1528
|
+
|
|
1529
|
+
export const formatIdError = createTypeErrorFormatter<IdError>(
|
|
1530
|
+
(error) => `Value ${error.value} is not a valid Id.`,
|
|
1531
|
+
);
|
|
1318
1532
|
|
|
1319
1533
|
/**
|
|
1320
1534
|
* Creates an {@link Id}.
|
|
@@ -1324,13 +1538,65 @@ export const idTypeValueLength = 21;
|
|
|
1324
1538
|
* ```ts
|
|
1325
1539
|
* // string & Brand<"Id">
|
|
1326
1540
|
* const id = createId(deps);
|
|
1541
|
+
*
|
|
1542
|
+
* // string & Brand<"Id"> & Brand<"Todo">
|
|
1543
|
+
* const todoId = createId<"Todo">(deps);
|
|
1327
1544
|
* ```
|
|
1328
1545
|
*/
|
|
1329
|
-
export const createId =
|
|
1330
|
-
deps
|
|
1546
|
+
export const createId = <B extends string = never>(
|
|
1547
|
+
deps: RandomBytesDep,
|
|
1548
|
+
): [B] extends [never] ? Id : Id & Brand<B> =>
|
|
1549
|
+
uint8ArrayToBase64Url(deps.randomBytes.create(16)) as unknown as [B] extends [
|
|
1550
|
+
never,
|
|
1551
|
+
]
|
|
1552
|
+
? Id
|
|
1553
|
+
: Id & Brand<B>;
|
|
1331
1554
|
|
|
1332
1555
|
/**
|
|
1333
|
-
*
|
|
1556
|
+
* Creates an {@link Id} from a string using SHA-256.
|
|
1557
|
+
*
|
|
1558
|
+
* When integrating with external systems that use different ID formats, use
|
|
1559
|
+
* this function to convert external IDs into valid Evolu IDs.
|
|
1560
|
+
*
|
|
1561
|
+
* In Evolu's CRDT, the ID serves as the unique identifier for conflict
|
|
1562
|
+
* resolution across distributed clients. When multiple clients create records
|
|
1563
|
+
* with the same external identifier, they must resolve to the same Evolu ID to
|
|
1564
|
+
* ensure data consistency.
|
|
1565
|
+
*
|
|
1566
|
+
* ### Example
|
|
1567
|
+
*
|
|
1568
|
+
* ```ts
|
|
1569
|
+
* // Both clients will generate the same ID
|
|
1570
|
+
* const id1 = createIdFromString("user-api-123");
|
|
1571
|
+
* const id2 = createIdFromString("user-api-123");
|
|
1572
|
+
* console.log(id1 === id2); // true
|
|
1573
|
+
*
|
|
1574
|
+
* upsert("todo", {
|
|
1575
|
+
* id: createIdFromString("external-todo-456"),
|
|
1576
|
+
* title: "Synced from external system",
|
|
1577
|
+
* });
|
|
1578
|
+
* ```
|
|
1579
|
+
*
|
|
1580
|
+
* **Important**: This transformation is one-way. We cannot recover the original
|
|
1581
|
+
* external string from the generated {@link Id}. If we need to preserve the
|
|
1582
|
+
* original external ID, store it in a separate column.
|
|
1583
|
+
*
|
|
1584
|
+
* @category String
|
|
1585
|
+
*/
|
|
1586
|
+
export const createIdFromString = <B extends string = never>(
|
|
1587
|
+
value: string,
|
|
1588
|
+
): [B] extends [never] ? Id : Id & Brand<B> => {
|
|
1589
|
+
const hash = sha256(utf8ToBytes(value));
|
|
1590
|
+
// Take first 16 bytes of hash and convert to Id
|
|
1591
|
+
const id = idBytesToId(hash.slice(0, 16) as IdBytes);
|
|
1592
|
+
|
|
1593
|
+
return id as [B] extends [never] ? Id : Id & Brand<B>;
|
|
1594
|
+
};
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* Creates a branded {@link Id} Type for a table's primary key.
|
|
1598
|
+
*
|
|
1599
|
+
* The table name becomes an additional brand for type safety.
|
|
1334
1600
|
*
|
|
1335
1601
|
* ### Example
|
|
1336
1602
|
*
|
|
@@ -1342,53 +1608,62 @@ export const createId = (deps: NanoIdLibDep): Id =>
|
|
|
1342
1608
|
*
|
|
1343
1609
|
* @category String
|
|
1344
1610
|
*/
|
|
1345
|
-
export const id = <Table extends TypeName>(table: Table):
|
|
1611
|
+
export const id = <Table extends TypeName>(table: Table): TableId<Table> => {
|
|
1612
|
+
const fromUnknown = (value: unknown) => {
|
|
1613
|
+
const parentResult = String.fromUnknown(value);
|
|
1614
|
+
if (!parentResult.ok) return parentResult;
|
|
1615
|
+
return fromParent(parentResult.value);
|
|
1616
|
+
};
|
|
1617
|
+
|
|
1346
1618
|
const fromParent = (value: string) => {
|
|
1347
1619
|
const idResult = Id.fromParent(value);
|
|
1348
1620
|
if (!idResult.ok) {
|
|
1349
|
-
return err<
|
|
1621
|
+
return err<TableIdError<Table>>({ type: "TableId", value, table });
|
|
1350
1622
|
}
|
|
1351
1623
|
return ok(idResult.value as Id & Brand<Table>);
|
|
1352
1624
|
};
|
|
1353
1625
|
|
|
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
1626
|
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
|
-
}),
|
|
1627
|
+
...createType("Id", { fromUnknown, fromParent }),
|
|
1367
1628
|
table,
|
|
1368
1629
|
};
|
|
1369
1630
|
};
|
|
1370
1631
|
|
|
1371
|
-
export interface
|
|
1632
|
+
export interface TableId<Table extends TypeName>
|
|
1372
1633
|
extends Type<
|
|
1373
1634
|
"Id",
|
|
1374
1635
|
string & Brand<"Id"> & Brand<Table>,
|
|
1375
1636
|
string,
|
|
1376
|
-
|
|
1637
|
+
TableIdError<Table>,
|
|
1377
1638
|
string,
|
|
1378
1639
|
StringError
|
|
1379
1640
|
> {
|
|
1380
1641
|
table: Table;
|
|
1381
1642
|
}
|
|
1382
1643
|
|
|
1383
|
-
export interface
|
|
1384
|
-
extends TypeError<"
|
|
1644
|
+
export interface TableIdError<Table extends TypeName = TypeName>
|
|
1645
|
+
extends TypeError<"TableId"> {
|
|
1385
1646
|
readonly table: Table;
|
|
1386
1647
|
}
|
|
1387
1648
|
|
|
1388
|
-
export const
|
|
1649
|
+
export const formatTableIdError = createTypeErrorFormatter<TableIdError>(
|
|
1389
1650
|
(error) => `Invalid ${error.type} table Id: ${error.value}`,
|
|
1390
1651
|
);
|
|
1391
1652
|
|
|
1653
|
+
/** Binary representation of an {@link Id}. */
|
|
1654
|
+
export const IdBytes = brand("IdBytes", length(16)(Uint8Array));
|
|
1655
|
+
export type IdBytes = typeof IdBytes.Type;
|
|
1656
|
+
|
|
1657
|
+
export const idBytesTypeValueLength = 16 as NonNegativeInt;
|
|
1658
|
+
|
|
1659
|
+
export const idToIdBytes = (id: Id): IdBytes =>
|
|
1660
|
+
// Id is Base64Url (validated by Id.from), cast is safe
|
|
1661
|
+
base64UrlToUint8Array(id as unknown as Base64Url) as IdBytes;
|
|
1662
|
+
|
|
1663
|
+
export const idBytesToId = (idBytes: IdBytes): Id =>
|
|
1664
|
+
// Base64Url encoding of 16 bytes always produces valid Id (22 chars)
|
|
1665
|
+
uint8ArrayToBase64Url(idBytes) as unknown as Id;
|
|
1666
|
+
|
|
1392
1667
|
/**
|
|
1393
1668
|
* Positive number.
|
|
1394
1669
|
*
|
|
@@ -1554,6 +1829,11 @@ export type NonNegativeInt = typeof NonNegativeInt.Type;
|
|
|
1554
1829
|
export const PositiveInt = positive(NonNegativeInt);
|
|
1555
1830
|
export type PositiveInt = typeof PositiveInt.Type;
|
|
1556
1831
|
|
|
1832
|
+
/** Maximum safe positive integer value for practically infinite operations. */
|
|
1833
|
+
export const maxPositiveInt = PositiveInt.orThrow(
|
|
1834
|
+
globalThis.Number.MAX_SAFE_INTEGER,
|
|
1835
|
+
);
|
|
1836
|
+
|
|
1557
1837
|
/** @category Number */
|
|
1558
1838
|
export const NonPositiveInt = nonPositive(Int);
|
|
1559
1839
|
export type NonPositiveInt = typeof NonPositiveInt.Type;
|
|
@@ -1778,10 +2058,6 @@ export const formatBetweenError = createTypeErrorFormatter<BetweenError>(
|
|
|
1778
2058
|
`The value ${error.value} is not between ${error.min} and ${error.max}, inclusive.`,
|
|
1779
2059
|
);
|
|
1780
2060
|
|
|
1781
|
-
/** @category Number */
|
|
1782
|
-
export const Between1And10 = between(1, 10)(Number);
|
|
1783
|
-
export type Between1And10 = typeof Between1And10.Type;
|
|
1784
|
-
|
|
1785
2061
|
/**
|
|
1786
2062
|
* Literal {@link Type}.
|
|
1787
2063
|
*
|
|
@@ -1808,12 +2084,10 @@ export const literal = <T extends Literal>(expected: T): LiteralType<T> => {
|
|
|
1808
2084
|
return {
|
|
1809
2085
|
...createType("Literal", {
|
|
1810
2086
|
fromUnknown,
|
|
1811
|
-
to: identity as (value: T) => WidenLiteral<T>,
|
|
1812
2087
|
fromParent: ok,
|
|
1813
|
-
toParent: identity,
|
|
1814
2088
|
}),
|
|
1815
2089
|
expected,
|
|
1816
|
-
}
|
|
2090
|
+
} as LiteralType<T>;
|
|
1817
2091
|
};
|
|
1818
2092
|
|
|
1819
2093
|
export interface LiteralType<T extends Literal>
|
|
@@ -1833,141 +2107,6 @@ export const formatLiteralError = createTypeErrorFormatter<LiteralError>(
|
|
|
1833
2107
|
)}.`,
|
|
1834
2108
|
);
|
|
1835
2109
|
|
|
1836
|
-
/**
|
|
1837
|
-
* {@link Type} that transforms values between `FromType` and `ToType`.
|
|
1838
|
-
*
|
|
1839
|
-
* - `fromParent`: Converts `FromType` to `ToType`, may fail.
|
|
1840
|
-
* - `toParent`: Converts `ToType` back to `FromType`, must not fail.
|
|
1841
|
-
*
|
|
1842
|
-
* ### Example
|
|
1843
|
-
*
|
|
1844
|
-
* // TODO: Examples
|
|
1845
|
-
*
|
|
1846
|
-
* @category Base Factories
|
|
1847
|
-
*/
|
|
1848
|
-
export const transform = <
|
|
1849
|
-
FromType extends AnyType,
|
|
1850
|
-
ToType extends AnyType,
|
|
1851
|
-
TransformError extends TypeError = never,
|
|
1852
|
-
>(
|
|
1853
|
-
fromType: FromType,
|
|
1854
|
-
toType: ToType,
|
|
1855
|
-
fromParent: (
|
|
1856
|
-
parentValue: InferType<FromType>,
|
|
1857
|
-
) => Result<InferType<ToType>, TransformError>,
|
|
1858
|
-
toParent: (value: InferType<ToType>) => InferType<FromType>,
|
|
1859
|
-
): TransformType<FromType, ToType, TransformError> => {
|
|
1860
|
-
const fromUnknown = (
|
|
1861
|
-
value: unknown,
|
|
1862
|
-
): Result<InferType<ToType>, InferErrors<FromType> | TransformError> => {
|
|
1863
|
-
const parentResult = fromType.fromUnknown(value);
|
|
1864
|
-
if (!parentResult.ok) return parentResult;
|
|
1865
|
-
return fromParent(parentResult.value as IntentionalNever);
|
|
1866
|
-
};
|
|
1867
|
-
|
|
1868
|
-
const to = (value: InferType<ToType>): InferInput<FromType> =>
|
|
1869
|
-
fromType.to(toParent(value)) as IntentionalNever;
|
|
1870
|
-
|
|
1871
|
-
return {
|
|
1872
|
-
...createType("Transform", {
|
|
1873
|
-
fromUnknown,
|
|
1874
|
-
to,
|
|
1875
|
-
fromParent,
|
|
1876
|
-
toParent,
|
|
1877
|
-
}),
|
|
1878
|
-
is: toType.is,
|
|
1879
|
-
fromType,
|
|
1880
|
-
toType,
|
|
1881
|
-
} as TransformType<FromType, ToType, TransformError>;
|
|
1882
|
-
};
|
|
1883
|
-
|
|
1884
|
-
/**
|
|
1885
|
-
* TransformType extends {@link Type} with additional `fromType` and `toType`
|
|
1886
|
-
* properties for reflection.
|
|
1887
|
-
*/
|
|
1888
|
-
export interface TransformType<
|
|
1889
|
-
FromType extends AnyType,
|
|
1890
|
-
ToType extends AnyType,
|
|
1891
|
-
TransformError extends TypeError = never,
|
|
1892
|
-
> extends Type<
|
|
1893
|
-
"Transform",
|
|
1894
|
-
InferType<ToType>,
|
|
1895
|
-
InferInput<FromType>,
|
|
1896
|
-
TransformError,
|
|
1897
|
-
InferType<FromType>,
|
|
1898
|
-
InferErrors<FromType>
|
|
1899
|
-
> {
|
|
1900
|
-
readonly fromType: FromType;
|
|
1901
|
-
readonly toType: ToType;
|
|
1902
|
-
|
|
1903
|
-
readonly fromParent: (
|
|
1904
|
-
value: InferType<FromType>,
|
|
1905
|
-
) => [TransformError] extends [never]
|
|
1906
|
-
? Ok<InferType<ToType>>
|
|
1907
|
-
: Result<InferType<ToType>, TransformError>;
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
/**
|
|
1911
|
-
* Trims leading and trailing whitespace from a string.
|
|
1912
|
-
*
|
|
1913
|
-
* ### Example
|
|
1914
|
-
*
|
|
1915
|
-
* ```ts
|
|
1916
|
-
* expect(TrimString.from("a ")).toEqual(ok("a"));
|
|
1917
|
-
* expect(TrimString.fromParent("a ").value).toEqual("a");
|
|
1918
|
-
* ```
|
|
1919
|
-
*
|
|
1920
|
-
* @category String
|
|
1921
|
-
*/
|
|
1922
|
-
export const TrimString = trim(String);
|
|
1923
|
-
|
|
1924
|
-
/**
|
|
1925
|
-
* Transforms a {@link Date} into a {@link DateIsoString} string and vice versa.
|
|
1926
|
-
*
|
|
1927
|
-
* ### Example
|
|
1928
|
-
*
|
|
1929
|
-
* TODO:
|
|
1930
|
-
*
|
|
1931
|
-
* @category String
|
|
1932
|
-
*/
|
|
1933
|
-
export const DateIso = transform(
|
|
1934
|
-
Date,
|
|
1935
|
-
DateIsoString,
|
|
1936
|
-
(value) => DateIsoString.fromParent(value.toISOString()),
|
|
1937
|
-
(value) => new globalThis.Date(value),
|
|
1938
|
-
);
|
|
1939
|
-
|
|
1940
|
-
/**
|
|
1941
|
-
* Transforms a {@link NonEmptyTrimmedString} into a {@link FiniteNumber}.
|
|
1942
|
-
*
|
|
1943
|
-
* ### Example
|
|
1944
|
-
*
|
|
1945
|
-
* ```ts
|
|
1946
|
-
* NumberFromString.from("42"); // ok(42)
|
|
1947
|
-
* NumberFromString.from("abc"); // err({ type: "NumberFromString", value: "abc" })
|
|
1948
|
-
* ```
|
|
1949
|
-
*
|
|
1950
|
-
* @category Number
|
|
1951
|
-
*/
|
|
1952
|
-
export const NumberFromString = transform(
|
|
1953
|
-
NonEmptyTrimmedString,
|
|
1954
|
-
FiniteNumber,
|
|
1955
|
-
(value) => {
|
|
1956
|
-
const result = FiniteNumber.fromParent(globalThis.Number(value));
|
|
1957
|
-
if (!result.ok)
|
|
1958
|
-
return err<NumberFromStringError>({ type: "NumberFromString", value });
|
|
1959
|
-
return result;
|
|
1960
|
-
},
|
|
1961
|
-
(num) => num.toString() as NonEmptyTrimmedString,
|
|
1962
|
-
);
|
|
1963
|
-
|
|
1964
|
-
export interface NumberFromStringError extends TypeError<"NumberFromString"> {}
|
|
1965
|
-
|
|
1966
|
-
export const formatNumberFromStringError =
|
|
1967
|
-
createTypeErrorFormatter<NumberFromStringError>((error) => {
|
|
1968
|
-
return `The value ${error.value} could not be converted to a finite number.`;
|
|
1969
|
-
});
|
|
1970
|
-
|
|
1971
2110
|
/**
|
|
1972
2111
|
* Array of a specific {@link Type}.
|
|
1973
2112
|
*
|
|
@@ -2020,9 +2159,6 @@ export const array = <ElementType extends AnyType>(
|
|
|
2020
2159
|
return ok(result);
|
|
2021
2160
|
};
|
|
2022
2161
|
|
|
2023
|
-
const to = (value: ReadonlyArray<InferType<ElementType>>) =>
|
|
2024
|
-
value.map(element.to) as ReadonlyArray<InferInput<ElementType>>;
|
|
2025
|
-
|
|
2026
2162
|
const fromParent = (
|
|
2027
2163
|
value: ReadonlyArray<InferParent<ElementType>>,
|
|
2028
2164
|
): Result<
|
|
@@ -2048,18 +2184,10 @@ export const array = <ElementType extends AnyType>(
|
|
|
2048
2184
|
return ok(result);
|
|
2049
2185
|
};
|
|
2050
2186
|
|
|
2051
|
-
const toParent = (values: ReadonlyArray<InferType<ElementType>>) =>
|
|
2052
|
-
values.map(element.toParent) as ReadonlyArray<InferParent<ElementType>>;
|
|
2053
|
-
|
|
2054
2187
|
return {
|
|
2055
|
-
...createType("Array", {
|
|
2056
|
-
fromUnknown,
|
|
2057
|
-
to,
|
|
2058
|
-
fromParent,
|
|
2059
|
-
toParent,
|
|
2060
|
-
}),
|
|
2188
|
+
...createType("Array", { fromUnknown, fromParent }),
|
|
2061
2189
|
element,
|
|
2062
|
-
}
|
|
2190
|
+
};
|
|
2063
2191
|
};
|
|
2064
2192
|
|
|
2065
2193
|
/** ArrayType extends Type with an additional `element` property for reflection. */
|
|
@@ -2192,16 +2320,6 @@ export const record = <
|
|
|
2192
2320
|
return ok(result);
|
|
2193
2321
|
};
|
|
2194
2322
|
|
|
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
2323
|
const fromParent = (
|
|
2206
2324
|
value: Readonly<Record<KeyParent, InferParent<Value>>>,
|
|
2207
2325
|
): Result<
|
|
@@ -2246,22 +2364,10 @@ export const record = <
|
|
|
2246
2364
|
return ok(result);
|
|
2247
2365
|
};
|
|
2248
2366
|
|
|
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
2367
|
return {
|
|
2260
2368
|
...createType("Record", {
|
|
2261
2369
|
fromUnknown,
|
|
2262
|
-
to,
|
|
2263
2370
|
fromParent,
|
|
2264
|
-
toParent,
|
|
2265
2371
|
}),
|
|
2266
2372
|
key: keyType,
|
|
2267
2373
|
value: valueType,
|
|
@@ -2561,25 +2667,6 @@ export function object(
|
|
|
2561
2667
|
return ok(result);
|
|
2562
2668
|
};
|
|
2563
2669
|
|
|
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
2670
|
const fromParent = (value: any): Result<any, any> => {
|
|
2584
2671
|
const errors: Record<string, any> = {};
|
|
2585
2672
|
const result: Record<string, any> = {};
|
|
@@ -2631,31 +2718,10 @@ export function object(
|
|
|
2631
2718
|
return ok(result);
|
|
2632
2719
|
};
|
|
2633
2720
|
|
|
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
2721
|
return {
|
|
2654
2722
|
...createType(record ? "ObjectWithRecord" : "Object", {
|
|
2655
2723
|
fromUnknown,
|
|
2656
|
-
to,
|
|
2657
2724
|
fromParent,
|
|
2658
|
-
toParent,
|
|
2659
2725
|
}),
|
|
2660
2726
|
props,
|
|
2661
2727
|
...(record ? { record } : {}),
|
|
@@ -2897,7 +2963,6 @@ export function union(...args: ReadonlyArray<any>): any {
|
|
|
2897
2963
|
* cases.
|
|
2898
2964
|
*/
|
|
2899
2965
|
|
|
2900
|
-
/* eslint-disable */
|
|
2901
2966
|
const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
|
|
2902
2967
|
|
|
2903
2968
|
const fromUnknown = (value: unknown) => {
|
|
@@ -2915,23 +2980,13 @@ export function union(...args: ReadonlyArray<any>): any {
|
|
|
2915
2980
|
});
|
|
2916
2981
|
};
|
|
2917
2982
|
|
|
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
2983
|
return {
|
|
2926
2984
|
...createType("Union", {
|
|
2927
2985
|
fromUnknown,
|
|
2928
|
-
to,
|
|
2929
2986
|
fromParent: fromUnknown,
|
|
2930
|
-
toParent: to,
|
|
2931
2987
|
}),
|
|
2932
2988
|
members,
|
|
2933
2989
|
};
|
|
2934
|
-
/* eslint-enable */
|
|
2935
2990
|
}
|
|
2936
2991
|
|
|
2937
2992
|
/**
|
|
@@ -3025,20 +3080,10 @@ export const recursive = <ParentType extends AnyType>(
|
|
|
3025
3080
|
type ??= create();
|
|
3026
3081
|
return type.fromUnknown(value);
|
|
3027
3082
|
},
|
|
3028
|
-
to: (value: T) => {
|
|
3029
|
-
type ??= create();
|
|
3030
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
3031
|
-
return type.to(value);
|
|
3032
|
-
},
|
|
3033
3083
|
fromParent: (value: Parent) => {
|
|
3034
3084
|
type ??= create();
|
|
3035
3085
|
return type.fromParent(value);
|
|
3036
3086
|
},
|
|
3037
|
-
toParent: (value: T) => {
|
|
3038
|
-
type ??= create();
|
|
3039
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
3040
|
-
return type.toParent(value);
|
|
3041
|
-
},
|
|
3042
3087
|
is: (value: unknown): value is T => {
|
|
3043
3088
|
type ??= create();
|
|
3044
3089
|
return type.is(value);
|
|
@@ -3176,9 +3221,6 @@ export const tuple = <Elements extends [AnyType, ...ReadonlyArray<AnyType>]>(
|
|
|
3176
3221
|
return ok(result);
|
|
3177
3222
|
};
|
|
3178
3223
|
|
|
3179
|
-
const to = (value: { [K in keyof Elements]: InferType<Elements[K]> }) =>
|
|
3180
|
-
value.map((val, index) => elements[index].to(val) as IntentionalNever);
|
|
3181
|
-
|
|
3182
3224
|
const fromParent = (value: {
|
|
3183
3225
|
[K in keyof Elements]: InferParent<Elements[K]>;
|
|
3184
3226
|
}) => {
|
|
@@ -3210,21 +3252,10 @@ export const tuple = <Elements extends [AnyType, ...ReadonlyArray<AnyType>]>(
|
|
|
3210
3252
|
return ok(result as { [K in keyof Elements]: InferType<Elements[K]> });
|
|
3211
3253
|
};
|
|
3212
3254
|
|
|
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
3255
|
return {
|
|
3223
3256
|
...createType("Tuple", {
|
|
3224
3257
|
fromUnknown,
|
|
3225
|
-
to,
|
|
3226
3258
|
fromParent,
|
|
3227
|
-
toParent,
|
|
3228
3259
|
}),
|
|
3229
3260
|
elements,
|
|
3230
3261
|
} as IntentionalNever;
|
|
@@ -3300,37 +3331,41 @@ export const formatInt64Error = createTypeErrorFormatter<Int64Error>(
|
|
|
3300
3331
|
`The value ${error.value} is not a valid 64-bit signed integer (Int64).`,
|
|
3301
3332
|
);
|
|
3302
3333
|
|
|
3303
|
-
|
|
3304
|
-
String,
|
|
3305
|
-
BigInt,
|
|
3306
|
-
(value) =>
|
|
3307
|
-
trySync(
|
|
3308
|
-
() => globalThis.BigInt(value),
|
|
3309
|
-
(): BigIntFromStringError => ({ type: "BigIntFromString", value }),
|
|
3310
|
-
),
|
|
3311
|
-
(value) => value.toString(),
|
|
3312
|
-
);
|
|
3334
|
+
// // co s timhle? je to string, ze ktereho lze udelat bigint
|
|
3313
3335
|
|
|
3314
|
-
export
|
|
3336
|
+
// export const BigIntFromString = transform(
|
|
3337
|
+
// String,
|
|
3338
|
+
// BigInt,
|
|
3339
|
+
// (value) =>
|
|
3340
|
+
// trySync(
|
|
3341
|
+
// () => globalThis.BigInt(value),
|
|
3342
|
+
// (): BigIntFromStringError => ({ type: "BigIntFromString", value }),
|
|
3343
|
+
// ),
|
|
3344
|
+
// (value) => value.toString(),
|
|
3345
|
+
// );
|
|
3315
3346
|
|
|
3316
|
-
export
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3347
|
+
// export interface BigIntFromStringError extends TypeError<"BigIntFromString"> {}
|
|
3348
|
+
|
|
3349
|
+
// export const formatBigIntFromStringError =
|
|
3350
|
+
// createTypeErrorFormatter<BigIntFromStringError>(
|
|
3351
|
+
// (error) => `The value ${error.value} could not be converted to a BigInt.`,
|
|
3352
|
+
// );
|
|
3320
3353
|
|
|
3321
3354
|
/**
|
|
3322
3355
|
* Stringified {@link Int64}.
|
|
3323
3356
|
*
|
|
3324
|
-
* @category
|
|
3357
|
+
* @category String
|
|
3325
3358
|
*/
|
|
3326
|
-
export const Int64String = brand("Int64",
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
})
|
|
3359
|
+
export const Int64String = brand("Int64", NonEmptyTrimmedString, (value) =>
|
|
3360
|
+
trySync(
|
|
3361
|
+
() => {
|
|
3362
|
+
const maybeInt = globalThis.BigInt(value);
|
|
3363
|
+
Int64.orThrow(maybeInt);
|
|
3364
|
+
return value;
|
|
3365
|
+
},
|
|
3366
|
+
(): Int64StringError => ({ type: "Int64String", value }),
|
|
3367
|
+
),
|
|
3368
|
+
);
|
|
3334
3369
|
|
|
3335
3370
|
export type Int64String = typeof Int64String.Type;
|
|
3336
3371
|
|
|
@@ -3435,42 +3470,14 @@ export const JsonArray = array(JsonValue);
|
|
|
3435
3470
|
*/
|
|
3436
3471
|
export const JsonObject = record(String, JsonValue);
|
|
3437
3472
|
|
|
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}`,
|
|
3473
|
+
export const parseJson = (value: string): Result<JsonValue, JsonError> =>
|
|
3474
|
+
trySync(
|
|
3475
|
+
() => JSON.parse(value) as JsonValue,
|
|
3476
|
+
(error): JsonError => ({
|
|
3477
|
+
type: `Json`,
|
|
3478
|
+
value,
|
|
3479
|
+
message: globalThis.String(error),
|
|
3480
|
+
}),
|
|
3474
3481
|
);
|
|
3475
3482
|
|
|
3476
3483
|
/**
|
|
@@ -3479,20 +3486,15 @@ export const formatJsonValueFromStringError =
|
|
|
3479
3486
|
* ### Example
|
|
3480
3487
|
*
|
|
3481
3488
|
* ```ts
|
|
3482
|
-
* const result = Json.from('{"key":"value"}'); //
|
|
3483
|
-
* const error = Json.from("invalid json"); //
|
|
3489
|
+
* const result = Json.from('{"key":"value"}'); // ok
|
|
3490
|
+
* const error = Json.from("invalid json"); // err
|
|
3484
3491
|
* ```
|
|
3485
3492
|
*
|
|
3486
3493
|
* @category String
|
|
3487
3494
|
*/
|
|
3488
3495
|
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
|
-
});
|
|
3496
|
+
const result = parseJson(value);
|
|
3497
|
+
if (!result.ok) return result;
|
|
3496
3498
|
return ok(value);
|
|
3497
3499
|
});
|
|
3498
3500
|
|
|
@@ -3506,6 +3508,80 @@ export const formatJsonError = createTypeErrorFormatter<JsonError>(
|
|
|
3506
3508
|
(error) => `Invalid JSON: ${error.value}. Error: ${error.message}`,
|
|
3507
3509
|
);
|
|
3508
3510
|
|
|
3511
|
+
export const jsonValueToJson = (value: JsonValue): Json =>
|
|
3512
|
+
JSON.stringify(value) as Json;
|
|
3513
|
+
|
|
3514
|
+
export const jsonToJsonValue = (value: Json): JsonValue =>
|
|
3515
|
+
JSON.parse(value) as JsonValue;
|
|
3516
|
+
|
|
3517
|
+
/**
|
|
3518
|
+
* Creates a branded JSON string {@link Type} and type-safe conversion functions
|
|
3519
|
+
* for a given Type.
|
|
3520
|
+
*
|
|
3521
|
+
* This factory creates:
|
|
3522
|
+
*
|
|
3523
|
+
* 1. A branded string Type that validates JSON parsing and structural conformity
|
|
3524
|
+
* 2. A serialization function (Type → branded JSON string)
|
|
3525
|
+
* 3. A parsing function (branded JSON string → Type, skipping validation)
|
|
3526
|
+
*
|
|
3527
|
+
* Optimized for Evolu's SQLite workflow where we store typed JSON strings and
|
|
3528
|
+
* need type-safe conversions without double parsing.
|
|
3529
|
+
*
|
|
3530
|
+
* ### Example
|
|
3531
|
+
*
|
|
3532
|
+
* ```ts
|
|
3533
|
+
* const Person = object({
|
|
3534
|
+
* name: NonEmptyString100,
|
|
3535
|
+
* age: FiniteNumber,
|
|
3536
|
+
* });
|
|
3537
|
+
* type Person = typeof Person.Type;
|
|
3538
|
+
*
|
|
3539
|
+
* const [PersonJson, personToPersonJson, personJsonToPerson] = json(
|
|
3540
|
+
* Person,
|
|
3541
|
+
* "PersonJson",
|
|
3542
|
+
* );
|
|
3543
|
+
* // string & Brand<"PersonJson">
|
|
3544
|
+
* type PersonJson = typeof PersonJson.Type;
|
|
3545
|
+
*
|
|
3546
|
+
* // Usage:
|
|
3547
|
+
* const person: Person = { name: "Alice", age: 30 };
|
|
3548
|
+
* const jsonString = personToPersonJson(person); // PersonJson
|
|
3549
|
+
* const backToPerson = personJsonToPerson(jsonString); // Person
|
|
3550
|
+
* ```
|
|
3551
|
+
*/
|
|
3552
|
+
export const json = <T extends AnyType, Name extends TypeName>(
|
|
3553
|
+
type: T,
|
|
3554
|
+
name: Name,
|
|
3555
|
+
): [
|
|
3556
|
+
BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>,
|
|
3557
|
+
(
|
|
3558
|
+
value: InferType<T>,
|
|
3559
|
+
) => InferType<
|
|
3560
|
+
BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>
|
|
3561
|
+
>,
|
|
3562
|
+
(
|
|
3563
|
+
value: InferType<
|
|
3564
|
+
BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>
|
|
3565
|
+
>,
|
|
3566
|
+
) => InferType<T>,
|
|
3567
|
+
] => {
|
|
3568
|
+
const BrandedJsonType = brand(name, String, (value) => {
|
|
3569
|
+
const parseResult = parseJson(value);
|
|
3570
|
+
if (!parseResult.ok) return parseResult;
|
|
3571
|
+
|
|
3572
|
+
const validationResult = type.fromUnknown(parseResult.value);
|
|
3573
|
+
if (!validationResult.ok) return validationResult;
|
|
3574
|
+
|
|
3575
|
+
return ok(value);
|
|
3576
|
+
}) as BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>;
|
|
3577
|
+
|
|
3578
|
+
return [
|
|
3579
|
+
BrandedJsonType,
|
|
3580
|
+
jsonValueToJson as IntentionalNever,
|
|
3581
|
+
jsonToJsonValue as IntentionalNever,
|
|
3582
|
+
];
|
|
3583
|
+
};
|
|
3584
|
+
|
|
3509
3585
|
/**
|
|
3510
3586
|
* Optional {@link Type}.
|
|
3511
3587
|
*
|
|
@@ -3530,9 +3606,7 @@ export const formatJsonError = createTypeErrorFormatter<JsonError>(
|
|
|
3530
3606
|
export const optional = <T extends AnyType>(type: T): OptionalType<T> => ({
|
|
3531
3607
|
...createType("Optional", {
|
|
3532
3608
|
fromUnknown: type.fromUnknown,
|
|
3533
|
-
to: type.to,
|
|
3534
3609
|
fromParent: type.fromParent,
|
|
3535
|
-
toParent: type.toParent,
|
|
3536
3610
|
}),
|
|
3537
3611
|
parent: type,
|
|
3538
3612
|
});
|
|
@@ -3556,7 +3630,7 @@ export const isOptionalType = (x: unknown): x is OptionalType<any> =>
|
|
|
3556
3630
|
/**
|
|
3557
3631
|
* Creates a partial object type where all properties are optional.
|
|
3558
3632
|
*
|
|
3559
|
-
* This is useful when
|
|
3633
|
+
* This is useful when we want to validate an object in which none of the keys
|
|
3560
3634
|
* are required, but if they are present they must conform to their
|
|
3561
3635
|
* corresponding Types.
|
|
3562
3636
|
*
|
|
@@ -3661,68 +3735,39 @@ export function omit<T extends ObjectType<any>, Keys extends keyof T["props"]>(
|
|
|
3661
3735
|
return object(newProps);
|
|
3662
3736
|
}
|
|
3663
3737
|
|
|
3738
|
+
export const maxMutationSize = 655360;
|
|
3739
|
+
|
|
3664
3740
|
/**
|
|
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
|
-
* ```
|
|
3741
|
+
* Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
|
|
3742
|
+
* the `maxProtocolMessageRangesSize`. The max size is 640KB in bytes, measured
|
|
3743
|
+
* via MessagePack. Evolu Protocol DbChange will be smaller thanks to various
|
|
3744
|
+
* optimizations.
|
|
3692
3745
|
*/
|
|
3693
|
-
export const
|
|
3746
|
+
export const validMutationSize = <T extends AnyType>(
|
|
3694
3747
|
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>;
|
|
3748
|
+
): BrandType<T, "ValidMutationSize", ValidMutationSizeError, InferErrors<T>> =>
|
|
3749
|
+
brand("ValidMutationSize", type, (value) =>
|
|
3750
|
+
pack(value).byteLength <= maxMutationSize
|
|
3751
|
+
? ok(value)
|
|
3752
|
+
: err<ValidMutationSizeError>({ type: "ValidMutationSize", value }),
|
|
3753
|
+
);
|
|
3714
3754
|
|
|
3715
|
-
|
|
3716
|
-
|
|
3755
|
+
export interface ValidMutationSizeError
|
|
3756
|
+
extends TypeError<"ValidMutationSize"> {}
|
|
3717
3757
|
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
3723
|
-
(value) => JSON.parse(value) as InferType<T>,
|
|
3758
|
+
export const formatValidMutationSizeError =
|
|
3759
|
+
createTypeErrorFormatter<ValidMutationSizeError>(
|
|
3760
|
+
(error) =>
|
|
3761
|
+
`The mutation size exceeds the maximum limit of ${maxMutationSize} bytes. The provided mutation has a size of ${pack(error.value).byteLength} bytes.`,
|
|
3724
3762
|
);
|
|
3725
|
-
|
|
3763
|
+
|
|
3764
|
+
export type ValidMutationSize<Props extends Record<string, AnyType>> =
|
|
3765
|
+
BrandType<
|
|
3766
|
+
ObjectType<Props>,
|
|
3767
|
+
"ValidMutationSize",
|
|
3768
|
+
ValidMutationSizeError,
|
|
3769
|
+
InferErrors<ObjectType<Props>>
|
|
3770
|
+
>;
|
|
3726
3771
|
|
|
3727
3772
|
/**
|
|
3728
3773
|
* Union of all `TypeError`s defined in the `Type.ts` file, including base type
|
|
@@ -3749,16 +3794,16 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
|
|
|
3749
3794
|
| InstanceOfError
|
|
3750
3795
|
| EvoluTypeError
|
|
3751
3796
|
| CurrencyCodeError
|
|
3752
|
-
|
|
|
3797
|
+
| DateIsoError
|
|
3753
3798
|
| TrimmedError
|
|
3754
3799
|
| MinLengthError
|
|
3755
3800
|
| MaxLengthError
|
|
3756
3801
|
| LengthError
|
|
3757
3802
|
| MnemonicError
|
|
3758
3803
|
| RegexError
|
|
3759
|
-
| NanoIdError
|
|
3760
3804
|
| SimplePasswordError
|
|
3761
3805
|
| IdError
|
|
3806
|
+
| TableIdError
|
|
3762
3807
|
| PositiveError
|
|
3763
3808
|
| NegativeError
|
|
3764
3809
|
| NonPositiveError
|
|
@@ -3774,10 +3819,9 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
|
|
|
3774
3819
|
| BetweenError
|
|
3775
3820
|
| LiteralError
|
|
3776
3821
|
| Int64Error
|
|
3777
|
-
| BigIntFromStringError
|
|
3778
3822
|
| Int64StringError
|
|
3779
|
-
| JsonValueFromStringError
|
|
3780
3823
|
| JsonError
|
|
3824
|
+
| ValidMutationSizeError
|
|
3781
3825
|
| ExtraErrors
|
|
3782
3826
|
// Composite errors
|
|
3783
3827
|
| ArrayError<TypeErrors<ExtraErrors>>
|
|
@@ -3792,70 +3836,74 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
|
|
|
3792
3836
|
| TupleError<TypeErrors<ExtraErrors>>;
|
|
3793
3837
|
|
|
3794
3838
|
/**
|
|
3795
|
-
*
|
|
3796
|
-
* {@link TypeErrors} and custom errors. It also lets us override the default
|
|
3797
|
-
* formatting for specific errors.
|
|
3839
|
+
* Formats Evolu Type errors into user-friendly messages.
|
|
3798
3840
|
*
|
|
3799
|
-
*
|
|
3800
|
-
*
|
|
3841
|
+
* Evolu Type typed errors ensure every error type must have a formatter.
|
|
3842
|
+
* TypeScript enforces this at compile-time, preventing unhandled validation
|
|
3843
|
+
* errors from reaching users.
|
|
3801
3844
|
*
|
|
3802
|
-
*
|
|
3845
|
+
* The `createFormatTypeError` function handles both built-in {@link TypeErrors}
|
|
3846
|
+
* and custom errors, and lets us override default formatting for specific
|
|
3847
|
+
* errors.
|
|
3848
|
+
*
|
|
3849
|
+
* ### Example
|
|
3803
3850
|
*
|
|
3804
3851
|
* ```ts
|
|
3805
|
-
* const
|
|
3806
|
-
*
|
|
3807
|
-
*
|
|
3852
|
+
* const formatTypeError = createFormatTypeError<
|
|
3853
|
+
* MinLengthError | MaxLengthError
|
|
3854
|
+
* >((error): string => {
|
|
3855
|
+
* switch (error.type) {
|
|
3856
|
+
* case "MinLength":
|
|
3857
|
+
* return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
|
|
3858
|
+
* case "MaxLength":
|
|
3859
|
+
* return `Text is too long (maximum ${error.max} characters)`;
|
|
3860
|
+
* }
|
|
3861
|
+
* });
|
|
3808
3862
|
* ```
|
|
3809
3863
|
*
|
|
3810
|
-
*
|
|
3864
|
+
* Alternatively, write a custom formatter from scratch without using
|
|
3865
|
+
* `createFormatTypeError`. This gives us full control over error formatting:
|
|
3811
3866
|
*
|
|
3812
3867
|
* ```ts
|
|
3813
|
-
*
|
|
3814
|
-
*
|
|
3868
|
+
* const Person = object({
|
|
3869
|
+
* name: NonEmptyTrimmedString100,
|
|
3870
|
+
* age: optional(PositiveInt),
|
|
3871
|
+
* });
|
|
3872
|
+
*
|
|
3873
|
+
* // Define only the errors actually used by Person Type
|
|
3874
|
+
* type PersonErrors =
|
|
3815
3875
|
* | StringError
|
|
3816
|
-
* | MinLengthError
|
|
3817
3876
|
* | MaxLengthError
|
|
3818
|
-
* |
|
|
3819
|
-
* | IdError
|
|
3877
|
+
* | MinLengthError
|
|
3820
3878
|
* | 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.
|
|
3879
|
+
* | PositiveError
|
|
3880
|
+
* | NonNegativeError
|
|
3881
|
+
* | IntError
|
|
3882
|
+
* | NumberError
|
|
3883
|
+
* | ObjectError<Record<string, PersonErrors>>;
|
|
3884
|
+
*
|
|
3885
|
+
* const formatTypeError: TypeErrorFormatter<PersonErrors> = (error) => {
|
|
3830
3886
|
* 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
3887
|
* case "String":
|
|
3842
|
-
* // We can reuse existing formatter.
|
|
3843
3888
|
* return formatStringError(error);
|
|
3889
|
+
* case "Number":
|
|
3890
|
+
* return "Must be a number";
|
|
3891
|
+
* case "MinLength":
|
|
3892
|
+
* return `Must be at least ${error.min} characters`;
|
|
3893
|
+
* case "MaxLength":
|
|
3894
|
+
* return `Cannot exceed ${error.max} characters`;
|
|
3844
3895
|
* case "Trimmed":
|
|
3845
|
-
* return "
|
|
3846
|
-
* case "
|
|
3847
|
-
* return "
|
|
3848
|
-
* case "
|
|
3849
|
-
* return
|
|
3850
|
-
*
|
|
3851
|
-
*
|
|
3852
|
-
* return `Union errors: ${error.errors.map(formatTypeError).join(", ")}`;
|
|
3896
|
+
* return "Cannot have leading or trailing spaces";
|
|
3897
|
+
* case "Positive":
|
|
3898
|
+
* return "Must be a positive number";
|
|
3899
|
+
* case "NonNegative":
|
|
3900
|
+
* return "Must be zero or positive";
|
|
3901
|
+
* case "Int":
|
|
3902
|
+
* return "Must be an integer";
|
|
3853
3903
|
* case "Object": {
|
|
3854
|
-
* if (
|
|
3855
|
-
*
|
|
3856
|
-
*
|
|
3857
|
-
* )
|
|
3858
|
-
* return "A developer made an error, this should not happen.";
|
|
3904
|
+
* if (error.reason.kind === "NotObject") return "Must be an object";
|
|
3905
|
+
* if (error.reason.kind === "ExtraKeys")
|
|
3906
|
+
* return "Contains unexpected fields";
|
|
3859
3907
|
* const firstError = Object.values(error.reason.errors).find(
|
|
3860
3908
|
* (e) => e !== undefined,
|
|
3861
3909
|
* )!;
|
|
@@ -3902,8 +3950,8 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
|
|
|
3902
3950
|
return formatIsTypeError(error);
|
|
3903
3951
|
case "CurrencyCode":
|
|
3904
3952
|
return formatCurrencyCodeError(error);
|
|
3905
|
-
case "
|
|
3906
|
-
return
|
|
3953
|
+
case "DateIso":
|
|
3954
|
+
return formatDateIsoError(error);
|
|
3907
3955
|
case "Trimmed":
|
|
3908
3956
|
return formatTrimmedError(error);
|
|
3909
3957
|
case "MinLength":
|
|
@@ -3918,6 +3966,8 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
|
|
|
3918
3966
|
return formatRegexError(error);
|
|
3919
3967
|
case "Id":
|
|
3920
3968
|
return formatIdError(error);
|
|
3969
|
+
case "TableId":
|
|
3970
|
+
return formatTableIdError(error);
|
|
3921
3971
|
case "Positive":
|
|
3922
3972
|
return formatPositiveError(error);
|
|
3923
3973
|
case "Negative":
|
|
@@ -3948,14 +3998,12 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
|
|
|
3948
3998
|
return formatLiteralError(error);
|
|
3949
3999
|
case "Int64":
|
|
3950
4000
|
return formatInt64Error(error);
|
|
3951
|
-
case "BigIntFromString":
|
|
3952
|
-
return formatBigIntFromStringError(error);
|
|
3953
4001
|
case "Int64String":
|
|
3954
4002
|
return formatInt64StringError(error);
|
|
3955
|
-
case "JsonValueFromString":
|
|
3956
|
-
return formatJsonValueFromStringError(error);
|
|
3957
4003
|
case "Json":
|
|
3958
4004
|
return formatJsonError(error);
|
|
4005
|
+
case "ValidMutationSize":
|
|
4006
|
+
return formatValidMutationSizeError(error);
|
|
3959
4007
|
// Composite Types
|
|
3960
4008
|
case "SimplePassword":
|
|
3961
4009
|
return formatSimplePasswordError(formatTypeError)(error);
|