@evolu/common 6.0.1-preview.2 → 6.0.1-preview.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Assert.d.ts +6 -3
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +7 -4
- package/dist/src/Brand.d.ts +75 -0
- package/dist/src/Brand.d.ts.map +1 -0
- package/dist/src/Brand.js +1 -0
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +1 -1
- package/dist/src/CallbackRegistry.d.ts +53 -0
- package/dist/src/CallbackRegistry.d.ts.map +1 -0
- package/dist/src/CallbackRegistry.js +25 -0
- package/dist/src/Console.d.ts +31 -6
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +72 -9
- package/dist/src/Crypto.d.ts +61 -34
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +29 -42
- package/dist/src/Evolu/Db.d.ts +159 -66
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +300 -705
- package/dist/src/Evolu/Diff.d.ts +3 -3
- package/dist/src/Evolu/Diff.d.ts.map +1 -1
- package/dist/src/Evolu/Diff.js +7 -5
- package/dist/src/Evolu/Evolu.d.ts +200 -115
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +287 -157
- package/dist/src/Evolu/Internal.d.ts +0 -2
- package/dist/src/Evolu/Internal.d.ts.map +1 -1
- package/dist/src/Evolu/Internal.js +0 -2
- package/dist/src/Evolu/LocalAuth.d.ts +144 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +171 -0
- package/dist/src/Evolu/Owner.d.ts +209 -124
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +118 -104
- package/dist/src/Evolu/Platform.d.ts +9 -7
- package/dist/src/Evolu/Platform.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.d.ts +256 -211
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +558 -359
- package/dist/src/Evolu/Public.d.ts +6 -8
- package/dist/src/Evolu/Public.d.ts.map +1 -1
- package/dist/src/Evolu/Public.js +2 -3
- package/dist/src/Evolu/PublicKysely.js +3 -3
- package/dist/src/Evolu/Query.d.ts +2 -1
- package/dist/src/Evolu/Query.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.d.ts +72 -4
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +124 -13
- package/dist/src/Evolu/Schema.d.ts +129 -73
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +169 -89
- package/dist/src/Evolu/Storage.d.ts +159 -14
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +42 -32
- package/dist/src/Evolu/Sync.d.ts +68 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +414 -20
- package/dist/src/Evolu/Timestamp.d.ts +35 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +27 -18
- package/dist/src/ManyToManyMap.d.ts +74 -10
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/ManyToManyMap.js +41 -6
- package/dist/src/Number.d.ts +2 -1
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/RefCountedResourceManager.d.ts +119 -0
- package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
- package/dist/src/RefCountedResourceManager.js +197 -0
- package/dist/src/Result.d.ts +176 -51
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +30 -241
- package/dist/src/Sqlite.d.ts +21 -4
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +50 -8
- package/dist/src/Task.d.ts +511 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +410 -0
- package/dist/src/Time.d.ts +59 -0
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +87 -4
- package/dist/src/Type.d.ts +474 -327
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +502 -436
- package/dist/src/Types.d.ts +0 -74
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -13
- package/dist/src/Worker.d.ts +39 -11
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +22 -4
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -3
- package/package.json +14 -13
- package/src/Assert.ts +8 -7
- package/src/Brand.ts +75 -0
- package/src/Buffer.ts +1 -1
- package/src/CallbackRegistry.ts +84 -0
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +94 -79
- package/src/Evolu/Db.ts +528 -1027
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +682 -329
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +422 -0
- package/src/Evolu/Owner.ts +282 -227
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +788 -640
- package/src/Evolu/Public.ts +7 -14
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Query.ts +2 -1
- package/src/Evolu/Relay.ts +238 -18
- package/src/Evolu/Schema.ts +391 -191
- package/src/Evolu/Storage.ts +275 -55
- package/src/Evolu/Sync.ts +708 -37
- package/src/Evolu/Timestamp.ts +38 -35
- package/src/ManyToManyMap.ts +127 -24
- package/src/Number.ts +2 -6
- package/src/Random.ts +3 -2
- package/src/RefCountedResourceManager.ts +368 -0
- package/src/Result.ts +183 -53
- package/src/Sqlite.ts +59 -23
- package/src/Task.ts +779 -0
- package/src/Time.ts +168 -4
- package/src/Type.ts +708 -660
- package/src/Types.ts +0 -76
- package/src/WebSocket.ts +23 -17
- package/src/Worker.ts +72 -23
- package/src/index.ts +3 -3
- package/dist/src/Callbacks.d.ts +0 -20
- package/dist/src/Callbacks.d.ts.map +0 -1
- package/dist/src/Callbacks.js +0 -18
- package/dist/src/Evolu/Config.d.ts +0 -69
- package/dist/src/Evolu/Config.d.ts.map +0 -1
- package/dist/src/Evolu/Config.js +0 -9
- package/dist/src/Evolu/Kysely.d.ts +0 -6
- package/dist/src/Evolu/Kysely.d.ts.map +0 -1
- package/dist/src/Evolu/Kysely.js +0 -21
- package/dist/src/NanoId.d.ts +0 -27
- package/dist/src/NanoId.d.ts.map +0 -1
- package/dist/src/NanoId.js +0 -6
- package/dist/src/Promise.d.ts +0 -180
- package/dist/src/Promise.d.ts.map +0 -1
- package/dist/src/Promise.js +0 -176
- package/src/Callbacks.ts +0 -43
- package/src/Evolu/Config.ts +0 -83
- package/src/Evolu/Kysely.ts +0 -38
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/dist/src/Type.js
CHANGED
|
@@ -1,80 +1,182 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 🧩
|
|
2
|
+
* 🧩 Type-safe runtime types
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Evolu {@link Type} is like a type guard that returns typed errors (via
|
|
5
|
+
* {@link Result}) instead of throwing. We either get a safely typed value or a
|
|
6
|
+
* precise, composable error value telling us exactly why validation failed.
|
|
5
7
|
*
|
|
6
|
-
*
|
|
8
|
+
* Why another validation library?
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* - **Result-based error handling** – no exceptions for normal control flow.
|
|
11
|
+
* - **Typed errors with decoupled formatters** – validation logic ≠ user
|
|
12
|
+
* messages.
|
|
13
|
+
* - **Consistent constraints via {@link Brand}** – every constraint becomes part
|
|
14
|
+
* of the type.
|
|
15
|
+
* - **No user-land chaining DSL** – designed with the upcoming ES pipe operator
|
|
16
|
+
* in mind.
|
|
17
|
+
* - **Selective validation** – parent validations are skipped when already proved
|
|
18
|
+
* by typing.
|
|
19
|
+
* - **Simple, top-down implementation** – readable source code from top to bottom
|
|
20
|
+
* with no hidden magic; just plain functions and composition.
|
|
10
21
|
*
|
|
11
|
-
*
|
|
12
|
-
* exceptions.
|
|
13
|
-
* - **Consistent constraints**: Enforcing {@link Brand} for all constraints.
|
|
14
|
-
* - **Typed errors with decoupled formatters**: Avoiding coupling error messages
|
|
15
|
-
* with validators.
|
|
16
|
-
* - **No user-land chaining**: Designed with ES pipe operator in mind.
|
|
17
|
-
* - **Selective validation/transformation**: Skipping parent Type validations and
|
|
18
|
-
* transformations when TypeScript's type system can be relied upon.
|
|
19
|
-
* - **Bidirectional transformations**: Supporting transformations in both
|
|
20
|
-
* directions.
|
|
21
|
-
* - **Minimal and transparent code**: No runtime dependencies or hidden magic.
|
|
22
|
+
* ### Base Types Quick Start
|
|
22
23
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* // Validate unknown values
|
|
26
|
+
* const value: unknown = "hello";
|
|
27
|
+
* const stringResult = String.fromUnknown(value);
|
|
28
|
+
* if (!stringResult.ok) {
|
|
29
|
+
* // console.error(formatStringError(stringResult.error));
|
|
30
|
+
* return stringResult; // inside a function returning Result<string, _>
|
|
31
|
+
* }
|
|
32
|
+
* // Safe branch: value is now string
|
|
33
|
+
* const upper = stringResult.value.toUpperCase();
|
|
34
|
+
*
|
|
35
|
+
* // Type guard style
|
|
36
|
+
* if (String.is(value)) {
|
|
37
|
+
* // narrowed to string
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* // Composing: arrays & objects
|
|
41
|
+
* const Numbers = array(Number); // ReadonlyArray<number>
|
|
42
|
+
* const Point = object({ x: Number, y: Number });
|
|
43
|
+
*
|
|
44
|
+
* Numbers.from([1, 2, 3]); // ok
|
|
45
|
+
* Point.from({ x: 1, y: 2 }); // ok
|
|
46
|
+
* Point.from({ x: 1, y: "2" }); // err -> nested Number error
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* ### Branding Basics
|
|
50
|
+
*
|
|
51
|
+
* Branding adds semantic meaning & constraints while preserving the runtime
|
|
52
|
+
* shape:
|
|
27
53
|
*
|
|
28
|
-
*
|
|
54
|
+
* ```ts
|
|
55
|
+
* const CurrencyCode = brand("CurrencyCode", String, (value) =>
|
|
56
|
+
* /^[A-Z]{3}$/.test(value)
|
|
57
|
+
* ? ok(value)
|
|
58
|
+
* : err<CurrencyCodeError>({ type: "CurrencyCode", value }),
|
|
59
|
+
* );
|
|
60
|
+
* type CurrencyCode = typeof CurrencyCode.Type; // string & Brand<"CurrencyCode">
|
|
29
61
|
*
|
|
30
|
-
*
|
|
31
|
-
* - A function to create a value of that type, which may fail.
|
|
32
|
-
* - A function to transform value back to its original representation, which
|
|
33
|
-
* cannot fail.
|
|
62
|
+
* interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
|
|
34
63
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
64
|
+
* const formatCurrencyCodeError =
|
|
65
|
+
* createTypeErrorFormatter<CurrencyCodeError>(
|
|
66
|
+
* (error) => `Invalid currency code: ${error.value}`,
|
|
67
|
+
* );
|
|
68
|
+
*
|
|
69
|
+
* const r = CurrencyCode.from("USD"); // ok("USD")
|
|
70
|
+
* const e = CurrencyCode.from("usd"); // err(...)
|
|
71
|
+
* ```
|
|
39
72
|
*
|
|
40
|
-
*
|
|
73
|
+
* See also reusable brand factories like `minLength`, `maxLength`, `trimmed`,
|
|
74
|
+
* `positive`, `between`, etc.
|
|
75
|
+
*
|
|
76
|
+
* ### Objects & Optional Fields
|
|
77
|
+
*
|
|
78
|
+
* ```ts
|
|
79
|
+
* const User = object({
|
|
80
|
+
* name: NonEmptyTrimmedString100,
|
|
81
|
+
* age: optional(PositiveInt),
|
|
82
|
+
* });
|
|
83
|
+
* type User = typeof User.Type;
|
|
41
84
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
85
|
+
* User.from({ name: "Alice" }); // ok
|
|
86
|
+
* User.from({ name: "Alice", age: -1 }); // err(PositiveInt)
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* ### Deriving JSON String Types
|
|
90
|
+
*
|
|
91
|
+
* ```ts
|
|
92
|
+
* const Person = object({
|
|
93
|
+
* name: NonEmptyString50,
|
|
94
|
+
* // Did you know that JSON.stringify converts NaN (a number) into null?
|
|
95
|
+
* // To prevent this, use FiniteNumber.
|
|
96
|
+
* age: FiniteNumber,
|
|
97
|
+
* });
|
|
98
|
+
* type Person = typeof Person.Type;
|
|
99
|
+
*
|
|
100
|
+
* const [PersonJson, personToPersonJson, personJsonToPerson] = json(
|
|
101
|
+
* Person,
|
|
102
|
+
* "PersonJson",
|
|
103
|
+
* );
|
|
104
|
+
* // string & Brand<"PersonJson">
|
|
105
|
+
* type PersonJson = typeof PersonJson.Type;
|
|
106
|
+
*
|
|
107
|
+
* const person = Person.orThrow({
|
|
108
|
+
* name: "Alice",
|
|
109
|
+
* age: 30,
|
|
110
|
+
* });
|
|
111
|
+
*
|
|
112
|
+
* const personJson = personToPersonJson(person);
|
|
113
|
+
* expect(personJsonToPerson(personJson)).toEqual(person);
|
|
114
|
+
* ```
|
|
44
115
|
*
|
|
45
|
-
*
|
|
46
|
-
* `fromParent` functions are just a typed alias to `fromUnknown` to ensure that
|
|
47
|
-
* `fromParent` and `toParent` can be called on any Type.
|
|
116
|
+
* ### Error Formatting
|
|
48
117
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
118
|
+
* Evolu separates validation logic from human-readable messages. There are two
|
|
119
|
+
* layers:
|
|
51
120
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
121
|
+
* 1. Per-type formatters (e.g. `formatStringError`) – simple, focused, already
|
|
122
|
+
* used earlier in the quick start example.
|
|
123
|
+
* 2. A unified formatter via `createFormatTypeError` – composes all built-in and
|
|
124
|
+
* custom errors (including nested composite types) and lets us override
|
|
125
|
+
* selected messages.
|
|
126
|
+
*
|
|
127
|
+
* #### 1. Per-Type Formatter (recap)
|
|
128
|
+
*
|
|
129
|
+
* ```ts
|
|
130
|
+
* const r = String.fromUnknown(42);
|
|
131
|
+
* if (!r.ok) console.error(formatStringError(r.error));
|
|
132
|
+
* ```
|
|
133
|
+
*
|
|
134
|
+
* #### 2. Unified Formatter with Overrides
|
|
135
|
+
*
|
|
136
|
+
* ```ts
|
|
137
|
+
* // Override only what we care about; fall back to built-ins for the rest.
|
|
138
|
+
* const formatTypeError = createFormatTypeError((error) => {
|
|
139
|
+
* if (error.type === "MinLength") return `Min length is ${error.min}`;
|
|
140
|
+
* });
|
|
141
|
+
*
|
|
142
|
+
* const User = object({ name: NonEmptyTrimmedString100 });
|
|
143
|
+
* const resultUser = User.from({ name: "" });
|
|
144
|
+
* if (!resultUser.ok) console.error(formatTypeError(resultUser.error));
|
|
145
|
+
*
|
|
146
|
+
* const badPoint = object({ x: Number, y: Number }).from({
|
|
147
|
+
* x: 1,
|
|
148
|
+
* y: "foo",
|
|
149
|
+
* });
|
|
150
|
+
* if (!badPoint.ok) console.error(formatTypeError(badPoint.error));
|
|
151
|
+
* ```
|
|
152
|
+
*
|
|
153
|
+
* The unified formatter walks nested structures (object / array / record /
|
|
154
|
+
* tuple / union) and applies overrides only where specified, greatly reducing
|
|
155
|
+
* boilerplate when formatting complex validation errors.
|
|
57
156
|
*
|
|
58
157
|
* ### Tip
|
|
59
158
|
*
|
|
60
159
|
* If necessary, write `globalThis.String` instead of `String` to avoid naming
|
|
61
|
-
* clashes with
|
|
160
|
+
* clashes with native types.
|
|
62
161
|
*
|
|
63
|
-
* ### Design Decision:
|
|
162
|
+
* ### Design Decision: No Bidirectional Transformations
|
|
64
163
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
164
|
+
* Evolu Type intentionally does not support bidirectional transformations. It
|
|
165
|
+
* previously did, but supporting that while keeping typed error fidelity added
|
|
166
|
+
* complexity that hurt readability & reliability. Most persistence pipelines
|
|
167
|
+
* (e.g. SQLite) already require explicit mapping of query results, so implicit
|
|
168
|
+
* reverse transforms would not buy much. We may revisit this if we can design a
|
|
169
|
+
* minimal, 100% safe API that preserves simplicity.
|
|
69
170
|
*
|
|
70
171
|
* @module
|
|
71
172
|
*/
|
|
173
|
+
import { utf8ToBytes } from "@noble/ciphers/utils.js";
|
|
174
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
72
175
|
import * as bip39 from "@scure/bip39";
|
|
73
|
-
import { wordlist } from "@scure/bip39/wordlists/english";
|
|
74
|
-
import {
|
|
75
|
-
import { identity } from "./Function.js";
|
|
176
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
177
|
+
import { pack } from "msgpackr";
|
|
76
178
|
import { isPlainObject } from "./Object.js";
|
|
77
|
-
import { err, ok, trySync } from "./Result.js";
|
|
179
|
+
import { err, getOrThrow, ok, trySync } from "./Result.js";
|
|
78
180
|
import { safelyStringifyUnknownValue } from "./String.js";
|
|
79
181
|
const EvoluTypeSymbol = Symbol("evolu.Type");
|
|
80
182
|
/**
|
|
@@ -88,6 +190,7 @@ const createType = (name, definition) => ({
|
|
|
88
190
|
name,
|
|
89
191
|
is: (value) => definition.fromUnknown(value).ok,
|
|
90
192
|
from: definition.fromUnknown,
|
|
193
|
+
orThrow: (value) => getOrThrow(definition.fromUnknown(value)),
|
|
91
194
|
[EvoluTypeSymbol]: true,
|
|
92
195
|
Type: undefined,
|
|
93
196
|
Input: undefined,
|
|
@@ -117,12 +220,6 @@ export const createTypeErrorFormatter = (format) => (error) => format({ ...error
|
|
|
117
220
|
* Base {@link Type}.
|
|
118
221
|
*
|
|
119
222
|
* 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
223
|
*
|
|
127
224
|
* ### Example
|
|
128
225
|
*
|
|
@@ -144,9 +241,7 @@ export const createTypeErrorFormatter = (format) => (error) => format({ ...error
|
|
|
144
241
|
*/
|
|
145
242
|
export const base = (name, fromUnknown) => createType(name, {
|
|
146
243
|
fromUnknown,
|
|
147
|
-
to: identity,
|
|
148
244
|
fromParent: (ok), // `fromParent` relies on types, so it can't fail for the Base Type
|
|
149
|
-
toParent: identity,
|
|
150
245
|
});
|
|
151
246
|
/**
|
|
152
247
|
* Creates a formatter function for a base {@link TypeError}.
|
|
@@ -280,9 +375,7 @@ export function brand(name, parent, refine) {
|
|
|
280
375
|
return {
|
|
281
376
|
...createType("Brand", {
|
|
282
377
|
fromUnknown,
|
|
283
|
-
to: identity,
|
|
284
378
|
fromParent: (refine ?? ok),
|
|
285
|
-
toParent: identity,
|
|
286
379
|
}),
|
|
287
380
|
brand: name,
|
|
288
381
|
parentType: parent,
|
|
@@ -312,37 +405,39 @@ export const formatCurrencyCodeError = createTypeErrorFormatter((error) => `Inva
|
|
|
312
405
|
* ### Example
|
|
313
406
|
*
|
|
314
407
|
* ```ts
|
|
315
|
-
* const result =
|
|
316
|
-
* const error =
|
|
408
|
+
* const result = DateIso.from("2023-01-01T12:00:00.000Z"); // ok
|
|
409
|
+
* const error = DateIso.from("10000-01-01T00:00:00.000Z"); // err
|
|
317
410
|
* ```
|
|
318
411
|
*
|
|
319
412
|
* @category String
|
|
320
413
|
*/
|
|
321
|
-
export const
|
|
414
|
+
export const DateIso = brand("DateIso", String, (value) => {
|
|
322
415
|
if (value.length !== 24) {
|
|
323
|
-
return err({ type: "
|
|
416
|
+
return err({ type: "DateIso", value });
|
|
417
|
+
}
|
|
418
|
+
const parsed = globalThis.Date.parse(value);
|
|
419
|
+
if (isNaN(parsed)) {
|
|
420
|
+
return err({ type: "DateIso", value });
|
|
324
421
|
}
|
|
325
|
-
|
|
326
|
-
|
|
422
|
+
// Round-trip test: ensure the string is actually a proper ISO format
|
|
423
|
+
const roundTrip = new globalThis.Date(parsed).toISOString();
|
|
424
|
+
if (roundTrip !== value) {
|
|
425
|
+
return err({ type: "DateIso", value });
|
|
327
426
|
}
|
|
328
427
|
return ok(value);
|
|
329
428
|
});
|
|
330
|
-
export const
|
|
429
|
+
export const formatDateIsoError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid ISO 8601 date string.`);
|
|
430
|
+
export const dateToDateIso = (value) => DateIso.fromParent(value.toISOString());
|
|
431
|
+
export const dateIsoToDate = (value) => new globalThis.Date(value);
|
|
331
432
|
/**
|
|
332
433
|
* Trimmed string.
|
|
333
434
|
*
|
|
334
|
-
* This Type Factory
|
|
335
|
-
*
|
|
336
|
-
* Factory.
|
|
435
|
+
* This Type Factory validates whether a string has no leading or trailing
|
|
436
|
+
* whitespaces.
|
|
337
437
|
*
|
|
338
|
-
* ###
|
|
438
|
+
* ### Example
|
|
339
439
|
*
|
|
340
440
|
* ```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
441
|
* const TrimmedNonEmptyString = trimmed(minLength(1)(String));
|
|
347
442
|
* // string & Brand<"MinLength1"> & Brand<"Trimmed">
|
|
348
443
|
* type TrimmedNonEmptyString = typeof TrimmedNonEmptyString.Type;
|
|
@@ -354,32 +449,6 @@ export const trimmed = (parent) => brand("Trimmed", parent, (value) => value.tri
|
|
|
354
449
|
? ok(value)
|
|
355
450
|
: err({ type: "Trimmed", value }));
|
|
356
451
|
export const formatTrimmedError = createTypeErrorFormatter((error) => `A value ${error.value} is not trimmed`);
|
|
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);
|
|
383
452
|
/**
|
|
384
453
|
* Trimmed string
|
|
385
454
|
*
|
|
@@ -389,6 +458,7 @@ export const trim = (parent) => transform(parent, trimmed(parent), (value) => ok
|
|
|
389
458
|
* @category String
|
|
390
459
|
*/
|
|
391
460
|
export const TrimmedString = trimmed(String);
|
|
461
|
+
export const trim = (value) => value.trim();
|
|
392
462
|
/**
|
|
393
463
|
* Minimum length.
|
|
394
464
|
*
|
|
@@ -494,9 +564,9 @@ export const regex = (name, pattern) => {
|
|
|
494
564
|
};
|
|
495
565
|
export const formatRegexError = createTypeErrorFormatter((error) => `Value ${error.value} does not match the pattern for ${error.name}: ${error.pattern}`);
|
|
496
566
|
/**
|
|
497
|
-
* URL-safe
|
|
567
|
+
* URL-safe string.
|
|
498
568
|
*
|
|
499
|
-
* A `
|
|
569
|
+
* A `UrlSafeString` uses a limited alphabet that is safe for URLs:
|
|
500
570
|
*
|
|
501
571
|
* - Uppercase letters (`A-Z`)
|
|
502
572
|
* - Lowercase letters (`a-z`)
|
|
@@ -504,29 +574,89 @@ export const formatRegexError = createTypeErrorFormatter((error) => `Value ${err
|
|
|
504
574
|
* - Dash (`-`)
|
|
505
575
|
* - Underscore (`_`)
|
|
506
576
|
*
|
|
577
|
+
* This is the same character set used by Base64Url encoding, but this type does
|
|
578
|
+
* not validate that the string is actually Base64Url-encoded data.
|
|
579
|
+
*
|
|
507
580
|
* ### Example
|
|
508
581
|
*
|
|
509
582
|
* ```ts
|
|
510
|
-
* const result =
|
|
583
|
+
* const result = UrlSafeString.from("abc123_-");
|
|
511
584
|
* if (result.ok) {
|
|
512
|
-
* console.log("Valid
|
|
585
|
+
* console.log("Valid URL-safe string:", result.value);
|
|
513
586
|
* } else {
|
|
514
|
-
* console.error("Invalid
|
|
587
|
+
* console.error("Invalid URL-safe string:", result.error);
|
|
515
588
|
* }
|
|
516
589
|
* ```
|
|
517
590
|
*
|
|
518
591
|
* @category String
|
|
519
592
|
*/
|
|
520
|
-
export const
|
|
593
|
+
export const UrlSafeString = regex("UrlSafeString", /^[A-Za-z0-9_-]+$/)(String);
|
|
521
594
|
/**
|
|
522
|
-
*
|
|
595
|
+
* Base64Url without padding.
|
|
523
596
|
*
|
|
524
|
-
*
|
|
597
|
+
* Encode with {@link uint8ArrayToBase64Url}, decode with
|
|
598
|
+
* {@link base64UrlToUint8Array}.
|
|
525
599
|
*
|
|
526
|
-
*
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
600
|
+
* @category String
|
|
601
|
+
*/
|
|
602
|
+
export const Base64Url = brand("Base64Url", String, (value) => {
|
|
603
|
+
// Round-trip validation ensures consistency across different base64url
|
|
604
|
+
// implementations (Node.js Buffer, native browser API, manual fallback).
|
|
605
|
+
// Only strings that decode and encode identically are accepted.
|
|
606
|
+
let roundTrip;
|
|
607
|
+
try {
|
|
608
|
+
roundTrip = uint8ArrayToBase64Url(base64UrlToUint8Array(value));
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
//
|
|
612
|
+
}
|
|
613
|
+
return roundTrip === value
|
|
614
|
+
? ok(value)
|
|
615
|
+
: err({ type: "Base64Url", value });
|
|
616
|
+
});
|
|
617
|
+
export const formatBase64UrlError = createTypeErrorFormatter((error) => `Value ${error.value} is not a valid Base64Url string.`);
|
|
618
|
+
const hasNodeBuffer = typeof globalThis.Buffer !== "undefined";
|
|
619
|
+
const base64UrlOptions = { alphabet: "base64url", omitPadding: true };
|
|
620
|
+
/** Encodes a Uint8Array to a {@link Base64Url} string. */
|
|
621
|
+
export const uint8ArrayToBase64Url = hasNodeBuffer
|
|
622
|
+
? (bytes) => globalThis.Buffer.from(bytes).toString("base64url")
|
|
623
|
+
: // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
624
|
+
typeof globalThis.Uint8Array.prototype?.toBase64 !== "undefined"
|
|
625
|
+
? (bytes) =>
|
|
626
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
627
|
+
bytes.toBase64(base64UrlOptions)
|
|
628
|
+
: (bytes) => {
|
|
629
|
+
const binaryString = Array.from(bytes, (byte) => globalThis.String.fromCodePoint(byte)).join("");
|
|
630
|
+
const base64 = globalThis.btoa(binaryString);
|
|
631
|
+
return base64
|
|
632
|
+
.replace(/\+/g, "-")
|
|
633
|
+
.replace(/\//g, "_")
|
|
634
|
+
.replace(/=/g, "");
|
|
635
|
+
};
|
|
636
|
+
/** Decodes a {@link Base64Url} string to a Uint8Array. */
|
|
637
|
+
export const base64UrlToUint8Array = hasNodeBuffer
|
|
638
|
+
? (str) => {
|
|
639
|
+
const nodeBuffer = globalThis.Buffer.from(str, "base64url");
|
|
640
|
+
return new globalThis.Uint8Array(nodeBuffer);
|
|
641
|
+
}
|
|
642
|
+
: // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
643
|
+
typeof globalThis.Uint8Array?.fromBase64 !== "undefined"
|
|
644
|
+
? (str) =>
|
|
645
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
646
|
+
globalThis.Uint8Array.fromBase64(str, base64UrlOptions)
|
|
647
|
+
: (str) => {
|
|
648
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
649
|
+
while (base64.length % 4 !== 0) {
|
|
650
|
+
base64 += "=";
|
|
651
|
+
}
|
|
652
|
+
const binaryString = globalThis.atob(base64);
|
|
653
|
+
return globalThis.Uint8Array.from(binaryString, (c) => c.charCodeAt(0));
|
|
654
|
+
};
|
|
655
|
+
/**
|
|
656
|
+
* Simple alphanumeric string for naming in file systems, URLs, and identifiers.
|
|
657
|
+
*
|
|
658
|
+
* Uses the same safe alphabet as {@link UrlSafeString} (letters, digits, `-`,
|
|
659
|
+
* `_`). See `UrlSafeString` for details.
|
|
530
660
|
*
|
|
531
661
|
* The string must be between 1 and 42 characters.
|
|
532
662
|
*
|
|
@@ -543,13 +673,9 @@ export const Base64Url = regex("Base64Url", /^[A-Za-z0-9_-]+$/)(String);
|
|
|
543
673
|
*
|
|
544
674
|
* @category String
|
|
545
675
|
*/
|
|
546
|
-
export const SimpleName =
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
*
|
|
550
|
-
* @category String
|
|
551
|
-
*/
|
|
552
|
-
export const NanoId = regex("NanoId", /^[A-Za-z0-9_-]{21}$/)(String);
|
|
676
|
+
export const SimpleName = brand("SimpleName", UrlSafeString, (value) => value.length >= 1 && value.length <= 42
|
|
677
|
+
? ok(value)
|
|
678
|
+
: err({ type: "SimpleName", value }));
|
|
553
679
|
/**
|
|
554
680
|
* Trimmed string between 8 and 64 characters, branded as `SimplePassword`.
|
|
555
681
|
*
|
|
@@ -558,15 +684,48 @@ export const NanoId = regex("NanoId", /^[A-Za-z0-9_-]{21}$/)(String);
|
|
|
558
684
|
export const SimplePassword = brand("SimplePassword", minLength(8)(maxLength(64)(TrimmedString)));
|
|
559
685
|
export const formatSimplePasswordError = (formatTypeError) => createTypeErrorFormatter((error) => `Invalid password: ${formatTypeError(error.parentError)}`);
|
|
560
686
|
/**
|
|
561
|
-
*
|
|
687
|
+
* Globally unique identifier.
|
|
688
|
+
*
|
|
689
|
+
* **Evolu Id** is 16 random bytes from a cryptographically secure random
|
|
690
|
+
* generator, encoded as 22-character Base64Url string. This provides strong
|
|
691
|
+
* collision resistance for distributed ID generation.
|
|
692
|
+
*
|
|
693
|
+
* ### Design Rationale
|
|
694
|
+
*
|
|
695
|
+
* Why Evolu Id over alternatives:
|
|
696
|
+
*
|
|
697
|
+
* - **NanoID**: No standard binary serialization format, and uses only ~126 bits
|
|
698
|
+
* of entropy (21 characters from 64-symbol alphabet) compared to Evolu Id's
|
|
699
|
+
* 128 bits.
|
|
700
|
+
* - **UUID (v4)**: String format is 36 characters (with hyphens) compared to
|
|
701
|
+
* Evolu Id's 22 characters. While UUIDs can be stored as 16 bytes, their
|
|
702
|
+
* standard string representation is verbose.
|
|
703
|
+
* - **UUID v7**: Includes timestamp in the ID, which leaks information about when
|
|
704
|
+
* data was created. This is a privacy concern for local-first applications
|
|
705
|
+
* where creation time must remain private.
|
|
562
706
|
*
|
|
563
|
-
*
|
|
564
|
-
* standard
|
|
707
|
+
* Evolu Id provides 128 bits of entropy, compact string representation (22
|
|
708
|
+
* characters), standard and native string serialization (Base64Url), and no
|
|
709
|
+
* privacy leaks.
|
|
710
|
+
*
|
|
711
|
+
* ### Future Consideration
|
|
712
|
+
*
|
|
713
|
+
* For database-heavy workloads where insert performance is critical, a hybrid
|
|
714
|
+
* approach could be considered: `timestamp ^ H(cluster_id, timestamp >> N)`
|
|
715
|
+
* where H is a keyed hash function and N is a configurable parameter. This
|
|
716
|
+
* would maintain spatial locality for database caches (improving insert
|
|
717
|
+
* performance by an order of magnitude) while adding entropy to prevent
|
|
718
|
+
* timestamp leakage and correlation across systems. The parameter N would allow
|
|
719
|
+
* trading off cache locality (larger N = better locality) versus entropy
|
|
720
|
+
* distribution. See https://brooker.co.za/blog/2025/10/22/uuidv7.html for
|
|
721
|
+
* details on this approach.
|
|
565
722
|
*
|
|
566
723
|
* @category String
|
|
567
724
|
*/
|
|
568
|
-
export const Id =
|
|
569
|
-
|
|
725
|
+
export const Id = brand("Id", String, (value) => value.length === 22 && Base64Url.fromParent(value).ok
|
|
726
|
+
? ok(value)
|
|
727
|
+
: err({ type: "Id", value }));
|
|
728
|
+
export const formatIdError = createTypeErrorFormatter((error) => `Value ${error.value} is not a valid Id.`);
|
|
570
729
|
/**
|
|
571
730
|
* Creates an {@link Id}.
|
|
572
731
|
*
|
|
@@ -575,11 +734,53 @@ export const idTypeValueLength = 21;
|
|
|
575
734
|
* ```ts
|
|
576
735
|
* // string & Brand<"Id">
|
|
577
736
|
* const id = createId(deps);
|
|
737
|
+
*
|
|
738
|
+
* // string & Brand<"Id"> & Brand<"Todo">
|
|
739
|
+
* const todoId = createId<"Todo">(deps);
|
|
578
740
|
* ```
|
|
579
741
|
*/
|
|
580
|
-
export const createId = (deps) => deps.
|
|
742
|
+
export const createId = (deps) => uint8ArrayToBase64Url(deps.randomBytes.create(16));
|
|
581
743
|
/**
|
|
582
|
-
*
|
|
744
|
+
* Creates an {@link Id} from a string using SHA-256.
|
|
745
|
+
*
|
|
746
|
+
* When integrating with external systems that use different ID formats, use
|
|
747
|
+
* this function to convert external IDs into valid Evolu IDs.
|
|
748
|
+
*
|
|
749
|
+
* In Evolu's CRDT, the ID serves as the unique identifier for conflict
|
|
750
|
+
* resolution across distributed clients. When multiple clients create records
|
|
751
|
+
* with the same external identifier, they must resolve to the same Evolu ID to
|
|
752
|
+
* ensure data consistency.
|
|
753
|
+
*
|
|
754
|
+
* ### Example
|
|
755
|
+
*
|
|
756
|
+
* ```ts
|
|
757
|
+
* // Both clients will generate the same ID
|
|
758
|
+
* const id1 = createIdFromString("user-api-123");
|
|
759
|
+
* const id2 = createIdFromString("user-api-123");
|
|
760
|
+
* console.log(id1 === id2); // true
|
|
761
|
+
*
|
|
762
|
+
* upsert("todo", {
|
|
763
|
+
* id: createIdFromString("external-todo-456"),
|
|
764
|
+
* title: "Synced from external system",
|
|
765
|
+
* });
|
|
766
|
+
* ```
|
|
767
|
+
*
|
|
768
|
+
* **Important**: This transformation is one-way. We cannot recover the original
|
|
769
|
+
* external string from the generated {@link Id}. If we need to preserve the
|
|
770
|
+
* original external ID, store it in a separate column.
|
|
771
|
+
*
|
|
772
|
+
* @category String
|
|
773
|
+
*/
|
|
774
|
+
export const createIdFromString = (value) => {
|
|
775
|
+
const hash = sha256(utf8ToBytes(value));
|
|
776
|
+
// Take first 16 bytes of hash and convert to Id
|
|
777
|
+
const id = idBytesToId(hash.slice(0, 16));
|
|
778
|
+
return id;
|
|
779
|
+
};
|
|
780
|
+
/**
|
|
781
|
+
* Creates a branded {@link Id} Type for a table's primary key.
|
|
782
|
+
*
|
|
783
|
+
* The table name becomes an additional brand for type safety.
|
|
583
784
|
*
|
|
584
785
|
* ### Example
|
|
585
786
|
*
|
|
@@ -592,30 +793,34 @@ export const createId = (deps) => deps.nanoIdLib.nanoid();
|
|
|
592
793
|
* @category String
|
|
593
794
|
*/
|
|
594
795
|
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
796
|
const fromUnknown = (value) => {
|
|
603
797
|
const parentResult = String.fromUnknown(value);
|
|
604
798
|
if (!parentResult.ok)
|
|
605
799
|
return parentResult;
|
|
606
800
|
return fromParent(parentResult.value);
|
|
607
801
|
};
|
|
802
|
+
const fromParent = (value) => {
|
|
803
|
+
const idResult = Id.fromParent(value);
|
|
804
|
+
if (!idResult.ok) {
|
|
805
|
+
return err({ type: "TableId", value, table });
|
|
806
|
+
}
|
|
807
|
+
return ok(idResult.value);
|
|
808
|
+
};
|
|
608
809
|
return {
|
|
609
|
-
...createType("Id", {
|
|
610
|
-
fromUnknown,
|
|
611
|
-
to: (value) => value,
|
|
612
|
-
fromParent,
|
|
613
|
-
toParent: (value) => value,
|
|
614
|
-
}),
|
|
810
|
+
...createType("Id", { fromUnknown, fromParent }),
|
|
615
811
|
table,
|
|
616
812
|
};
|
|
617
813
|
};
|
|
618
|
-
export const
|
|
814
|
+
export const formatTableIdError = createTypeErrorFormatter((error) => `Invalid ${error.type} table Id: ${error.value}`);
|
|
815
|
+
/** Binary representation of an {@link Id}. */
|
|
816
|
+
export const IdBytes = brand("IdBytes", length(16)(Uint8Array));
|
|
817
|
+
export const idBytesTypeValueLength = 16;
|
|
818
|
+
export const idToIdBytes = (id) =>
|
|
819
|
+
// Id is Base64Url (validated by Id.from), cast is safe
|
|
820
|
+
base64UrlToUint8Array(id);
|
|
821
|
+
export const idBytesToId = (idBytes) =>
|
|
822
|
+
// Base64Url encoding of 16 bytes always produces valid Id (22 chars)
|
|
823
|
+
uint8ArrayToBase64Url(idBytes);
|
|
619
824
|
/**
|
|
620
825
|
* Positive number.
|
|
621
826
|
*
|
|
@@ -708,6 +913,8 @@ export const Int = int(Number);
|
|
|
708
913
|
export const NonNegativeInt = nonNegative(Int);
|
|
709
914
|
/** @category Number */
|
|
710
915
|
export const PositiveInt = positive(NonNegativeInt);
|
|
916
|
+
/** Maximum safe positive integer value for practically infinite operations. */
|
|
917
|
+
export const maxPositiveInt = PositiveInt.orThrow(globalThis.Number.MAX_SAFE_INTEGER);
|
|
711
918
|
/** @category Number */
|
|
712
919
|
export const NonPositiveInt = nonPositive(Int);
|
|
713
920
|
/** @category Number */
|
|
@@ -802,8 +1009,6 @@ export const between = (min, max) => (parent) => brand(`Between${min}-${max}`, p
|
|
|
802
1009
|
? ok(value)
|
|
803
1010
|
: err({ type: "Between", value, min, max }));
|
|
804
1011
|
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
1012
|
/**
|
|
808
1013
|
* Literal {@link Type}.
|
|
809
1014
|
*
|
|
@@ -828,90 +1033,12 @@ export const literal = (expected) => {
|
|
|
828
1033
|
return {
|
|
829
1034
|
...createType("Literal", {
|
|
830
1035
|
fromUnknown,
|
|
831
|
-
to: identity,
|
|
832
1036
|
fromParent: ok,
|
|
833
|
-
toParent: identity,
|
|
834
1037
|
}),
|
|
835
1038
|
expected,
|
|
836
1039
|
};
|
|
837
1040
|
};
|
|
838
1041
|
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
1042
|
/**
|
|
916
1043
|
* Array of a specific {@link Type}.
|
|
917
1044
|
*
|
|
@@ -954,7 +1081,6 @@ export const array = (element) => {
|
|
|
954
1081
|
}
|
|
955
1082
|
return ok(result);
|
|
956
1083
|
};
|
|
957
|
-
const to = (value) => value.map(element.to);
|
|
958
1084
|
const fromParent = (value) => {
|
|
959
1085
|
const result = [];
|
|
960
1086
|
for (let i = 0; i < value.length; i++) {
|
|
@@ -974,14 +1100,8 @@ export const array = (element) => {
|
|
|
974
1100
|
}
|
|
975
1101
|
return ok(result);
|
|
976
1102
|
};
|
|
977
|
-
const toParent = (values) => values.map(element.toParent);
|
|
978
1103
|
return {
|
|
979
|
-
...createType("Array", {
|
|
980
|
-
fromUnknown,
|
|
981
|
-
to,
|
|
982
|
-
fromParent,
|
|
983
|
-
toParent,
|
|
984
|
-
}),
|
|
1104
|
+
...createType("Array", { fromUnknown, fromParent }),
|
|
985
1105
|
element,
|
|
986
1106
|
};
|
|
987
1107
|
};
|
|
@@ -1055,10 +1175,6 @@ export const record = (keyType, valueType) => {
|
|
|
1055
1175
|
}
|
|
1056
1176
|
return ok(result);
|
|
1057
1177
|
};
|
|
1058
|
-
const to = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
|
|
1059
|
-
keyType.to(key),
|
|
1060
|
-
valueType.to(val),
|
|
1061
|
-
]));
|
|
1062
1178
|
const fromParent = (value) => {
|
|
1063
1179
|
const result = {};
|
|
1064
1180
|
for (const [rawKey, rawValue] of Object.entries(value)) {
|
|
@@ -1090,16 +1206,10 @@ export const record = (keyType, valueType) => {
|
|
|
1090
1206
|
}
|
|
1091
1207
|
return ok(result);
|
|
1092
1208
|
};
|
|
1093
|
-
const toParent = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
|
|
1094
|
-
keyType.toParent(key),
|
|
1095
|
-
valueType.toParent(val),
|
|
1096
|
-
]));
|
|
1097
1209
|
return {
|
|
1098
1210
|
...createType("Record", {
|
|
1099
1211
|
fromUnknown,
|
|
1100
|
-
to,
|
|
1101
1212
|
fromParent,
|
|
1102
|
-
toParent,
|
|
1103
1213
|
}),
|
|
1104
1214
|
key: keyType,
|
|
1105
1215
|
value: valueType,
|
|
@@ -1186,22 +1296,6 @@ export function object(props, record) {
|
|
|
1186
1296
|
}
|
|
1187
1297
|
return ok(result);
|
|
1188
1298
|
};
|
|
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
1299
|
const fromParent = (value) => {
|
|
1206
1300
|
const errors = {};
|
|
1207
1301
|
const result = {};
|
|
@@ -1249,28 +1343,10 @@ export function object(props, record) {
|
|
|
1249
1343
|
}
|
|
1250
1344
|
return ok(result);
|
|
1251
1345
|
};
|
|
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
1346
|
return {
|
|
1269
1347
|
...createType(record ? "ObjectWithRecord" : "Object", {
|
|
1270
1348
|
fromUnknown,
|
|
1271
|
-
to,
|
|
1272
1349
|
fromParent,
|
|
1273
|
-
toParent,
|
|
1274
1350
|
}),
|
|
1275
1351
|
props,
|
|
1276
1352
|
...(record ? { record } : {}),
|
|
@@ -1323,7 +1399,6 @@ export function union(...args) {
|
|
|
1323
1399
|
* - Generate a specialized function to improve validation performance for such
|
|
1324
1400
|
* cases.
|
|
1325
1401
|
*/
|
|
1326
|
-
/* eslint-disable */
|
|
1327
1402
|
const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
|
|
1328
1403
|
const fromUnknown = (value) => {
|
|
1329
1404
|
const errors = [];
|
|
@@ -1339,23 +1414,13 @@ export function union(...args) {
|
|
|
1339
1414
|
errors,
|
|
1340
1415
|
});
|
|
1341
1416
|
};
|
|
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
1417
|
return {
|
|
1350
1418
|
...createType("Union", {
|
|
1351
1419
|
fromUnknown,
|
|
1352
|
-
to,
|
|
1353
1420
|
fromParent: fromUnknown,
|
|
1354
|
-
toParent: to,
|
|
1355
1421
|
}),
|
|
1356
1422
|
members,
|
|
1357
1423
|
};
|
|
1358
|
-
/* eslint-enable */
|
|
1359
1424
|
}
|
|
1360
1425
|
export const formatUnionError = (formatTypeError) => createTypeErrorFormatter((error) => {
|
|
1361
1426
|
const formattedErrors = error.errors
|
|
@@ -1411,20 +1476,10 @@ export const recursive = (create) => {
|
|
|
1411
1476
|
type ??= create();
|
|
1412
1477
|
return type.fromUnknown(value);
|
|
1413
1478
|
},
|
|
1414
|
-
to: (value) => {
|
|
1415
|
-
type ??= create();
|
|
1416
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
1417
|
-
return type.to(value);
|
|
1418
|
-
},
|
|
1419
1479
|
fromParent: (value) => {
|
|
1420
1480
|
type ??= create();
|
|
1421
1481
|
return type.fromParent(value);
|
|
1422
1482
|
},
|
|
1423
|
-
toParent: (value) => {
|
|
1424
|
-
type ??= create();
|
|
1425
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
1426
|
-
return type.toParent(value);
|
|
1427
|
-
},
|
|
1428
1483
|
is: (value) => {
|
|
1429
1484
|
type ??= create();
|
|
1430
1485
|
return type.is(value);
|
|
@@ -1528,7 +1583,6 @@ export const tuple = (...elements) => {
|
|
|
1528
1583
|
}
|
|
1529
1584
|
return ok(result);
|
|
1530
1585
|
};
|
|
1531
|
-
const to = (value) => value.map((val, index) => elements[index].to(val));
|
|
1532
1586
|
const fromParent = (value) => {
|
|
1533
1587
|
if (!Array.isArray(value) || value.length !== elements.length) {
|
|
1534
1588
|
return err({
|
|
@@ -1555,13 +1609,10 @@ export const tuple = (...elements) => {
|
|
|
1555
1609
|
}
|
|
1556
1610
|
return ok(result);
|
|
1557
1611
|
};
|
|
1558
|
-
const toParent = (value) => value.map((val, index) => elements[index].toParent(val));
|
|
1559
1612
|
return {
|
|
1560
1613
|
...createType("Tuple", {
|
|
1561
1614
|
fromUnknown,
|
|
1562
|
-
to,
|
|
1563
1615
|
fromParent,
|
|
1564
|
-
toParent,
|
|
1565
1616
|
}),
|
|
1566
1617
|
elements,
|
|
1567
1618
|
};
|
|
@@ -1595,22 +1646,32 @@ export const Int64 = brand("Int64", BigInt, (value) => value >= -922337203685477
|
|
|
1595
1646
|
? ok(value)
|
|
1596
1647
|
: err({ type: "Int64", value }));
|
|
1597
1648
|
export const formatInt64Error = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid 64-bit signed integer (Int64).`);
|
|
1598
|
-
|
|
1599
|
-
export const
|
|
1649
|
+
// // co s timhle? je to string, ze ktereho lze udelat bigint
|
|
1650
|
+
// export const BigIntFromString = transform(
|
|
1651
|
+
// String,
|
|
1652
|
+
// BigInt,
|
|
1653
|
+
// (value) =>
|
|
1654
|
+
// trySync(
|
|
1655
|
+
// () => globalThis.BigInt(value),
|
|
1656
|
+
// (): BigIntFromStringError => ({ type: "BigIntFromString", value }),
|
|
1657
|
+
// ),
|
|
1658
|
+
// (value) => value.toString(),
|
|
1659
|
+
// );
|
|
1660
|
+
// export interface BigIntFromStringError extends TypeError<"BigIntFromString"> {}
|
|
1661
|
+
// export const formatBigIntFromStringError =
|
|
1662
|
+
// createTypeErrorFormatter<BigIntFromStringError>(
|
|
1663
|
+
// (error) => `The value ${error.value} could not be converted to a BigInt.`,
|
|
1664
|
+
// );
|
|
1600
1665
|
/**
|
|
1601
1666
|
* Stringified {@link Int64}.
|
|
1602
1667
|
*
|
|
1603
|
-
* @category
|
|
1668
|
+
* @category String
|
|
1604
1669
|
*/
|
|
1605
|
-
export const Int64String = brand("Int64",
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
return ok(value);
|
|
1611
|
-
}
|
|
1612
|
-
return err({ type: "Int64String", value });
|
|
1613
|
-
});
|
|
1670
|
+
export const Int64String = brand("Int64", NonEmptyTrimmedString, (value) => trySync(() => {
|
|
1671
|
+
const maybeInt = globalThis.BigInt(value);
|
|
1672
|
+
Int64.orThrow(maybeInt);
|
|
1673
|
+
return value;
|
|
1674
|
+
}, () => ({ type: "Int64String", value })));
|
|
1614
1675
|
export const formatInt64StringError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Int64 string.`);
|
|
1615
1676
|
/**
|
|
1616
1677
|
* JSON-compatible value: string, {@link FiniteNumber}, boolean, null,
|
|
@@ -1631,48 +1692,83 @@ export const JsonArray = array(JsonValue);
|
|
|
1631
1692
|
* @category Object
|
|
1632
1693
|
*/
|
|
1633
1694
|
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`,
|
|
1695
|
+
export const parseJson = (value) => trySync(() => JSON.parse(value), (error) => ({
|
|
1696
|
+
type: `Json`,
|
|
1649
1697
|
value,
|
|
1650
1698
|
message: globalThis.String(error),
|
|
1651
|
-
}))
|
|
1652
|
-
export const formatJsonValueFromStringError = createTypeErrorFormatter((error) => `Invalid JSONValue: ${error.value}. Error: ${error.message}`);
|
|
1699
|
+
}));
|
|
1653
1700
|
/**
|
|
1654
1701
|
* JSON-string {@link Type}.
|
|
1655
1702
|
*
|
|
1656
1703
|
* ### Example
|
|
1657
1704
|
*
|
|
1658
1705
|
* ```ts
|
|
1659
|
-
* const result = Json.from('{"key":"value"}'); //
|
|
1660
|
-
* const error = Json.from("invalid json"); //
|
|
1706
|
+
* const result = Json.from('{"key":"value"}'); // ok
|
|
1707
|
+
* const error = Json.from("invalid json"); // err
|
|
1661
1708
|
* ```
|
|
1662
1709
|
*
|
|
1663
1710
|
* @category String
|
|
1664
1711
|
*/
|
|
1665
1712
|
export const Json = brand("Json", String, (value) => {
|
|
1666
|
-
const result =
|
|
1713
|
+
const result = parseJson(value);
|
|
1667
1714
|
if (!result.ok)
|
|
1668
|
-
return
|
|
1669
|
-
type: "Json",
|
|
1670
|
-
value,
|
|
1671
|
-
message: result.error.message,
|
|
1672
|
-
});
|
|
1715
|
+
return result;
|
|
1673
1716
|
return ok(value);
|
|
1674
1717
|
});
|
|
1675
1718
|
export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON: ${error.value}. Error: ${error.message}`);
|
|
1719
|
+
export const jsonValueToJson = (value) => JSON.stringify(value);
|
|
1720
|
+
export const jsonToJsonValue = (value) => JSON.parse(value);
|
|
1721
|
+
/**
|
|
1722
|
+
* Creates a branded JSON string {@link Type} and type-safe conversion functions
|
|
1723
|
+
* for a given Type.
|
|
1724
|
+
*
|
|
1725
|
+
* This factory creates:
|
|
1726
|
+
*
|
|
1727
|
+
* 1. A branded string Type that validates JSON parsing and structural conformity
|
|
1728
|
+
* 2. A serialization function (Type → branded JSON string)
|
|
1729
|
+
* 3. A parsing function (branded JSON string → Type, skipping validation)
|
|
1730
|
+
*
|
|
1731
|
+
* Optimized for Evolu's SQLite workflow where we store typed JSON strings and
|
|
1732
|
+
* need type-safe conversions without double parsing.
|
|
1733
|
+
*
|
|
1734
|
+
* ### Example
|
|
1735
|
+
*
|
|
1736
|
+
* ```ts
|
|
1737
|
+
* const Person = object({
|
|
1738
|
+
* name: NonEmptyString100,
|
|
1739
|
+
* age: FiniteNumber,
|
|
1740
|
+
* });
|
|
1741
|
+
* type Person = typeof Person.Type;
|
|
1742
|
+
*
|
|
1743
|
+
* const [PersonJson, personToPersonJson, personJsonToPerson] = json(
|
|
1744
|
+
* Person,
|
|
1745
|
+
* "PersonJson",
|
|
1746
|
+
* );
|
|
1747
|
+
* // string & Brand<"PersonJson">
|
|
1748
|
+
* type PersonJson = typeof PersonJson.Type;
|
|
1749
|
+
*
|
|
1750
|
+
* // Usage:
|
|
1751
|
+
* const person: Person = { name: "Alice", age: 30 };
|
|
1752
|
+
* const jsonString = personToPersonJson(person); // PersonJson
|
|
1753
|
+
* const backToPerson = personJsonToPerson(jsonString); // Person
|
|
1754
|
+
* ```
|
|
1755
|
+
*/
|
|
1756
|
+
export const json = (type, name) => {
|
|
1757
|
+
const BrandedJsonType = brand(name, String, (value) => {
|
|
1758
|
+
const parseResult = parseJson(value);
|
|
1759
|
+
if (!parseResult.ok)
|
|
1760
|
+
return parseResult;
|
|
1761
|
+
const validationResult = type.fromUnknown(parseResult.value);
|
|
1762
|
+
if (!validationResult.ok)
|
|
1763
|
+
return validationResult;
|
|
1764
|
+
return ok(value);
|
|
1765
|
+
});
|
|
1766
|
+
return [
|
|
1767
|
+
BrandedJsonType,
|
|
1768
|
+
jsonValueToJson,
|
|
1769
|
+
jsonToJsonValue,
|
|
1770
|
+
];
|
|
1771
|
+
};
|
|
1676
1772
|
/**
|
|
1677
1773
|
* Optional {@link Type}.
|
|
1678
1774
|
*
|
|
@@ -1697,9 +1793,7 @@ export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON
|
|
|
1697
1793
|
export const optional = (type) => ({
|
|
1698
1794
|
...createType("Optional", {
|
|
1699
1795
|
fromUnknown: type.fromUnknown,
|
|
1700
|
-
to: type.to,
|
|
1701
1796
|
fromParent: type.fromParent,
|
|
1702
|
-
toParent: type.toParent,
|
|
1703
1797
|
}),
|
|
1704
1798
|
parent: type,
|
|
1705
1799
|
});
|
|
@@ -1708,7 +1802,7 @@ export const isOptionalType = (x) => typeof x === "object" && x != null && "name
|
|
|
1708
1802
|
/**
|
|
1709
1803
|
* Creates a partial object type where all properties are optional.
|
|
1710
1804
|
*
|
|
1711
|
-
* This is useful when
|
|
1805
|
+
* This is useful when we want to validate an object in which none of the keys
|
|
1712
1806
|
* are required, but if they are present they must conform to their
|
|
1713
1807
|
* corresponding Types.
|
|
1714
1808
|
*
|
|
@@ -1779,114 +1873,86 @@ export function omit(objectType, ...keys) {
|
|
|
1779
1873
|
}
|
|
1780
1874
|
return object(newProps);
|
|
1781
1875
|
}
|
|
1876
|
+
export const maxMutationSize = 655360;
|
|
1782
1877
|
/**
|
|
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
|
-
* ```
|
|
1878
|
+
* Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
|
|
1879
|
+
* the `maxProtocolMessageRangesSize`. The max size is 640KB in bytes, measured
|
|
1880
|
+
* via MessagePack. Evolu Protocol DbChange will be smaller thanks to various
|
|
1881
|
+
* optimizations.
|
|
1810
1882
|
*/
|
|
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
|
-
};
|
|
1883
|
+
export const validMutationSize = (type) => brand("ValidMutationSize", type, (value) => pack(value).byteLength <= maxMutationSize
|
|
1884
|
+
? ok(value)
|
|
1885
|
+
: err({ type: "ValidMutationSize", value }));
|
|
1886
|
+
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
1887
|
/**
|
|
1826
|
-
*
|
|
1827
|
-
* {@link TypeErrors} and custom errors. It also lets us override the default
|
|
1828
|
-
* formatting for specific errors.
|
|
1888
|
+
* Formats Evolu Type errors into user-friendly messages.
|
|
1829
1889
|
*
|
|
1830
|
-
*
|
|
1831
|
-
*
|
|
1890
|
+
* Evolu Type typed errors ensure every error type must have a formatter.
|
|
1891
|
+
* TypeScript enforces this at compile-time, preventing unhandled validation
|
|
1892
|
+
* errors from reaching users.
|
|
1832
1893
|
*
|
|
1833
|
-
*
|
|
1894
|
+
* The `createFormatTypeError` function handles both built-in {@link TypeErrors}
|
|
1895
|
+
* and custom errors, and lets us override default formatting for specific
|
|
1896
|
+
* errors.
|
|
1897
|
+
*
|
|
1898
|
+
* ### Example
|
|
1834
1899
|
*
|
|
1835
1900
|
* ```ts
|
|
1836
|
-
* const
|
|
1837
|
-
*
|
|
1838
|
-
*
|
|
1901
|
+
* const formatTypeError = createFormatTypeError<
|
|
1902
|
+
* MinLengthError | MaxLengthError
|
|
1903
|
+
* >((error): string => {
|
|
1904
|
+
* switch (error.type) {
|
|
1905
|
+
* case "MinLength":
|
|
1906
|
+
* return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
|
|
1907
|
+
* case "MaxLength":
|
|
1908
|
+
* return `Text is too long (maximum ${error.max} characters)`;
|
|
1909
|
+
* }
|
|
1910
|
+
* });
|
|
1839
1911
|
* ```
|
|
1840
1912
|
*
|
|
1841
|
-
*
|
|
1913
|
+
* Alternatively, write a custom formatter from scratch without using
|
|
1914
|
+
* `createFormatTypeError`. This gives us full control over error formatting:
|
|
1842
1915
|
*
|
|
1843
1916
|
* ```ts
|
|
1844
|
-
*
|
|
1845
|
-
*
|
|
1917
|
+
* const Person = object({
|
|
1918
|
+
* name: NonEmptyTrimmedString100,
|
|
1919
|
+
* age: optional(PositiveInt),
|
|
1920
|
+
* });
|
|
1921
|
+
*
|
|
1922
|
+
* // Define only the errors actually used by Person Type
|
|
1923
|
+
* type PersonErrors =
|
|
1846
1924
|
* | StringError
|
|
1847
|
-
* | MinLengthError
|
|
1848
1925
|
* | MaxLengthError
|
|
1849
|
-
* |
|
|
1850
|
-
* | IdError
|
|
1926
|
+
* | MinLengthError
|
|
1851
1927
|
* | 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.
|
|
1928
|
+
* | PositiveError
|
|
1929
|
+
* | NonNegativeError
|
|
1930
|
+
* | IntError
|
|
1931
|
+
* | NumberError
|
|
1932
|
+
* | ObjectError<Record<string, PersonErrors>>;
|
|
1933
|
+
*
|
|
1934
|
+
* const formatTypeError: TypeErrorFormatter<PersonErrors> = (error) => {
|
|
1861
1935
|
* 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
1936
|
* case "String":
|
|
1873
|
-
* // We can reuse existing formatter.
|
|
1874
1937
|
* return formatStringError(error);
|
|
1938
|
+
* case "Number":
|
|
1939
|
+
* return "Must be a number";
|
|
1940
|
+
* case "MinLength":
|
|
1941
|
+
* return `Must be at least ${error.min} characters`;
|
|
1942
|
+
* case "MaxLength":
|
|
1943
|
+
* return `Cannot exceed ${error.max} characters`;
|
|
1875
1944
|
* case "Trimmed":
|
|
1876
|
-
* return "
|
|
1877
|
-
* case "
|
|
1878
|
-
* return "
|
|
1879
|
-
* case "
|
|
1880
|
-
* return
|
|
1881
|
-
*
|
|
1882
|
-
*
|
|
1883
|
-
* return `Union errors: ${error.errors.map(formatTypeError).join(", ")}`;
|
|
1945
|
+
* return "Cannot have leading or trailing spaces";
|
|
1946
|
+
* case "Positive":
|
|
1947
|
+
* return "Must be a positive number";
|
|
1948
|
+
* case "NonNegative":
|
|
1949
|
+
* return "Must be zero or positive";
|
|
1950
|
+
* case "Int":
|
|
1951
|
+
* return "Must be an integer";
|
|
1884
1952
|
* case "Object": {
|
|
1885
|
-
* if (
|
|
1886
|
-
*
|
|
1887
|
-
*
|
|
1888
|
-
* )
|
|
1889
|
-
* return "A developer made an error, this should not happen.";
|
|
1953
|
+
* if (error.reason.kind === "NotObject") return "Must be an object";
|
|
1954
|
+
* if (error.reason.kind === "ExtraKeys")
|
|
1955
|
+
* return "Contains unexpected fields";
|
|
1890
1956
|
* const firstError = Object.values(error.reason.errors).find(
|
|
1891
1957
|
* (e) => e !== undefined,
|
|
1892
1958
|
* )!;
|
|
@@ -1927,8 +1993,8 @@ export const createFormatTypeError = (extraFormatter) => {
|
|
|
1927
1993
|
return formatIsTypeError(error);
|
|
1928
1994
|
case "CurrencyCode":
|
|
1929
1995
|
return formatCurrencyCodeError(error);
|
|
1930
|
-
case "
|
|
1931
|
-
return
|
|
1996
|
+
case "DateIso":
|
|
1997
|
+
return formatDateIsoError(error);
|
|
1932
1998
|
case "Trimmed":
|
|
1933
1999
|
return formatTrimmedError(error);
|
|
1934
2000
|
case "MinLength":
|
|
@@ -1943,6 +2009,8 @@ export const createFormatTypeError = (extraFormatter) => {
|
|
|
1943
2009
|
return formatRegexError(error);
|
|
1944
2010
|
case "Id":
|
|
1945
2011
|
return formatIdError(error);
|
|
2012
|
+
case "TableId":
|
|
2013
|
+
return formatTableIdError(error);
|
|
1946
2014
|
case "Positive":
|
|
1947
2015
|
return formatPositiveError(error);
|
|
1948
2016
|
case "Negative":
|
|
@@ -1973,14 +2041,12 @@ export const createFormatTypeError = (extraFormatter) => {
|
|
|
1973
2041
|
return formatLiteralError(error);
|
|
1974
2042
|
case "Int64":
|
|
1975
2043
|
return formatInt64Error(error);
|
|
1976
|
-
case "BigIntFromString":
|
|
1977
|
-
return formatBigIntFromStringError(error);
|
|
1978
2044
|
case "Int64String":
|
|
1979
2045
|
return formatInt64StringError(error);
|
|
1980
|
-
case "JsonValueFromString":
|
|
1981
|
-
return formatJsonValueFromStringError(error);
|
|
1982
2046
|
case "Json":
|
|
1983
2047
|
return formatJsonError(error);
|
|
2048
|
+
case "ValidMutationSize":
|
|
2049
|
+
return formatValidMutationSizeError(error);
|
|
1984
2050
|
// Composite Types
|
|
1985
2051
|
case "SimplePassword":
|
|
1986
2052
|
return formatSimplePasswordError(formatTypeError)(error);
|