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