@evolu/common 6.0.1-preview.8 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/src/Array.d.ts +256 -12
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +215 -9
- package/dist/src/Assert.d.ts +0 -13
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +0 -15
- package/dist/src/Brand.d.ts +75 -0
- package/dist/src/Brand.d.ts.map +1 -0
- package/dist/src/Brand.js +1 -0
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +8 -7
- package/dist/src/Cache.d.ts +44 -0
- package/dist/src/Cache.d.ts.map +1 -0
- package/dist/src/Cache.js +52 -0
- package/dist/src/Callbacks.d.ts +45 -12
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +14 -7
- package/dist/src/Console.d.ts +31 -6
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +72 -9
- package/dist/src/Crypto.d.ts +56 -42
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +40 -53
- package/dist/src/Evolu/Db.d.ts +162 -74
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +284 -702
- package/dist/src/Evolu/Diff.d.ts +3 -3
- package/dist/src/Evolu/Diff.d.ts.map +1 -1
- package/dist/src/Evolu/Diff.js +7 -5
- package/dist/src/Evolu/Evolu.d.ts +214 -134
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +189 -180
- package/dist/src/Evolu/Internal.d.ts +0 -2
- package/dist/src/Evolu/Internal.d.ts.map +1 -1
- package/dist/src/Evolu/Internal.js +0 -2
- package/dist/src/Evolu/LocalAuth.d.ts +150 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +174 -0
- package/dist/src/Evolu/Owner.d.ts +273 -120
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +130 -104
- package/dist/src/Evolu/Platform.d.ts +9 -7
- package/dist/src/Evolu/Platform.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.d.ts +268 -240
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +600 -454
- package/dist/src/Evolu/Public.d.ts +6 -8
- package/dist/src/Evolu/Public.d.ts.map +1 -1
- package/dist/src/Evolu/Public.js +2 -3
- package/dist/src/Evolu/PublicKysely.d.ts.map +1 -1
- package/dist/src/Evolu/PublicKysely.js +3 -4
- package/dist/src/Evolu/Query.d.ts +2 -1
- package/dist/src/Evolu/Query.d.ts.map +1 -1
- package/dist/src/Evolu/Query.js +1 -1
- package/dist/src/Evolu/Relay.d.ts +91 -8
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +214 -88
- package/dist/src/Evolu/Schema.d.ts +125 -47
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +175 -31
- package/dist/src/Evolu/Storage.d.ts +249 -27
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +198 -92
- package/dist/src/Evolu/Sync.d.ts +68 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +469 -20
- package/dist/src/Evolu/Timestamp.d.ts +83 -30
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +79 -34
- package/dist/src/Identicon.d.ts +35 -0
- package/dist/src/Identicon.d.ts.map +1 -0
- package/dist/src/Identicon.js +143 -0
- package/dist/src/Instances.d.ts +34 -0
- package/dist/src/Instances.d.ts.map +1 -0
- package/dist/src/Instances.js +44 -0
- package/dist/src/Number.d.ts +4 -3
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +5 -4
- package/dist/src/Object.d.ts +10 -4
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +9 -3
- package/dist/src/Platform.d.ts +20 -0
- package/dist/src/Platform.d.ts.map +1 -0
- package/dist/src/Platform.js +22 -0
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Relation.d.ts +79 -0
- package/dist/src/Relation.d.ts.map +1 -0
- package/dist/src/Relation.js +127 -0
- package/dist/src/Resources.d.ts +118 -0
- package/dist/src/Resources.d.ts.map +1 -0
- package/dist/src/Resources.js +197 -0
- package/dist/src/Result.d.ts +184 -52
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +30 -241
- package/dist/src/Skiplist.js +2 -1
- package/dist/src/Sqlite.d.ts +89 -5
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +136 -9
- package/dist/src/Task.d.ts +586 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +469 -0
- package/dist/src/Time.d.ts +66 -1
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +99 -5
- package/dist/src/Type.d.ts +676 -343
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +718 -467
- package/dist/src/Types.d.ts +1 -75
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -18
- package/dist/src/Worker.d.ts +39 -11
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +22 -4
- package/dist/src/index.d.ts +8 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +8 -3
- package/package.json +15 -14
- package/src/Array.ts +301 -19
- package/src/Assert.ts +0 -21
- package/src/Brand.ts +75 -0
- package/src/Buffer.ts +7 -7
- package/src/Cache.ts +85 -0
- package/src/Callbacks.ts +62 -22
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +94 -90
- package/src/Evolu/Db.ts +519 -1026
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +477 -361
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +463 -0
- package/src/Evolu/Owner.ts +355 -228
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +842 -742
- package/src/Evolu/Public.ts +7 -14
- package/src/Evolu/PublicKysely.ts +4 -5
- package/src/Evolu/Query.ts +3 -2
- package/src/Evolu/Relay.ts +406 -103
- package/src/Evolu/Schema.ts +323 -91
- package/src/Evolu/Storage.ts +559 -137
- package/src/Evolu/Sync.ts +819 -36
- package/src/Evolu/Timestamp.ts +90 -58
- package/src/Identicon.ts +197 -0
- package/src/Instances.ts +90 -0
- package/src/Number.ts +6 -10
- package/src/Object.ts +13 -5
- package/src/Platform.ts +26 -0
- package/src/Random.ts +3 -2
- package/src/Relation.ts +234 -0
- package/src/Resources.ts +367 -0
- package/src/Result.ts +191 -54
- package/src/Skiplist.ts +1 -1
- package/src/Sqlite.ts +152 -17
- package/src/Task.ts +901 -0
- package/src/Time.ts +180 -5
- package/src/Type.ts +1135 -730
- package/src/Types.ts +1 -77
- package/src/WebSocket.ts +27 -25
- package/src/Worker.ts +72 -23
- package/src/index.ts +8 -3
- package/dist/src/Evolu/Config.d.ts +0 -69
- package/dist/src/Evolu/Config.d.ts.map +0 -1
- package/dist/src/Evolu/Config.js +0 -9
- package/dist/src/Evolu/Kysely.d.ts +0 -6
- package/dist/src/Evolu/Kysely.d.ts.map +0 -1
- package/dist/src/Evolu/Kysely.js +0 -21
- package/dist/src/ManyToManyMap.d.ts +0 -26
- package/dist/src/ManyToManyMap.d.ts.map +0 -1
- package/dist/src/ManyToManyMap.js +0 -92
- package/dist/src/NanoId.d.ts +0 -27
- package/dist/src/NanoId.d.ts.map +0 -1
- package/dist/src/NanoId.js +0 -6
- package/dist/src/Promise.d.ts +0 -180
- package/dist/src/Promise.d.ts.map +0 -1
- package/dist/src/Promise.js +0 -176
- package/src/Evolu/Config.ts +0 -83
- package/src/Evolu/Kysely.ts +0 -38
- package/src/ManyToManyMap.ts +0 -140
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/dist/src/Type.js
CHANGED
|
@@ -1,80 +1,213 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 🧩
|
|
2
|
+
* 🧩 Type-safe runtime types
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Evolu {@link Type} is like a type guard that returns typed errors (via
|
|
5
|
+
* {@link Result}) instead of throwing. We either get a safely typed value or a
|
|
6
|
+
* composable typed error telling us exactly why validation failed.
|
|
5
7
|
*
|
|
6
|
-
*
|
|
8
|
+
* Why another validation library?
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* - **Result-based error handling** – no exceptions for normal control flow.
|
|
11
|
+
* - **Typed errors with decoupled formatters** – validation logic ≠ user
|
|
12
|
+
* messages.
|
|
13
|
+
* - **Consistent constraints via {@link Brand}** – every constraint becomes part
|
|
14
|
+
* of the type.
|
|
15
|
+
* - **Skippable validation** – parent validations can be skipped when already
|
|
16
|
+
* proved by types.
|
|
17
|
+
* - **Simple, top-down implementation** – readable source code from top to
|
|
18
|
+
* bottom.
|
|
19
|
+
* - **No user-land chaining DSL** – prepared for TC39 Hack pipes.
|
|
10
20
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* with validators.
|
|
16
|
-
* - **No user-land chaining**: Designed with ES pipe operator in mind.
|
|
17
|
-
* - **Selective validation/transformation**: Skipping parent Type validations and
|
|
18
|
-
* transformations when TypeScript's type system can be relied upon.
|
|
19
|
-
* - **Bidirectional transformations**: Supporting transformations in both
|
|
20
|
-
* directions.
|
|
21
|
-
* - **Minimal and transparent code**: No runtime dependencies or hidden magic.
|
|
21
|
+
* A distinctive feature of Evolu Type compared to other validation libraries is
|
|
22
|
+
* that it returns typed errors rather than string messages. This allows
|
|
23
|
+
* TypeScript to enforce that all validation errors are handled by type
|
|
24
|
+
* checking, significantly improving the developer experience.
|
|
22
25
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* practical demonstrations of the API. Or dang, just read the code. It's
|
|
26
|
-
* simple.
|
|
26
|
+
* Evolu Type supports [Standard Schema](https://standardschema.dev/) for
|
|
27
|
+
* interoperability with 40+ validation-compatible tools and frameworks.
|
|
27
28
|
*
|
|
28
|
-
*
|
|
29
|
+
* ### Base Types Quick Start
|
|
29
30
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* // Validate unknown values
|
|
33
|
+
* const value: unknown = "hello";
|
|
34
|
+
* const stringResult = String.fromUnknown(value);
|
|
35
|
+
* if (!stringResult.ok) {
|
|
36
|
+
* // console.error(formatStringError(stringResult.error));
|
|
37
|
+
* return stringResult; // inside a function returning Result<string, _>
|
|
38
|
+
* }
|
|
39
|
+
* // Safe branch: value is now string
|
|
40
|
+
* const upper = stringResult.value.toUpperCase();
|
|
41
|
+
*
|
|
42
|
+
* // Type guard style
|
|
43
|
+
* if (String.is(value)) {
|
|
44
|
+
* // narrowed to string
|
|
45
|
+
* }
|
|
46
|
+
*
|
|
47
|
+
* // Composing: arrays & objects
|
|
48
|
+
* const Numbers = array(Number); // ReadonlyArray<number>
|
|
49
|
+
* const Point = object({ x: Number, y: Number });
|
|
50
|
+
*
|
|
51
|
+
* Numbers.from([1, 2, 3]); // ok
|
|
52
|
+
* Point.from({ x: 1, y: 2 }); // ok
|
|
53
|
+
* Point.from({ x: 1, y: "2" }); // err -> nested Number error
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* ### Branding Basics
|
|
57
|
+
*
|
|
58
|
+
* Branding adds semantic meaning & constraints while preserving the runtime
|
|
59
|
+
* shape:
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* const CurrencyCode = brand("CurrencyCode", String, (value) =>
|
|
63
|
+
* /^[A-Z]{3}$/.test(value)
|
|
64
|
+
* ? ok(value)
|
|
65
|
+
* : err<CurrencyCodeError>({ type: "CurrencyCode", value }),
|
|
66
|
+
* );
|
|
67
|
+
* type CurrencyCode = typeof CurrencyCode.Type; // string & Brand<"CurrencyCode">
|
|
68
|
+
*
|
|
69
|
+
* interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
|
|
70
|
+
*
|
|
71
|
+
* const formatCurrencyCodeError =
|
|
72
|
+
* createTypeErrorFormatter<CurrencyCodeError>(
|
|
73
|
+
* (error) => `Invalid currency code: ${error.value}`,
|
|
74
|
+
* );
|
|
75
|
+
*
|
|
76
|
+
* const r = CurrencyCode.from("USD"); // ok("USD")
|
|
77
|
+
* const e = CurrencyCode.from("usd"); // err(...)
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* See also reusable brand factories like `minLength`, `maxLength`, `trimmed`,
|
|
81
|
+
* `positive`, `between`, etc.
|
|
34
82
|
*
|
|
35
|
-
*
|
|
36
|
-
* unknown value into something and can continue with further refinements or
|
|
37
|
-
* transformations. For example, `NonEmptyTrimmedString100` chain looks like
|
|
38
|
-
* this:
|
|
83
|
+
* ### Objects & Optional Fields
|
|
39
84
|
*
|
|
40
|
-
*
|
|
85
|
+
* ```ts
|
|
86
|
+
* const User = object({
|
|
87
|
+
* name: NonEmptyTrimmedString100,
|
|
88
|
+
* age: optional(PositiveInt),
|
|
89
|
+
* });
|
|
90
|
+
* type User = typeof User.Type;
|
|
91
|
+
*
|
|
92
|
+
* User.from({ name: "Alice" }); // ok
|
|
93
|
+
* User.from({ name: "Alice", age: -1 }); // err(PositiveInt)
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* ### Deriving JSON String Types
|
|
41
97
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
98
|
+
* ```ts
|
|
99
|
+
* const Person = object({
|
|
100
|
+
* name: NonEmptyString50,
|
|
101
|
+
* // Did you know that JSON.stringify converts NaN (a number) into null?
|
|
102
|
+
* // To prevent this, use FiniteNumber.
|
|
103
|
+
* age: FiniteNumber,
|
|
104
|
+
* });
|
|
105
|
+
* type Person = typeof Person.Type;
|
|
106
|
+
*
|
|
107
|
+
* const [PersonJson, personToPersonJson, personJsonToPerson] = json(
|
|
108
|
+
* Person,
|
|
109
|
+
* "PersonJson",
|
|
110
|
+
* );
|
|
111
|
+
* // string & Brand<"PersonJson">
|
|
112
|
+
* type PersonJson = typeof PersonJson.Type;
|
|
113
|
+
*
|
|
114
|
+
* const person = Person.orThrow({
|
|
115
|
+
* name: "Alice",
|
|
116
|
+
* age: 30,
|
|
117
|
+
* });
|
|
118
|
+
*
|
|
119
|
+
* const personJson = personToPersonJson(person);
|
|
120
|
+
* expect(personJsonToPerson(personJson)).toEqual(person);
|
|
121
|
+
* ```
|
|
122
|
+
*
|
|
123
|
+
* ### Error Formatting
|
|
124
|
+
*
|
|
125
|
+
* Evolu separates validation logic from human-readable messages. There are two
|
|
126
|
+
* layers:
|
|
127
|
+
*
|
|
128
|
+
* 1. Per-type formatters (e.g. `formatStringError`) – simple, focused, already
|
|
129
|
+
* used earlier in the quick start example.
|
|
130
|
+
* 2. A unified formatter via `createFormatTypeError` – composes all built-in and
|
|
131
|
+
* custom errors (including nested composite types) and lets us override
|
|
132
|
+
* selected messages.
|
|
133
|
+
*
|
|
134
|
+
* #### 1. Per-Type Formatter (recap)
|
|
135
|
+
*
|
|
136
|
+
* ```ts
|
|
137
|
+
* const r = String.fromUnknown(42);
|
|
138
|
+
* if (!r.ok) console.error(formatStringError(r.error));
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* #### 2. Unified Formatter with Overrides
|
|
142
|
+
*
|
|
143
|
+
* ```ts
|
|
144
|
+
* // Override only what we care about; fall back to built-ins for the rest.
|
|
145
|
+
* const formatTypeError = createFormatTypeError((error) => {
|
|
146
|
+
* if (error.type === "MinLength") return `Min length is ${error.min}`;
|
|
147
|
+
* });
|
|
44
148
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
149
|
+
* const User = object({ name: NonEmptyTrimmedString100 });
|
|
150
|
+
* const resultUser = User.from({ name: "" });
|
|
151
|
+
* if (!resultUser.ok) console.error(formatTypeError(resultUser.error));
|
|
48
152
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
153
|
+
* const badPoint = object({ x: Number, y: Number }).from({
|
|
154
|
+
* x: 1,
|
|
155
|
+
* y: "foo",
|
|
156
|
+
* });
|
|
157
|
+
* if (!badPoint.ok) console.error(formatTypeError(badPoint.error));
|
|
158
|
+
* ```
|
|
51
159
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* `to` will not restore the original representation. You can also use
|
|
56
|
-
* {@link assert}: `assert(false, "Untrim is not possible")`.
|
|
160
|
+
* The unified formatter walks nested structures (object / array / record /
|
|
161
|
+
* tuple / union) and applies overrides only where specified, greatly reducing
|
|
162
|
+
* boilerplate when formatting complex validation errors.
|
|
57
163
|
*
|
|
58
164
|
* ### Tip
|
|
59
165
|
*
|
|
60
166
|
* If necessary, write `globalThis.String` instead of `String` to avoid naming
|
|
61
|
-
* clashes with
|
|
167
|
+
* clashes with native types.
|
|
168
|
+
*
|
|
169
|
+
* ### Design Decision: No Bidirectional Transformations
|
|
170
|
+
*
|
|
171
|
+
* Evolu Type intentionally does not support bidirectional transformations. It
|
|
172
|
+
* previously did, but supporting that while keeping typed error fidelity added
|
|
173
|
+
* complexity that hurt readability & reliability. Most persistence pipelines
|
|
174
|
+
* (e.g. SQLite) already require explicit mapping of query results, so implicit
|
|
175
|
+
* reverse transforms would not buy much. We may revisit this if we can design a
|
|
176
|
+
* minimal, 100% safe API that preserves simplicity.
|
|
177
|
+
*
|
|
178
|
+
* ### Prepared for TC39 Hack Pipes
|
|
179
|
+
*
|
|
180
|
+
* Take a look how `SimplePassword` is defined:
|
|
181
|
+
*
|
|
182
|
+
* ```ts
|
|
183
|
+
* export const SimplePassword = brand(
|
|
184
|
+
* "SimplePassword",
|
|
185
|
+
* minLength(8)(maxLength(64)(TrimmedString)),
|
|
186
|
+
* );
|
|
187
|
+
* ```
|
|
188
|
+
*
|
|
189
|
+
* Nested functions are often OK (if not, make a helper) and read well, but with
|
|
190
|
+
* TC39 Hack pipes it would be clearer:
|
|
62
191
|
*
|
|
63
|
-
*
|
|
192
|
+
* ```ts
|
|
193
|
+
* // TrimmedString
|
|
194
|
+
* // |> minLength(8)(%)
|
|
195
|
+
* // |> maxLength(64)(%)
|
|
196
|
+
* // |> brand("SimplePassword", %)
|
|
197
|
+
* ```
|
|
64
198
|
*
|
|
65
|
-
*
|
|
66
|
-
* the model by ensuring that every valid input has a corresponding valid
|
|
67
|
-
* output, eliminating the risk of edge cases caused by irreversible
|
|
68
|
-
* operations.
|
|
199
|
+
* Note `minLength` and `maxLength` are curried because they are factories.
|
|
69
200
|
*
|
|
70
201
|
* @module
|
|
71
202
|
*/
|
|
203
|
+
import { utf8ToBytes } from "@noble/ciphers/utils.js";
|
|
204
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
72
205
|
import * as bip39 from "@scure/bip39";
|
|
73
|
-
import { wordlist } from "@scure/bip39/wordlists/english";
|
|
74
|
-
import {
|
|
75
|
-
import { identity } from "./Function.js";
|
|
206
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
207
|
+
import { pack } from "msgpackr";
|
|
76
208
|
import { isPlainObject } from "./Object.js";
|
|
77
|
-
import {
|
|
209
|
+
import { hasNodeBuffer } from "./Platform.js";
|
|
210
|
+
import { err, getOrNull, getOrThrow, ok, trySync } from "./Result.js";
|
|
78
211
|
import { safelyStringifyUnknownValue } from "./String.js";
|
|
79
212
|
const EvoluTypeSymbol = Symbol("evolu.Type");
|
|
80
213
|
/**
|
|
@@ -88,6 +221,8 @@ const createType = (name, definition) => ({
|
|
|
88
221
|
name,
|
|
89
222
|
is: (value) => definition.fromUnknown(value).ok,
|
|
90
223
|
from: definition.fromUnknown,
|
|
224
|
+
orThrow: (value) => getOrThrow(definition.fromUnknown(value)),
|
|
225
|
+
orNull: (value) => getOrNull(definition.fromUnknown(value)),
|
|
91
226
|
[EvoluTypeSymbol]: true,
|
|
92
227
|
Type: undefined,
|
|
93
228
|
Input: undefined,
|
|
@@ -95,6 +230,24 @@ const createType = (name, definition) => ({
|
|
|
95
230
|
Parent: undefined,
|
|
96
231
|
ParentError: undefined,
|
|
97
232
|
Errors: undefined,
|
|
233
|
+
"~standard": {
|
|
234
|
+
version: 1,
|
|
235
|
+
vendor: "evolu",
|
|
236
|
+
validate: (value) => {
|
|
237
|
+
const result = definition.fromUnknown(value);
|
|
238
|
+
if (result.ok) {
|
|
239
|
+
return { value: result.value };
|
|
240
|
+
}
|
|
241
|
+
cachedStandardSchemaFormatTypeError ??= createFormatTypeError();
|
|
242
|
+
return {
|
|
243
|
+
issues: typeErrorToStandardSchemaIssues(result.error, cachedStandardSchemaFormatTypeError),
|
|
244
|
+
};
|
|
245
|
+
},
|
|
246
|
+
types: {
|
|
247
|
+
input: undefined,
|
|
248
|
+
output: undefined,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
98
251
|
});
|
|
99
252
|
/**
|
|
100
253
|
* Creates a formatter function for {@link TypeError}.
|
|
@@ -117,12 +270,6 @@ export const createTypeErrorFormatter = (format) => (error) => format({ ...error
|
|
|
117
270
|
* Base {@link Type}.
|
|
118
271
|
*
|
|
119
272
|
* 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
273
|
*
|
|
127
274
|
* ### Example
|
|
128
275
|
*
|
|
@@ -144,9 +291,7 @@ export const createTypeErrorFormatter = (format) => (error) => format({ ...error
|
|
|
144
291
|
*/
|
|
145
292
|
export const base = (name, fromUnknown) => createType(name, {
|
|
146
293
|
fromUnknown,
|
|
147
|
-
to: identity,
|
|
148
294
|
fromParent: (ok), // `fromParent` relies on types, so it can't fail for the Base Type
|
|
149
|
-
toParent: identity,
|
|
150
295
|
});
|
|
151
296
|
/**
|
|
152
297
|
* Creates a formatter function for a base {@link TypeError}.
|
|
@@ -233,7 +378,7 @@ export const instanceOf = (ctor) => ({
|
|
|
233
378
|
: err({ type: "InstanceOf", value, ctor: ctor.name })),
|
|
234
379
|
ctor,
|
|
235
380
|
});
|
|
236
|
-
export const formatInstanceOfError = createTypeErrorFormatter((error) => `
|
|
381
|
+
export const formatInstanceOfError = createTypeErrorFormatter((error) => `The value ${error.value} is not an instance of ${error.ctor}.`);
|
|
237
382
|
/**
|
|
238
383
|
* JavaScript Date.
|
|
239
384
|
*
|
|
@@ -280,9 +425,7 @@ export function brand(name, parent, refine) {
|
|
|
280
425
|
return {
|
|
281
426
|
...createType("Brand", {
|
|
282
427
|
fromUnknown,
|
|
283
|
-
to: identity,
|
|
284
428
|
fromParent: (refine ?? ok),
|
|
285
|
-
toParent: identity,
|
|
286
429
|
}),
|
|
287
430
|
brand: name,
|
|
288
431
|
parentType: parent,
|
|
@@ -296,7 +439,7 @@ export function brand(name, parent, refine) {
|
|
|
296
439
|
export const CurrencyCode = brand("CurrencyCode", String, (value) => /^[A-Z]{3}$/.test(value)
|
|
297
440
|
? ok(value)
|
|
298
441
|
: err({ type: "CurrencyCode", value }));
|
|
299
|
-
export const formatCurrencyCodeError = createTypeErrorFormatter((error) => `Invalid currency code: ${error.value}
|
|
442
|
+
export const formatCurrencyCodeError = createTypeErrorFormatter((error) => `Invalid currency code: ${error.value}.`);
|
|
300
443
|
/**
|
|
301
444
|
* ISO 8601 date-time string.
|
|
302
445
|
*
|
|
@@ -312,37 +455,39 @@ export const formatCurrencyCodeError = createTypeErrorFormatter((error) => `Inva
|
|
|
312
455
|
* ### Example
|
|
313
456
|
*
|
|
314
457
|
* ```ts
|
|
315
|
-
* const result =
|
|
316
|
-
* const error =
|
|
458
|
+
* const result = DateIso.from("2023-01-01T12:00:00.000Z"); // ok
|
|
459
|
+
* const error = DateIso.from("10000-01-01T00:00:00.000Z"); // err
|
|
317
460
|
* ```
|
|
318
461
|
*
|
|
319
462
|
* @category String
|
|
320
463
|
*/
|
|
321
|
-
export const
|
|
464
|
+
export const DateIso = brand("DateIso", String, (value) => {
|
|
322
465
|
if (value.length !== 24) {
|
|
323
|
-
return err({ type: "
|
|
466
|
+
return err({ type: "DateIso", value });
|
|
324
467
|
}
|
|
325
|
-
|
|
326
|
-
|
|
468
|
+
const parsed = globalThis.Date.parse(value);
|
|
469
|
+
if (isNaN(parsed)) {
|
|
470
|
+
return err({ type: "DateIso", value });
|
|
471
|
+
}
|
|
472
|
+
// Round-trip test: ensure the string is actually a proper ISO format
|
|
473
|
+
const roundTrip = new globalThis.Date(parsed).toISOString();
|
|
474
|
+
if (roundTrip !== value) {
|
|
475
|
+
return err({ type: "DateIso", value });
|
|
327
476
|
}
|
|
328
477
|
return ok(value);
|
|
329
478
|
});
|
|
330
|
-
export const
|
|
479
|
+
export const formatDateIsoError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid ISO 8601 date string.`);
|
|
480
|
+
export const dateToDateIso = (value) => DateIso.fromParent(value.toISOString());
|
|
481
|
+
export const dateIsoToDate = (value) => new globalThis.Date(value);
|
|
331
482
|
/**
|
|
332
483
|
* Trimmed string.
|
|
333
484
|
*
|
|
334
|
-
* This Type Factory
|
|
335
|
-
*
|
|
336
|
-
* Factory.
|
|
485
|
+
* This Type Factory validates whether a string has no leading or trailing
|
|
486
|
+
* whitespaces.
|
|
337
487
|
*
|
|
338
|
-
* ###
|
|
488
|
+
* ### Example
|
|
339
489
|
*
|
|
340
490
|
* ```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
491
|
* const TrimmedNonEmptyString = trimmed(minLength(1)(String));
|
|
347
492
|
* // string & Brand<"MinLength1"> & Brand<"Trimmed">
|
|
348
493
|
* type TrimmedNonEmptyString = typeof TrimmedNonEmptyString.Type;
|
|
@@ -353,33 +498,7 @@ export const formatDateIsoStringError = createTypeErrorFormatter((error) => `The
|
|
|
353
498
|
export const trimmed = (parent) => brand("Trimmed", parent, (value) => value.trim().length === value.length
|
|
354
499
|
? ok(value)
|
|
355
500
|
: 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);
|
|
501
|
+
export const formatTrimmedError = createTypeErrorFormatter((error) => `The value ${error.value} must be trimmed.`);
|
|
383
502
|
/**
|
|
384
503
|
* Trimmed string
|
|
385
504
|
*
|
|
@@ -389,6 +508,7 @@ export const trim = (parent) => transform(parent, trimmed(parent), (value) => ok
|
|
|
389
508
|
* @category String
|
|
390
509
|
*/
|
|
391
510
|
export const TrimmedString = trimmed(String);
|
|
511
|
+
export const trim = (value) => value.trim();
|
|
392
512
|
/**
|
|
393
513
|
* Minimum length.
|
|
394
514
|
*
|
|
@@ -403,7 +523,7 @@ export const TrimmedString = trimmed(String);
|
|
|
403
523
|
* @category Array
|
|
404
524
|
*/
|
|
405
525
|
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) => `
|
|
526
|
+
export const formatMinLengthError = createTypeErrorFormatter((error) => `The value ${error.value} does not meet the minimum length of ${error.min}.`);
|
|
407
527
|
/**
|
|
408
528
|
* Maximum length.
|
|
409
529
|
*
|
|
@@ -418,7 +538,7 @@ export const formatMinLengthError = createTypeErrorFormatter((error) => `Value $
|
|
|
418
538
|
* @category Array
|
|
419
539
|
*/
|
|
420
540
|
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) => `
|
|
541
|
+
export const formatMaxLengthError = createTypeErrorFormatter((error) => `The value ${error.value} exceeds the maximum length of ${error.max}.`);
|
|
422
542
|
/**
|
|
423
543
|
* Exact length.
|
|
424
544
|
*
|
|
@@ -435,7 +555,7 @@ export const formatMaxLengthError = createTypeErrorFormatter((error) => `Value $
|
|
|
435
555
|
export const length = (exact) => (parent) => brand(`Length${exact}`, parent, (value) => value.length === exact
|
|
436
556
|
? ok(value)
|
|
437
557
|
: err({ type: "Length", value, exact }));
|
|
438
|
-
export const formatLengthError = createTypeErrorFormatter((error) => `
|
|
558
|
+
export const formatLengthError = createTypeErrorFormatter((error) => `The value ${error.value} does not have the required length of ${error.exact}.`);
|
|
439
559
|
/** @category String */
|
|
440
560
|
export const NonEmptyString = minLength(1)(String);
|
|
441
561
|
/** @category String */
|
|
@@ -468,7 +588,7 @@ export const NonEmptyTrimmedString1000 = minLength(1)(TrimmedString1000);
|
|
|
468
588
|
export const Mnemonic = brand("Mnemonic", NonEmptyTrimmedString, (value) => bip39.validateMnemonic(value, wordlist)
|
|
469
589
|
? ok(value)
|
|
470
590
|
: err({ type: "Mnemonic", value }));
|
|
471
|
-
export const formatMnemonicError = createTypeErrorFormatter((error) => `Invalid BIP39 mnemonic: ${error.value}
|
|
591
|
+
export const formatMnemonicError = createTypeErrorFormatter((error) => `Invalid BIP39 mnemonic: ${error.value}.`);
|
|
472
592
|
/**
|
|
473
593
|
* String matching a regular expression.
|
|
474
594
|
*
|
|
@@ -492,11 +612,11 @@ export const regex = (name, pattern) => {
|
|
|
492
612
|
: err({ type: "Regex", name, value, pattern });
|
|
493
613
|
});
|
|
494
614
|
};
|
|
495
|
-
export const formatRegexError = createTypeErrorFormatter((error) => `
|
|
615
|
+
export const formatRegexError = createTypeErrorFormatter((error) => `The value ${error.value} does not match the pattern for ${error.name}: ${error.pattern}.`);
|
|
496
616
|
/**
|
|
497
|
-
* URL-safe
|
|
617
|
+
* URL-safe string.
|
|
498
618
|
*
|
|
499
|
-
* A `
|
|
619
|
+
* A `UrlSafeString` uses a limited alphabet that is safe for URLs:
|
|
500
620
|
*
|
|
501
621
|
* - Uppercase letters (`A-Z`)
|
|
502
622
|
* - Lowercase letters (`a-z`)
|
|
@@ -504,31 +624,90 @@ export const formatRegexError = createTypeErrorFormatter((error) => `Value ${err
|
|
|
504
624
|
* - Dash (`-`)
|
|
505
625
|
* - Underscore (`_`)
|
|
506
626
|
*
|
|
627
|
+
* This is the same character set used by Base64Url encoding, but this type does
|
|
628
|
+
* not validate that the string is actually Base64Url-encoded data.
|
|
629
|
+
*
|
|
507
630
|
* ### Example
|
|
508
631
|
*
|
|
509
632
|
* ```ts
|
|
510
|
-
* const result =
|
|
633
|
+
* const result = UrlSafeString.from("abc123_-");
|
|
511
634
|
* if (result.ok) {
|
|
512
|
-
* console.log("Valid
|
|
635
|
+
* console.log("Valid URL-safe string:", result.value);
|
|
513
636
|
* } else {
|
|
514
|
-
* console.error("Invalid
|
|
637
|
+
* console.error("Invalid URL-safe string:", result.error);
|
|
515
638
|
* }
|
|
516
639
|
* ```
|
|
517
640
|
*
|
|
518
641
|
* @category String
|
|
519
642
|
*/
|
|
520
|
-
export const
|
|
643
|
+
export const UrlSafeString = regex("UrlSafeString", /^[A-Za-z0-9_-]+$/)(String);
|
|
521
644
|
/**
|
|
522
|
-
*
|
|
645
|
+
* Base64Url without padding.
|
|
523
646
|
*
|
|
524
|
-
*
|
|
647
|
+
* Encode with {@link uint8ArrayToBase64Url}, decode with
|
|
648
|
+
* {@link base64UrlToUint8Array}.
|
|
525
649
|
*
|
|
526
|
-
*
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
650
|
+
* @category String
|
|
651
|
+
*/
|
|
652
|
+
export const Base64Url = brand("Base64Url", String, (value) => {
|
|
653
|
+
// Round-trip validation ensures consistency across different base64url
|
|
654
|
+
// implementations (Node.js Buffer, native browser API, manual fallback).
|
|
655
|
+
// Only strings that decode and encode identically are accepted.
|
|
656
|
+
let roundTrip;
|
|
657
|
+
try {
|
|
658
|
+
roundTrip = uint8ArrayToBase64Url(base64UrlToUint8Array(value));
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
//
|
|
662
|
+
}
|
|
663
|
+
return roundTrip === value
|
|
664
|
+
? ok(value)
|
|
665
|
+
: err({ type: "Base64Url", value });
|
|
666
|
+
});
|
|
667
|
+
export const formatBase64UrlError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Base64Url string.`);
|
|
668
|
+
const base64UrlOptions = { alphabet: "base64url", omitPadding: true };
|
|
669
|
+
/** Encodes a Uint8Array to a {@link Base64Url} string. */
|
|
670
|
+
export const uint8ArrayToBase64Url = hasNodeBuffer
|
|
671
|
+
? (bytes) => globalThis.Buffer.from(bytes).toString("base64url")
|
|
672
|
+
: // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
673
|
+
typeof globalThis.Uint8Array.prototype?.toBase64 !== "undefined"
|
|
674
|
+
? (bytes) =>
|
|
675
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
676
|
+
bytes.toBase64(base64UrlOptions)
|
|
677
|
+
: (bytes) => {
|
|
678
|
+
const binaryString = Array.from(bytes, (byte) => globalThis.String.fromCodePoint(byte)).join("");
|
|
679
|
+
const base64 = globalThis.btoa(binaryString);
|
|
680
|
+
return base64
|
|
681
|
+
.replace(/\+/g, "-")
|
|
682
|
+
.replace(/\//g, "_")
|
|
683
|
+
.replace(/=/g, "");
|
|
684
|
+
};
|
|
685
|
+
/** Decodes a {@link Base64Url} string to a Uint8Array. */
|
|
686
|
+
export const base64UrlToUint8Array = hasNodeBuffer
|
|
687
|
+
? (str) => {
|
|
688
|
+
const nodeBuffer = globalThis.Buffer.from(str, "base64url");
|
|
689
|
+
return new globalThis.Uint8Array(nodeBuffer);
|
|
690
|
+
}
|
|
691
|
+
: // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
692
|
+
typeof globalThis.Uint8Array?.fromBase64 !== "undefined"
|
|
693
|
+
? (str) =>
|
|
694
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
695
|
+
globalThis.Uint8Array.fromBase64(str, base64UrlOptions)
|
|
696
|
+
: (str) => {
|
|
697
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
698
|
+
while (base64.length % 4 !== 0) {
|
|
699
|
+
base64 += "=";
|
|
700
|
+
}
|
|
701
|
+
const binaryString = globalThis.atob(base64);
|
|
702
|
+
return globalThis.Uint8Array.from(binaryString, (c) => c.charCodeAt(0));
|
|
703
|
+
};
|
|
704
|
+
/**
|
|
705
|
+
* Simple alphanumeric string for naming in file systems, URLs, and identifiers.
|
|
706
|
+
*
|
|
707
|
+
* Uses the same safe alphabet as {@link UrlSafeString} (letters, digits, `-`,
|
|
708
|
+
* `_`). See `UrlSafeString` for details.
|
|
530
709
|
*
|
|
531
|
-
* The string must be between 1 and
|
|
710
|
+
* The string must be between 1 and 64 characters.
|
|
532
711
|
*
|
|
533
712
|
* ### Example
|
|
534
713
|
*
|
|
@@ -543,43 +722,153 @@ export const Base64Url = regex("Base64Url", /^[A-Za-z0-9_-]+$/)(String);
|
|
|
543
722
|
*
|
|
544
723
|
* @category String
|
|
545
724
|
*/
|
|
546
|
-
export const SimpleName =
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
*
|
|
550
|
-
* @category String
|
|
551
|
-
*/
|
|
552
|
-
export const NanoId = regex("NanoId", /^[A-Za-z0-9_-]{21}$/)(String);
|
|
725
|
+
export const SimpleName = brand("SimpleName", UrlSafeString, (value) => value.length >= 1 && value.length <= 64
|
|
726
|
+
? ok(value)
|
|
727
|
+
: err({ type: "SimpleName", value }));
|
|
553
728
|
/**
|
|
554
729
|
* Trimmed string between 8 and 64 characters, branded as `SimplePassword`.
|
|
555
730
|
*
|
|
731
|
+
* Take a look how `SimplePassword` is defined:
|
|
732
|
+
*
|
|
733
|
+
* ```ts
|
|
734
|
+
* export const SimplePassword = brand(
|
|
735
|
+
* "SimplePassword",
|
|
736
|
+
* minLength(8)(maxLength(64)(TrimmedString)),
|
|
737
|
+
* );
|
|
738
|
+
* ```
|
|
739
|
+
*
|
|
740
|
+
* Nested functions are often OK (if not, make a helper), but with TC39 Hack
|
|
741
|
+
* pipes it would be clearer:
|
|
742
|
+
*
|
|
743
|
+
* ```ts
|
|
744
|
+
* // TrimmedString
|
|
745
|
+
* // |> minLength(8)(%)
|
|
746
|
+
* // |> maxLength(64)(%)
|
|
747
|
+
* // |> brand("SimplePassword", %)
|
|
748
|
+
* ```
|
|
749
|
+
*
|
|
556
750
|
* @category String
|
|
557
751
|
*/
|
|
558
752
|
export const SimplePassword = brand("SimplePassword", minLength(8)(maxLength(64)(TrimmedString)));
|
|
559
753
|
export const formatSimplePasswordError = (formatTypeError) => createTypeErrorFormatter((error) => `Invalid password: ${formatTypeError(error.parentError)}`);
|
|
560
754
|
/**
|
|
561
|
-
*
|
|
755
|
+
* Evolu Id: 16 bytes encoded as a 22‑character Base64Url string.
|
|
756
|
+
*
|
|
757
|
+
* There are three ways to create an Evolu Id:
|
|
758
|
+
*
|
|
759
|
+
* - {@link createId} – default cryptographically secure random bytes
|
|
760
|
+
* (privacy‑preserving)
|
|
761
|
+
* - {@link createIdFromString} – deterministic: first 16 bytes of SHA‑256 of a
|
|
762
|
+
* string
|
|
763
|
+
* - {@link createIdAsUuidv7} – optional: embeds timestamp bits (UUID v7 layout)
|
|
764
|
+
*
|
|
765
|
+
* Privacy: the default random Id does not leak creation time and is safe to
|
|
766
|
+
* share or log. The UUID v7 variant leaks creation time anywhere the Id is
|
|
767
|
+
* copied (logs, URLs, exports); only use it when you explicitly want insertion
|
|
768
|
+
* locality for very large write‑heavy tables and accept timestamp exposure.
|
|
769
|
+
*
|
|
770
|
+
* ### Future
|
|
562
771
|
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
772
|
+
* A possible hybrid masked‑time approach (`timestamp ^ H(cluster_id, timestamp
|
|
773
|
+
*
|
|
774
|
+
* > > N)`) could provide locality without exposing raw creation time. See
|
|
775
|
+
* > > https://brooker.co.za/blog/2025/10/22/uuidv7.html
|
|
565
776
|
*
|
|
566
777
|
* @category String
|
|
567
778
|
*/
|
|
568
|
-
export const Id =
|
|
569
|
-
|
|
779
|
+
export const Id = brand("Id", String, (value) => value.length === 22 && Base64Url.fromParent(value).ok
|
|
780
|
+
? ok(value)
|
|
781
|
+
: err({ type: "Id", value }));
|
|
782
|
+
export const formatIdError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Id.`);
|
|
570
783
|
/**
|
|
571
|
-
* Creates
|
|
784
|
+
* Creates a random {@link Id}. This is the recommended default.
|
|
785
|
+
*
|
|
786
|
+
* Use {@link createIdFromString} for deterministic mapping of external IDs or
|
|
787
|
+
* {@link createIdAsUuidv7} when you accept timestamp leakage for index
|
|
788
|
+
* locality.
|
|
572
789
|
*
|
|
573
790
|
* ### Example
|
|
574
791
|
*
|
|
575
792
|
* ```ts
|
|
576
|
-
* // string & Brand<"Id">
|
|
577
793
|
* const id = createId(deps);
|
|
794
|
+
* const todoId = createId<"Todo">(deps);
|
|
578
795
|
* ```
|
|
579
796
|
*/
|
|
580
|
-
export const createId = (deps) =>
|
|
797
|
+
export const createId = (deps) => {
|
|
798
|
+
const id = uint8ArrayToBase64Url(deps.randomBytes.create(16));
|
|
799
|
+
return id;
|
|
800
|
+
};
|
|
581
801
|
/**
|
|
582
|
-
*
|
|
802
|
+
* Creates an {@link Id} from a string using SHA-256.
|
|
803
|
+
*
|
|
804
|
+
* When integrating with external systems that use different ID formats, use
|
|
805
|
+
* this function to convert external IDs into valid Evolu IDs.
|
|
806
|
+
*
|
|
807
|
+
* In Evolu's CRDT, the ID serves as the unique identifier for conflict
|
|
808
|
+
* resolution across distributed clients. When multiple clients create records
|
|
809
|
+
* with the same external identifier, they must resolve to the same Evolu ID to
|
|
810
|
+
* ensure data consistency.
|
|
811
|
+
*
|
|
812
|
+
* ### Example
|
|
813
|
+
*
|
|
814
|
+
* ```ts
|
|
815
|
+
* // Both clients will generate the same ID
|
|
816
|
+
* const id1 = createIdFromString("user-api-123");
|
|
817
|
+
* const id2 = createIdFromString("user-api-123");
|
|
818
|
+
* console.log(id1 === id2); // true
|
|
819
|
+
*
|
|
820
|
+
* upsert("todo", {
|
|
821
|
+
* id: createIdFromString("external-todo-456"),
|
|
822
|
+
* title: "Synced from external system",
|
|
823
|
+
* });
|
|
824
|
+
* ```
|
|
825
|
+
*
|
|
826
|
+
* **Important**: This transformation uses the first 16 bytes of SHA-256 hash of
|
|
827
|
+
* the string bytes, therefore it's not possible to recover the original
|
|
828
|
+
* external string from the generated {@link Id}. If you need to preserve the
|
|
829
|
+
* original external ID, store it in a separate column.
|
|
830
|
+
*
|
|
831
|
+
* @category String
|
|
832
|
+
*/
|
|
833
|
+
export const createIdFromString = (value) => {
|
|
834
|
+
const hash = sha256(utf8ToBytes(value));
|
|
835
|
+
// Take first 16 bytes of hash and convert to Id
|
|
836
|
+
const id = idBytesToId(hash.slice(0, 16));
|
|
837
|
+
return id;
|
|
838
|
+
};
|
|
839
|
+
/**
|
|
840
|
+
* Creates an {@link Id} embedding timestamp bits (UUID v7 layout) before
|
|
841
|
+
* Base64Url encoding.
|
|
842
|
+
*
|
|
843
|
+
* Tradeoff: better insertion locality / index performance for huge datasets vs
|
|
844
|
+
* leaking creation time everywhere the Id appears. Evolu uses {@link createId}
|
|
845
|
+
* by default to avoid activity leakage; choose this only if you explicitly
|
|
846
|
+
* accept timestamp exposure.
|
|
847
|
+
*
|
|
848
|
+
* ### Example
|
|
849
|
+
*
|
|
850
|
+
* ```ts
|
|
851
|
+
* const id = createIdAsUuidv7({ randomBytes, time });
|
|
852
|
+
* const todoId = createIdAsUuidv7<"Todo">({ randomBytes, time });
|
|
853
|
+
* ```
|
|
854
|
+
*/
|
|
855
|
+
export const createIdAsUuidv7 = (deps) => {
|
|
856
|
+
const id = deps.randomBytes.create(16);
|
|
857
|
+
const timestamp = globalThis.BigInt(deps.time.now());
|
|
858
|
+
id[0] = globalThis.Number((timestamp >> 40n) & 0xffn);
|
|
859
|
+
id[1] = globalThis.Number((timestamp >> 32n) & 0xffn);
|
|
860
|
+
id[2] = globalThis.Number((timestamp >> 24n) & 0xffn);
|
|
861
|
+
id[3] = globalThis.Number((timestamp >> 16n) & 0xffn);
|
|
862
|
+
id[4] = globalThis.Number((timestamp >> 8n) & 0xffn);
|
|
863
|
+
id[5] = globalThis.Number(timestamp & 0xffn);
|
|
864
|
+
id[6] = (id[6] & 0x0f) | 0x70;
|
|
865
|
+
id[8] = (id[8] & 0x3f) | 0x80;
|
|
866
|
+
return id;
|
|
867
|
+
};
|
|
868
|
+
/**
|
|
869
|
+
* Creates a branded {@link Id} Type for a table's primary key.
|
|
870
|
+
*
|
|
871
|
+
* The table name becomes an additional brand for type safety.
|
|
583
872
|
*
|
|
584
873
|
* ### Example
|
|
585
874
|
*
|
|
@@ -592,32 +881,36 @@ export const createId = (deps) => deps.nanoIdLib.nanoid();
|
|
|
592
881
|
* @category String
|
|
593
882
|
*/
|
|
594
883
|
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
884
|
const fromUnknown = (value) => {
|
|
603
885
|
const parentResult = String.fromUnknown(value);
|
|
604
886
|
if (!parentResult.ok)
|
|
605
887
|
return parentResult;
|
|
606
888
|
return fromParent(parentResult.value);
|
|
607
889
|
};
|
|
890
|
+
const fromParent = (value) => {
|
|
891
|
+
const idResult = Id.fromParent(value);
|
|
892
|
+
if (!idResult.ok) {
|
|
893
|
+
return err({ type: "TableId", value, table });
|
|
894
|
+
}
|
|
895
|
+
return ok(idResult.value);
|
|
896
|
+
};
|
|
608
897
|
return {
|
|
609
|
-
...createType("Id", {
|
|
610
|
-
fromUnknown,
|
|
611
|
-
to: (value) => value,
|
|
612
|
-
fromParent,
|
|
613
|
-
toParent: (value) => value,
|
|
614
|
-
}),
|
|
898
|
+
...createType("Id", { fromUnknown, fromParent }),
|
|
615
899
|
table,
|
|
616
900
|
};
|
|
617
901
|
};
|
|
618
|
-
export const
|
|
619
|
-
/**
|
|
620
|
-
|
|
902
|
+
export const formatTableIdError = createTypeErrorFormatter((error) => `Invalid Id for table ${error.table}: ${error.value}.`);
|
|
903
|
+
/** Binary representation of an {@link Id}. */
|
|
904
|
+
export const IdBytes = brand("IdBytes", length(16)(Uint8Array));
|
|
905
|
+
export const idBytesTypeValueLength = 16;
|
|
906
|
+
export const idToIdBytes = (id) =>
|
|
907
|
+
// Id is Base64Url (validated by Id.from), cast is safe
|
|
908
|
+
base64UrlToUint8Array(id);
|
|
909
|
+
export const idBytesToId = (idBytes) =>
|
|
910
|
+
// Base64Url encoding of 16 bytes always produces valid Id (22 chars)
|
|
911
|
+
uint8ArrayToBase64Url(idBytes);
|
|
912
|
+
/**
|
|
913
|
+
* Positive number (> 0).
|
|
621
914
|
*
|
|
622
915
|
* ### Example
|
|
623
916
|
*
|
|
@@ -631,9 +924,9 @@ export const formatIdError = createTypeErrorFormatter((error) => `Invalid ${erro
|
|
|
631
924
|
* @category Number
|
|
632
925
|
*/
|
|
633
926
|
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}
|
|
927
|
+
export const formatPositiveError = createTypeErrorFormatter((error) => `The value ${error.value} must be positive (> 0).`);
|
|
635
928
|
/**
|
|
636
|
-
* Negative number.
|
|
929
|
+
* Negative number (< 0).
|
|
637
930
|
*
|
|
638
931
|
* ### Example
|
|
639
932
|
*
|
|
@@ -644,9 +937,9 @@ export const formatPositiveError = createTypeErrorFormatter((error) => `The valu
|
|
|
644
937
|
* @category Number
|
|
645
938
|
*/
|
|
646
939
|
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}
|
|
940
|
+
export const formatNegativeError = createTypeErrorFormatter((error) => `The value ${error.value} must be negative (< 0).`);
|
|
648
941
|
/**
|
|
649
|
-
* Non-positive number.
|
|
942
|
+
* Non-positive number (≤ 0).
|
|
650
943
|
*
|
|
651
944
|
* ### Example
|
|
652
945
|
*
|
|
@@ -659,9 +952,9 @@ export const formatNegativeError = createTypeErrorFormatter((error) => `The valu
|
|
|
659
952
|
export const nonPositive = (parent) => brand("NonPositive", parent, (value) => value <= 0
|
|
660
953
|
? ok(value)
|
|
661
954
|
: err({ type: "NonPositive", value }));
|
|
662
|
-
export const formatNonPositiveError = createTypeErrorFormatter((error) => `The value ${error.value}
|
|
955
|
+
export const formatNonPositiveError = createTypeErrorFormatter((error) => `The value ${error.value} must be non-positive (≤ 0).`);
|
|
663
956
|
/**
|
|
664
|
-
* Non-negative number.
|
|
957
|
+
* Non-negative number (≥ 0).
|
|
665
958
|
*
|
|
666
959
|
* ### Example
|
|
667
960
|
*
|
|
@@ -674,14 +967,30 @@ export const formatNonPositiveError = createTypeErrorFormatter((error) => `The v
|
|
|
674
967
|
export const nonNegative = (parent) => brand("NonNegative", parent, (value) => value >= 0
|
|
675
968
|
? ok(value)
|
|
676
969
|
: err({ type: "NonNegative", value }));
|
|
677
|
-
export const formatNonNegativeError = createTypeErrorFormatter((error) => `The value ${error.value}
|
|
678
|
-
/**
|
|
970
|
+
export const formatNonNegativeError = createTypeErrorFormatter((error) => `The value ${error.value} must be non-negative (≥ 0).`);
|
|
971
|
+
/**
|
|
972
|
+
* Non-negative number (≥ 0).
|
|
973
|
+
*
|
|
974
|
+
* @category Number
|
|
975
|
+
*/
|
|
679
976
|
export const NonNegativeNumber = nonNegative(Number);
|
|
680
|
-
/**
|
|
977
|
+
/**
|
|
978
|
+
* Positive number (> 0).
|
|
979
|
+
*
|
|
980
|
+
* @category Number
|
|
981
|
+
*/
|
|
681
982
|
export const PositiveNumber = positive(NonNegativeNumber);
|
|
682
|
-
/**
|
|
983
|
+
/**
|
|
984
|
+
* Non-positive number (≤ 0).
|
|
985
|
+
*
|
|
986
|
+
* @category Number
|
|
987
|
+
*/
|
|
683
988
|
export const NonPositiveNumber = nonPositive(Number);
|
|
684
|
-
/**
|
|
989
|
+
/**
|
|
990
|
+
* Negative number (< 0).
|
|
991
|
+
*
|
|
992
|
+
* @category Number
|
|
993
|
+
*/
|
|
685
994
|
export const NegativeNumber = negative(NonPositiveNumber);
|
|
686
995
|
/**
|
|
687
996
|
* Integer within the safe range of JavaScript numbers.
|
|
@@ -697,20 +1006,38 @@ export const NegativeNumber = negative(NonPositiveNumber);
|
|
|
697
1006
|
export const int = (parent) => brand("Int", parent, (value) => globalThis.Number.isSafeInteger(value)
|
|
698
1007
|
? ok(value)
|
|
699
1008
|
: err({ type: "Int", value }));
|
|
700
|
-
export const formatIntError = createTypeErrorFormatter((error) => `The value ${error.value}
|
|
1009
|
+
export const formatIntError = createTypeErrorFormatter((error) => `The value ${error.value} must be an integer.`);
|
|
701
1010
|
/**
|
|
702
1011
|
* Integer within the safe range of JavaScript numbers.
|
|
703
1012
|
*
|
|
704
1013
|
* @category Number
|
|
705
1014
|
*/
|
|
706
1015
|
export const Int = int(Number);
|
|
707
|
-
/**
|
|
1016
|
+
/**
|
|
1017
|
+
* Non-negative integer (≥ 0).
|
|
1018
|
+
*
|
|
1019
|
+
* @category Number
|
|
1020
|
+
*/
|
|
708
1021
|
export const NonNegativeInt = nonNegative(Int);
|
|
709
|
-
/**
|
|
1022
|
+
/**
|
|
1023
|
+
* Positive integer (> 0).
|
|
1024
|
+
*
|
|
1025
|
+
* @category Number
|
|
1026
|
+
*/
|
|
710
1027
|
export const PositiveInt = positive(NonNegativeInt);
|
|
711
|
-
/**
|
|
1028
|
+
/** Maximum safe positive integer value for practically infinite operations. */
|
|
1029
|
+
export const maxPositiveInt = PositiveInt.orThrow(globalThis.Number.MAX_SAFE_INTEGER);
|
|
1030
|
+
/**
|
|
1031
|
+
* Non-positive integer (≤ 0).
|
|
1032
|
+
*
|
|
1033
|
+
* @category Number
|
|
1034
|
+
*/
|
|
712
1035
|
export const NonPositiveInt = nonPositive(Int);
|
|
713
|
-
/**
|
|
1036
|
+
/**
|
|
1037
|
+
* Negative integer (< 0).
|
|
1038
|
+
*
|
|
1039
|
+
* @category Number
|
|
1040
|
+
*/
|
|
714
1041
|
export const NegativeInt = negative(NonPositiveInt);
|
|
715
1042
|
/**
|
|
716
1043
|
* Number greater than a specified value.
|
|
@@ -750,7 +1077,7 @@ export const formatLessThanOrEqualToError = createTypeErrorFormatter((error) =>
|
|
|
750
1077
|
export const nonNaN = (parent) => brand("NonNaN", parent, (value) => !globalThis.Number.isNaN(value)
|
|
751
1078
|
? ok(value)
|
|
752
1079
|
: err({ type: "NonNaN", value }));
|
|
753
|
-
export const formatNonNaNError = createTypeErrorFormatter((
|
|
1080
|
+
export const formatNonNaNError = createTypeErrorFormatter(() => `The value must not be NaN.`);
|
|
754
1081
|
/** @category Number */
|
|
755
1082
|
export const NonNaNNumber = nonNaN(Number);
|
|
756
1083
|
/**
|
|
@@ -761,7 +1088,7 @@ export const NonNaNNumber = nonNaN(Number);
|
|
|
761
1088
|
export const finite = (parent) => brand("Finite", parent, (value) => globalThis.Number.isFinite(value)
|
|
762
1089
|
? ok(value)
|
|
763
1090
|
: err({ type: "Finite", value }));
|
|
764
|
-
export const formatFiniteError = createTypeErrorFormatter((error) => `The value ${error.value}
|
|
1091
|
+
export const formatFiniteError = createTypeErrorFormatter((error) => `The value ${error.value} must be finite.`);
|
|
765
1092
|
/**
|
|
766
1093
|
* Finite number.
|
|
767
1094
|
*
|
|
@@ -802,8 +1129,6 @@ export const between = (min, max) => (parent) => brand(`Between${min}-${max}`, p
|
|
|
802
1129
|
? ok(value)
|
|
803
1130
|
: err({ type: "Between", value, min, max }));
|
|
804
1131
|
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
1132
|
/**
|
|
808
1133
|
* Literal {@link Type}.
|
|
809
1134
|
*
|
|
@@ -828,90 +1153,12 @@ export const literal = (expected) => {
|
|
|
828
1153
|
return {
|
|
829
1154
|
...createType("Literal", {
|
|
830
1155
|
fromUnknown,
|
|
831
|
-
to: identity,
|
|
832
1156
|
fromParent: ok,
|
|
833
|
-
toParent: identity,
|
|
834
1157
|
}),
|
|
835
1158
|
expected,
|
|
836
1159
|
};
|
|
837
1160
|
};
|
|
838
1161
|
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
1162
|
/**
|
|
916
1163
|
* Array of a specific {@link Type}.
|
|
917
1164
|
*
|
|
@@ -954,7 +1201,6 @@ export const array = (element) => {
|
|
|
954
1201
|
}
|
|
955
1202
|
return ok(result);
|
|
956
1203
|
};
|
|
957
|
-
const to = (value) => value.map(element.to);
|
|
958
1204
|
const fromParent = (value) => {
|
|
959
1205
|
const result = [];
|
|
960
1206
|
for (let i = 0; i < value.length; i++) {
|
|
@@ -974,14 +1220,8 @@ export const array = (element) => {
|
|
|
974
1220
|
}
|
|
975
1221
|
return ok(result);
|
|
976
1222
|
};
|
|
977
|
-
const toParent = (values) => values.map(element.toParent);
|
|
978
1223
|
return {
|
|
979
|
-
...createType("Array", {
|
|
980
|
-
fromUnknown,
|
|
981
|
-
to,
|
|
982
|
-
fromParent,
|
|
983
|
-
toParent,
|
|
984
|
-
}),
|
|
1224
|
+
...createType("Array", { fromUnknown, fromParent }),
|
|
985
1225
|
element,
|
|
986
1226
|
};
|
|
987
1227
|
};
|
|
@@ -1055,10 +1295,6 @@ export const record = (keyType, valueType) => {
|
|
|
1055
1295
|
}
|
|
1056
1296
|
return ok(result);
|
|
1057
1297
|
};
|
|
1058
|
-
const to = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
|
|
1059
|
-
keyType.to(key),
|
|
1060
|
-
valueType.to(val),
|
|
1061
|
-
]));
|
|
1062
1298
|
const fromParent = (value) => {
|
|
1063
1299
|
const result = {};
|
|
1064
1300
|
for (const [rawKey, rawValue] of Object.entries(value)) {
|
|
@@ -1090,16 +1326,10 @@ export const record = (keyType, valueType) => {
|
|
|
1090
1326
|
}
|
|
1091
1327
|
return ok(result);
|
|
1092
1328
|
};
|
|
1093
|
-
const toParent = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
|
|
1094
|
-
keyType.toParent(key),
|
|
1095
|
-
valueType.toParent(val),
|
|
1096
|
-
]));
|
|
1097
1329
|
return {
|
|
1098
1330
|
...createType("Record", {
|
|
1099
1331
|
fromUnknown,
|
|
1100
|
-
to,
|
|
1101
1332
|
fromParent,
|
|
1102
|
-
toParent,
|
|
1103
1333
|
}),
|
|
1104
1334
|
key: keyType,
|
|
1105
1335
|
value: valueType,
|
|
@@ -1186,22 +1416,6 @@ export function object(props, record) {
|
|
|
1186
1416
|
}
|
|
1187
1417
|
return ok(result);
|
|
1188
1418
|
};
|
|
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
1419
|
const fromParent = (value) => {
|
|
1206
1420
|
const errors = {};
|
|
1207
1421
|
const result = {};
|
|
@@ -1249,28 +1463,10 @@ export function object(props, record) {
|
|
|
1249
1463
|
}
|
|
1250
1464
|
return ok(result);
|
|
1251
1465
|
};
|
|
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
1466
|
return {
|
|
1269
1467
|
...createType(record ? "ObjectWithRecord" : "Object", {
|
|
1270
1468
|
fromUnknown,
|
|
1271
|
-
to,
|
|
1272
1469
|
fromParent,
|
|
1273
|
-
toParent,
|
|
1274
1470
|
}),
|
|
1275
1471
|
props,
|
|
1276
1472
|
...(record ? { record } : {}),
|
|
@@ -1323,7 +1519,6 @@ export function union(...args) {
|
|
|
1323
1519
|
* - Generate a specialized function to improve validation performance for such
|
|
1324
1520
|
* cases.
|
|
1325
1521
|
*/
|
|
1326
|
-
/* eslint-disable */
|
|
1327
1522
|
const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
|
|
1328
1523
|
const fromUnknown = (value) => {
|
|
1329
1524
|
const errors = [];
|
|
@@ -1339,23 +1534,13 @@ export function union(...args) {
|
|
|
1339
1534
|
errors,
|
|
1340
1535
|
});
|
|
1341
1536
|
};
|
|
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
1537
|
return {
|
|
1350
1538
|
...createType("Union", {
|
|
1351
1539
|
fromUnknown,
|
|
1352
|
-
to,
|
|
1353
1540
|
fromParent: fromUnknown,
|
|
1354
|
-
toParent: to,
|
|
1355
1541
|
}),
|
|
1356
1542
|
members,
|
|
1357
1543
|
};
|
|
1358
|
-
/* eslint-enable */
|
|
1359
1544
|
}
|
|
1360
1545
|
export const formatUnionError = (formatTypeError) => createTypeErrorFormatter((error) => {
|
|
1361
1546
|
const formattedErrors = error.errors
|
|
@@ -1411,20 +1596,10 @@ export const recursive = (create) => {
|
|
|
1411
1596
|
type ??= create();
|
|
1412
1597
|
return type.fromUnknown(value);
|
|
1413
1598
|
},
|
|
1414
|
-
to: (value) => {
|
|
1415
|
-
type ??= create();
|
|
1416
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
1417
|
-
return type.to(value);
|
|
1418
|
-
},
|
|
1419
1599
|
fromParent: (value) => {
|
|
1420
1600
|
type ??= create();
|
|
1421
1601
|
return type.fromParent(value);
|
|
1422
1602
|
},
|
|
1423
|
-
toParent: (value) => {
|
|
1424
|
-
type ??= create();
|
|
1425
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
1426
|
-
return type.toParent(value);
|
|
1427
|
-
},
|
|
1428
1603
|
is: (value) => {
|
|
1429
1604
|
type ??= create();
|
|
1430
1605
|
return type.is(value);
|
|
@@ -1528,7 +1703,6 @@ export const tuple = (...elements) => {
|
|
|
1528
1703
|
}
|
|
1529
1704
|
return ok(result);
|
|
1530
1705
|
};
|
|
1531
|
-
const to = (value) => value.map((val, index) => elements[index].to(val));
|
|
1532
1706
|
const fromParent = (value) => {
|
|
1533
1707
|
if (!Array.isArray(value) || value.length !== elements.length) {
|
|
1534
1708
|
return err({
|
|
@@ -1555,13 +1729,10 @@ export const tuple = (...elements) => {
|
|
|
1555
1729
|
}
|
|
1556
1730
|
return ok(result);
|
|
1557
1731
|
};
|
|
1558
|
-
const toParent = (value) => value.map((val, index) => elements[index].toParent(val));
|
|
1559
1732
|
return {
|
|
1560
1733
|
...createType("Tuple", {
|
|
1561
1734
|
fromUnknown,
|
|
1562
|
-
to,
|
|
1563
1735
|
fromParent,
|
|
1564
|
-
toParent,
|
|
1565
1736
|
}),
|
|
1566
1737
|
elements,
|
|
1567
1738
|
};
|
|
@@ -1595,22 +1766,16 @@ export const Int64 = brand("Int64", BigInt, (value) => value >= -922337203685477
|
|
|
1595
1766
|
? ok(value)
|
|
1596
1767
|
: err({ type: "Int64", value }));
|
|
1597
1768
|
export const formatInt64Error = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid 64-bit signed integer (Int64).`);
|
|
1598
|
-
export const BigIntFromString = transform(String, BigInt, (value) => trySync(() => globalThis.BigInt(value), () => ({ type: "BigIntFromString", value })), (value) => value.toString());
|
|
1599
|
-
export const formatBigIntFromStringError = createTypeErrorFormatter((error) => `The value ${error.value} could not be converted to a BigInt.`);
|
|
1600
1769
|
/**
|
|
1601
1770
|
* Stringified {@link Int64}.
|
|
1602
1771
|
*
|
|
1603
|
-
* @category
|
|
1772
|
+
* @category String
|
|
1604
1773
|
*/
|
|
1605
|
-
export const Int64String = brand("Int64",
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
return ok(value);
|
|
1611
|
-
}
|
|
1612
|
-
return err({ type: "Int64String", value });
|
|
1613
|
-
});
|
|
1774
|
+
export const Int64String = brand("Int64", NonEmptyTrimmedString, (value) => trySync(() => {
|
|
1775
|
+
const maybeInt = globalThis.BigInt(value);
|
|
1776
|
+
Int64.orThrow(maybeInt);
|
|
1777
|
+
return value;
|
|
1778
|
+
}, () => ({ type: "Int64String", value })));
|
|
1614
1779
|
export const formatInt64StringError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Int64 string.`);
|
|
1615
1780
|
/**
|
|
1616
1781
|
* JSON-compatible value: string, {@link FiniteNumber}, boolean, null,
|
|
@@ -1631,48 +1796,83 @@ export const JsonArray = array(JsonValue);
|
|
|
1631
1796
|
* @category Object
|
|
1632
1797
|
*/
|
|
1633
1798
|
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`,
|
|
1799
|
+
export const parseJson = (value) => trySync(() => JSON.parse(value), (error) => ({
|
|
1800
|
+
type: `Json`,
|
|
1649
1801
|
value,
|
|
1650
1802
|
message: globalThis.String(error),
|
|
1651
|
-
}))
|
|
1652
|
-
export const formatJsonValueFromStringError = createTypeErrorFormatter((error) => `Invalid JSONValue: ${error.value}. Error: ${error.message}`);
|
|
1803
|
+
}));
|
|
1653
1804
|
/**
|
|
1654
1805
|
* JSON-string {@link Type}.
|
|
1655
1806
|
*
|
|
1656
1807
|
* ### Example
|
|
1657
1808
|
*
|
|
1658
1809
|
* ```ts
|
|
1659
|
-
* const result = Json.from('{"key":"value"}'); //
|
|
1660
|
-
* const error = Json.from("invalid json"); //
|
|
1810
|
+
* const result = Json.from('{"key":"value"}'); // ok
|
|
1811
|
+
* const error = Json.from("invalid json"); // err
|
|
1661
1812
|
* ```
|
|
1662
1813
|
*
|
|
1663
1814
|
* @category String
|
|
1664
1815
|
*/
|
|
1665
1816
|
export const Json = brand("Json", String, (value) => {
|
|
1666
|
-
const result =
|
|
1817
|
+
const result = parseJson(value);
|
|
1667
1818
|
if (!result.ok)
|
|
1668
|
-
return
|
|
1669
|
-
type: "Json",
|
|
1670
|
-
value,
|
|
1671
|
-
message: result.error.message,
|
|
1672
|
-
});
|
|
1819
|
+
return result;
|
|
1673
1820
|
return ok(value);
|
|
1674
1821
|
});
|
|
1675
1822
|
export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON: ${error.value}. Error: ${error.message}`);
|
|
1823
|
+
export const jsonValueToJson = (value) => JSON.stringify(value);
|
|
1824
|
+
export const jsonToJsonValue = (value) => JSON.parse(value);
|
|
1825
|
+
/**
|
|
1826
|
+
* Creates a branded JSON string {@link Type} and type-safe conversion functions
|
|
1827
|
+
* for a given Type.
|
|
1828
|
+
*
|
|
1829
|
+
* This factory creates:
|
|
1830
|
+
*
|
|
1831
|
+
* 1. A branded string Type that validates JSON parsing and structural conformity
|
|
1832
|
+
* 2. A serialization function (Type → branded JSON string)
|
|
1833
|
+
* 3. A parsing function (branded JSON string → Type, skipping validation)
|
|
1834
|
+
*
|
|
1835
|
+
* Optimized for Evolu's SQLite workflow where we store typed JSON strings and
|
|
1836
|
+
* need type-safe conversions without double parsing.
|
|
1837
|
+
*
|
|
1838
|
+
* ### Example
|
|
1839
|
+
*
|
|
1840
|
+
* ```ts
|
|
1841
|
+
* const Person = object({
|
|
1842
|
+
* name: NonEmptyString100,
|
|
1843
|
+
* age: FiniteNumber,
|
|
1844
|
+
* });
|
|
1845
|
+
* type Person = typeof Person.Type;
|
|
1846
|
+
*
|
|
1847
|
+
* const [PersonJson, personToPersonJson, personJsonToPerson] = json(
|
|
1848
|
+
* Person,
|
|
1849
|
+
* "PersonJson",
|
|
1850
|
+
* );
|
|
1851
|
+
* // string & Brand<"PersonJson">
|
|
1852
|
+
* type PersonJson = typeof PersonJson.Type;
|
|
1853
|
+
*
|
|
1854
|
+
* // Usage:
|
|
1855
|
+
* const person: Person = { name: "Alice", age: 30 };
|
|
1856
|
+
* const jsonString = personToPersonJson(person); // PersonJson
|
|
1857
|
+
* const backToPerson = personJsonToPerson(jsonString); // Person
|
|
1858
|
+
* ```
|
|
1859
|
+
*/
|
|
1860
|
+
export const json = (type, name) => {
|
|
1861
|
+
const BrandedJsonType = brand(name, String, (value) => {
|
|
1862
|
+
const parseResult = parseJson(value);
|
|
1863
|
+
if (!parseResult.ok)
|
|
1864
|
+
return parseResult;
|
|
1865
|
+
const validationResult = type.fromUnknown(parseResult.value);
|
|
1866
|
+
if (!validationResult.ok)
|
|
1867
|
+
return validationResult;
|
|
1868
|
+
return ok(value);
|
|
1869
|
+
});
|
|
1870
|
+
return [
|
|
1871
|
+
BrandedJsonType,
|
|
1872
|
+
jsonValueToJson,
|
|
1873
|
+
jsonToJsonValue,
|
|
1874
|
+
];
|
|
1875
|
+
};
|
|
1676
1876
|
/**
|
|
1677
1877
|
* Optional {@link Type}.
|
|
1678
1878
|
*
|
|
@@ -1697,9 +1897,7 @@ export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON
|
|
|
1697
1897
|
export const optional = (type) => ({
|
|
1698
1898
|
...createType("Optional", {
|
|
1699
1899
|
fromUnknown: type.fromUnknown,
|
|
1700
|
-
to: type.to,
|
|
1701
1900
|
fromParent: type.fromParent,
|
|
1702
|
-
toParent: type.toParent,
|
|
1703
1901
|
}),
|
|
1704
1902
|
parent: type,
|
|
1705
1903
|
});
|
|
@@ -1708,7 +1906,7 @@ export const isOptionalType = (x) => typeof x === "object" && x != null && "name
|
|
|
1708
1906
|
/**
|
|
1709
1907
|
* Creates a partial object type where all properties are optional.
|
|
1710
1908
|
*
|
|
1711
|
-
* This is useful when
|
|
1909
|
+
* This is useful when we want to validate an object in which none of the keys
|
|
1712
1910
|
* are required, but if they are present they must conform to their
|
|
1713
1911
|
* corresponding Types.
|
|
1714
1912
|
*
|
|
@@ -1779,114 +1977,86 @@ export function omit(objectType, ...keys) {
|
|
|
1779
1977
|
}
|
|
1780
1978
|
return object(newProps);
|
|
1781
1979
|
}
|
|
1980
|
+
export const maxMutationSize = 655360;
|
|
1782
1981
|
/**
|
|
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
|
-
* ```
|
|
1982
|
+
* Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
|
|
1983
|
+
* the `maxProtocolMessageRangesSize`. The max size is 640KB in bytes, measured
|
|
1984
|
+
* via MessagePack. Evolu Protocol DbChange will be smaller thanks to various
|
|
1985
|
+
* optimizations.
|
|
1810
1986
|
*/
|
|
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
|
-
};
|
|
1987
|
+
export const validMutationSize = (type) => brand("ValidMutationSize", type, (value) => pack(value).byteLength <= maxMutationSize
|
|
1988
|
+
? ok(value)
|
|
1989
|
+
: err({ type: "ValidMutationSize", value }));
|
|
1990
|
+
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
1991
|
/**
|
|
1826
|
-
*
|
|
1827
|
-
*
|
|
1828
|
-
*
|
|
1992
|
+
* Formats Evolu Type errors into user-friendly messages.
|
|
1993
|
+
*
|
|
1994
|
+
* Evolu Type typed errors ensure every error type must have a formatter.
|
|
1995
|
+
* TypeScript enforces this at compile-time, preventing unhandled validation
|
|
1996
|
+
* errors from reaching users.
|
|
1829
1997
|
*
|
|
1830
|
-
*
|
|
1831
|
-
*
|
|
1998
|
+
* The `createFormatTypeError` function handles both built-in {@link TypeErrors}
|
|
1999
|
+
* and custom errors, and lets us override default formatting for specific
|
|
2000
|
+
* errors.
|
|
1832
2001
|
*
|
|
1833
|
-
* ###
|
|
2002
|
+
* ### Example
|
|
1834
2003
|
*
|
|
1835
2004
|
* ```ts
|
|
1836
|
-
* const
|
|
1837
|
-
*
|
|
1838
|
-
*
|
|
2005
|
+
* const formatTypeError = createFormatTypeError<
|
|
2006
|
+
* MinLengthError | MaxLengthError
|
|
2007
|
+
* >((error): string => {
|
|
2008
|
+
* switch (error.type) {
|
|
2009
|
+
* case "MinLength":
|
|
2010
|
+
* return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
|
|
2011
|
+
* case "MaxLength":
|
|
2012
|
+
* return `Text is too long (maximum ${error.max} characters)`;
|
|
2013
|
+
* }
|
|
2014
|
+
* });
|
|
1839
2015
|
* ```
|
|
1840
2016
|
*
|
|
1841
|
-
*
|
|
2017
|
+
* Alternatively, write a custom formatter from scratch without using
|
|
2018
|
+
* `createFormatTypeError`. This gives us full control over error formatting:
|
|
1842
2019
|
*
|
|
1843
2020
|
* ```ts
|
|
1844
|
-
*
|
|
1845
|
-
*
|
|
2021
|
+
* const Person = object({
|
|
2022
|
+
* name: NonEmptyTrimmedString100,
|
|
2023
|
+
* age: optional(PositiveInt),
|
|
2024
|
+
* });
|
|
2025
|
+
*
|
|
2026
|
+
* // Define only the errors actually used by Person Type
|
|
2027
|
+
* type PersonErrors =
|
|
1846
2028
|
* | StringError
|
|
1847
|
-
* | MinLengthError
|
|
1848
2029
|
* | MaxLengthError
|
|
1849
|
-
* |
|
|
1850
|
-
* | IdError
|
|
2030
|
+
* | MinLengthError
|
|
1851
2031
|
* | 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.
|
|
2032
|
+
* | PositiveError
|
|
2033
|
+
* | NonNegativeError
|
|
2034
|
+
* | IntError
|
|
2035
|
+
* | NumberError
|
|
2036
|
+
* | ObjectError<Record<string, PersonErrors>>;
|
|
2037
|
+
*
|
|
2038
|
+
* const formatTypeError: TypeErrorFormatter<PersonErrors> = (error) => {
|
|
1861
2039
|
* 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
2040
|
* case "String":
|
|
1873
|
-
* // We can reuse existing formatter.
|
|
1874
2041
|
* return formatStringError(error);
|
|
2042
|
+
* case "Number":
|
|
2043
|
+
* return "Must be a number";
|
|
2044
|
+
* case "MinLength":
|
|
2045
|
+
* return `Must be at least ${error.min} characters`;
|
|
2046
|
+
* case "MaxLength":
|
|
2047
|
+
* return `Cannot exceed ${error.max} characters`;
|
|
1875
2048
|
* case "Trimmed":
|
|
1876
|
-
* return "
|
|
1877
|
-
* case "
|
|
1878
|
-
* return "
|
|
1879
|
-
* case "
|
|
1880
|
-
* return
|
|
1881
|
-
*
|
|
1882
|
-
*
|
|
1883
|
-
* return `Union errors: ${error.errors.map(formatTypeError).join(", ")}`;
|
|
2049
|
+
* return "Cannot have leading or trailing spaces";
|
|
2050
|
+
* case "Positive":
|
|
2051
|
+
* return "Must be a positive number";
|
|
2052
|
+
* case "NonNegative":
|
|
2053
|
+
* return "Must be zero or positive";
|
|
2054
|
+
* case "Int":
|
|
2055
|
+
* return "Must be an integer";
|
|
1884
2056
|
* case "Object": {
|
|
1885
|
-
* if (
|
|
1886
|
-
*
|
|
1887
|
-
*
|
|
1888
|
-
* )
|
|
1889
|
-
* return "A developer made an error, this should not happen.";
|
|
2057
|
+
* if (error.reason.kind === "NotObject") return "Must be an object";
|
|
2058
|
+
* if (error.reason.kind === "ExtraKeys")
|
|
2059
|
+
* return "Contains unexpected fields";
|
|
1890
2060
|
* const firstError = Object.values(error.reason.errors).find(
|
|
1891
2061
|
* (e) => e !== undefined,
|
|
1892
2062
|
* )!;
|
|
@@ -1927,8 +2097,8 @@ export const createFormatTypeError = (extraFormatter) => {
|
|
|
1927
2097
|
return formatIsTypeError(error);
|
|
1928
2098
|
case "CurrencyCode":
|
|
1929
2099
|
return formatCurrencyCodeError(error);
|
|
1930
|
-
case "
|
|
1931
|
-
return
|
|
2100
|
+
case "DateIso":
|
|
2101
|
+
return formatDateIsoError(error);
|
|
1932
2102
|
case "Trimmed":
|
|
1933
2103
|
return formatTrimmedError(error);
|
|
1934
2104
|
case "MinLength":
|
|
@@ -1943,6 +2113,8 @@ export const createFormatTypeError = (extraFormatter) => {
|
|
|
1943
2113
|
return formatRegexError(error);
|
|
1944
2114
|
case "Id":
|
|
1945
2115
|
return formatIdError(error);
|
|
2116
|
+
case "TableId":
|
|
2117
|
+
return formatTableIdError(error);
|
|
1946
2118
|
case "Positive":
|
|
1947
2119
|
return formatPositiveError(error);
|
|
1948
2120
|
case "Negative":
|
|
@@ -1973,14 +2145,12 @@ export const createFormatTypeError = (extraFormatter) => {
|
|
|
1973
2145
|
return formatLiteralError(error);
|
|
1974
2146
|
case "Int64":
|
|
1975
2147
|
return formatInt64Error(error);
|
|
1976
|
-
case "BigIntFromString":
|
|
1977
|
-
return formatBigIntFromStringError(error);
|
|
1978
2148
|
case "Int64String":
|
|
1979
2149
|
return formatInt64StringError(error);
|
|
1980
|
-
case "JsonValueFromString":
|
|
1981
|
-
return formatJsonValueFromStringError(error);
|
|
1982
2150
|
case "Json":
|
|
1983
2151
|
return formatJsonError(error);
|
|
2152
|
+
case "ValidMutationSize":
|
|
2153
|
+
return formatValidMutationSizeError(error);
|
|
1984
2154
|
// Composite Types
|
|
1985
2155
|
case "SimplePassword":
|
|
1986
2156
|
return formatSimplePasswordError(formatTypeError)(error);
|
|
@@ -1996,7 +2166,88 @@ export const createFormatTypeError = (extraFormatter) => {
|
|
|
1996
2166
|
return formatUnionError(formatTypeError)(error);
|
|
1997
2167
|
case "Tuple":
|
|
1998
2168
|
return formatTupleError(formatTypeError)(error);
|
|
2169
|
+
default: {
|
|
2170
|
+
// Fallback for unknown error types
|
|
2171
|
+
const unknownError = error;
|
|
2172
|
+
return `A value ${safelyStringifyUnknownValue(unknownError.value)} is not valid for type ${unknownError.type}.`;
|
|
2173
|
+
}
|
|
1999
2174
|
}
|
|
2000
2175
|
};
|
|
2001
2176
|
return formatTypeError;
|
|
2002
2177
|
};
|
|
2178
|
+
/**
|
|
2179
|
+
* Converts an Evolu {@link TypeError} to Standard Schema V1 issues format.
|
|
2180
|
+
*
|
|
2181
|
+
* This function recursively converts Evolu's typed errors into the Standard
|
|
2182
|
+
* Schema issue format with proper path tracking for nested structures.
|
|
2183
|
+
*
|
|
2184
|
+
* @category Utilities
|
|
2185
|
+
*/
|
|
2186
|
+
export const typeErrorToStandardSchemaIssues = (error, formatTypeError, path = []) => {
|
|
2187
|
+
if (error.type === "Array") {
|
|
2188
|
+
const arrayError = error;
|
|
2189
|
+
if (arrayError.reason.kind === "NotArray") {
|
|
2190
|
+
return [{ message: formatTypeError(error), path }];
|
|
2191
|
+
}
|
|
2192
|
+
return typeErrorToStandardSchemaIssues(arrayError.reason.error, formatTypeError, [...path, arrayError.reason.index]);
|
|
2193
|
+
}
|
|
2194
|
+
if (error.type === "Object") {
|
|
2195
|
+
const objectError = error;
|
|
2196
|
+
if (objectError.reason.kind === "NotObject" ||
|
|
2197
|
+
objectError.reason.kind === "ExtraKeys") {
|
|
2198
|
+
return [{ message: formatTypeError(error), path }];
|
|
2199
|
+
}
|
|
2200
|
+
const issues = [];
|
|
2201
|
+
for (const [key, propError] of Object.entries(objectError.reason.errors)) {
|
|
2202
|
+
issues.push(...typeErrorToStandardSchemaIssues(propError, formatTypeError, [...path, key]));
|
|
2203
|
+
}
|
|
2204
|
+
return issues;
|
|
2205
|
+
}
|
|
2206
|
+
if (error.type === "ObjectWithRecord") {
|
|
2207
|
+
const objectWithRecordError = error;
|
|
2208
|
+
if (objectWithRecordError.reason.kind === "NotObject") {
|
|
2209
|
+
return [{ message: formatTypeError(error), path }];
|
|
2210
|
+
}
|
|
2211
|
+
if (objectWithRecordError.reason.kind === "IndexKey" ||
|
|
2212
|
+
objectWithRecordError.reason.kind === "IndexValue") {
|
|
2213
|
+
return typeErrorToStandardSchemaIssues(objectWithRecordError.reason.error, formatTypeError, [...path, objectWithRecordError.reason.key]);
|
|
2214
|
+
}
|
|
2215
|
+
const issues = [];
|
|
2216
|
+
for (const [key, propError] of Object.entries(objectWithRecordError.reason.errors)) {
|
|
2217
|
+
issues.push(...typeErrorToStandardSchemaIssues(propError, formatTypeError, [...path, key]));
|
|
2218
|
+
}
|
|
2219
|
+
return issues;
|
|
2220
|
+
}
|
|
2221
|
+
if (error.type === "Record") {
|
|
2222
|
+
const recordError = error;
|
|
2223
|
+
if (recordError.reason.kind === "NotRecord") {
|
|
2224
|
+
return [{ message: formatTypeError(error), path }];
|
|
2225
|
+
}
|
|
2226
|
+
return typeErrorToStandardSchemaIssues(recordError.reason.error, formatTypeError, [...path, recordError.reason.key]);
|
|
2227
|
+
}
|
|
2228
|
+
if (error.type === "Tuple") {
|
|
2229
|
+
const tupleError = error;
|
|
2230
|
+
if (tupleError.reason.kind === "InvalidLength") {
|
|
2231
|
+
return [{ message: formatTypeError(error), path }];
|
|
2232
|
+
}
|
|
2233
|
+
return typeErrorToStandardSchemaIssues(tupleError.reason.error, formatTypeError, [...path, tupleError.reason.index]);
|
|
2234
|
+
}
|
|
2235
|
+
if (error.type === "Union") {
|
|
2236
|
+
const unionError = error;
|
|
2237
|
+
return unionError.errors.flatMap((err) => typeErrorToStandardSchemaIssues(err, formatTypeError, path));
|
|
2238
|
+
}
|
|
2239
|
+
if (error.type === "Brand") {
|
|
2240
|
+
const brandError = error;
|
|
2241
|
+
if ("parentError" in brandError) {
|
|
2242
|
+
return typeErrorToStandardSchemaIssues(brandError.parentError, formatTypeError, path);
|
|
2243
|
+
}
|
|
2244
|
+
return [{ message: formatTypeError(error), path }];
|
|
2245
|
+
}
|
|
2246
|
+
return [{ message: formatTypeError(error), path }];
|
|
2247
|
+
};
|
|
2248
|
+
/**
|
|
2249
|
+
* Shared formatter cache for Standard Schema integration - avoids circular
|
|
2250
|
+
* dependency by lazily creating the formatter on first use rather than during
|
|
2251
|
+
* module initialization.
|
|
2252
|
+
*/
|
|
2253
|
+
let cachedStandardSchemaFormatTypeError;
|