@evolu/common 6.0.1-preview.2 → 6.0.1-preview.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Assert.d.ts +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 +137 -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 +152 -127
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +88 -107
- 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 +170 -186
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +487 -343
- 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 +4 -3
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +50 -12
- 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 +77 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +453 -20
- package/dist/src/Evolu/Timestamp.d.ts +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 +147 -51
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +7 -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 +462 -327
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +490 -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 +505 -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 +221 -235
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +681 -614
- 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 +62 -16
- package/src/Evolu/Schema.ts +391 -191
- package/src/Evolu/Storage.ts +275 -55
- package/src/Evolu/Sync.ts +758 -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 +152 -53
- package/src/Sqlite.ts +59 -23
- package/src/Task.ts +779 -0
- package/src/Time.ts +168 -4
- package/src/Type.ts +696 -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,36 @@ 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:
|
|
562
696
|
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
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.
|
|
706
|
+
*
|
|
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.
|
|
565
710
|
*
|
|
566
711
|
* @category String
|
|
567
712
|
*/
|
|
568
|
-
export const Id =
|
|
569
|
-
|
|
713
|
+
export const Id = brand("Id", String, (value) => value.length === 22 && Base64Url.fromParent(value).ok
|
|
714
|
+
? ok(value)
|
|
715
|
+
: err({ type: "Id", value }));
|
|
716
|
+
export const formatIdError = createTypeErrorFormatter((error) => `Value ${error.value} is not a valid Id.`);
|
|
570
717
|
/**
|
|
571
718
|
* Creates an {@link Id}.
|
|
572
719
|
*
|
|
@@ -575,11 +722,53 @@ export const idTypeValueLength = 21;
|
|
|
575
722
|
* ```ts
|
|
576
723
|
* // string & Brand<"Id">
|
|
577
724
|
* const id = createId(deps);
|
|
725
|
+
*
|
|
726
|
+
* // string & Brand<"Id"> & Brand<"Todo">
|
|
727
|
+
* const todoId = createId<"Todo">(deps);
|
|
578
728
|
* ```
|
|
579
729
|
*/
|
|
580
|
-
export const createId = (deps) => deps.
|
|
730
|
+
export const createId = (deps) => uint8ArrayToBase64Url(deps.randomBytes.create(16));
|
|
731
|
+
/**
|
|
732
|
+
* Creates an {@link Id} from a string using SHA-256.
|
|
733
|
+
*
|
|
734
|
+
* When integrating with external systems that use different ID formats, use
|
|
735
|
+
* this function to convert external IDs into valid Evolu IDs.
|
|
736
|
+
*
|
|
737
|
+
* In Evolu's CRDT, the ID serves as the unique identifier for conflict
|
|
738
|
+
* resolution across distributed clients. When multiple clients create records
|
|
739
|
+
* with the same external identifier, they must resolve to the same Evolu ID to
|
|
740
|
+
* ensure data consistency.
|
|
741
|
+
*
|
|
742
|
+
* ### Example
|
|
743
|
+
*
|
|
744
|
+
* ```ts
|
|
745
|
+
* // Both clients will generate the same ID
|
|
746
|
+
* const id1 = createIdFromString("user-api-123");
|
|
747
|
+
* const id2 = createIdFromString("user-api-123");
|
|
748
|
+
* console.log(id1 === id2); // true
|
|
749
|
+
*
|
|
750
|
+
* upsert("todo", {
|
|
751
|
+
* id: createIdFromString("external-todo-456"),
|
|
752
|
+
* title: "Synced from external system",
|
|
753
|
+
* });
|
|
754
|
+
* ```
|
|
755
|
+
*
|
|
756
|
+
* **Important**: This transformation is one-way. We cannot recover the original
|
|
757
|
+
* external string from the generated {@link Id}. If we need to preserve the
|
|
758
|
+
* original external ID, store it in a separate column.
|
|
759
|
+
*
|
|
760
|
+
* @category String
|
|
761
|
+
*/
|
|
762
|
+
export const createIdFromString = (value) => {
|
|
763
|
+
const hash = sha256(utf8ToBytes(value));
|
|
764
|
+
// Take first 16 bytes of hash and convert to Id
|
|
765
|
+
const id = idBytesToId(hash.slice(0, 16));
|
|
766
|
+
return id;
|
|
767
|
+
};
|
|
581
768
|
/**
|
|
582
|
-
*
|
|
769
|
+
* Creates a branded {@link Id} Type for a table's primary key.
|
|
770
|
+
*
|
|
771
|
+
* The table name becomes an additional brand for type safety.
|
|
583
772
|
*
|
|
584
773
|
* ### Example
|
|
585
774
|
*
|
|
@@ -592,30 +781,34 @@ export const createId = (deps) => deps.nanoIdLib.nanoid();
|
|
|
592
781
|
* @category String
|
|
593
782
|
*/
|
|
594
783
|
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
784
|
const fromUnknown = (value) => {
|
|
603
785
|
const parentResult = String.fromUnknown(value);
|
|
604
786
|
if (!parentResult.ok)
|
|
605
787
|
return parentResult;
|
|
606
788
|
return fromParent(parentResult.value);
|
|
607
789
|
};
|
|
790
|
+
const fromParent = (value) => {
|
|
791
|
+
const idResult = Id.fromParent(value);
|
|
792
|
+
if (!idResult.ok) {
|
|
793
|
+
return err({ type: "TableId", value, table });
|
|
794
|
+
}
|
|
795
|
+
return ok(idResult.value);
|
|
796
|
+
};
|
|
608
797
|
return {
|
|
609
|
-
...createType("Id", {
|
|
610
|
-
fromUnknown,
|
|
611
|
-
to: (value) => value,
|
|
612
|
-
fromParent,
|
|
613
|
-
toParent: (value) => value,
|
|
614
|
-
}),
|
|
798
|
+
...createType("Id", { fromUnknown, fromParent }),
|
|
615
799
|
table,
|
|
616
800
|
};
|
|
617
801
|
};
|
|
618
|
-
export const
|
|
802
|
+
export const formatTableIdError = createTypeErrorFormatter((error) => `Invalid ${error.type} table Id: ${error.value}`);
|
|
803
|
+
/** Binary representation of an {@link Id}. */
|
|
804
|
+
export const IdBytes = brand("IdBytes", length(16)(Uint8Array));
|
|
805
|
+
export const idBytesTypeValueLength = 16;
|
|
806
|
+
export const idToIdBytes = (id) =>
|
|
807
|
+
// Id is Base64Url (validated by Id.from), cast is safe
|
|
808
|
+
base64UrlToUint8Array(id);
|
|
809
|
+
export const idBytesToId = (idBytes) =>
|
|
810
|
+
// Base64Url encoding of 16 bytes always produces valid Id (22 chars)
|
|
811
|
+
uint8ArrayToBase64Url(idBytes);
|
|
619
812
|
/**
|
|
620
813
|
* Positive number.
|
|
621
814
|
*
|
|
@@ -708,6 +901,8 @@ export const Int = int(Number);
|
|
|
708
901
|
export const NonNegativeInt = nonNegative(Int);
|
|
709
902
|
/** @category Number */
|
|
710
903
|
export const PositiveInt = positive(NonNegativeInt);
|
|
904
|
+
/** Maximum safe positive integer value for practically infinite operations. */
|
|
905
|
+
export const maxPositiveInt = PositiveInt.orThrow(globalThis.Number.MAX_SAFE_INTEGER);
|
|
711
906
|
/** @category Number */
|
|
712
907
|
export const NonPositiveInt = nonPositive(Int);
|
|
713
908
|
/** @category Number */
|
|
@@ -802,8 +997,6 @@ export const between = (min, max) => (parent) => brand(`Between${min}-${max}`, p
|
|
|
802
997
|
? ok(value)
|
|
803
998
|
: err({ type: "Between", value, min, max }));
|
|
804
999
|
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
1000
|
/**
|
|
808
1001
|
* Literal {@link Type}.
|
|
809
1002
|
*
|
|
@@ -828,90 +1021,12 @@ export const literal = (expected) => {
|
|
|
828
1021
|
return {
|
|
829
1022
|
...createType("Literal", {
|
|
830
1023
|
fromUnknown,
|
|
831
|
-
to: identity,
|
|
832
1024
|
fromParent: ok,
|
|
833
|
-
toParent: identity,
|
|
834
1025
|
}),
|
|
835
1026
|
expected,
|
|
836
1027
|
};
|
|
837
1028
|
};
|
|
838
1029
|
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
1030
|
/**
|
|
916
1031
|
* Array of a specific {@link Type}.
|
|
917
1032
|
*
|
|
@@ -954,7 +1069,6 @@ export const array = (element) => {
|
|
|
954
1069
|
}
|
|
955
1070
|
return ok(result);
|
|
956
1071
|
};
|
|
957
|
-
const to = (value) => value.map(element.to);
|
|
958
1072
|
const fromParent = (value) => {
|
|
959
1073
|
const result = [];
|
|
960
1074
|
for (let i = 0; i < value.length; i++) {
|
|
@@ -974,14 +1088,8 @@ export const array = (element) => {
|
|
|
974
1088
|
}
|
|
975
1089
|
return ok(result);
|
|
976
1090
|
};
|
|
977
|
-
const toParent = (values) => values.map(element.toParent);
|
|
978
1091
|
return {
|
|
979
|
-
...createType("Array", {
|
|
980
|
-
fromUnknown,
|
|
981
|
-
to,
|
|
982
|
-
fromParent,
|
|
983
|
-
toParent,
|
|
984
|
-
}),
|
|
1092
|
+
...createType("Array", { fromUnknown, fromParent }),
|
|
985
1093
|
element,
|
|
986
1094
|
};
|
|
987
1095
|
};
|
|
@@ -1055,10 +1163,6 @@ export const record = (keyType, valueType) => {
|
|
|
1055
1163
|
}
|
|
1056
1164
|
return ok(result);
|
|
1057
1165
|
};
|
|
1058
|
-
const to = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
|
|
1059
|
-
keyType.to(key),
|
|
1060
|
-
valueType.to(val),
|
|
1061
|
-
]));
|
|
1062
1166
|
const fromParent = (value) => {
|
|
1063
1167
|
const result = {};
|
|
1064
1168
|
for (const [rawKey, rawValue] of Object.entries(value)) {
|
|
@@ -1090,16 +1194,10 @@ export const record = (keyType, valueType) => {
|
|
|
1090
1194
|
}
|
|
1091
1195
|
return ok(result);
|
|
1092
1196
|
};
|
|
1093
|
-
const toParent = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
|
|
1094
|
-
keyType.toParent(key),
|
|
1095
|
-
valueType.toParent(val),
|
|
1096
|
-
]));
|
|
1097
1197
|
return {
|
|
1098
1198
|
...createType("Record", {
|
|
1099
1199
|
fromUnknown,
|
|
1100
|
-
to,
|
|
1101
1200
|
fromParent,
|
|
1102
|
-
toParent,
|
|
1103
1201
|
}),
|
|
1104
1202
|
key: keyType,
|
|
1105
1203
|
value: valueType,
|
|
@@ -1186,22 +1284,6 @@ export function object(props, record) {
|
|
|
1186
1284
|
}
|
|
1187
1285
|
return ok(result);
|
|
1188
1286
|
};
|
|
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
1287
|
const fromParent = (value) => {
|
|
1206
1288
|
const errors = {};
|
|
1207
1289
|
const result = {};
|
|
@@ -1249,28 +1331,10 @@ export function object(props, record) {
|
|
|
1249
1331
|
}
|
|
1250
1332
|
return ok(result);
|
|
1251
1333
|
};
|
|
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
1334
|
return {
|
|
1269
1335
|
...createType(record ? "ObjectWithRecord" : "Object", {
|
|
1270
1336
|
fromUnknown,
|
|
1271
|
-
to,
|
|
1272
1337
|
fromParent,
|
|
1273
|
-
toParent,
|
|
1274
1338
|
}),
|
|
1275
1339
|
props,
|
|
1276
1340
|
...(record ? { record } : {}),
|
|
@@ -1323,7 +1387,6 @@ export function union(...args) {
|
|
|
1323
1387
|
* - Generate a specialized function to improve validation performance for such
|
|
1324
1388
|
* cases.
|
|
1325
1389
|
*/
|
|
1326
|
-
/* eslint-disable */
|
|
1327
1390
|
const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
|
|
1328
1391
|
const fromUnknown = (value) => {
|
|
1329
1392
|
const errors = [];
|
|
@@ -1339,23 +1402,13 @@ export function union(...args) {
|
|
|
1339
1402
|
errors,
|
|
1340
1403
|
});
|
|
1341
1404
|
};
|
|
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
1405
|
return {
|
|
1350
1406
|
...createType("Union", {
|
|
1351
1407
|
fromUnknown,
|
|
1352
|
-
to,
|
|
1353
1408
|
fromParent: fromUnknown,
|
|
1354
|
-
toParent: to,
|
|
1355
1409
|
}),
|
|
1356
1410
|
members,
|
|
1357
1411
|
};
|
|
1358
|
-
/* eslint-enable */
|
|
1359
1412
|
}
|
|
1360
1413
|
export const formatUnionError = (formatTypeError) => createTypeErrorFormatter((error) => {
|
|
1361
1414
|
const formattedErrors = error.errors
|
|
@@ -1411,20 +1464,10 @@ export const recursive = (create) => {
|
|
|
1411
1464
|
type ??= create();
|
|
1412
1465
|
return type.fromUnknown(value);
|
|
1413
1466
|
},
|
|
1414
|
-
to: (value) => {
|
|
1415
|
-
type ??= create();
|
|
1416
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
1417
|
-
return type.to(value);
|
|
1418
|
-
},
|
|
1419
1467
|
fromParent: (value) => {
|
|
1420
1468
|
type ??= create();
|
|
1421
1469
|
return type.fromParent(value);
|
|
1422
1470
|
},
|
|
1423
|
-
toParent: (value) => {
|
|
1424
|
-
type ??= create();
|
|
1425
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
1426
|
-
return type.toParent(value);
|
|
1427
|
-
},
|
|
1428
1471
|
is: (value) => {
|
|
1429
1472
|
type ??= create();
|
|
1430
1473
|
return type.is(value);
|
|
@@ -1528,7 +1571,6 @@ export const tuple = (...elements) => {
|
|
|
1528
1571
|
}
|
|
1529
1572
|
return ok(result);
|
|
1530
1573
|
};
|
|
1531
|
-
const to = (value) => value.map((val, index) => elements[index].to(val));
|
|
1532
1574
|
const fromParent = (value) => {
|
|
1533
1575
|
if (!Array.isArray(value) || value.length !== elements.length) {
|
|
1534
1576
|
return err({
|
|
@@ -1555,13 +1597,10 @@ export const tuple = (...elements) => {
|
|
|
1555
1597
|
}
|
|
1556
1598
|
return ok(result);
|
|
1557
1599
|
};
|
|
1558
|
-
const toParent = (value) => value.map((val, index) => elements[index].toParent(val));
|
|
1559
1600
|
return {
|
|
1560
1601
|
...createType("Tuple", {
|
|
1561
1602
|
fromUnknown,
|
|
1562
|
-
to,
|
|
1563
1603
|
fromParent,
|
|
1564
|
-
toParent,
|
|
1565
1604
|
}),
|
|
1566
1605
|
elements,
|
|
1567
1606
|
};
|
|
@@ -1595,22 +1634,32 @@ export const Int64 = brand("Int64", BigInt, (value) => value >= -922337203685477
|
|
|
1595
1634
|
? ok(value)
|
|
1596
1635
|
: err({ type: "Int64", value }));
|
|
1597
1636
|
export const formatInt64Error = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid 64-bit signed integer (Int64).`);
|
|
1598
|
-
|
|
1599
|
-
export const
|
|
1637
|
+
// // co s timhle? je to string, ze ktereho lze udelat bigint
|
|
1638
|
+
// export const BigIntFromString = transform(
|
|
1639
|
+
// String,
|
|
1640
|
+
// BigInt,
|
|
1641
|
+
// (value) =>
|
|
1642
|
+
// trySync(
|
|
1643
|
+
// () => globalThis.BigInt(value),
|
|
1644
|
+
// (): BigIntFromStringError => ({ type: "BigIntFromString", value }),
|
|
1645
|
+
// ),
|
|
1646
|
+
// (value) => value.toString(),
|
|
1647
|
+
// );
|
|
1648
|
+
// export interface BigIntFromStringError extends TypeError<"BigIntFromString"> {}
|
|
1649
|
+
// export const formatBigIntFromStringError =
|
|
1650
|
+
// createTypeErrorFormatter<BigIntFromStringError>(
|
|
1651
|
+
// (error) => `The value ${error.value} could not be converted to a BigInt.`,
|
|
1652
|
+
// );
|
|
1600
1653
|
/**
|
|
1601
1654
|
* Stringified {@link Int64}.
|
|
1602
1655
|
*
|
|
1603
|
-
* @category
|
|
1656
|
+
* @category String
|
|
1604
1657
|
*/
|
|
1605
|
-
export const Int64String = brand("Int64",
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
return ok(value);
|
|
1611
|
-
}
|
|
1612
|
-
return err({ type: "Int64String", value });
|
|
1613
|
-
});
|
|
1658
|
+
export const Int64String = brand("Int64", NonEmptyTrimmedString, (value) => trySync(() => {
|
|
1659
|
+
const maybeInt = globalThis.BigInt(value);
|
|
1660
|
+
Int64.orThrow(maybeInt);
|
|
1661
|
+
return value;
|
|
1662
|
+
}, () => ({ type: "Int64String", value })));
|
|
1614
1663
|
export const formatInt64StringError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Int64 string.`);
|
|
1615
1664
|
/**
|
|
1616
1665
|
* JSON-compatible value: string, {@link FiniteNumber}, boolean, null,
|
|
@@ -1631,48 +1680,83 @@ export const JsonArray = array(JsonValue);
|
|
|
1631
1680
|
* @category Object
|
|
1632
1681
|
*/
|
|
1633
1682
|
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`,
|
|
1683
|
+
export const parseJson = (value) => trySync(() => JSON.parse(value), (error) => ({
|
|
1684
|
+
type: `Json`,
|
|
1649
1685
|
value,
|
|
1650
1686
|
message: globalThis.String(error),
|
|
1651
|
-
}))
|
|
1652
|
-
export const formatJsonValueFromStringError = createTypeErrorFormatter((error) => `Invalid JSONValue: ${error.value}. Error: ${error.message}`);
|
|
1687
|
+
}));
|
|
1653
1688
|
/**
|
|
1654
1689
|
* JSON-string {@link Type}.
|
|
1655
1690
|
*
|
|
1656
1691
|
* ### Example
|
|
1657
1692
|
*
|
|
1658
1693
|
* ```ts
|
|
1659
|
-
* const result = Json.from('{"key":"value"}'); //
|
|
1660
|
-
* const error = Json.from("invalid json"); //
|
|
1694
|
+
* const result = Json.from('{"key":"value"}'); // ok
|
|
1695
|
+
* const error = Json.from("invalid json"); // err
|
|
1661
1696
|
* ```
|
|
1662
1697
|
*
|
|
1663
1698
|
* @category String
|
|
1664
1699
|
*/
|
|
1665
1700
|
export const Json = brand("Json", String, (value) => {
|
|
1666
|
-
const result =
|
|
1701
|
+
const result = parseJson(value);
|
|
1667
1702
|
if (!result.ok)
|
|
1668
|
-
return
|
|
1669
|
-
type: "Json",
|
|
1670
|
-
value,
|
|
1671
|
-
message: result.error.message,
|
|
1672
|
-
});
|
|
1703
|
+
return result;
|
|
1673
1704
|
return ok(value);
|
|
1674
1705
|
});
|
|
1675
1706
|
export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON: ${error.value}. Error: ${error.message}`);
|
|
1707
|
+
export const jsonValueToJson = (value) => JSON.stringify(value);
|
|
1708
|
+
export const jsonToJsonValue = (value) => JSON.parse(value);
|
|
1709
|
+
/**
|
|
1710
|
+
* Creates a branded JSON string {@link Type} and type-safe conversion functions
|
|
1711
|
+
* for a given Type.
|
|
1712
|
+
*
|
|
1713
|
+
* This factory creates:
|
|
1714
|
+
*
|
|
1715
|
+
* 1. A branded string Type that validates JSON parsing and structural conformity
|
|
1716
|
+
* 2. A serialization function (Type → branded JSON string)
|
|
1717
|
+
* 3. A parsing function (branded JSON string → Type, skipping validation)
|
|
1718
|
+
*
|
|
1719
|
+
* Optimized for Evolu's SQLite workflow where we store typed JSON strings and
|
|
1720
|
+
* need type-safe conversions without double parsing.
|
|
1721
|
+
*
|
|
1722
|
+
* ### Example
|
|
1723
|
+
*
|
|
1724
|
+
* ```ts
|
|
1725
|
+
* const Person = object({
|
|
1726
|
+
* name: NonEmptyString100,
|
|
1727
|
+
* age: FiniteNumber,
|
|
1728
|
+
* });
|
|
1729
|
+
* type Person = typeof Person.Type;
|
|
1730
|
+
*
|
|
1731
|
+
* const [PersonJson, personToPersonJson, personJsonToPerson] = json(
|
|
1732
|
+
* Person,
|
|
1733
|
+
* "PersonJson",
|
|
1734
|
+
* );
|
|
1735
|
+
* // string & Brand<"PersonJson">
|
|
1736
|
+
* type PersonJson = typeof PersonJson.Type;
|
|
1737
|
+
*
|
|
1738
|
+
* // Usage:
|
|
1739
|
+
* const person: Person = { name: "Alice", age: 30 };
|
|
1740
|
+
* const jsonString = personToPersonJson(person); // PersonJson
|
|
1741
|
+
* const backToPerson = personJsonToPerson(jsonString); // Person
|
|
1742
|
+
* ```
|
|
1743
|
+
*/
|
|
1744
|
+
export const json = (type, name) => {
|
|
1745
|
+
const BrandedJsonType = brand(name, String, (value) => {
|
|
1746
|
+
const parseResult = parseJson(value);
|
|
1747
|
+
if (!parseResult.ok)
|
|
1748
|
+
return parseResult;
|
|
1749
|
+
const validationResult = type.fromUnknown(parseResult.value);
|
|
1750
|
+
if (!validationResult.ok)
|
|
1751
|
+
return validationResult;
|
|
1752
|
+
return ok(value);
|
|
1753
|
+
});
|
|
1754
|
+
return [
|
|
1755
|
+
BrandedJsonType,
|
|
1756
|
+
jsonValueToJson,
|
|
1757
|
+
jsonToJsonValue,
|
|
1758
|
+
];
|
|
1759
|
+
};
|
|
1676
1760
|
/**
|
|
1677
1761
|
* Optional {@link Type}.
|
|
1678
1762
|
*
|
|
@@ -1697,9 +1781,7 @@ export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON
|
|
|
1697
1781
|
export const optional = (type) => ({
|
|
1698
1782
|
...createType("Optional", {
|
|
1699
1783
|
fromUnknown: type.fromUnknown,
|
|
1700
|
-
to: type.to,
|
|
1701
1784
|
fromParent: type.fromParent,
|
|
1702
|
-
toParent: type.toParent,
|
|
1703
1785
|
}),
|
|
1704
1786
|
parent: type,
|
|
1705
1787
|
});
|
|
@@ -1708,7 +1790,7 @@ export const isOptionalType = (x) => typeof x === "object" && x != null && "name
|
|
|
1708
1790
|
/**
|
|
1709
1791
|
* Creates a partial object type where all properties are optional.
|
|
1710
1792
|
*
|
|
1711
|
-
* This is useful when
|
|
1793
|
+
* This is useful when we want to validate an object in which none of the keys
|
|
1712
1794
|
* are required, but if they are present they must conform to their
|
|
1713
1795
|
* corresponding Types.
|
|
1714
1796
|
*
|
|
@@ -1779,114 +1861,86 @@ export function omit(objectType, ...keys) {
|
|
|
1779
1861
|
}
|
|
1780
1862
|
return object(newProps);
|
|
1781
1863
|
}
|
|
1864
|
+
export const maxMutationSize = 655360;
|
|
1782
1865
|
/**
|
|
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
|
-
* ```
|
|
1866
|
+
* Evolu has to limit the maximum mutation size. Otherwise, sync couldn't use
|
|
1867
|
+
* the `maxProtocolMessageRangesSize`. The max size is 640KB in bytes, measured
|
|
1868
|
+
* via MessagePack. Evolu Protocol DbChange will be smaller thanks to various
|
|
1869
|
+
* optimizations.
|
|
1810
1870
|
*/
|
|
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
|
-
};
|
|
1871
|
+
export const validMutationSize = (type) => brand("ValidMutationSize", type, (value) => pack(value).byteLength <= maxMutationSize
|
|
1872
|
+
? ok(value)
|
|
1873
|
+
: err({ type: "ValidMutationSize", value }));
|
|
1874
|
+
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
1875
|
/**
|
|
1826
|
-
*
|
|
1827
|
-
* {@link TypeErrors} and custom errors. It also lets us override the default
|
|
1828
|
-
* formatting for specific errors.
|
|
1876
|
+
* Formats Evolu Type errors into user-friendly messages.
|
|
1829
1877
|
*
|
|
1830
|
-
*
|
|
1831
|
-
*
|
|
1878
|
+
* Evolu Type typed errors ensure every error type must have a formatter.
|
|
1879
|
+
* TypeScript enforces this at compile-time, preventing unhandled validation
|
|
1880
|
+
* errors from reaching users.
|
|
1832
1881
|
*
|
|
1833
|
-
*
|
|
1882
|
+
* The `createFormatTypeError` function handles both built-in {@link TypeErrors}
|
|
1883
|
+
* and custom errors, and lets us override default formatting for specific
|
|
1884
|
+
* errors.
|
|
1885
|
+
*
|
|
1886
|
+
* ### Example
|
|
1834
1887
|
*
|
|
1835
1888
|
* ```ts
|
|
1836
|
-
* const
|
|
1837
|
-
*
|
|
1838
|
-
*
|
|
1889
|
+
* const formatTypeError = createFormatTypeError<
|
|
1890
|
+
* MinLengthError | MaxLengthError
|
|
1891
|
+
* >((error): string => {
|
|
1892
|
+
* switch (error.type) {
|
|
1893
|
+
* case "MinLength":
|
|
1894
|
+
* return `Text must be at least ${error.min} character${error.min === 1 ? "" : "s"} long`;
|
|
1895
|
+
* case "MaxLength":
|
|
1896
|
+
* return `Text is too long (maximum ${error.max} characters)`;
|
|
1897
|
+
* }
|
|
1898
|
+
* });
|
|
1839
1899
|
* ```
|
|
1840
1900
|
*
|
|
1841
|
-
*
|
|
1901
|
+
* Alternatively, write a custom formatter from scratch without using
|
|
1902
|
+
* `createFormatTypeError`. This gives us full control over error formatting:
|
|
1842
1903
|
*
|
|
1843
1904
|
* ```ts
|
|
1844
|
-
*
|
|
1845
|
-
*
|
|
1905
|
+
* const Person = object({
|
|
1906
|
+
* name: NonEmptyTrimmedString100,
|
|
1907
|
+
* age: optional(PositiveInt),
|
|
1908
|
+
* });
|
|
1909
|
+
*
|
|
1910
|
+
* // Define only the errors actually used by Person Type
|
|
1911
|
+
* type PersonErrors =
|
|
1846
1912
|
* | StringError
|
|
1847
|
-
* | MinLengthError
|
|
1848
1913
|
* | MaxLengthError
|
|
1849
|
-
* |
|
|
1850
|
-
* | IdError
|
|
1914
|
+
* | MinLengthError
|
|
1851
1915
|
* | 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.
|
|
1916
|
+
* | PositiveError
|
|
1917
|
+
* | NonNegativeError
|
|
1918
|
+
* | IntError
|
|
1919
|
+
* | NumberError
|
|
1920
|
+
* | ObjectError<Record<string, PersonErrors>>;
|
|
1921
|
+
*
|
|
1922
|
+
* const formatTypeError: TypeErrorFormatter<PersonErrors> = (error) => {
|
|
1861
1923
|
* 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
1924
|
* case "String":
|
|
1873
|
-
* // We can reuse existing formatter.
|
|
1874
1925
|
* return formatStringError(error);
|
|
1926
|
+
* case "Number":
|
|
1927
|
+
* return "Must be a number";
|
|
1928
|
+
* case "MinLength":
|
|
1929
|
+
* return `Must be at least ${error.min} characters`;
|
|
1930
|
+
* case "MaxLength":
|
|
1931
|
+
* return `Cannot exceed ${error.max} characters`;
|
|
1875
1932
|
* case "Trimmed":
|
|
1876
|
-
* return "
|
|
1877
|
-
* case "
|
|
1878
|
-
* return "
|
|
1879
|
-
* case "
|
|
1880
|
-
* return
|
|
1881
|
-
*
|
|
1882
|
-
*
|
|
1883
|
-
* return `Union errors: ${error.errors.map(formatTypeError).join(", ")}`;
|
|
1933
|
+
* return "Cannot have leading or trailing spaces";
|
|
1934
|
+
* case "Positive":
|
|
1935
|
+
* return "Must be a positive number";
|
|
1936
|
+
* case "NonNegative":
|
|
1937
|
+
* return "Must be zero or positive";
|
|
1938
|
+
* case "Int":
|
|
1939
|
+
* return "Must be an integer";
|
|
1884
1940
|
* case "Object": {
|
|
1885
|
-
* if (
|
|
1886
|
-
*
|
|
1887
|
-
*
|
|
1888
|
-
* )
|
|
1889
|
-
* return "A developer made an error, this should not happen.";
|
|
1941
|
+
* if (error.reason.kind === "NotObject") return "Must be an object";
|
|
1942
|
+
* if (error.reason.kind === "ExtraKeys")
|
|
1943
|
+
* return "Contains unexpected fields";
|
|
1890
1944
|
* const firstError = Object.values(error.reason.errors).find(
|
|
1891
1945
|
* (e) => e !== undefined,
|
|
1892
1946
|
* )!;
|
|
@@ -1927,8 +1981,8 @@ export const createFormatTypeError = (extraFormatter) => {
|
|
|
1927
1981
|
return formatIsTypeError(error);
|
|
1928
1982
|
case "CurrencyCode":
|
|
1929
1983
|
return formatCurrencyCodeError(error);
|
|
1930
|
-
case "
|
|
1931
|
-
return
|
|
1984
|
+
case "DateIso":
|
|
1985
|
+
return formatDateIsoError(error);
|
|
1932
1986
|
case "Trimmed":
|
|
1933
1987
|
return formatTrimmedError(error);
|
|
1934
1988
|
case "MinLength":
|
|
@@ -1943,6 +1997,8 @@ export const createFormatTypeError = (extraFormatter) => {
|
|
|
1943
1997
|
return formatRegexError(error);
|
|
1944
1998
|
case "Id":
|
|
1945
1999
|
return formatIdError(error);
|
|
2000
|
+
case "TableId":
|
|
2001
|
+
return formatTableIdError(error);
|
|
1946
2002
|
case "Positive":
|
|
1947
2003
|
return formatPositiveError(error);
|
|
1948
2004
|
case "Negative":
|
|
@@ -1973,14 +2029,12 @@ export const createFormatTypeError = (extraFormatter) => {
|
|
|
1973
2029
|
return formatLiteralError(error);
|
|
1974
2030
|
case "Int64":
|
|
1975
2031
|
return formatInt64Error(error);
|
|
1976
|
-
case "BigIntFromString":
|
|
1977
|
-
return formatBigIntFromStringError(error);
|
|
1978
2032
|
case "Int64String":
|
|
1979
2033
|
return formatInt64StringError(error);
|
|
1980
|
-
case "JsonValueFromString":
|
|
1981
|
-
return formatJsonValueFromStringError(error);
|
|
1982
2034
|
case "Json":
|
|
1983
2035
|
return formatJsonError(error);
|
|
2036
|
+
case "ValidMutationSize":
|
|
2037
|
+
return formatValidMutationSizeError(error);
|
|
1984
2038
|
// Composite Types
|
|
1985
2039
|
case "SimplePassword":
|
|
1986
2040
|
return formatSimplePasswordError(formatTypeError)(error);
|