@evolu/common 6.0.1-preview.19 → 6.0.1-preview.20
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.map +1 -1
- package/dist/src/Assert.js +1 -1
- 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 +48 -37
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +27 -50
- package/dist/src/Evolu/Db.d.ts +138 -66
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +248 -645
- 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 +79 -116
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +275 -132
- 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 +129 -83
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +80 -89
- 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 +114 -191
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +409 -416
- 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/Relay.d.ts +1 -2
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +11 -9
- package/dist/src/Evolu/Schema.d.ts +88 -27
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +141 -24
- package/dist/src/Evolu/Storage.d.ts +158 -14
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +32 -32
- package/dist/src/Evolu/Sync.d.ts +77 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +453 -20
- package/dist/src/Evolu/Timestamp.d.ts +29 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +20 -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/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 +144 -22
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +5 -2
- package/dist/src/Sqlite.d.ts +20 -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 +431 -341
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +458 -466
- 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 +2 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -3
- package/package.json +7 -7
- package/src/Assert.ts +2 -4
- package/src/Buffer.ts +1 -1
- package/src/CallbackRegistry.ts +84 -0
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +78 -91
- package/src/Evolu/Db.ts +455 -947
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +545 -307
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +422 -0
- package/src/Evolu/Owner.ts +191 -131
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +536 -653
- package/src/Evolu/Public.ts +7 -9
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Relay.ts +17 -12
- package/src/Evolu/Schema.ts +271 -66
- package/src/Evolu/Storage.ts +263 -55
- package/src/Evolu/Sync.ts +758 -37
- package/src/Evolu/Timestamp.ts +30 -35
- package/src/ManyToManyMap.ts +127 -24
- package/src/Random.ts +3 -2
- package/src/RefCountedResourceManager.ts +368 -0
- package/src/Result.ts +149 -23
- package/src/Sqlite.ts +59 -24
- package/src/Task.ts +779 -0
- package/src/Time.ts +168 -4
- package/src/Type.ts +657 -695
- package/src/WebSocket.ts +23 -17
- package/src/Worker.ts +72 -23
- package/src/index.ts +2 -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 -82
- 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 -97
- 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,87 +1,187 @@
|
|
|
1
|
-
/* eslint-disable jsdoc/no-undefined-types */
|
|
2
1
|
/**
|
|
3
|
-
* 🧩
|
|
2
|
+
* 🧩 Type-safe runtime types
|
|
4
3
|
*
|
|
5
|
-
*
|
|
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.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
+
* Why another validation library?
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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.
|
|
11
21
|
*
|
|
12
|
-
*
|
|
13
|
-
* exceptions.
|
|
14
|
-
* - **Consistent constraints**: Enforcing {@link Brand} for all constraints.
|
|
15
|
-
* - **Typed errors with decoupled formatters**: Avoiding coupling error messages
|
|
16
|
-
* with validators.
|
|
17
|
-
* - **No user-land chaining**: Designed with ES pipe operator in mind.
|
|
18
|
-
* - **Selective validation/transformation**: Skipping parent Type validations and
|
|
19
|
-
* transformations when TypeScript's type system can be relied upon.
|
|
20
|
-
* - **Bidirectional transformations**: Supporting transformations in both
|
|
21
|
-
* directions.
|
|
22
|
-
* - **Minimal and transparent code**: No runtime dependencies or hidden magic.
|
|
22
|
+
* ### Base Types Quick Start
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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();
|
|
28
34
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* cannot fail.
|
|
35
|
+
* // Type guard style
|
|
36
|
+
* if (String.is(value)) {
|
|
37
|
+
* // narrowed to string
|
|
38
|
+
* }
|
|
34
39
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* this:
|
|
40
|
+
* // Composing: arrays & objects
|
|
41
|
+
* const Numbers = array(Number); // ReadonlyArray<number>
|
|
42
|
+
* const Point = object({ x: Number, y: Number });
|
|
39
43
|
*
|
|
40
|
-
*
|
|
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
|
+
* ```
|
|
41
48
|
*
|
|
42
|
-
*
|
|
43
|
-
* `TrimmedString`, the parent Type is `String`.
|
|
49
|
+
* ### Branding Basics
|
|
44
50
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* `fromParent` and `toParent` can be called on any Type.
|
|
51
|
+
* Branding adds semantic meaning & constraints while preserving the runtime
|
|
52
|
+
* shape:
|
|
48
53
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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">
|
|
51
61
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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;
|
|
84
|
+
*
|
|
85
|
+
* User.from({ name: "Alice" }); // ok
|
|
86
|
+
* User.from({ name: "Alice", age: -1 }); // err(PositiveInt)
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* ### Deriving JSON String Types
|
|
90
|
+
*
|
|
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;
|
|
99
|
+
*
|
|
100
|
+
* const [PersonJson, personToPersonJson, personJsonToPerson] = json(
|
|
101
|
+
* Person,
|
|
102
|
+
* "PersonJson",
|
|
103
|
+
* );
|
|
104
|
+
* // string & Brand<"PersonJson">
|
|
105
|
+
* type PersonJson = typeof PersonJson.Type;
|
|
106
|
+
*
|
|
107
|
+
* const person = Person.orThrow({
|
|
108
|
+
* name: "Alice",
|
|
109
|
+
* age: 30,
|
|
110
|
+
* });
|
|
111
|
+
*
|
|
112
|
+
* const personJson = personToPersonJson(person);
|
|
113
|
+
* expect(personJsonToPerson(personJson)).toEqual(person);
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
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
|
+
* ```
|
|
152
|
+
*
|
|
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
|
|
|
73
|
-
import { utf8ToBytes } from "@noble/ciphers/utils";
|
|
74
|
-
import { sha256 } from "@noble/hashes/sha2";
|
|
174
|
+
import { utf8ToBytes } from "@noble/ciphers/utils.js";
|
|
175
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
75
176
|
import * as bip39 from "@scure/bip39";
|
|
76
|
-
import { wordlist } from "@scure/bip39/wordlists/english";
|
|
77
|
-
import {
|
|
78
|
-
import {
|
|
79
|
-
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";
|
|
80
181
|
import { isPlainObject } from "./Object.js";
|
|
81
|
-
import {
|
|
182
|
+
import { err, getOrThrow, ok, Result, trySync } from "./Result.js";
|
|
82
183
|
import { safelyStringifyUnknownValue } from "./String.js";
|
|
83
184
|
import type { Literal, Simplify, WidenLiteral } from "./Types.js";
|
|
84
|
-
import type { Brand } from "./Brand.js";
|
|
85
185
|
import { IntentionalNever } from "./Types.js";
|
|
86
186
|
|
|
87
187
|
export interface Type<
|
|
@@ -109,41 +209,60 @@ export interface Type<
|
|
|
109
209
|
readonly from: (value: Input) => Result<T, ParentError | Error>;
|
|
110
210
|
|
|
111
211
|
/**
|
|
112
|
-
* Creates `T` from an
|
|
212
|
+
* Creates `T` from an `Input` value, throwing an error if validation fails.
|
|
113
213
|
*
|
|
114
|
-
* This is
|
|
115
|
-
*/
|
|
116
|
-
readonly fromUnknown: (value: unknown) => Result<T, ParentError | Error>;
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* The opposite of `from` and `fromUnknown`.
|
|
214
|
+
* This is a convenience method that combines `from` with `getOrThrow`.
|
|
120
215
|
*
|
|
121
|
-
*
|
|
216
|
+
* **When to use:**
|
|
122
217
|
*
|
|
123
|
-
*
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
*
|
|
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
|
|
129
224
|
*
|
|
130
|
-
*
|
|
131
|
-
* already partially validated/transformed value.
|
|
225
|
+
* **When NOT to use:**
|
|
132
226
|
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
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
|
|
135
230
|
*
|
|
136
231
|
* ### Example
|
|
137
232
|
*
|
|
138
233
|
* ```ts
|
|
139
|
-
* //
|
|
140
|
-
* 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
|
+
* }
|
|
141
248
|
* ```
|
|
142
249
|
*/
|
|
143
|
-
readonly
|
|
250
|
+
readonly orThrow: (value: Input) => T;
|
|
251
|
+
|
|
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>;
|
|
144
258
|
|
|
145
|
-
/**
|
|
146
|
-
|
|
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>;
|
|
147
266
|
|
|
148
267
|
/**
|
|
149
268
|
* A **type guard** that checks whether an unknown value satisfies the
|
|
@@ -264,26 +383,61 @@ export interface TypeErrorWithReason<
|
|
|
264
383
|
|
|
265
384
|
export type AnyType = Type<any, any, any, any, any, any>;
|
|
266
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Extracts the name from a {@link Type}.
|
|
388
|
+
*
|
|
389
|
+
* @category Utilities
|
|
390
|
+
*/
|
|
267
391
|
export type InferName<A extends AnyType> =
|
|
268
392
|
A extends Type<infer Name, any, any, any, any, any> ? Name : never;
|
|
269
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Extracts the type from a {@link Type}.
|
|
396
|
+
*
|
|
397
|
+
* @category Utilities
|
|
398
|
+
*/
|
|
270
399
|
export type InferType<A extends AnyType> =
|
|
271
400
|
A extends Type<any, infer T, any, any, any, any> ? T : never;
|
|
272
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Extracts the input type from a {@link Type}.
|
|
404
|
+
*
|
|
405
|
+
* @category Utilities
|
|
406
|
+
*/
|
|
273
407
|
export type InferInput<A extends AnyType> =
|
|
274
408
|
A extends Type<any, any, infer Input, any, any, any> ? Input : never;
|
|
275
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Extracts the specific error type from a {@link Type}.
|
|
412
|
+
*
|
|
413
|
+
* @category Utilities
|
|
414
|
+
*/
|
|
276
415
|
export type InferError<A extends AnyType> =
|
|
277
416
|
A extends Type<any, any, any, infer Error, any, any> ? Error : never;
|
|
278
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Extracts the parent type from a {@link Type}.
|
|
420
|
+
*
|
|
421
|
+
* @category Utilities
|
|
422
|
+
*/
|
|
279
423
|
export type InferParent<A extends AnyType> =
|
|
280
424
|
A extends Type<any, any, any, any, infer Parent, any> ? Parent : never;
|
|
281
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Extracts the parent error type from a {@link Type}.
|
|
428
|
+
*
|
|
429
|
+
* @category Utilities
|
|
430
|
+
*/
|
|
282
431
|
export type InferParentError<A extends AnyType> =
|
|
283
432
|
A extends Type<any, any, any, any, any, infer ParentError>
|
|
284
433
|
? ParentError
|
|
285
434
|
: never;
|
|
286
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Extracts all error types (Error | ParentError) from a {@link Type}.
|
|
438
|
+
*
|
|
439
|
+
* @category Utilities
|
|
440
|
+
*/
|
|
287
441
|
export type InferErrors<T extends AnyType> =
|
|
288
442
|
T extends Type<any, any, any, infer Error, any, infer ParentError>
|
|
289
443
|
? Error | ParentError
|
|
@@ -313,6 +467,7 @@ const createType = <
|
|
|
313
467
|
| "name"
|
|
314
468
|
| "is"
|
|
315
469
|
| "from"
|
|
470
|
+
| "orThrow"
|
|
316
471
|
| typeof EvoluTypeSymbol
|
|
317
472
|
| "Type"
|
|
318
473
|
| "Input"
|
|
@@ -326,6 +481,7 @@ const createType = <
|
|
|
326
481
|
name,
|
|
327
482
|
is: (value: unknown): value is T => definition.fromUnknown(value).ok,
|
|
328
483
|
from: definition.fromUnknown,
|
|
484
|
+
orThrow: (value: Input): T => getOrThrow(definition.fromUnknown(value)),
|
|
329
485
|
[EvoluTypeSymbol]: true,
|
|
330
486
|
Type: undefined as unknown as T,
|
|
331
487
|
Input: undefined as unknown as Input,
|
|
@@ -368,12 +524,6 @@ export type TypeErrorFormatter<Error extends TypeError> = (
|
|
|
368
524
|
* Base {@link Type}.
|
|
369
525
|
*
|
|
370
526
|
* A Base Type validates that a value conforms to a specific TypeScript type.
|
|
371
|
-
* Unlike refinements or transformations, Base Types establish the fundamental
|
|
372
|
-
* shape of a value before any branding or transformation occurs.
|
|
373
|
-
*
|
|
374
|
-
* - To **refine** a Base Type further, use the {@link brand} Type Factory.
|
|
375
|
-
* - To **transform** a Base Type into a different representation, use the
|
|
376
|
-
* {@link transform} Type Factory.
|
|
377
527
|
*
|
|
378
528
|
* ### Example
|
|
379
529
|
*
|
|
@@ -399,9 +549,7 @@ export const base = <Name extends TypeName, T, Error extends TypeError>(
|
|
|
399
549
|
): Type<Name, T, T, Error> =>
|
|
400
550
|
createType(name, {
|
|
401
551
|
fromUnknown,
|
|
402
|
-
to: identity,
|
|
403
552
|
fromParent: ok<T>, // `fromParent` relies on types, so it can't fail for the Base Type
|
|
404
|
-
toParent: identity,
|
|
405
553
|
});
|
|
406
554
|
|
|
407
555
|
/**
|
|
@@ -610,7 +758,7 @@ export const formatIsTypeError = createTypeErrorFormatter<EvoluTypeError>(
|
|
|
610
758
|
* The `brand` Type Factory takes the name of a new {@link Brand}, a parent Type
|
|
611
759
|
* to be branded, and the optional `refine` function for additional constraint.
|
|
612
760
|
*
|
|
613
|
-
*
|
|
761
|
+
* The `refine` function can be omitted if we only want to add a brand.
|
|
614
762
|
*
|
|
615
763
|
* ### Examples
|
|
616
764
|
*
|
|
@@ -689,7 +837,7 @@ export const formatIsTypeError = createTypeErrorFormatter<EvoluTypeError>(
|
|
|
689
837
|
* confirmPassword: SimplePassword,
|
|
690
838
|
* });
|
|
691
839
|
*
|
|
692
|
-
* const ValidForm = brand("
|
|
840
|
+
* const ValidForm = brand("ValidForm", Form, (value) => {
|
|
693
841
|
* if (value.password !== value.confirmPassword)
|
|
694
842
|
* return err<ValidFormError>({
|
|
695
843
|
* type: "ValidForm",
|
|
@@ -783,9 +931,7 @@ export function brand<
|
|
|
783
931
|
return {
|
|
784
932
|
...createType("Brand", {
|
|
785
933
|
fromUnknown,
|
|
786
|
-
to: identity,
|
|
787
934
|
fromParent: (refine ?? ok) as IntentionalNever,
|
|
788
|
-
toParent: identity,
|
|
789
935
|
}),
|
|
790
936
|
brand: name,
|
|
791
937
|
parentType: parent,
|
|
@@ -851,30 +997,41 @@ export const formatCurrencyCodeError =
|
|
|
851
997
|
* ### Example
|
|
852
998
|
*
|
|
853
999
|
* ```ts
|
|
854
|
-
* const result =
|
|
855
|
-
* 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
|
|
856
1002
|
* ```
|
|
857
1003
|
*
|
|
858
1004
|
* @category String
|
|
859
1005
|
*/
|
|
860
|
-
export const
|
|
1006
|
+
export const DateIso = brand("DateIso", String, (value) => {
|
|
861
1007
|
if (value.length !== 24) {
|
|
862
|
-
return err<
|
|
1008
|
+
return err<DateIsoError>({ type: "DateIso", value });
|
|
863
1009
|
}
|
|
864
|
-
|
|
865
|
-
|
|
1010
|
+
const parsed = globalThis.Date.parse(value);
|
|
1011
|
+
if (isNaN(parsed)) {
|
|
1012
|
+
return err<DateIsoError>({ type: "DateIso", value });
|
|
1013
|
+
}
|
|
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 });
|
|
866
1018
|
}
|
|
867
1019
|
return ok(value);
|
|
868
1020
|
});
|
|
869
1021
|
|
|
870
|
-
export type
|
|
1022
|
+
export type DateIso = typeof DateIso.Type;
|
|
871
1023
|
|
|
872
|
-
export interface
|
|
1024
|
+
export interface DateIsoError extends TypeError<"DateIso"> {}
|
|
873
1025
|
|
|
874
|
-
export const
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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);
|
|
878
1035
|
|
|
879
1036
|
/**
|
|
880
1037
|
* Helper type for Type Factory that creates a branded Type.
|
|
@@ -917,18 +1074,12 @@ export type BrandFactory<
|
|
|
917
1074
|
/**
|
|
918
1075
|
* Trimmed string.
|
|
919
1076
|
*
|
|
920
|
-
* This Type Factory
|
|
921
|
-
*
|
|
922
|
-
* Factory.
|
|
1077
|
+
* This Type Factory validates whether a string has no leading or trailing
|
|
1078
|
+
* whitespaces.
|
|
923
1079
|
*
|
|
924
|
-
* ###
|
|
1080
|
+
* ### Example
|
|
925
1081
|
*
|
|
926
1082
|
* ```ts
|
|
927
|
-
* // this Type already exists
|
|
928
|
-
* const TrimmedString = trimmed(String);
|
|
929
|
-
* type TrimmedString = typeof TrimmedString.Type;
|
|
930
|
-
*
|
|
931
|
-
* // we can make any branded Type trimmed:
|
|
932
1083
|
* const TrimmedNonEmptyString = trimmed(minLength(1)(String));
|
|
933
1084
|
* // string & Brand<"MinLength1"> & Brand<"Trimmed">
|
|
934
1085
|
* type TrimmedNonEmptyString = typeof TrimmedNonEmptyString.Type;
|
|
@@ -951,64 +1102,6 @@ export const formatTrimmedError = createTypeErrorFormatter<TrimmedError>(
|
|
|
951
1102
|
(error) => `A value ${error.value} is not trimmed`,
|
|
952
1103
|
);
|
|
953
1104
|
|
|
954
|
-
export type TransformBrandFactory<
|
|
955
|
-
Name extends TypeName,
|
|
956
|
-
Input,
|
|
957
|
-
TransformError extends TypeError = never,
|
|
958
|
-
> = <
|
|
959
|
-
PName extends TypeName,
|
|
960
|
-
P extends Input,
|
|
961
|
-
PInput,
|
|
962
|
-
PParent,
|
|
963
|
-
PError extends TypeError = never,
|
|
964
|
-
PParentError extends TypeError = never,
|
|
965
|
-
>(
|
|
966
|
-
parent: Type<PName, P, PInput, PError, PParent, PParentError>,
|
|
967
|
-
) => TransformType<
|
|
968
|
-
Type<PName, P, PInput, PError, PParent, PParentError>,
|
|
969
|
-
BrandType<
|
|
970
|
-
Type<PName, P, PInput, PError, PParent, PParentError>,
|
|
971
|
-
Name,
|
|
972
|
-
never,
|
|
973
|
-
PError | PParentError
|
|
974
|
-
>,
|
|
975
|
-
TransformError
|
|
976
|
-
>;
|
|
977
|
-
|
|
978
|
-
/**
|
|
979
|
-
* Trims leading and trailing whitespace from a string.
|
|
980
|
-
*
|
|
981
|
-
* This Type Factory **transforms** the input string by removing whitespace from
|
|
982
|
-
* both ends. For validation only, use {@link trimmed} Type Factory.
|
|
983
|
-
*
|
|
984
|
-
* ### Example
|
|
985
|
-
*
|
|
986
|
-
* ```ts
|
|
987
|
-
* const TrimString = trim(String);
|
|
988
|
-
* expect(TrimString.from("a ")).toEqual(ok("a"));
|
|
989
|
-
* expect(TrimString.fromParent("a ").value).toEqual("a");
|
|
990
|
-
*
|
|
991
|
-
* const TrimNonEmptyString = trim(NonEmptyString);
|
|
992
|
-
* expect(TrimNonEmptyString.from("a " as NonEmptyString)).toEqual(ok("a"));
|
|
993
|
-
* expect(
|
|
994
|
-
* TrimNonEmptyString.fromParent("a " as NonEmptyString).value,
|
|
995
|
-
* ).toEqual("a");
|
|
996
|
-
* ```
|
|
997
|
-
*
|
|
998
|
-
* **Note:** This transformation is irreversible. Calling `toParent` will not
|
|
999
|
-
* restore the original representation.
|
|
1000
|
-
*
|
|
1001
|
-
* @category String
|
|
1002
|
-
*/
|
|
1003
|
-
|
|
1004
|
-
export const trim: TransformBrandFactory<"Trimmed", string> = (parent) =>
|
|
1005
|
-
transform(
|
|
1006
|
-
parent,
|
|
1007
|
-
trimmed(parent) as BrandType<typeof parent, "Trimmed">,
|
|
1008
|
-
(value) => ok(value.trim() as InferType<typeof parent> & Brand<"Trimmed">),
|
|
1009
|
-
(value) => value,
|
|
1010
|
-
);
|
|
1011
|
-
|
|
1012
1105
|
/**
|
|
1013
1106
|
* Trimmed string
|
|
1014
1107
|
*
|
|
@@ -1020,6 +1113,9 @@ export const trim: TransformBrandFactory<"Trimmed", string> = (parent) =>
|
|
|
1020
1113
|
export const TrimmedString = trimmed(String);
|
|
1021
1114
|
export type TrimmedString = typeof TrimmedString.Type;
|
|
1022
1115
|
|
|
1116
|
+
export const trim = (value: string): TrimmedString =>
|
|
1117
|
+
value.trim() as TrimmedString;
|
|
1118
|
+
|
|
1023
1119
|
/**
|
|
1024
1120
|
* Minimum length.
|
|
1025
1121
|
*
|
|
@@ -1216,9 +1312,9 @@ export const formatRegexError = createTypeErrorFormatter<RegexError>(
|
|
|
1216
1312
|
);
|
|
1217
1313
|
|
|
1218
1314
|
/**
|
|
1219
|
-
* URL-safe
|
|
1315
|
+
* URL-safe string.
|
|
1220
1316
|
*
|
|
1221
|
-
* A `
|
|
1317
|
+
* A `UrlSafeString` uses a limited alphabet that is safe for URLs:
|
|
1222
1318
|
*
|
|
1223
1319
|
* - Uppercase letters (`A-Z`)
|
|
1224
1320
|
* - Lowercase letters (`a-z`)
|
|
@@ -1226,42 +1322,116 @@ export const formatRegexError = createTypeErrorFormatter<RegexError>(
|
|
|
1226
1322
|
* - Dash (`-`)
|
|
1227
1323
|
* - Underscore (`_`)
|
|
1228
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
|
+
*
|
|
1229
1328
|
* ### Example
|
|
1230
1329
|
*
|
|
1231
1330
|
* ```ts
|
|
1232
|
-
* const result =
|
|
1331
|
+
* const result = UrlSafeString.from("abc123_-");
|
|
1233
1332
|
* if (result.ok) {
|
|
1234
|
-
* console.log("Valid
|
|
1333
|
+
* console.log("Valid URL-safe string:", result.value);
|
|
1235
1334
|
* } else {
|
|
1236
|
-
* console.error("Invalid
|
|
1335
|
+
* console.error("Invalid URL-safe string:", result.error);
|
|
1237
1336
|
* }
|
|
1238
1337
|
* ```
|
|
1239
1338
|
*
|
|
1240
1339
|
* @category String
|
|
1241
1340
|
*/
|
|
1242
|
-
export const
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
)(String);
|
|
1246
|
-
export type Base64Url = typeof Base64Url.Type;
|
|
1247
|
-
export type Base64UrlError = typeof Base64Url.Error;
|
|
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;
|
|
1248
1344
|
|
|
1249
1345
|
/**
|
|
1250
|
-
*
|
|
1251
|
-
*
|
|
1346
|
+
* Base64Url without padding.
|
|
1347
|
+
*
|
|
1348
|
+
* Encode with {@link uint8ArrayToBase64Url}, decode with
|
|
1349
|
+
* {@link base64UrlToUint8Array}.
|
|
1350
|
+
*
|
|
1351
|
+
* @category String
|
|
1252
1352
|
*/
|
|
1253
|
-
export const
|
|
1254
|
-
"
|
|
1353
|
+
export const Base64Url = brand(
|
|
1354
|
+
"Base64Url",
|
|
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
|
+
);
|
|
1373
|
+
export type Base64Url = typeof Base64Url.Type;
|
|
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
|
+
};
|
|
1255
1429
|
|
|
1256
1430
|
/**
|
|
1257
|
-
* Simple alphanumeric string for naming.
|
|
1431
|
+
* Simple alphanumeric string for naming in file systems, URLs, and identifiers.
|
|
1258
1432
|
*
|
|
1259
|
-
*
|
|
1260
|
-
*
|
|
1261
|
-
* - Uppercase letters (`A-Z`)
|
|
1262
|
-
* - Lowercase letters (`a-z`)
|
|
1263
|
-
* - Digits (`0-9`)
|
|
1264
|
-
* - Dash (`-`)
|
|
1433
|
+
* Uses the same safe alphabet as {@link UrlSafeString} (letters, digits, `-`,
|
|
1434
|
+
* `_`). See `UrlSafeString` for details.
|
|
1265
1435
|
*
|
|
1266
1436
|
* The string must be between 1 and 42 characters.
|
|
1267
1437
|
*
|
|
@@ -1278,18 +1448,13 @@ export const base64UrlAlphabet =
|
|
|
1278
1448
|
*
|
|
1279
1449
|
* @category String
|
|
1280
1450
|
*/
|
|
1281
|
-
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
|
+
);
|
|
1282
1456
|
export type SimpleName = typeof SimpleName.Type;
|
|
1283
|
-
export
|
|
1284
|
-
|
|
1285
|
-
/**
|
|
1286
|
-
* Default NanoId.
|
|
1287
|
-
*
|
|
1288
|
-
* @category String
|
|
1289
|
-
*/
|
|
1290
|
-
export const NanoId = regex("NanoId", /^[A-Za-z0-9_-]{21}$/)(String);
|
|
1291
|
-
export type NanoId = typeof NanoId.Type;
|
|
1292
|
-
export type NanoIdError = typeof NanoId.Error;
|
|
1457
|
+
export interface SimpleNameError extends TypeError<"SimpleName"> {}
|
|
1293
1458
|
|
|
1294
1459
|
/**
|
|
1295
1460
|
* Trimmed string between 8 and 64 characters, branded as `SimplePassword`.
|
|
@@ -1314,17 +1479,44 @@ export const formatSimplePasswordError = (
|
|
|
1314
1479
|
);
|
|
1315
1480
|
|
|
1316
1481
|
/**
|
|
1317
|
-
*
|
|
1482
|
+
* Globally unique identifier.
|
|
1483
|
+
*
|
|
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
|
|
1318
1489
|
*
|
|
1319
|
-
*
|
|
1320
|
-
*
|
|
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.
|
|
1321
1505
|
*
|
|
1322
1506
|
* @category String
|
|
1323
1507
|
*/
|
|
1324
|
-
export const Id =
|
|
1508
|
+
export const Id = brand("Id", String, (value) =>
|
|
1509
|
+
value.length === 22 && Base64Url.fromParent(value).ok
|
|
1510
|
+
? ok(value)
|
|
1511
|
+
: err<IdError>({ type: "Id", value }),
|
|
1512
|
+
);
|
|
1325
1513
|
export type Id = typeof Id.Type;
|
|
1326
1514
|
|
|
1327
|
-
export
|
|
1515
|
+
export interface IdError extends TypeError<"Id"> {}
|
|
1516
|
+
|
|
1517
|
+
export const formatIdError = createTypeErrorFormatter<IdError>(
|
|
1518
|
+
(error) => `Value ${error.value} is not a valid Id.`,
|
|
1519
|
+
);
|
|
1328
1520
|
|
|
1329
1521
|
/**
|
|
1330
1522
|
* Creates an {@link Id}.
|
|
@@ -1340,16 +1532,19 @@ export const idTypeValueLength = 21;
|
|
|
1340
1532
|
* ```
|
|
1341
1533
|
*/
|
|
1342
1534
|
export const createId = <B extends string = never>(
|
|
1343
|
-
deps:
|
|
1535
|
+
deps: RandomBytesDep,
|
|
1344
1536
|
): [B] extends [never] ? Id : Id & Brand<B> =>
|
|
1345
|
-
deps.
|
|
1537
|
+
uint8ArrayToBase64Url(deps.randomBytes.create(16)) as unknown as [B] extends [
|
|
1538
|
+
never,
|
|
1539
|
+
]
|
|
1540
|
+
? Id
|
|
1541
|
+
: Id & Brand<B>;
|
|
1346
1542
|
|
|
1347
1543
|
/**
|
|
1348
1544
|
* Creates an {@link Id} from a string using SHA-256.
|
|
1349
1545
|
*
|
|
1350
|
-
*
|
|
1351
|
-
*
|
|
1352
|
-
* function to convert external IDs into valid Evolu IDs.
|
|
1546
|
+
* When integrating with external systems that use different ID formats, use
|
|
1547
|
+
* this function to convert external IDs into valid Evolu IDs.
|
|
1353
1548
|
*
|
|
1354
1549
|
* In Evolu's CRDT, the ID serves as the unique identifier for conflict
|
|
1355
1550
|
* resolution across distributed clients. When multiple clients create records
|
|
@@ -1370,9 +1565,9 @@ export const createId = <B extends string = never>(
|
|
|
1370
1565
|
* });
|
|
1371
1566
|
* ```
|
|
1372
1567
|
*
|
|
1373
|
-
* **Important**: This transformation is one-way.
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1568
|
+
* **Important**: This transformation is one-way. We cannot recover the original
|
|
1569
|
+
* external string from the generated {@link Id}. If we need to preserve the
|
|
1570
|
+
* original external ID, store it in a separate column.
|
|
1376
1571
|
*
|
|
1377
1572
|
* @category String
|
|
1378
1573
|
*/
|
|
@@ -1380,27 +1575,16 @@ export const createIdFromString = <B extends string = never>(
|
|
|
1380
1575
|
value: string,
|
|
1381
1576
|
): [B] extends [never] ? Id : Id & Brand<B> => {
|
|
1382
1577
|
const hash = sha256(utf8ToBytes(value));
|
|
1578
|
+
// Take first 16 bytes of hash and convert to Id
|
|
1579
|
+
const id = idBytesToId(hash.slice(0, 16) as IdBytes);
|
|
1383
1580
|
|
|
1384
|
-
|
|
1385
|
-
let buffer = 0;
|
|
1386
|
-
let bits = 0;
|
|
1387
|
-
|
|
1388
|
-
for (const byte of hash) {
|
|
1389
|
-
buffer = (buffer << 8) | byte;
|
|
1390
|
-
bits += 8;
|
|
1391
|
-
|
|
1392
|
-
while (bits >= 6 && output.length < idTypeValueLength) {
|
|
1393
|
-
bits -= 6;
|
|
1394
|
-
const index = (buffer >> bits) & 0b111111;
|
|
1395
|
-
output += base64UrlAlphabet[index];
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
return output as [B] extends [never] ? Id : Id & Brand<B>;
|
|
1581
|
+
return id as [B] extends [never] ? Id : Id & Brand<B>;
|
|
1400
1582
|
};
|
|
1401
1583
|
|
|
1402
1584
|
/**
|
|
1403
|
-
*
|
|
1585
|
+
* Creates a branded {@link Id} Type for a table's primary key.
|
|
1586
|
+
*
|
|
1587
|
+
* The table name becomes an additional brand for type safety.
|
|
1404
1588
|
*
|
|
1405
1589
|
* ### Example
|
|
1406
1590
|
*
|
|
@@ -1412,53 +1596,62 @@ export const createIdFromString = <B extends string = never>(
|
|
|
1412
1596
|
*
|
|
1413
1597
|
* @category String
|
|
1414
1598
|
*/
|
|
1415
|
-
export const id = <Table extends TypeName>(table: Table):
|
|
1599
|
+
export const id = <Table extends TypeName>(table: Table): TableId<Table> => {
|
|
1600
|
+
const fromUnknown = (value: unknown) => {
|
|
1601
|
+
const parentResult = String.fromUnknown(value);
|
|
1602
|
+
if (!parentResult.ok) return parentResult;
|
|
1603
|
+
return fromParent(parentResult.value);
|
|
1604
|
+
};
|
|
1605
|
+
|
|
1416
1606
|
const fromParent = (value: string) => {
|
|
1417
1607
|
const idResult = Id.fromParent(value);
|
|
1418
1608
|
if (!idResult.ok) {
|
|
1419
|
-
return err<
|
|
1609
|
+
return err<TableIdError<Table>>({ type: "TableId", value, table });
|
|
1420
1610
|
}
|
|
1421
1611
|
return ok(idResult.value as Id & Brand<Table>);
|
|
1422
1612
|
};
|
|
1423
1613
|
|
|
1424
|
-
const fromUnknown = (value: unknown) => {
|
|
1425
|
-
const parentResult = String.fromUnknown(value);
|
|
1426
|
-
if (!parentResult.ok) return parentResult;
|
|
1427
|
-
return fromParent(parentResult.value);
|
|
1428
|
-
};
|
|
1429
|
-
|
|
1430
1614
|
return {
|
|
1431
|
-
...createType("Id", {
|
|
1432
|
-
fromUnknown,
|
|
1433
|
-
to: (value: Id & Brand<Table>) => value as string,
|
|
1434
|
-
fromParent,
|
|
1435
|
-
toParent: (value: Id & Brand<Table>) => value as string,
|
|
1436
|
-
}),
|
|
1615
|
+
...createType("Id", { fromUnknown, fromParent }),
|
|
1437
1616
|
table,
|
|
1438
1617
|
};
|
|
1439
1618
|
};
|
|
1440
1619
|
|
|
1441
|
-
export interface
|
|
1620
|
+
export interface TableId<Table extends TypeName>
|
|
1442
1621
|
extends Type<
|
|
1443
1622
|
"Id",
|
|
1444
1623
|
string & Brand<"Id"> & Brand<Table>,
|
|
1445
1624
|
string,
|
|
1446
|
-
|
|
1625
|
+
TableIdError<Table>,
|
|
1447
1626
|
string,
|
|
1448
1627
|
StringError
|
|
1449
1628
|
> {
|
|
1450
1629
|
table: Table;
|
|
1451
1630
|
}
|
|
1452
1631
|
|
|
1453
|
-
export interface
|
|
1454
|
-
extends TypeError<"
|
|
1632
|
+
export interface TableIdError<Table extends TypeName = TypeName>
|
|
1633
|
+
extends TypeError<"TableId"> {
|
|
1455
1634
|
readonly table: Table;
|
|
1456
1635
|
}
|
|
1457
1636
|
|
|
1458
|
-
export const
|
|
1637
|
+
export const formatTableIdError = createTypeErrorFormatter<TableIdError>(
|
|
1459
1638
|
(error) => `Invalid ${error.type} table Id: ${error.value}`,
|
|
1460
1639
|
);
|
|
1461
1640
|
|
|
1641
|
+
/** Binary representation of an {@link Id}. */
|
|
1642
|
+
export const IdBytes = brand("IdBytes", length(16)(Uint8Array));
|
|
1643
|
+
export type IdBytes = typeof IdBytes.Type;
|
|
1644
|
+
|
|
1645
|
+
export const idBytesTypeValueLength = 16 as NonNegativeInt;
|
|
1646
|
+
|
|
1647
|
+
export const idToIdBytes = (id: Id): IdBytes =>
|
|
1648
|
+
// Id is Base64Url (validated by Id.from), cast is safe
|
|
1649
|
+
base64UrlToUint8Array(id as unknown as Base64Url) as IdBytes;
|
|
1650
|
+
|
|
1651
|
+
export const idBytesToId = (idBytes: IdBytes): Id =>
|
|
1652
|
+
// Base64Url encoding of 16 bytes always produces valid Id (22 chars)
|
|
1653
|
+
uint8ArrayToBase64Url(idBytes) as unknown as Id;
|
|
1654
|
+
|
|
1462
1655
|
/**
|
|
1463
1656
|
* Positive number.
|
|
1464
1657
|
*
|
|
@@ -1624,6 +1817,11 @@ export type NonNegativeInt = typeof NonNegativeInt.Type;
|
|
|
1624
1817
|
export const PositiveInt = positive(NonNegativeInt);
|
|
1625
1818
|
export type PositiveInt = typeof PositiveInt.Type;
|
|
1626
1819
|
|
|
1820
|
+
/** Maximum safe positive integer value for practically infinite operations. */
|
|
1821
|
+
export const maxPositiveInt = PositiveInt.orThrow(
|
|
1822
|
+
globalThis.Number.MAX_SAFE_INTEGER,
|
|
1823
|
+
);
|
|
1824
|
+
|
|
1627
1825
|
/** @category Number */
|
|
1628
1826
|
export const NonPositiveInt = nonPositive(Int);
|
|
1629
1827
|
export type NonPositiveInt = typeof NonPositiveInt.Type;
|
|
@@ -1848,10 +2046,6 @@ export const formatBetweenError = createTypeErrorFormatter<BetweenError>(
|
|
|
1848
2046
|
`The value ${error.value} is not between ${error.min} and ${error.max}, inclusive.`,
|
|
1849
2047
|
);
|
|
1850
2048
|
|
|
1851
|
-
/** @category Number */
|
|
1852
|
-
export const Between1And10 = between(1, 10)(Number);
|
|
1853
|
-
export type Between1And10 = typeof Between1And10.Type;
|
|
1854
|
-
|
|
1855
2049
|
/**
|
|
1856
2050
|
* Literal {@link Type}.
|
|
1857
2051
|
*
|
|
@@ -1878,12 +2072,10 @@ export const literal = <T extends Literal>(expected: T): LiteralType<T> => {
|
|
|
1878
2072
|
return {
|
|
1879
2073
|
...createType("Literal", {
|
|
1880
2074
|
fromUnknown,
|
|
1881
|
-
to: identity as (value: T) => WidenLiteral<T>,
|
|
1882
2075
|
fromParent: ok,
|
|
1883
|
-
toParent: identity,
|
|
1884
2076
|
}),
|
|
1885
2077
|
expected,
|
|
1886
|
-
}
|
|
2078
|
+
} as LiteralType<T>;
|
|
1887
2079
|
};
|
|
1888
2080
|
|
|
1889
2081
|
export interface LiteralType<T extends Literal>
|
|
@@ -1903,145 +2095,6 @@ export const formatLiteralError = createTypeErrorFormatter<LiteralError>(
|
|
|
1903
2095
|
)}.`,
|
|
1904
2096
|
);
|
|
1905
2097
|
|
|
1906
|
-
/**
|
|
1907
|
-
* {@link Type} that transforms values between `FromType` and `ToType`.
|
|
1908
|
-
*
|
|
1909
|
-
* - `fromParent`: Converts `FromType` to `ToType`, may fail.
|
|
1910
|
-
* - `toParent`: Converts `ToType` back to `FromType`, must not fail.
|
|
1911
|
-
*
|
|
1912
|
-
* ### Example
|
|
1913
|
-
*
|
|
1914
|
-
* // TODO: Examples
|
|
1915
|
-
*
|
|
1916
|
-
* @category Base Factories
|
|
1917
|
-
*/
|
|
1918
|
-
export const transform = <
|
|
1919
|
-
FromType extends AnyType,
|
|
1920
|
-
ToType extends AnyType,
|
|
1921
|
-
TransformError extends TypeError = never,
|
|
1922
|
-
>(
|
|
1923
|
-
fromType: FromType,
|
|
1924
|
-
toType: ToType,
|
|
1925
|
-
fromParent: (
|
|
1926
|
-
parentValue: InferType<FromType>,
|
|
1927
|
-
) => Result<InferType<ToType>, TransformError>,
|
|
1928
|
-
toParent: (value: InferType<ToType>) => InferType<FromType>,
|
|
1929
|
-
): TransformType<FromType, ToType, TransformError> => {
|
|
1930
|
-
const fromUnknown = (
|
|
1931
|
-
value: unknown,
|
|
1932
|
-
): Result<InferType<ToType>, InferErrors<FromType> | TransformError> => {
|
|
1933
|
-
const parentResult = fromType.fromUnknown(value);
|
|
1934
|
-
if (!parentResult.ok) return parentResult;
|
|
1935
|
-
return fromParent(parentResult.value as IntentionalNever);
|
|
1936
|
-
};
|
|
1937
|
-
|
|
1938
|
-
const to = (value: InferType<ToType>): InferInput<FromType> =>
|
|
1939
|
-
fromType.to(toParent(value)) as IntentionalNever;
|
|
1940
|
-
|
|
1941
|
-
return {
|
|
1942
|
-
...createType("Transform", {
|
|
1943
|
-
fromUnknown,
|
|
1944
|
-
to,
|
|
1945
|
-
fromParent,
|
|
1946
|
-
toParent,
|
|
1947
|
-
}),
|
|
1948
|
-
is: toType.is,
|
|
1949
|
-
fromType,
|
|
1950
|
-
toType,
|
|
1951
|
-
} as TransformType<FromType, ToType, TransformError>;
|
|
1952
|
-
};
|
|
1953
|
-
|
|
1954
|
-
/**
|
|
1955
|
-
* TransformType extends {@link Type} with additional `fromType` and `toType`
|
|
1956
|
-
* properties for reflection.
|
|
1957
|
-
*/
|
|
1958
|
-
export interface TransformType<
|
|
1959
|
-
FromType extends AnyType,
|
|
1960
|
-
ToType extends AnyType,
|
|
1961
|
-
TransformError extends TypeError = never,
|
|
1962
|
-
> extends Type<
|
|
1963
|
-
"Transform",
|
|
1964
|
-
InferType<ToType>,
|
|
1965
|
-
InferInput<FromType>,
|
|
1966
|
-
TransformError,
|
|
1967
|
-
InferType<FromType>,
|
|
1968
|
-
InferErrors<FromType>
|
|
1969
|
-
> {
|
|
1970
|
-
readonly fromType: FromType;
|
|
1971
|
-
readonly toType: ToType;
|
|
1972
|
-
|
|
1973
|
-
readonly fromParent: (
|
|
1974
|
-
value: InferType<FromType>,
|
|
1975
|
-
) => [TransformError] extends [never]
|
|
1976
|
-
? Ok<InferType<ToType>>
|
|
1977
|
-
: Result<InferType<ToType>, TransformError>;
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
/**
|
|
1981
|
-
* Trims leading and trailing whitespace from a string.
|
|
1982
|
-
*
|
|
1983
|
-
* ### Example
|
|
1984
|
-
*
|
|
1985
|
-
* ```ts
|
|
1986
|
-
* expect(TrimString.from("a ")).toEqual(ok("a"));
|
|
1987
|
-
* expect(TrimString.fromParent("a ").value).toEqual("a");
|
|
1988
|
-
* ```
|
|
1989
|
-
*
|
|
1990
|
-
* @category String
|
|
1991
|
-
*/
|
|
1992
|
-
export const TrimString = trim(String);
|
|
1993
|
-
|
|
1994
|
-
/**
|
|
1995
|
-
* Transforms a {@link Date} into a {@link DateIsoString} string and vice versa.
|
|
1996
|
-
*
|
|
1997
|
-
* ### Example
|
|
1998
|
-
*
|
|
1999
|
-
* ```ts
|
|
2000
|
-
* DateIso.from(new Date("2023-12-25T10:30:00.000Z")); // ok("2023-12-25T10:30:00.000Z")
|
|
2001
|
-
* DateIso.to("2023-12-25T10:30:00.000Z"); // Date object
|
|
2002
|
-
* DateIso.from(new Date("invalid")); // err({ type: "DateIsoString", value: "Invalid Date" })
|
|
2003
|
-
* ```
|
|
2004
|
-
*
|
|
2005
|
-
* @category String
|
|
2006
|
-
*/
|
|
2007
|
-
export const DateIso = transform(
|
|
2008
|
-
Date,
|
|
2009
|
-
DateIsoString,
|
|
2010
|
-
(value) => DateIsoString.fromParent(value.toISOString()),
|
|
2011
|
-
(value) => new globalThis.Date(value),
|
|
2012
|
-
);
|
|
2013
|
-
|
|
2014
|
-
/**
|
|
2015
|
-
* Transforms a {@link NonEmptyTrimmedString} into a {@link FiniteNumber}.
|
|
2016
|
-
*
|
|
2017
|
-
* ### Example
|
|
2018
|
-
*
|
|
2019
|
-
* ```ts
|
|
2020
|
-
* NumberFromString.from("42"); // ok(42)
|
|
2021
|
-
* NumberFromString.from("abc"); // err({ type: "NumberFromString", value: "abc" })
|
|
2022
|
-
* ```
|
|
2023
|
-
*
|
|
2024
|
-
* @category Number
|
|
2025
|
-
*/
|
|
2026
|
-
export const NumberFromString = transform(
|
|
2027
|
-
NonEmptyTrimmedString,
|
|
2028
|
-
FiniteNumber,
|
|
2029
|
-
(value) => {
|
|
2030
|
-
const result = FiniteNumber.fromParent(globalThis.Number(value));
|
|
2031
|
-
if (!result.ok)
|
|
2032
|
-
return err<NumberFromStringError>({ type: "NumberFromString", value });
|
|
2033
|
-
return result;
|
|
2034
|
-
},
|
|
2035
|
-
(num) => num.toString() as NonEmptyTrimmedString,
|
|
2036
|
-
);
|
|
2037
|
-
|
|
2038
|
-
export interface NumberFromStringError extends TypeError<"NumberFromString"> {}
|
|
2039
|
-
|
|
2040
|
-
export const formatNumberFromStringError =
|
|
2041
|
-
createTypeErrorFormatter<NumberFromStringError>((error) => {
|
|
2042
|
-
return `The value ${error.value} could not be converted to a finite number.`;
|
|
2043
|
-
});
|
|
2044
|
-
|
|
2045
2098
|
/**
|
|
2046
2099
|
* Array of a specific {@link Type}.
|
|
2047
2100
|
*
|
|
@@ -2094,9 +2147,6 @@ export const array = <ElementType extends AnyType>(
|
|
|
2094
2147
|
return ok(result);
|
|
2095
2148
|
};
|
|
2096
2149
|
|
|
2097
|
-
const to = (value: ReadonlyArray<InferType<ElementType>>) =>
|
|
2098
|
-
value.map(element.to) as ReadonlyArray<InferInput<ElementType>>;
|
|
2099
|
-
|
|
2100
2150
|
const fromParent = (
|
|
2101
2151
|
value: ReadonlyArray<InferParent<ElementType>>,
|
|
2102
2152
|
): Result<
|
|
@@ -2122,18 +2172,10 @@ export const array = <ElementType extends AnyType>(
|
|
|
2122
2172
|
return ok(result);
|
|
2123
2173
|
};
|
|
2124
2174
|
|
|
2125
|
-
const toParent = (values: ReadonlyArray<InferType<ElementType>>) =>
|
|
2126
|
-
values.map(element.toParent) as ReadonlyArray<InferParent<ElementType>>;
|
|
2127
|
-
|
|
2128
2175
|
return {
|
|
2129
|
-
...createType("Array", {
|
|
2130
|
-
fromUnknown,
|
|
2131
|
-
to,
|
|
2132
|
-
fromParent,
|
|
2133
|
-
toParent,
|
|
2134
|
-
}),
|
|
2176
|
+
...createType("Array", { fromUnknown, fromParent }),
|
|
2135
2177
|
element,
|
|
2136
|
-
}
|
|
2178
|
+
};
|
|
2137
2179
|
};
|
|
2138
2180
|
|
|
2139
2181
|
/** ArrayType extends Type with an additional `element` property for reflection. */
|
|
@@ -2266,16 +2308,6 @@ export const record = <
|
|
|
2266
2308
|
return ok(result);
|
|
2267
2309
|
};
|
|
2268
2310
|
|
|
2269
|
-
const to = (
|
|
2270
|
-
value: Readonly<Record<KeyT, InferType<Value>>>,
|
|
2271
|
-
): Readonly<Record<KeyInput, InferInput<Value>>> =>
|
|
2272
|
-
Object.fromEntries(
|
|
2273
|
-
Object.entries(value).map(([key, val]) => [
|
|
2274
|
-
keyType.to(key as KeyT),
|
|
2275
|
-
valueType.to(val),
|
|
2276
|
-
]),
|
|
2277
|
-
) as Readonly<Record<KeyInput, InferInput<Value>>>;
|
|
2278
|
-
|
|
2279
2311
|
const fromParent = (
|
|
2280
2312
|
value: Readonly<Record<KeyParent, InferParent<Value>>>,
|
|
2281
2313
|
): Result<
|
|
@@ -2320,22 +2352,10 @@ export const record = <
|
|
|
2320
2352
|
return ok(result);
|
|
2321
2353
|
};
|
|
2322
2354
|
|
|
2323
|
-
const toParent = (
|
|
2324
|
-
value: Readonly<Record<KeyT, InferType<Value>>>,
|
|
2325
|
-
): Readonly<Record<KeyParent, InferParent<Value>>> =>
|
|
2326
|
-
Object.fromEntries(
|
|
2327
|
-
Object.entries(value).map(([key, val]) => [
|
|
2328
|
-
keyType.toParent(key as KeyT),
|
|
2329
|
-
valueType.toParent(val),
|
|
2330
|
-
]),
|
|
2331
|
-
) as Readonly<Record<KeyParent, InferParent<Value>>>;
|
|
2332
|
-
|
|
2333
2355
|
return {
|
|
2334
2356
|
...createType("Record", {
|
|
2335
2357
|
fromUnknown,
|
|
2336
|
-
to,
|
|
2337
2358
|
fromParent,
|
|
2338
|
-
toParent,
|
|
2339
2359
|
}),
|
|
2340
2360
|
key: keyType,
|
|
2341
2361
|
value: valueType,
|
|
@@ -2635,25 +2655,6 @@ export function object(
|
|
|
2635
2655
|
return ok(result);
|
|
2636
2656
|
};
|
|
2637
2657
|
|
|
2638
|
-
const to = (value: any) => {
|
|
2639
|
-
const entries: [string, any][] = [];
|
|
2640
|
-
for (const key of propKeys) {
|
|
2641
|
-
if (!(key in value) && isOptionalType(props[key])) {
|
|
2642
|
-
continue;
|
|
2643
|
-
}
|
|
2644
|
-
entries.push([key, props[key].to(value[key])]);
|
|
2645
|
-
}
|
|
2646
|
-
if (record) {
|
|
2647
|
-
const recordEntries = Object.entries(value).filter(
|
|
2648
|
-
([key]) => !propKeys.includes(key),
|
|
2649
|
-
);
|
|
2650
|
-
for (const [key, val] of recordEntries) {
|
|
2651
|
-
entries.push([record.key.to(key), record.value.to(val)]);
|
|
2652
|
-
}
|
|
2653
|
-
}
|
|
2654
|
-
return Object.fromEntries(entries);
|
|
2655
|
-
};
|
|
2656
|
-
|
|
2657
2658
|
const fromParent = (value: any): Result<any, any> => {
|
|
2658
2659
|
const errors: Record<string, any> = {};
|
|
2659
2660
|
const result: Record<string, any> = {};
|
|
@@ -2705,31 +2706,10 @@ export function object(
|
|
|
2705
2706
|
return ok(result);
|
|
2706
2707
|
};
|
|
2707
2708
|
|
|
2708
|
-
const toParent = (value: any) => {
|
|
2709
|
-
const entries: [string, any][] = [];
|
|
2710
|
-
for (const key of propKeys) {
|
|
2711
|
-
if (!(key in value) && isOptionalType(props[key])) {
|
|
2712
|
-
continue;
|
|
2713
|
-
}
|
|
2714
|
-
entries.push([key, props[key].toParent(value[key])]);
|
|
2715
|
-
}
|
|
2716
|
-
if (record) {
|
|
2717
|
-
const recordEntries = Object.entries(value).filter(
|
|
2718
|
-
([key]) => !propKeys.includes(key),
|
|
2719
|
-
);
|
|
2720
|
-
for (const [key, val] of recordEntries) {
|
|
2721
|
-
entries.push([record.key.toParent(key), record.value.toParent(val)]);
|
|
2722
|
-
}
|
|
2723
|
-
}
|
|
2724
|
-
return Object.fromEntries(entries);
|
|
2725
|
-
};
|
|
2726
|
-
|
|
2727
2709
|
return {
|
|
2728
2710
|
...createType(record ? "ObjectWithRecord" : "Object", {
|
|
2729
2711
|
fromUnknown,
|
|
2730
|
-
to,
|
|
2731
2712
|
fromParent,
|
|
2732
|
-
toParent,
|
|
2733
2713
|
}),
|
|
2734
2714
|
props,
|
|
2735
2715
|
...(record ? { record } : {}),
|
|
@@ -2971,7 +2951,6 @@ export function union(...args: ReadonlyArray<any>): any {
|
|
|
2971
2951
|
* cases.
|
|
2972
2952
|
*/
|
|
2973
2953
|
|
|
2974
|
-
/* eslint-disable */
|
|
2975
2954
|
const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
|
|
2976
2955
|
|
|
2977
2956
|
const fromUnknown = (value: unknown) => {
|
|
@@ -2989,23 +2968,13 @@ export function union(...args: ReadonlyArray<any>): any {
|
|
|
2989
2968
|
});
|
|
2990
2969
|
};
|
|
2991
2970
|
|
|
2992
|
-
const to = (value: any) => {
|
|
2993
|
-
for (const member of members) {
|
|
2994
|
-
if (member.is(value)) return member.to(value);
|
|
2995
|
-
}
|
|
2996
|
-
assert(false, "No matching member found in Union Type `to` function");
|
|
2997
|
-
};
|
|
2998
|
-
|
|
2999
2971
|
return {
|
|
3000
2972
|
...createType("Union", {
|
|
3001
2973
|
fromUnknown,
|
|
3002
|
-
to,
|
|
3003
2974
|
fromParent: fromUnknown,
|
|
3004
|
-
toParent: to,
|
|
3005
2975
|
}),
|
|
3006
2976
|
members,
|
|
3007
2977
|
};
|
|
3008
|
-
/* eslint-enable */
|
|
3009
2978
|
}
|
|
3010
2979
|
|
|
3011
2980
|
/**
|
|
@@ -3099,20 +3068,10 @@ export const recursive = <ParentType extends AnyType>(
|
|
|
3099
3068
|
type ??= create();
|
|
3100
3069
|
return type.fromUnknown(value);
|
|
3101
3070
|
},
|
|
3102
|
-
to: (value: T) => {
|
|
3103
|
-
type ??= create();
|
|
3104
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
3105
|
-
return type.to(value);
|
|
3106
|
-
},
|
|
3107
3071
|
fromParent: (value: Parent) => {
|
|
3108
3072
|
type ??= create();
|
|
3109
3073
|
return type.fromParent(value);
|
|
3110
3074
|
},
|
|
3111
|
-
toParent: (value: T) => {
|
|
3112
|
-
type ??= create();
|
|
3113
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
3114
|
-
return type.toParent(value);
|
|
3115
|
-
},
|
|
3116
3075
|
is: (value: unknown): value is T => {
|
|
3117
3076
|
type ??= create();
|
|
3118
3077
|
return type.is(value);
|
|
@@ -3250,9 +3209,6 @@ export const tuple = <Elements extends [AnyType, ...ReadonlyArray<AnyType>]>(
|
|
|
3250
3209
|
return ok(result);
|
|
3251
3210
|
};
|
|
3252
3211
|
|
|
3253
|
-
const to = (value: { [K in keyof Elements]: InferType<Elements[K]> }) =>
|
|
3254
|
-
value.map((val, index) => elements[index].to(val) as IntentionalNever);
|
|
3255
|
-
|
|
3256
3212
|
const fromParent = (value: {
|
|
3257
3213
|
[K in keyof Elements]: InferParent<Elements[K]>;
|
|
3258
3214
|
}) => {
|
|
@@ -3284,21 +3240,10 @@ export const tuple = <Elements extends [AnyType, ...ReadonlyArray<AnyType>]>(
|
|
|
3284
3240
|
return ok(result as { [K in keyof Elements]: InferType<Elements[K]> });
|
|
3285
3241
|
};
|
|
3286
3242
|
|
|
3287
|
-
const toParent = (value: { [K in keyof Elements]: InferType<Elements[K]> }): {
|
|
3288
|
-
[K in keyof Elements]: InferParent<Elements[K]>;
|
|
3289
|
-
} =>
|
|
3290
|
-
value.map(
|
|
3291
|
-
(val, index) => elements[index].toParent(val) as IntentionalNever,
|
|
3292
|
-
) as {
|
|
3293
|
-
[K in keyof Elements]: InferParent<Elements[K]>;
|
|
3294
|
-
};
|
|
3295
|
-
|
|
3296
3243
|
return {
|
|
3297
3244
|
...createType("Tuple", {
|
|
3298
3245
|
fromUnknown,
|
|
3299
|
-
to,
|
|
3300
3246
|
fromParent,
|
|
3301
|
-
toParent,
|
|
3302
3247
|
}),
|
|
3303
3248
|
elements,
|
|
3304
3249
|
} as IntentionalNever;
|
|
@@ -3374,37 +3319,41 @@ export const formatInt64Error = createTypeErrorFormatter<Int64Error>(
|
|
|
3374
3319
|
`The value ${error.value} is not a valid 64-bit signed integer (Int64).`,
|
|
3375
3320
|
);
|
|
3376
3321
|
|
|
3377
|
-
|
|
3378
|
-
String,
|
|
3379
|
-
BigInt,
|
|
3380
|
-
(value) =>
|
|
3381
|
-
trySync(
|
|
3382
|
-
() => globalThis.BigInt(value),
|
|
3383
|
-
(): BigIntFromStringError => ({ type: "BigIntFromString", value }),
|
|
3384
|
-
),
|
|
3385
|
-
(value) => value.toString(),
|
|
3386
|
-
);
|
|
3322
|
+
// // co s timhle? je to string, ze ktereho lze udelat bigint
|
|
3387
3323
|
|
|
3388
|
-
export
|
|
3324
|
+
// export const BigIntFromString = transform(
|
|
3325
|
+
// String,
|
|
3326
|
+
// BigInt,
|
|
3327
|
+
// (value) =>
|
|
3328
|
+
// trySync(
|
|
3329
|
+
// () => globalThis.BigInt(value),
|
|
3330
|
+
// (): BigIntFromStringError => ({ type: "BigIntFromString", value }),
|
|
3331
|
+
// ),
|
|
3332
|
+
// (value) => value.toString(),
|
|
3333
|
+
// );
|
|
3389
3334
|
|
|
3390
|
-
export
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3335
|
+
// export interface BigIntFromStringError extends TypeError<"BigIntFromString"> {}
|
|
3336
|
+
|
|
3337
|
+
// export const formatBigIntFromStringError =
|
|
3338
|
+
// createTypeErrorFormatter<BigIntFromStringError>(
|
|
3339
|
+
// (error) => `The value ${error.value} could not be converted to a BigInt.`,
|
|
3340
|
+
// );
|
|
3394
3341
|
|
|
3395
3342
|
/**
|
|
3396
3343
|
* Stringified {@link Int64}.
|
|
3397
3344
|
*
|
|
3398
|
-
* @category
|
|
3345
|
+
* @category String
|
|
3399
3346
|
*/
|
|
3400
|
-
export const Int64String = brand("Int64",
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
})
|
|
3347
|
+
export const Int64String = brand("Int64", NonEmptyTrimmedString, (value) =>
|
|
3348
|
+
trySync(
|
|
3349
|
+
() => {
|
|
3350
|
+
const maybeInt = globalThis.BigInt(value);
|
|
3351
|
+
Int64.orThrow(maybeInt);
|
|
3352
|
+
return value;
|
|
3353
|
+
},
|
|
3354
|
+
(): Int64StringError => ({ type: "Int64String", value }),
|
|
3355
|
+
),
|
|
3356
|
+
);
|
|
3408
3357
|
|
|
3409
3358
|
export type Int64String = typeof Int64String.Type;
|
|
3410
3359
|
|
|
@@ -3509,42 +3458,14 @@ export const JsonArray = array(JsonValue);
|
|
|
3509
3458
|
*/
|
|
3510
3459
|
export const JsonObject = record(String, JsonValue);
|
|
3511
3460
|
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
* JsonValueFromString.to({ key: "value" }); // -> '{"key":"value"}'
|
|
3521
|
-
* ```
|
|
3522
|
-
*
|
|
3523
|
-
* @category String
|
|
3524
|
-
*/
|
|
3525
|
-
export const JsonValueFromString = transform(
|
|
3526
|
-
String,
|
|
3527
|
-
JsonValue,
|
|
3528
|
-
(value) =>
|
|
3529
|
-
trySync(
|
|
3530
|
-
() => JSON.parse(value) as JsonValue,
|
|
3531
|
-
(error): JsonValueFromStringError => ({
|
|
3532
|
-
type: `JsonValueFromString`,
|
|
3533
|
-
value,
|
|
3534
|
-
message: globalThis.String(error),
|
|
3535
|
-
}),
|
|
3536
|
-
),
|
|
3537
|
-
(value) => JSON.stringify(value),
|
|
3538
|
-
);
|
|
3539
|
-
|
|
3540
|
-
export interface JsonValueFromStringError
|
|
3541
|
-
extends TypeError<"JsonValueFromString"> {
|
|
3542
|
-
readonly message: string;
|
|
3543
|
-
}
|
|
3544
|
-
|
|
3545
|
-
export const formatJsonValueFromStringError =
|
|
3546
|
-
createTypeErrorFormatter<JsonValueFromStringError>(
|
|
3547
|
-
(error) => `Invalid JSONValue: ${error.value}. Error: ${error.message}`,
|
|
3461
|
+
export const parseJson = (value: string): Result<JsonValue, JsonError> =>
|
|
3462
|
+
trySync(
|
|
3463
|
+
() => JSON.parse(value) as JsonValue,
|
|
3464
|
+
(error): JsonError => ({
|
|
3465
|
+
type: `Json`,
|
|
3466
|
+
value,
|
|
3467
|
+
message: globalThis.String(error),
|
|
3468
|
+
}),
|
|
3548
3469
|
);
|
|
3549
3470
|
|
|
3550
3471
|
/**
|
|
@@ -3553,20 +3474,15 @@ export const formatJsonValueFromStringError =
|
|
|
3553
3474
|
* ### Example
|
|
3554
3475
|
*
|
|
3555
3476
|
* ```ts
|
|
3556
|
-
* const result = Json.from('{"key":"value"}'); //
|
|
3557
|
-
* const error = Json.from("invalid json"); //
|
|
3477
|
+
* const result = Json.from('{"key":"value"}'); // ok
|
|
3478
|
+
* const error = Json.from("invalid json"); // err
|
|
3558
3479
|
* ```
|
|
3559
3480
|
*
|
|
3560
3481
|
* @category String
|
|
3561
3482
|
*/
|
|
3562
3483
|
export const Json = brand("Json", String, (value) => {
|
|
3563
|
-
const result =
|
|
3564
|
-
if (!result.ok)
|
|
3565
|
-
return err<JsonError>({
|
|
3566
|
-
type: "Json",
|
|
3567
|
-
value,
|
|
3568
|
-
message: result.error.message,
|
|
3569
|
-
});
|
|
3484
|
+
const result = parseJson(value);
|
|
3485
|
+
if (!result.ok) return result;
|
|
3570
3486
|
return ok(value);
|
|
3571
3487
|
});
|
|
3572
3488
|
|
|
@@ -3580,6 +3496,80 @@ export const formatJsonError = createTypeErrorFormatter<JsonError>(
|
|
|
3580
3496
|
(error) => `Invalid JSON: ${error.value}. Error: ${error.message}`,
|
|
3581
3497
|
);
|
|
3582
3498
|
|
|
3499
|
+
export const jsonValueToJson = (value: JsonValue): Json =>
|
|
3500
|
+
JSON.stringify(value) as Json;
|
|
3501
|
+
|
|
3502
|
+
export const jsonToJsonValue = (value: Json): JsonValue =>
|
|
3503
|
+
JSON.parse(value) as JsonValue;
|
|
3504
|
+
|
|
3505
|
+
/**
|
|
3506
|
+
* Creates a branded JSON string {@link Type} and type-safe conversion functions
|
|
3507
|
+
* for a given Type.
|
|
3508
|
+
*
|
|
3509
|
+
* This factory creates:
|
|
3510
|
+
*
|
|
3511
|
+
* 1. A branded string Type that validates JSON parsing and structural conformity
|
|
3512
|
+
* 2. A serialization function (Type → branded JSON string)
|
|
3513
|
+
* 3. A parsing function (branded JSON string → Type, skipping validation)
|
|
3514
|
+
*
|
|
3515
|
+
* Optimized for Evolu's SQLite workflow where we store typed JSON strings and
|
|
3516
|
+
* need type-safe conversions without double parsing.
|
|
3517
|
+
*
|
|
3518
|
+
* ### Example
|
|
3519
|
+
*
|
|
3520
|
+
* ```ts
|
|
3521
|
+
* const Person = object({
|
|
3522
|
+
* name: NonEmptyString100,
|
|
3523
|
+
* age: FiniteNumber,
|
|
3524
|
+
* });
|
|
3525
|
+
* type Person = typeof Person.Type;
|
|
3526
|
+
*
|
|
3527
|
+
* const [PersonJson, personToPersonJson, personJsonToPerson] = json(
|
|
3528
|
+
* Person,
|
|
3529
|
+
* "PersonJson",
|
|
3530
|
+
* );
|
|
3531
|
+
* // string & Brand<"PersonJson">
|
|
3532
|
+
* type PersonJson = typeof PersonJson.Type;
|
|
3533
|
+
*
|
|
3534
|
+
* // Usage:
|
|
3535
|
+
* const person: Person = { name: "Alice", age: 30 };
|
|
3536
|
+
* const jsonString = personToPersonJson(person); // PersonJson
|
|
3537
|
+
* const backToPerson = personJsonToPerson(jsonString); // Person
|
|
3538
|
+
* ```
|
|
3539
|
+
*/
|
|
3540
|
+
export const json = <T extends AnyType, Name extends TypeName>(
|
|
3541
|
+
type: T,
|
|
3542
|
+
name: Name,
|
|
3543
|
+
): [
|
|
3544
|
+
BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>,
|
|
3545
|
+
(
|
|
3546
|
+
value: InferType<T>,
|
|
3547
|
+
) => InferType<
|
|
3548
|
+
BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>
|
|
3549
|
+
>,
|
|
3550
|
+
(
|
|
3551
|
+
value: InferType<
|
|
3552
|
+
BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>
|
|
3553
|
+
>,
|
|
3554
|
+
) => InferType<T>,
|
|
3555
|
+
] => {
|
|
3556
|
+
const BrandedJsonType = brand(name, String, (value) => {
|
|
3557
|
+
const parseResult = parseJson(value);
|
|
3558
|
+
if (!parseResult.ok) return parseResult;
|
|
3559
|
+
|
|
3560
|
+
const validationResult = type.fromUnknown(parseResult.value);
|
|
3561
|
+
if (!validationResult.ok) return validationResult;
|
|
3562
|
+
|
|
3563
|
+
return ok(value);
|
|
3564
|
+
}) as BrandType<typeof String, Name, JsonError | InferErrors<T>, StringError>;
|
|
3565
|
+
|
|
3566
|
+
return [
|
|
3567
|
+
BrandedJsonType,
|
|
3568
|
+
jsonValueToJson as IntentionalNever,
|
|
3569
|
+
jsonToJsonValue as IntentionalNever,
|
|
3570
|
+
];
|
|
3571
|
+
};
|
|
3572
|
+
|
|
3583
3573
|
/**
|
|
3584
3574
|
* Optional {@link Type}.
|
|
3585
3575
|
*
|
|
@@ -3604,9 +3594,7 @@ export const formatJsonError = createTypeErrorFormatter<JsonError>(
|
|
|
3604
3594
|
export const optional = <T extends AnyType>(type: T): OptionalType<T> => ({
|
|
3605
3595
|
...createType("Optional", {
|
|
3606
3596
|
fromUnknown: type.fromUnknown,
|
|
3607
|
-
to: type.to,
|
|
3608
3597
|
fromParent: type.fromParent,
|
|
3609
|
-
toParent: type.toParent,
|
|
3610
3598
|
}),
|
|
3611
3599
|
parent: type,
|
|
3612
3600
|
});
|
|
@@ -3630,7 +3618,7 @@ export const isOptionalType = (x: unknown): x is OptionalType<any> =>
|
|
|
3630
3618
|
/**
|
|
3631
3619
|
* Creates a partial object type where all properties are optional.
|
|
3632
3620
|
*
|
|
3633
|
-
* This is useful when
|
|
3621
|
+
* This is useful when we want to validate an object in which none of the keys
|
|
3634
3622
|
* are required, but if they are present they must conform to their
|
|
3635
3623
|
* corresponding Types.
|
|
3636
3624
|
*
|
|
@@ -3735,68 +3723,39 @@ export function omit<T extends ObjectType<any>, Keys extends keyof T["props"]>(
|
|
|
3735
3723
|
return object(newProps);
|
|
3736
3724
|
}
|
|
3737
3725
|
|
|
3726
|
+
export const maxMutationSize = 655360;
|
|
3727
|
+
|
|
3738
3728
|
/**
|
|
3739
|
-
*
|
|
3740
|
-
*
|
|
3741
|
-
*
|
|
3742
|
-
*
|
|
3743
|
-
* ### Example
|
|
3744
|
-
*
|
|
3745
|
-
* ```ts
|
|
3746
|
-
* const Person = object({
|
|
3747
|
-
* name: NonEmptyString50,
|
|
3748
|
-
* age: FiniteNumber,
|
|
3749
|
-
* });
|
|
3750
|
-
* type Person = typeof Person.Type;
|
|
3751
|
-
*
|
|
3752
|
-
* const PersonJson = json(Person, "PersonJson");
|
|
3753
|
-
* // string & Brand<"PersonJson">
|
|
3754
|
-
* type PersonJson = typeof PersonJson.Type;
|
|
3755
|
-
*
|
|
3756
|
-
* // Person -> string & Brand<"PersonJson">
|
|
3757
|
-
* const personJson = PersonJson.from({ name: "Alice", age: 30 });
|
|
3758
|
-
* expect(personJson).toEqual(ok('{"name":"Alice","age":30}'));
|
|
3759
|
-
*
|
|
3760
|
-
* // string & Brand<"PersonJson"> -> Person
|
|
3761
|
-
* const person = PersonJson.to(personJson);
|
|
3762
|
-
*
|
|
3763
|
-
* // serialize/parse any JSON value
|
|
3764
|
-
* const AnyJson = json(JsonValue, "AnyJson");
|
|
3765
|
-
* ```
|
|
3729
|
+
* Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
|
|
3730
|
+
* the `maxProtocolMessageRangesSize`. The max size is 640KB in bytes, measured
|
|
3731
|
+
* via MessagePack. Evolu Protocol DbChange will be smaller thanks to various
|
|
3732
|
+
* optimizations.
|
|
3766
3733
|
*/
|
|
3767
|
-
export const
|
|
3734
|
+
export const validMutationSize = <T extends AnyType>(
|
|
3768
3735
|
type: T,
|
|
3769
|
-
|
|
3770
|
-
)
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
JsonValueFromStringError | T["Errors"],
|
|
3776
|
-
StringError
|
|
3777
|
-
>
|
|
3778
|
-
> => {
|
|
3779
|
-
type E = JsonValueFromStringError | T["Errors"];
|
|
3780
|
-
|
|
3781
|
-
const BrandedJsonString: BrandType<typeof String, Name, E, StringError> =
|
|
3782
|
-
brand(name, String, (value) => {
|
|
3783
|
-
const jsonValue = JsonValueFromString.fromParent(value);
|
|
3784
|
-
if (!jsonValue.ok) return jsonValue as Err<E>;
|
|
3785
|
-
|
|
3786
|
-
const parsed = type.fromUnknown(jsonValue.value);
|
|
3787
|
-
if (!parsed.ok) return parsed as Err<E>;
|
|
3736
|
+
): BrandType<T, "ValidMutationSize", ValidMutationSizeError, InferErrors<T>> =>
|
|
3737
|
+
brand("ValidMutationSize", type, (value) =>
|
|
3738
|
+
pack(value).byteLength <= maxMutationSize
|
|
3739
|
+
? ok(value)
|
|
3740
|
+
: err<ValidMutationSizeError>({ type: "ValidMutationSize", value }),
|
|
3741
|
+
);
|
|
3788
3742
|
|
|
3789
|
-
|
|
3790
|
-
|
|
3743
|
+
export interface ValidMutationSizeError
|
|
3744
|
+
extends TypeError<"ValidMutationSize"> {}
|
|
3791
3745
|
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
3797
|
-
(value) => JSON.parse(value) as InferType<T>,
|
|
3746
|
+
export const formatValidMutationSizeError =
|
|
3747
|
+
createTypeErrorFormatter<ValidMutationSizeError>(
|
|
3748
|
+
(error) =>
|
|
3749
|
+
`The mutation size exceeds the maximum limit of ${maxMutationSize} bytes. The provided mutation has a size of ${pack(error.value).byteLength} bytes.`,
|
|
3798
3750
|
);
|
|
3799
|
-
|
|
3751
|
+
|
|
3752
|
+
export type ValidMutationSize<Props extends Record<string, AnyType>> =
|
|
3753
|
+
BrandType<
|
|
3754
|
+
ObjectType<Props>,
|
|
3755
|
+
"ValidMutationSize",
|
|
3756
|
+
ValidMutationSizeError,
|
|
3757
|
+
InferErrors<ObjectType<Props>>
|
|
3758
|
+
>;
|
|
3800
3759
|
|
|
3801
3760
|
/**
|
|
3802
3761
|
* Union of all `TypeError`s defined in the `Type.ts` file, including base type
|
|
@@ -3823,16 +3782,16 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
|
|
|
3823
3782
|
| InstanceOfError
|
|
3824
3783
|
| EvoluTypeError
|
|
3825
3784
|
| CurrencyCodeError
|
|
3826
|
-
|
|
|
3785
|
+
| DateIsoError
|
|
3827
3786
|
| TrimmedError
|
|
3828
3787
|
| MinLengthError
|
|
3829
3788
|
| MaxLengthError
|
|
3830
3789
|
| LengthError
|
|
3831
3790
|
| MnemonicError
|
|
3832
3791
|
| RegexError
|
|
3833
|
-
| NanoIdError
|
|
3834
3792
|
| SimplePasswordError
|
|
3835
3793
|
| IdError
|
|
3794
|
+
| TableIdError
|
|
3836
3795
|
| PositiveError
|
|
3837
3796
|
| NegativeError
|
|
3838
3797
|
| NonPositiveError
|
|
@@ -3848,10 +3807,9 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
|
|
|
3848
3807
|
| BetweenError
|
|
3849
3808
|
| LiteralError
|
|
3850
3809
|
| Int64Error
|
|
3851
|
-
| BigIntFromStringError
|
|
3852
3810
|
| Int64StringError
|
|
3853
|
-
| JsonValueFromStringError
|
|
3854
3811
|
| JsonError
|
|
3812
|
+
| ValidMutationSizeError
|
|
3855
3813
|
| ExtraErrors
|
|
3856
3814
|
// Composite errors
|
|
3857
3815
|
| ArrayError<TypeErrors<ExtraErrors>>
|
|
@@ -3866,70 +3824,74 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
|
|
|
3866
3824
|
| TupleError<TypeErrors<ExtraErrors>>;
|
|
3867
3825
|
|
|
3868
3826
|
/**
|
|
3869
|
-
*
|
|
3870
|
-
* {@link TypeErrors} and custom errors. It also lets us override the default
|
|
3871
|
-
* formatting for specific errors.
|
|
3827
|
+
* Formats Evolu Type errors into user-friendly messages.
|
|
3872
3828
|
*
|
|
3873
|
-
*
|
|
3874
|
-
*
|
|
3829
|
+
* Evolu Type typed errors ensure every error type must have a formatter.
|
|
3830
|
+
* TypeScript enforces this at compile-time, preventing unhandled validation
|
|
3831
|
+
* errors from reaching users.
|
|
3875
3832
|
*
|
|
3876
|
-
*
|
|
3833
|
+
* The `createFormatTypeError` function handles both built-in {@link TypeErrors}
|
|
3834
|
+
* and custom errors, and lets us override default formatting for specific
|
|
3835
|
+
* errors.
|
|
3836
|
+
*
|
|
3837
|
+
* ### Example
|
|
3877
3838
|
*
|
|
3878
3839
|
* ```ts
|
|
3879
|
-
* const
|
|
3880
|
-
*
|
|
3881
|
-
*
|
|
3840
|
+
* const formatTypeError = createFormatTypeError<
|
|
3841
|
+
* MinLengthError | MaxLengthError
|
|
3842
|
+
* >((error): string => {
|
|
3843
|
+
* switch (error.type) {
|
|
3844
|
+
* case "MinLength":
|
|
3845
|
+
* return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
|
|
3846
|
+
* case "MaxLength":
|
|
3847
|
+
* return `Text is too long (maximum ${error.max} characters)`;
|
|
3848
|
+
* }
|
|
3849
|
+
* });
|
|
3882
3850
|
* ```
|
|
3883
3851
|
*
|
|
3884
|
-
*
|
|
3852
|
+
* Alternatively, write a custom formatter from scratch without using
|
|
3853
|
+
* `createFormatTypeError`. This gives us full control over error formatting:
|
|
3885
3854
|
*
|
|
3886
3855
|
* ```ts
|
|
3887
|
-
*
|
|
3888
|
-
*
|
|
3856
|
+
* const Person = object({
|
|
3857
|
+
* name: NonEmptyTrimmedString100,
|
|
3858
|
+
* age: optional(PositiveInt),
|
|
3859
|
+
* });
|
|
3860
|
+
*
|
|
3861
|
+
* // Define only the errors actually used by Person Type
|
|
3862
|
+
* type PersonErrors =
|
|
3889
3863
|
* | StringError
|
|
3890
|
-
* | MinLengthError
|
|
3891
3864
|
* | MaxLengthError
|
|
3892
|
-
* |
|
|
3893
|
-
* | IdError
|
|
3865
|
+
* | MinLengthError
|
|
3894
3866
|
* | TrimmedError
|
|
3895
|
-
* |
|
|
3896
|
-
* |
|
|
3897
|
-
*
|
|
3898
|
-
* |
|
|
3899
|
-
* |
|
|
3900
|
-
*
|
|
3901
|
-
* const formatTypeError: TypeErrorFormatter<
|
|
3902
|
-
* // In the real code, we would use the createTypeErrorFormatter helper
|
|
3903
|
-
* // that safely stringifies error value.
|
|
3867
|
+
* | PositiveError
|
|
3868
|
+
* | NonNegativeError
|
|
3869
|
+
* | IntError
|
|
3870
|
+
* | NumberError
|
|
3871
|
+
* | ObjectError<Record<string, PersonErrors>>;
|
|
3872
|
+
*
|
|
3873
|
+
* const formatTypeError: TypeErrorFormatter<PersonErrors> = (error) => {
|
|
3904
3874
|
* switch (error.type) {
|
|
3905
|
-
* case "Id":
|
|
3906
|
-
* return `Invalid Id on table: ${error.table}.`;
|
|
3907
|
-
* case "MaxLength":
|
|
3908
|
-
* return `Max length is ${error.max}.`;
|
|
3909
|
-
* case "MinLength":
|
|
3910
|
-
* return `Min length is ${error.min}.`;
|
|
3911
|
-
* case "Mnemonic":
|
|
3912
|
-
* return `Invalid mnemonic: ${String(error.value)}`;
|
|
3913
|
-
* case "Null":
|
|
3914
|
-
* return `Not null`;
|
|
3915
3875
|
* case "String":
|
|
3916
|
-
* // We can reuse existing formatter.
|
|
3917
3876
|
* return formatStringError(error);
|
|
3877
|
+
* case "Number":
|
|
3878
|
+
* return "Must be a number";
|
|
3879
|
+
* case "MinLength":
|
|
3880
|
+
* return `Must be at least ${error.min} characters`;
|
|
3881
|
+
* case "MaxLength":
|
|
3882
|
+
* return `Cannot exceed ${error.max} characters`;
|
|
3918
3883
|
* case "Trimmed":
|
|
3919
|
-
* return "
|
|
3920
|
-
* case "
|
|
3921
|
-
* return "
|
|
3922
|
-
* case "
|
|
3923
|
-
* return
|
|
3924
|
-
*
|
|
3925
|
-
*
|
|
3926
|
-
* return `Union errors: ${error.errors.map(formatTypeError).join(", ")}`;
|
|
3884
|
+
* return "Cannot have leading or trailing spaces";
|
|
3885
|
+
* case "Positive":
|
|
3886
|
+
* return "Must be a positive number";
|
|
3887
|
+
* case "NonNegative":
|
|
3888
|
+
* return "Must be zero or positive";
|
|
3889
|
+
* case "Int":
|
|
3890
|
+
* return "Must be an integer";
|
|
3927
3891
|
* case "Object": {
|
|
3928
|
-
* if (
|
|
3929
|
-
*
|
|
3930
|
-
*
|
|
3931
|
-
* )
|
|
3932
|
-
* return "A developer made an error, this should not happen.";
|
|
3892
|
+
* if (error.reason.kind === "NotObject") return "Must be an object";
|
|
3893
|
+
* if (error.reason.kind === "ExtraKeys")
|
|
3894
|
+
* return "Contains unexpected fields";
|
|
3933
3895
|
* const firstError = Object.values(error.reason.errors).find(
|
|
3934
3896
|
* (e) => e !== undefined,
|
|
3935
3897
|
* )!;
|
|
@@ -3976,8 +3938,8 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
|
|
|
3976
3938
|
return formatIsTypeError(error);
|
|
3977
3939
|
case "CurrencyCode":
|
|
3978
3940
|
return formatCurrencyCodeError(error);
|
|
3979
|
-
case "
|
|
3980
|
-
return
|
|
3941
|
+
case "DateIso":
|
|
3942
|
+
return formatDateIsoError(error);
|
|
3981
3943
|
case "Trimmed":
|
|
3982
3944
|
return formatTrimmedError(error);
|
|
3983
3945
|
case "MinLength":
|
|
@@ -3992,6 +3954,8 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
|
|
|
3992
3954
|
return formatRegexError(error);
|
|
3993
3955
|
case "Id":
|
|
3994
3956
|
return formatIdError(error);
|
|
3957
|
+
case "TableId":
|
|
3958
|
+
return formatTableIdError(error);
|
|
3995
3959
|
case "Positive":
|
|
3996
3960
|
return formatPositiveError(error);
|
|
3997
3961
|
case "Negative":
|
|
@@ -4022,14 +3986,12 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
|
|
|
4022
3986
|
return formatLiteralError(error);
|
|
4023
3987
|
case "Int64":
|
|
4024
3988
|
return formatInt64Error(error);
|
|
4025
|
-
case "BigIntFromString":
|
|
4026
|
-
return formatBigIntFromStringError(error);
|
|
4027
3989
|
case "Int64String":
|
|
4028
3990
|
return formatInt64StringError(error);
|
|
4029
|
-
case "JsonValueFromString":
|
|
4030
|
-
return formatJsonValueFromStringError(error);
|
|
4031
3991
|
case "Json":
|
|
4032
3992
|
return formatJsonError(error);
|
|
3993
|
+
case "ValidMutationSize":
|
|
3994
|
+
return formatValidMutationSizeError(error);
|
|
4033
3995
|
// Composite Types
|
|
4034
3996
|
case "SimplePassword":
|
|
4035
3997
|
return formatSimplePasswordError(formatTypeError)(error);
|