@evolu/common 6.0.1-preview.19 → 6.0.1-preview.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +1 -1
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +1 -1
- package/dist/src/CallbackRegistry.d.ts +53 -0
- package/dist/src/CallbackRegistry.d.ts.map +1 -0
- package/dist/src/CallbackRegistry.js +25 -0
- package/dist/src/Console.d.ts +31 -6
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +72 -9
- package/dist/src/Crypto.d.ts +48 -37
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +27 -50
- package/dist/src/Evolu/Db.d.ts +138 -66
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +248 -645
- package/dist/src/Evolu/Diff.d.ts +3 -3
- package/dist/src/Evolu/Diff.d.ts.map +1 -1
- package/dist/src/Evolu/Diff.js +7 -5
- package/dist/src/Evolu/Evolu.d.ts +79 -116
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +275 -132
- package/dist/src/Evolu/Internal.d.ts +0 -2
- package/dist/src/Evolu/Internal.d.ts.map +1 -1
- package/dist/src/Evolu/Internal.js +0 -2
- package/dist/src/Evolu/LocalAuth.d.ts +144 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +171 -0
- package/dist/src/Evolu/Owner.d.ts +129 -83
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +80 -89
- package/dist/src/Evolu/Platform.d.ts +9 -7
- package/dist/src/Evolu/Platform.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.d.ts +114 -191
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +409 -416
- package/dist/src/Evolu/Public.d.ts +6 -8
- package/dist/src/Evolu/Public.d.ts.map +1 -1
- package/dist/src/Evolu/Public.js +2 -3
- package/dist/src/Evolu/PublicKysely.js +3 -3
- package/dist/src/Evolu/Relay.d.ts +1 -2
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +11 -9
- package/dist/src/Evolu/Schema.d.ts +88 -27
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +141 -24
- package/dist/src/Evolu/Storage.d.ts +158 -14
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +32 -32
- package/dist/src/Evolu/Sync.d.ts +77 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +453 -20
- package/dist/src/Evolu/Timestamp.d.ts +29 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +20 -18
- package/dist/src/ManyToManyMap.d.ts +74 -10
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/ManyToManyMap.js +41 -6
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/RefCountedResourceManager.d.ts +119 -0
- package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
- package/dist/src/RefCountedResourceManager.js +197 -0
- package/dist/src/Result.d.ts +144 -22
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +5 -2
- package/dist/src/Sqlite.d.ts +20 -4
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +50 -8
- package/dist/src/Task.d.ts +511 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +410 -0
- package/dist/src/Time.d.ts +59 -0
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +87 -4
- package/dist/src/Type.d.ts +431 -341
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +458 -466
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -13
- package/dist/src/Worker.d.ts +39 -11
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +22 -4
- package/dist/src/index.d.ts +2 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -3
- package/package.json +7 -7
- package/src/Assert.ts +2 -4
- package/src/Buffer.ts +1 -1
- package/src/CallbackRegistry.ts +84 -0
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +78 -91
- package/src/Evolu/Db.ts +455 -947
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +545 -307
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +422 -0
- package/src/Evolu/Owner.ts +191 -131
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +536 -653
- package/src/Evolu/Public.ts +7 -9
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Relay.ts +17 -12
- package/src/Evolu/Schema.ts +271 -66
- package/src/Evolu/Storage.ts +263 -55
- package/src/Evolu/Sync.ts +758 -37
- package/src/Evolu/Timestamp.ts +30 -35
- package/src/ManyToManyMap.ts +127 -24
- package/src/Random.ts +3 -2
- package/src/RefCountedResourceManager.ts +368 -0
- package/src/Result.ts +149 -23
- package/src/Sqlite.ts +59 -24
- package/src/Task.ts +779 -0
- package/src/Time.ts +168 -4
- package/src/Type.ts +657 -695
- package/src/WebSocket.ts +23 -17
- package/src/Worker.ts +72 -23
- package/src/index.ts +2 -3
- package/dist/src/Callbacks.d.ts +0 -20
- package/dist/src/Callbacks.d.ts.map +0 -1
- package/dist/src/Callbacks.js +0 -18
- package/dist/src/Evolu/Config.d.ts +0 -82
- package/dist/src/Evolu/Config.d.ts.map +0 -1
- package/dist/src/Evolu/Config.js +0 -9
- package/dist/src/Evolu/Kysely.d.ts +0 -6
- package/dist/src/Evolu/Kysely.d.ts.map +0 -1
- package/dist/src/Evolu/Kysely.js +0 -21
- package/dist/src/NanoId.d.ts +0 -27
- package/dist/src/NanoId.d.ts.map +0 -1
- package/dist/src/NanoId.js +0 -6
- package/dist/src/Promise.d.ts +0 -180
- package/dist/src/Promise.d.ts.map +0 -1
- package/dist/src/Promise.js +0 -176
- package/src/Callbacks.ts +0 -43
- package/src/Evolu/Config.ts +0 -97
- package/src/Evolu/Kysely.ts +0 -38
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/dist/src/Type.js
CHANGED
|
@@ -1,82 +1,182 @@
|
|
|
1
|
-
/* eslint-disable jsdoc/no-undefined-types */
|
|
2
1
|
/**
|
|
3
|
-
* 🧩
|
|
2
|
+
* 🧩 Type-safe runtime types
|
|
4
3
|
*
|
|
5
|
-
*
|
|
4
|
+
* Evolu {@link Type} is like a type guard that returns typed errors (via
|
|
5
|
+
* {@link Result}) instead of throwing. We either get a safely typed value or a
|
|
6
|
+
* precise, composable error value telling us exactly why validation failed.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
+
* Why another validation library?
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* - **Result-based error handling** – no exceptions for normal control flow.
|
|
11
|
+
* - **Typed errors with decoupled formatters** – validation logic ≠ user
|
|
12
|
+
* messages.
|
|
13
|
+
* - **Consistent constraints via {@link Brand}** – every constraint becomes part
|
|
14
|
+
* of the type.
|
|
15
|
+
* - **No user-land chaining DSL** – designed with the upcoming ES pipe operator
|
|
16
|
+
* in mind.
|
|
17
|
+
* - **Selective validation** – parent validations are skipped when already proved
|
|
18
|
+
* by typing.
|
|
19
|
+
* - **Simple, top-down implementation** – readable source code from top to bottom
|
|
20
|
+
* with no hidden magic; just plain functions and composition.
|
|
11
21
|
*
|
|
12
|
-
*
|
|
13
|
-
* exceptions.
|
|
14
|
-
* - **Consistent constraints**: Enforcing {@link Brand} for all constraints.
|
|
15
|
-
* - **Typed errors with decoupled formatters**: Avoiding coupling error messages
|
|
16
|
-
* with validators.
|
|
17
|
-
* - **No user-land chaining**: Designed with ES pipe operator in mind.
|
|
18
|
-
* - **Selective validation/transformation**: Skipping parent Type validations and
|
|
19
|
-
* transformations when TypeScript's type system can be relied upon.
|
|
20
|
-
* - **Bidirectional transformations**: Supporting transformations in both
|
|
21
|
-
* directions.
|
|
22
|
-
* - **Minimal and transparent code**: No runtime dependencies or hidden magic.
|
|
22
|
+
* ### Base Types Quick Start
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* // Validate unknown values
|
|
26
|
+
* const value: unknown = "hello";
|
|
27
|
+
* const stringResult = String.fromUnknown(value);
|
|
28
|
+
* if (!stringResult.ok) {
|
|
29
|
+
* // console.error(formatStringError(stringResult.error));
|
|
30
|
+
* return stringResult; // inside a function returning Result<string, _>
|
|
31
|
+
* }
|
|
32
|
+
* // Safe branch: value is now string
|
|
33
|
+
* const upper = stringResult.value.toUpperCase();
|
|
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:
|
|
53
|
+
*
|
|
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">
|
|
61
|
+
*
|
|
62
|
+
* interface CurrencyCodeError extends TypeError<"CurrencyCode"> {}
|
|
63
|
+
*
|
|
64
|
+
* const formatCurrencyCodeError =
|
|
65
|
+
* createTypeErrorFormatter<CurrencyCodeError>(
|
|
66
|
+
* (error) => `Invalid currency code: ${error.value}`,
|
|
67
|
+
* );
|
|
68
|
+
*
|
|
69
|
+
* const r = CurrencyCode.from("USD"); // ok("USD")
|
|
70
|
+
* const e = CurrencyCode.from("usd"); // err(...)
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* See also reusable brand factories like `minLength`, `maxLength`, `trimmed`,
|
|
74
|
+
* `positive`, `between`, etc.
|
|
75
|
+
*
|
|
76
|
+
* ### Objects & Optional Fields
|
|
77
|
+
*
|
|
78
|
+
* ```ts
|
|
79
|
+
* const User = object({
|
|
80
|
+
* name: NonEmptyTrimmedString100,
|
|
81
|
+
* age: optional(PositiveInt),
|
|
82
|
+
* });
|
|
83
|
+
* type User = typeof User.Type;
|
|
84
|
+
*
|
|
85
|
+
* User.from({ name: "Alice" }); // ok
|
|
86
|
+
* User.from({ name: "Alice", age: -1 }); // err(PositiveInt)
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* ### Deriving JSON String Types
|
|
90
|
+
*
|
|
91
|
+
* ```ts
|
|
92
|
+
* const Person = object({
|
|
93
|
+
* name: NonEmptyString50,
|
|
94
|
+
* // Did you know that JSON.stringify converts NaN (a number) into null?
|
|
95
|
+
* // To prevent this, use FiniteNumber.
|
|
96
|
+
* age: FiniteNumber,
|
|
97
|
+
* });
|
|
98
|
+
* type Person = typeof Person.Type;
|
|
99
|
+
*
|
|
100
|
+
* const [PersonJson, personToPersonJson, personJsonToPerson] = json(
|
|
101
|
+
* Person,
|
|
102
|
+
* "PersonJson",
|
|
103
|
+
* );
|
|
104
|
+
* // string & Brand<"PersonJson">
|
|
105
|
+
* type PersonJson = typeof PersonJson.Type;
|
|
28
106
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* cannot fail.
|
|
107
|
+
* const person = Person.orThrow({
|
|
108
|
+
* name: "Alice",
|
|
109
|
+
* age: 30,
|
|
110
|
+
* });
|
|
34
111
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* this:
|
|
112
|
+
* const personJson = personToPersonJson(person);
|
|
113
|
+
* expect(personJsonToPerson(personJson)).toEqual(person);
|
|
114
|
+
* ```
|
|
39
115
|
*
|
|
40
|
-
*
|
|
116
|
+
* ### Error Formatting
|
|
41
117
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
118
|
+
* Evolu separates validation logic from human-readable messages. There are two
|
|
119
|
+
* layers:
|
|
44
120
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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.
|
|
48
126
|
*
|
|
49
|
-
*
|
|
50
|
-
* parent Types when we can rely on TypeScript types.
|
|
127
|
+
* #### 1. Per-Type Formatter (recap)
|
|
51
128
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
129
|
+
* ```ts
|
|
130
|
+
* const r = String.fromUnknown(42);
|
|
131
|
+
* if (!r.ok) console.error(formatStringError(r.error));
|
|
132
|
+
* ```
|
|
133
|
+
*
|
|
134
|
+
* #### 2. Unified Formatter with Overrides
|
|
135
|
+
*
|
|
136
|
+
* ```ts
|
|
137
|
+
* // Override only what we care about; fall back to built-ins for the rest.
|
|
138
|
+
* const formatTypeError = createFormatTypeError((error) => {
|
|
139
|
+
* if (error.type === "MinLength") return `Min length is ${error.min}`;
|
|
140
|
+
* });
|
|
141
|
+
*
|
|
142
|
+
* const User = object({ name: NonEmptyTrimmedString100 });
|
|
143
|
+
* const resultUser = User.from({ name: "" });
|
|
144
|
+
* if (!resultUser.ok) console.error(formatTypeError(resultUser.error));
|
|
145
|
+
*
|
|
146
|
+
* const badPoint = object({ x: Number, y: Number }).from({
|
|
147
|
+
* x: 1,
|
|
148
|
+
* y: "foo",
|
|
149
|
+
* });
|
|
150
|
+
* if (!badPoint.ok) console.error(formatTypeError(badPoint.error));
|
|
151
|
+
* ```
|
|
152
|
+
*
|
|
153
|
+
* The unified formatter walks nested structures (object / array / record /
|
|
154
|
+
* tuple / union) and applies overrides only where specified, greatly reducing
|
|
155
|
+
* boilerplate when formatting complex validation errors.
|
|
57
156
|
*
|
|
58
157
|
* ### Tip
|
|
59
158
|
*
|
|
60
159
|
* If necessary, write `globalThis.String` instead of `String` to avoid naming
|
|
61
|
-
* clashes with
|
|
160
|
+
* clashes with native types.
|
|
62
161
|
*
|
|
63
|
-
* ### Design Decision:
|
|
162
|
+
* ### Design Decision: No Bidirectional Transformations
|
|
64
163
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
164
|
+
* Evolu Type intentionally does not support bidirectional transformations. It
|
|
165
|
+
* previously did, but supporting that while keeping typed error fidelity added
|
|
166
|
+
* complexity that hurt readability & reliability. Most persistence pipelines
|
|
167
|
+
* (e.g. SQLite) already require explicit mapping of query results, so implicit
|
|
168
|
+
* reverse transforms would not buy much. We may revisit this if we can design a
|
|
169
|
+
* minimal, 100% safe API that preserves simplicity.
|
|
69
170
|
*
|
|
70
171
|
* @module
|
|
71
172
|
*/
|
|
72
|
-
import { utf8ToBytes } from "@noble/ciphers/utils";
|
|
73
|
-
import { sha256 } from "@noble/hashes/sha2";
|
|
173
|
+
import { utf8ToBytes } from "@noble/ciphers/utils.js";
|
|
174
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
74
175
|
import * as bip39 from "@scure/bip39";
|
|
75
|
-
import { wordlist } from "@scure/bip39/wordlists/english";
|
|
76
|
-
import {
|
|
77
|
-
import { identity } from "./Function.js";
|
|
176
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
177
|
+
import { pack } from "msgpackr";
|
|
78
178
|
import { isPlainObject } from "./Object.js";
|
|
79
|
-
import { err, ok, trySync } from "./Result.js";
|
|
179
|
+
import { err, getOrThrow, ok, trySync } from "./Result.js";
|
|
80
180
|
import { safelyStringifyUnknownValue } from "./String.js";
|
|
81
181
|
const EvoluTypeSymbol = Symbol("evolu.Type");
|
|
82
182
|
/**
|
|
@@ -90,6 +190,7 @@ const createType = (name, definition) => ({
|
|
|
90
190
|
name,
|
|
91
191
|
is: (value) => definition.fromUnknown(value).ok,
|
|
92
192
|
from: definition.fromUnknown,
|
|
193
|
+
orThrow: (value) => getOrThrow(definition.fromUnknown(value)),
|
|
93
194
|
[EvoluTypeSymbol]: true,
|
|
94
195
|
Type: undefined,
|
|
95
196
|
Input: undefined,
|
|
@@ -119,12 +220,6 @@ export const createTypeErrorFormatter = (format) => (error) => format({ ...error
|
|
|
119
220
|
* Base {@link Type}.
|
|
120
221
|
*
|
|
121
222
|
* A Base Type validates that a value conforms to a specific TypeScript type.
|
|
122
|
-
* Unlike refinements or transformations, Base Types establish the fundamental
|
|
123
|
-
* shape of a value before any branding or transformation occurs.
|
|
124
|
-
*
|
|
125
|
-
* - To **refine** a Base Type further, use the {@link brand} Type Factory.
|
|
126
|
-
* - To **transform** a Base Type into a different representation, use the
|
|
127
|
-
* {@link transform} Type Factory.
|
|
128
223
|
*
|
|
129
224
|
* ### Example
|
|
130
225
|
*
|
|
@@ -146,9 +241,7 @@ export const createTypeErrorFormatter = (format) => (error) => format({ ...error
|
|
|
146
241
|
*/
|
|
147
242
|
export const base = (name, fromUnknown) => createType(name, {
|
|
148
243
|
fromUnknown,
|
|
149
|
-
to: identity,
|
|
150
244
|
fromParent: (ok), // `fromParent` relies on types, so it can't fail for the Base Type
|
|
151
|
-
toParent: identity,
|
|
152
245
|
});
|
|
153
246
|
/**
|
|
154
247
|
* Creates a formatter function for a base {@link TypeError}.
|
|
@@ -282,9 +375,7 @@ export function brand(name, parent, refine) {
|
|
|
282
375
|
return {
|
|
283
376
|
...createType("Brand", {
|
|
284
377
|
fromUnknown,
|
|
285
|
-
to: identity,
|
|
286
378
|
fromParent: (refine ?? ok),
|
|
287
|
-
toParent: identity,
|
|
288
379
|
}),
|
|
289
380
|
brand: name,
|
|
290
381
|
parentType: parent,
|
|
@@ -314,37 +405,39 @@ export const formatCurrencyCodeError = createTypeErrorFormatter((error) => `Inva
|
|
|
314
405
|
* ### Example
|
|
315
406
|
*
|
|
316
407
|
* ```ts
|
|
317
|
-
* const result =
|
|
318
|
-
* 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
|
|
319
410
|
* ```
|
|
320
411
|
*
|
|
321
412
|
* @category String
|
|
322
413
|
*/
|
|
323
|
-
export const
|
|
414
|
+
export const DateIso = brand("DateIso", String, (value) => {
|
|
324
415
|
if (value.length !== 24) {
|
|
325
|
-
return err({ type: "
|
|
416
|
+
return err({ type: "DateIso", value });
|
|
326
417
|
}
|
|
327
|
-
|
|
328
|
-
|
|
418
|
+
const parsed = globalThis.Date.parse(value);
|
|
419
|
+
if (isNaN(parsed)) {
|
|
420
|
+
return err({ type: "DateIso", value });
|
|
421
|
+
}
|
|
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 });
|
|
329
426
|
}
|
|
330
427
|
return ok(value);
|
|
331
428
|
});
|
|
332
|
-
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);
|
|
333
432
|
/**
|
|
334
433
|
* Trimmed string.
|
|
335
434
|
*
|
|
336
|
-
* This Type Factory
|
|
337
|
-
*
|
|
338
|
-
* Factory.
|
|
435
|
+
* This Type Factory validates whether a string has no leading or trailing
|
|
436
|
+
* whitespaces.
|
|
339
437
|
*
|
|
340
|
-
* ###
|
|
438
|
+
* ### Example
|
|
341
439
|
*
|
|
342
440
|
* ```ts
|
|
343
|
-
* // this Type already exists
|
|
344
|
-
* const TrimmedString = trimmed(String);
|
|
345
|
-
* type TrimmedString = typeof TrimmedString.Type;
|
|
346
|
-
*
|
|
347
|
-
* // we can make any branded Type trimmed:
|
|
348
441
|
* const TrimmedNonEmptyString = trimmed(minLength(1)(String));
|
|
349
442
|
* // string & Brand<"MinLength1"> & Brand<"Trimmed">
|
|
350
443
|
* type TrimmedNonEmptyString = typeof TrimmedNonEmptyString.Type;
|
|
@@ -356,32 +449,6 @@ export const trimmed = (parent) => brand("Trimmed", parent, (value) => value.tri
|
|
|
356
449
|
? ok(value)
|
|
357
450
|
: err({ type: "Trimmed", value }));
|
|
358
451
|
export const formatTrimmedError = createTypeErrorFormatter((error) => `A value ${error.value} is not trimmed`);
|
|
359
|
-
/**
|
|
360
|
-
* Trims leading and trailing whitespace from a string.
|
|
361
|
-
*
|
|
362
|
-
* This Type Factory **transforms** the input string by removing whitespace from
|
|
363
|
-
* both ends. For validation only, use {@link trimmed} Type Factory.
|
|
364
|
-
*
|
|
365
|
-
* ### Example
|
|
366
|
-
*
|
|
367
|
-
* ```ts
|
|
368
|
-
* const TrimString = trim(String);
|
|
369
|
-
* expect(TrimString.from("a ")).toEqual(ok("a"));
|
|
370
|
-
* expect(TrimString.fromParent("a ").value).toEqual("a");
|
|
371
|
-
*
|
|
372
|
-
* const TrimNonEmptyString = trim(NonEmptyString);
|
|
373
|
-
* expect(TrimNonEmptyString.from("a " as NonEmptyString)).toEqual(ok("a"));
|
|
374
|
-
* expect(
|
|
375
|
-
* TrimNonEmptyString.fromParent("a " as NonEmptyString).value,
|
|
376
|
-
* ).toEqual("a");
|
|
377
|
-
* ```
|
|
378
|
-
*
|
|
379
|
-
* **Note:** This transformation is irreversible. Calling `toParent` will not
|
|
380
|
-
* restore the original representation.
|
|
381
|
-
*
|
|
382
|
-
* @category String
|
|
383
|
-
*/
|
|
384
|
-
export const trim = (parent) => transform(parent, trimmed(parent), (value) => ok(value.trim()), (value) => value);
|
|
385
452
|
/**
|
|
386
453
|
* Trimmed string
|
|
387
454
|
*
|
|
@@ -391,6 +458,7 @@ export const trim = (parent) => transform(parent, trimmed(parent), (value) => ok
|
|
|
391
458
|
* @category String
|
|
392
459
|
*/
|
|
393
460
|
export const TrimmedString = trimmed(String);
|
|
461
|
+
export const trim = (value) => value.trim();
|
|
394
462
|
/**
|
|
395
463
|
* Minimum length.
|
|
396
464
|
*
|
|
@@ -496,9 +564,9 @@ export const regex = (name, pattern) => {
|
|
|
496
564
|
};
|
|
497
565
|
export const formatRegexError = createTypeErrorFormatter((error) => `Value ${error.value} does not match the pattern for ${error.name}: ${error.pattern}`);
|
|
498
566
|
/**
|
|
499
|
-
* URL-safe
|
|
567
|
+
* URL-safe string.
|
|
500
568
|
*
|
|
501
|
-
* A `
|
|
569
|
+
* A `UrlSafeString` uses a limited alphabet that is safe for URLs:
|
|
502
570
|
*
|
|
503
571
|
* - Uppercase letters (`A-Z`)
|
|
504
572
|
* - Lowercase letters (`a-z`)
|
|
@@ -506,34 +574,89 @@ export const formatRegexError = createTypeErrorFormatter((error) => `Value ${err
|
|
|
506
574
|
* - Dash (`-`)
|
|
507
575
|
* - Underscore (`_`)
|
|
508
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
|
+
*
|
|
509
580
|
* ### Example
|
|
510
581
|
*
|
|
511
582
|
* ```ts
|
|
512
|
-
* const result =
|
|
583
|
+
* const result = UrlSafeString.from("abc123_-");
|
|
513
584
|
* if (result.ok) {
|
|
514
|
-
* console.log("Valid
|
|
585
|
+
* console.log("Valid URL-safe string:", result.value);
|
|
515
586
|
* } else {
|
|
516
|
-
* console.error("Invalid
|
|
587
|
+
* console.error("Invalid URL-safe string:", result.error);
|
|
517
588
|
* }
|
|
518
589
|
* ```
|
|
519
590
|
*
|
|
520
591
|
* @category String
|
|
521
592
|
*/
|
|
522
|
-
export const
|
|
593
|
+
export const UrlSafeString = regex("UrlSafeString", /^[A-Za-z0-9_-]+$/)(String);
|
|
523
594
|
/**
|
|
524
|
-
*
|
|
525
|
-
*
|
|
595
|
+
* Base64Url without padding.
|
|
596
|
+
*
|
|
597
|
+
* Encode with {@link uint8ArrayToBase64Url}, decode with
|
|
598
|
+
* {@link base64UrlToUint8Array}.
|
|
599
|
+
*
|
|
600
|
+
* @category String
|
|
526
601
|
*/
|
|
527
|
-
export const
|
|
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
|
+
};
|
|
528
655
|
/**
|
|
529
|
-
* Simple alphanumeric string for naming.
|
|
656
|
+
* Simple alphanumeric string for naming in file systems, URLs, and identifiers.
|
|
530
657
|
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
* - Uppercase letters (`A-Z`)
|
|
534
|
-
* - Lowercase letters (`a-z`)
|
|
535
|
-
* - Digits (`0-9`)
|
|
536
|
-
* - Dash (`-`)
|
|
658
|
+
* Uses the same safe alphabet as {@link UrlSafeString} (letters, digits, `-`,
|
|
659
|
+
* `_`). See `UrlSafeString` for details.
|
|
537
660
|
*
|
|
538
661
|
* The string must be between 1 and 42 characters.
|
|
539
662
|
*
|
|
@@ -550,13 +673,9 @@ export const base64UrlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_G
|
|
|
550
673
|
*
|
|
551
674
|
* @category String
|
|
552
675
|
*/
|
|
553
|
-
export const SimpleName =
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
*
|
|
557
|
-
* @category String
|
|
558
|
-
*/
|
|
559
|
-
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 }));
|
|
560
679
|
/**
|
|
561
680
|
* Trimmed string between 8 and 64 characters, branded as `SimplePassword`.
|
|
562
681
|
*
|
|
@@ -565,15 +684,36 @@ export const NanoId = regex("NanoId", /^[A-Za-z0-9_-]{21}$/)(String);
|
|
|
565
684
|
export const SimplePassword = brand("SimplePassword", minLength(8)(maxLength(64)(TrimmedString)));
|
|
566
685
|
export const formatSimplePasswordError = (formatTypeError) => createTypeErrorFormatter((error) => `Invalid password: ${formatTypeError(error.parentError)}`);
|
|
567
686
|
/**
|
|
568
|
-
*
|
|
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:
|
|
569
696
|
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
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.
|
|
572
710
|
*
|
|
573
711
|
* @category String
|
|
574
712
|
*/
|
|
575
|
-
export const Id =
|
|
576
|
-
|
|
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.`);
|
|
577
717
|
/**
|
|
578
718
|
* Creates an {@link Id}.
|
|
579
719
|
*
|
|
@@ -587,13 +727,12 @@ export const idTypeValueLength = 21;
|
|
|
587
727
|
* const todoId = createId<"Todo">(deps);
|
|
588
728
|
* ```
|
|
589
729
|
*/
|
|
590
|
-
export const createId = (deps) => deps.
|
|
730
|
+
export const createId = (deps) => uint8ArrayToBase64Url(deps.randomBytes.create(16));
|
|
591
731
|
/**
|
|
592
732
|
* Creates an {@link Id} from a string using SHA-256.
|
|
593
733
|
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
* function to convert external IDs into valid Evolu IDs.
|
|
734
|
+
* When integrating with external systems that use different ID formats, use
|
|
735
|
+
* this function to convert external IDs into valid Evolu IDs.
|
|
597
736
|
*
|
|
598
737
|
* In Evolu's CRDT, the ID serves as the unique identifier for conflict
|
|
599
738
|
* resolution across distributed clients. When multiple clients create records
|
|
@@ -614,30 +753,22 @@ export const createId = (deps) => deps.nanoIdLib.nanoid();
|
|
|
614
753
|
* });
|
|
615
754
|
* ```
|
|
616
755
|
*
|
|
617
|
-
* **Important**: This transformation is one-way.
|
|
618
|
-
*
|
|
619
|
-
*
|
|
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.
|
|
620
759
|
*
|
|
621
760
|
* @category String
|
|
622
761
|
*/
|
|
623
762
|
export const createIdFromString = (value) => {
|
|
624
763
|
const hash = sha256(utf8ToBytes(value));
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
for (const byte of hash) {
|
|
629
|
-
buffer = (buffer << 8) | byte;
|
|
630
|
-
bits += 8;
|
|
631
|
-
while (bits >= 6 && output.length < idTypeValueLength) {
|
|
632
|
-
bits -= 6;
|
|
633
|
-
const index = (buffer >> bits) & 0b111111;
|
|
634
|
-
output += base64UrlAlphabet[index];
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
return output;
|
|
764
|
+
// Take first 16 bytes of hash and convert to Id
|
|
765
|
+
const id = idBytesToId(hash.slice(0, 16));
|
|
766
|
+
return id;
|
|
638
767
|
};
|
|
639
768
|
/**
|
|
640
|
-
*
|
|
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.
|
|
641
772
|
*
|
|
642
773
|
* ### Example
|
|
643
774
|
*
|
|
@@ -650,30 +781,34 @@ export const createIdFromString = (value) => {
|
|
|
650
781
|
* @category String
|
|
651
782
|
*/
|
|
652
783
|
export const id = (table) => {
|
|
653
|
-
const fromParent = (value) => {
|
|
654
|
-
const idResult = Id.fromParent(value);
|
|
655
|
-
if (!idResult.ok) {
|
|
656
|
-
return err({ type: "Id", value, table });
|
|
657
|
-
}
|
|
658
|
-
return ok(idResult.value);
|
|
659
|
-
};
|
|
660
784
|
const fromUnknown = (value) => {
|
|
661
785
|
const parentResult = String.fromUnknown(value);
|
|
662
786
|
if (!parentResult.ok)
|
|
663
787
|
return parentResult;
|
|
664
788
|
return fromParent(parentResult.value);
|
|
665
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
|
+
};
|
|
666
797
|
return {
|
|
667
|
-
...createType("Id", {
|
|
668
|
-
fromUnknown,
|
|
669
|
-
to: (value) => value,
|
|
670
|
-
fromParent,
|
|
671
|
-
toParent: (value) => value,
|
|
672
|
-
}),
|
|
798
|
+
...createType("Id", { fromUnknown, fromParent }),
|
|
673
799
|
table,
|
|
674
800
|
};
|
|
675
801
|
};
|
|
676
|
-
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);
|
|
677
812
|
/**
|
|
678
813
|
* Positive number.
|
|
679
814
|
*
|
|
@@ -766,6 +901,8 @@ export const Int = int(Number);
|
|
|
766
901
|
export const NonNegativeInt = nonNegative(Int);
|
|
767
902
|
/** @category Number */
|
|
768
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);
|
|
769
906
|
/** @category Number */
|
|
770
907
|
export const NonPositiveInt = nonPositive(Int);
|
|
771
908
|
/** @category Number */
|
|
@@ -860,8 +997,6 @@ export const between = (min, max) => (parent) => brand(`Between${min}-${max}`, p
|
|
|
860
997
|
? ok(value)
|
|
861
998
|
: err({ type: "Between", value, min, max }));
|
|
862
999
|
export const formatBetweenError = createTypeErrorFormatter((error) => `The value ${error.value} is not between ${error.min} and ${error.max}, inclusive.`);
|
|
863
|
-
/** @category Number */
|
|
864
|
-
export const Between1And10 = between(1, 10)(Number);
|
|
865
1000
|
/**
|
|
866
1001
|
* Literal {@link Type}.
|
|
867
1002
|
*
|
|
@@ -886,94 +1021,12 @@ export const literal = (expected) => {
|
|
|
886
1021
|
return {
|
|
887
1022
|
...createType("Literal", {
|
|
888
1023
|
fromUnknown,
|
|
889
|
-
to: identity,
|
|
890
1024
|
fromParent: ok,
|
|
891
|
-
toParent: identity,
|
|
892
1025
|
}),
|
|
893
1026
|
expected,
|
|
894
1027
|
};
|
|
895
1028
|
};
|
|
896
1029
|
export const formatLiteralError = createTypeErrorFormatter((error) => `The value ${error.value} is not strictly equal to the expected literal: ${globalThis.String(error.expected)}.`);
|
|
897
|
-
/**
|
|
898
|
-
* {@link Type} that transforms values between `FromType` and `ToType`.
|
|
899
|
-
*
|
|
900
|
-
* - `fromParent`: Converts `FromType` to `ToType`, may fail.
|
|
901
|
-
* - `toParent`: Converts `ToType` back to `FromType`, must not fail.
|
|
902
|
-
*
|
|
903
|
-
* ### Example
|
|
904
|
-
*
|
|
905
|
-
* // TODO: Examples
|
|
906
|
-
*
|
|
907
|
-
* @category Base Factories
|
|
908
|
-
*/
|
|
909
|
-
export const transform = (fromType, toType, fromParent, toParent) => {
|
|
910
|
-
const fromUnknown = (value) => {
|
|
911
|
-
const parentResult = fromType.fromUnknown(value);
|
|
912
|
-
if (!parentResult.ok)
|
|
913
|
-
return parentResult;
|
|
914
|
-
return fromParent(parentResult.value);
|
|
915
|
-
};
|
|
916
|
-
const to = (value) => fromType.to(toParent(value));
|
|
917
|
-
return {
|
|
918
|
-
...createType("Transform", {
|
|
919
|
-
fromUnknown,
|
|
920
|
-
to,
|
|
921
|
-
fromParent,
|
|
922
|
-
toParent,
|
|
923
|
-
}),
|
|
924
|
-
is: toType.is,
|
|
925
|
-
fromType,
|
|
926
|
-
toType,
|
|
927
|
-
};
|
|
928
|
-
};
|
|
929
|
-
/**
|
|
930
|
-
* Trims leading and trailing whitespace from a string.
|
|
931
|
-
*
|
|
932
|
-
* ### Example
|
|
933
|
-
*
|
|
934
|
-
* ```ts
|
|
935
|
-
* expect(TrimString.from("a ")).toEqual(ok("a"));
|
|
936
|
-
* expect(TrimString.fromParent("a ").value).toEqual("a");
|
|
937
|
-
* ```
|
|
938
|
-
*
|
|
939
|
-
* @category String
|
|
940
|
-
*/
|
|
941
|
-
export const TrimString = trim(String);
|
|
942
|
-
/**
|
|
943
|
-
* Transforms a {@link Date} into a {@link DateIsoString} string and vice versa.
|
|
944
|
-
*
|
|
945
|
-
* ### Example
|
|
946
|
-
*
|
|
947
|
-
* ```ts
|
|
948
|
-
* DateIso.from(new Date("2023-12-25T10:30:00.000Z")); // ok("2023-12-25T10:30:00.000Z")
|
|
949
|
-
* DateIso.to("2023-12-25T10:30:00.000Z"); // Date object
|
|
950
|
-
* DateIso.from(new Date("invalid")); // err({ type: "DateIsoString", value: "Invalid Date" })
|
|
951
|
-
* ```
|
|
952
|
-
*
|
|
953
|
-
* @category String
|
|
954
|
-
*/
|
|
955
|
-
export const DateIso = transform(Date, DateIsoString, (value) => DateIsoString.fromParent(value.toISOString()), (value) => new globalThis.Date(value));
|
|
956
|
-
/**
|
|
957
|
-
* Transforms a {@link NonEmptyTrimmedString} into a {@link FiniteNumber}.
|
|
958
|
-
*
|
|
959
|
-
* ### Example
|
|
960
|
-
*
|
|
961
|
-
* ```ts
|
|
962
|
-
* NumberFromString.from("42"); // ok(42)
|
|
963
|
-
* NumberFromString.from("abc"); // err({ type: "NumberFromString", value: "abc" })
|
|
964
|
-
* ```
|
|
965
|
-
*
|
|
966
|
-
* @category Number
|
|
967
|
-
*/
|
|
968
|
-
export const NumberFromString = transform(NonEmptyTrimmedString, FiniteNumber, (value) => {
|
|
969
|
-
const result = FiniteNumber.fromParent(globalThis.Number(value));
|
|
970
|
-
if (!result.ok)
|
|
971
|
-
return err({ type: "NumberFromString", value });
|
|
972
|
-
return result;
|
|
973
|
-
}, (num) => num.toString());
|
|
974
|
-
export const formatNumberFromStringError = createTypeErrorFormatter((error) => {
|
|
975
|
-
return `The value ${error.value} could not be converted to a finite number.`;
|
|
976
|
-
});
|
|
977
1030
|
/**
|
|
978
1031
|
* Array of a specific {@link Type}.
|
|
979
1032
|
*
|
|
@@ -1016,7 +1069,6 @@ export const array = (element) => {
|
|
|
1016
1069
|
}
|
|
1017
1070
|
return ok(result);
|
|
1018
1071
|
};
|
|
1019
|
-
const to = (value) => value.map(element.to);
|
|
1020
1072
|
const fromParent = (value) => {
|
|
1021
1073
|
const result = [];
|
|
1022
1074
|
for (let i = 0; i < value.length; i++) {
|
|
@@ -1036,14 +1088,8 @@ export const array = (element) => {
|
|
|
1036
1088
|
}
|
|
1037
1089
|
return ok(result);
|
|
1038
1090
|
};
|
|
1039
|
-
const toParent = (values) => values.map(element.toParent);
|
|
1040
1091
|
return {
|
|
1041
|
-
...createType("Array", {
|
|
1042
|
-
fromUnknown,
|
|
1043
|
-
to,
|
|
1044
|
-
fromParent,
|
|
1045
|
-
toParent,
|
|
1046
|
-
}),
|
|
1092
|
+
...createType("Array", { fromUnknown, fromParent }),
|
|
1047
1093
|
element,
|
|
1048
1094
|
};
|
|
1049
1095
|
};
|
|
@@ -1117,10 +1163,6 @@ export const record = (keyType, valueType) => {
|
|
|
1117
1163
|
}
|
|
1118
1164
|
return ok(result);
|
|
1119
1165
|
};
|
|
1120
|
-
const to = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
|
|
1121
|
-
keyType.to(key),
|
|
1122
|
-
valueType.to(val),
|
|
1123
|
-
]));
|
|
1124
1166
|
const fromParent = (value) => {
|
|
1125
1167
|
const result = {};
|
|
1126
1168
|
for (const [rawKey, rawValue] of Object.entries(value)) {
|
|
@@ -1152,16 +1194,10 @@ export const record = (keyType, valueType) => {
|
|
|
1152
1194
|
}
|
|
1153
1195
|
return ok(result);
|
|
1154
1196
|
};
|
|
1155
|
-
const toParent = (value) => Object.fromEntries(Object.entries(value).map(([key, val]) => [
|
|
1156
|
-
keyType.toParent(key),
|
|
1157
|
-
valueType.toParent(val),
|
|
1158
|
-
]));
|
|
1159
1197
|
return {
|
|
1160
1198
|
...createType("Record", {
|
|
1161
1199
|
fromUnknown,
|
|
1162
|
-
to,
|
|
1163
1200
|
fromParent,
|
|
1164
|
-
toParent,
|
|
1165
1201
|
}),
|
|
1166
1202
|
key: keyType,
|
|
1167
1203
|
value: valueType,
|
|
@@ -1248,22 +1284,6 @@ export function object(props, record) {
|
|
|
1248
1284
|
}
|
|
1249
1285
|
return ok(result);
|
|
1250
1286
|
};
|
|
1251
|
-
const to = (value) => {
|
|
1252
|
-
const entries = [];
|
|
1253
|
-
for (const key of propKeys) {
|
|
1254
|
-
if (!(key in value) && isOptionalType(props[key])) {
|
|
1255
|
-
continue;
|
|
1256
|
-
}
|
|
1257
|
-
entries.push([key, props[key].to(value[key])]);
|
|
1258
|
-
}
|
|
1259
|
-
if (record) {
|
|
1260
|
-
const recordEntries = Object.entries(value).filter(([key]) => !propKeys.includes(key));
|
|
1261
|
-
for (const [key, val] of recordEntries) {
|
|
1262
|
-
entries.push([record.key.to(key), record.value.to(val)]);
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
return Object.fromEntries(entries);
|
|
1266
|
-
};
|
|
1267
1287
|
const fromParent = (value) => {
|
|
1268
1288
|
const errors = {};
|
|
1269
1289
|
const result = {};
|
|
@@ -1311,28 +1331,10 @@ export function object(props, record) {
|
|
|
1311
1331
|
}
|
|
1312
1332
|
return ok(result);
|
|
1313
1333
|
};
|
|
1314
|
-
const toParent = (value) => {
|
|
1315
|
-
const entries = [];
|
|
1316
|
-
for (const key of propKeys) {
|
|
1317
|
-
if (!(key in value) && isOptionalType(props[key])) {
|
|
1318
|
-
continue;
|
|
1319
|
-
}
|
|
1320
|
-
entries.push([key, props[key].toParent(value[key])]);
|
|
1321
|
-
}
|
|
1322
|
-
if (record) {
|
|
1323
|
-
const recordEntries = Object.entries(value).filter(([key]) => !propKeys.includes(key));
|
|
1324
|
-
for (const [key, val] of recordEntries) {
|
|
1325
|
-
entries.push([record.key.toParent(key), record.value.toParent(val)]);
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
return Object.fromEntries(entries);
|
|
1329
|
-
};
|
|
1330
1334
|
return {
|
|
1331
1335
|
...createType(record ? "ObjectWithRecord" : "Object", {
|
|
1332
1336
|
fromUnknown,
|
|
1333
|
-
to,
|
|
1334
1337
|
fromParent,
|
|
1335
|
-
toParent,
|
|
1336
1338
|
}),
|
|
1337
1339
|
props,
|
|
1338
1340
|
...(record ? { record } : {}),
|
|
@@ -1385,7 +1387,6 @@ export function union(...args) {
|
|
|
1385
1387
|
* - Generate a specialized function to improve validation performance for such
|
|
1386
1388
|
* cases.
|
|
1387
1389
|
*/
|
|
1388
|
-
/* eslint-disable */
|
|
1389
1390
|
const members = args.map((arg) => (isType(arg) ? arg : literal(arg)));
|
|
1390
1391
|
const fromUnknown = (value) => {
|
|
1391
1392
|
const errors = [];
|
|
@@ -1401,23 +1402,13 @@ export function union(...args) {
|
|
|
1401
1402
|
errors,
|
|
1402
1403
|
});
|
|
1403
1404
|
};
|
|
1404
|
-
const to = (value) => {
|
|
1405
|
-
for (const member of members) {
|
|
1406
|
-
if (member.is(value))
|
|
1407
|
-
return member.to(value);
|
|
1408
|
-
}
|
|
1409
|
-
assert(false, "No matching member found in Union Type `to` function");
|
|
1410
|
-
};
|
|
1411
1405
|
return {
|
|
1412
1406
|
...createType("Union", {
|
|
1413
1407
|
fromUnknown,
|
|
1414
|
-
to,
|
|
1415
1408
|
fromParent: fromUnknown,
|
|
1416
|
-
toParent: to,
|
|
1417
1409
|
}),
|
|
1418
1410
|
members,
|
|
1419
1411
|
};
|
|
1420
|
-
/* eslint-enable */
|
|
1421
1412
|
}
|
|
1422
1413
|
export const formatUnionError = (formatTypeError) => createTypeErrorFormatter((error) => {
|
|
1423
1414
|
const formattedErrors = error.errors
|
|
@@ -1473,20 +1464,10 @@ export const recursive = (create) => {
|
|
|
1473
1464
|
type ??= create();
|
|
1474
1465
|
return type.fromUnknown(value);
|
|
1475
1466
|
},
|
|
1476
|
-
to: (value) => {
|
|
1477
|
-
type ??= create();
|
|
1478
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
1479
|
-
return type.to(value);
|
|
1480
|
-
},
|
|
1481
1467
|
fromParent: (value) => {
|
|
1482
1468
|
type ??= create();
|
|
1483
1469
|
return type.fromParent(value);
|
|
1484
1470
|
},
|
|
1485
|
-
toParent: (value) => {
|
|
1486
|
-
type ??= create();
|
|
1487
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
1488
|
-
return type.toParent(value);
|
|
1489
|
-
},
|
|
1490
1471
|
is: (value) => {
|
|
1491
1472
|
type ??= create();
|
|
1492
1473
|
return type.is(value);
|
|
@@ -1590,7 +1571,6 @@ export const tuple = (...elements) => {
|
|
|
1590
1571
|
}
|
|
1591
1572
|
return ok(result);
|
|
1592
1573
|
};
|
|
1593
|
-
const to = (value) => value.map((val, index) => elements[index].to(val));
|
|
1594
1574
|
const fromParent = (value) => {
|
|
1595
1575
|
if (!Array.isArray(value) || value.length !== elements.length) {
|
|
1596
1576
|
return err({
|
|
@@ -1617,13 +1597,10 @@ export const tuple = (...elements) => {
|
|
|
1617
1597
|
}
|
|
1618
1598
|
return ok(result);
|
|
1619
1599
|
};
|
|
1620
|
-
const toParent = (value) => value.map((val, index) => elements[index].toParent(val));
|
|
1621
1600
|
return {
|
|
1622
1601
|
...createType("Tuple", {
|
|
1623
1602
|
fromUnknown,
|
|
1624
|
-
to,
|
|
1625
1603
|
fromParent,
|
|
1626
|
-
toParent,
|
|
1627
1604
|
}),
|
|
1628
1605
|
elements,
|
|
1629
1606
|
};
|
|
@@ -1657,22 +1634,32 @@ export const Int64 = brand("Int64", BigInt, (value) => value >= -922337203685477
|
|
|
1657
1634
|
? ok(value)
|
|
1658
1635
|
: err({ type: "Int64", value }));
|
|
1659
1636
|
export const formatInt64Error = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid 64-bit signed integer (Int64).`);
|
|
1660
|
-
|
|
1661
|
-
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
|
+
// );
|
|
1662
1653
|
/**
|
|
1663
1654
|
* Stringified {@link Int64}.
|
|
1664
1655
|
*
|
|
1665
|
-
* @category
|
|
1656
|
+
* @category String
|
|
1666
1657
|
*/
|
|
1667
|
-
export const Int64String = brand("Int64",
|
|
1668
|
-
const
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
return ok(value);
|
|
1673
|
-
}
|
|
1674
|
-
return err({ type: "Int64String", value });
|
|
1675
|
-
});
|
|
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 })));
|
|
1676
1663
|
export const formatInt64StringError = createTypeErrorFormatter((error) => `The value ${error.value} is not a valid Int64 string.`);
|
|
1677
1664
|
/**
|
|
1678
1665
|
* JSON-compatible value: string, {@link FiniteNumber}, boolean, null,
|
|
@@ -1693,48 +1680,83 @@ export const JsonArray = array(JsonValue);
|
|
|
1693
1680
|
* @category Object
|
|
1694
1681
|
*/
|
|
1695
1682
|
export const JsonObject = record(String, JsonValue);
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
* JsonValue back into a JSON string.
|
|
1699
|
-
*
|
|
1700
|
-
* ### Example
|
|
1701
|
-
*
|
|
1702
|
-
* ```ts
|
|
1703
|
-
* JsonValueFromString.from(`{"key":"value"}`); // -> ok({ key: "value" })
|
|
1704
|
-
* JsonValueFromString.to({ key: "value" }); // -> '{"key":"value"}'
|
|
1705
|
-
* ```
|
|
1706
|
-
*
|
|
1707
|
-
* @category String
|
|
1708
|
-
*/
|
|
1709
|
-
export const JsonValueFromString = transform(String, JsonValue, (value) => trySync(() => JSON.parse(value), (error) => ({
|
|
1710
|
-
type: `JsonValueFromString`,
|
|
1683
|
+
export const parseJson = (value) => trySync(() => JSON.parse(value), (error) => ({
|
|
1684
|
+
type: `Json`,
|
|
1711
1685
|
value,
|
|
1712
1686
|
message: globalThis.String(error),
|
|
1713
|
-
}))
|
|
1714
|
-
export const formatJsonValueFromStringError = createTypeErrorFormatter((error) => `Invalid JSONValue: ${error.value}. Error: ${error.message}`);
|
|
1687
|
+
}));
|
|
1715
1688
|
/**
|
|
1716
1689
|
* JSON-string {@link Type}.
|
|
1717
1690
|
*
|
|
1718
1691
|
* ### Example
|
|
1719
1692
|
*
|
|
1720
1693
|
* ```ts
|
|
1721
|
-
* const result = Json.from('{"key":"value"}'); //
|
|
1722
|
-
* const error = Json.from("invalid json"); //
|
|
1694
|
+
* const result = Json.from('{"key":"value"}'); // ok
|
|
1695
|
+
* const error = Json.from("invalid json"); // err
|
|
1723
1696
|
* ```
|
|
1724
1697
|
*
|
|
1725
1698
|
* @category String
|
|
1726
1699
|
*/
|
|
1727
1700
|
export const Json = brand("Json", String, (value) => {
|
|
1728
|
-
const result =
|
|
1701
|
+
const result = parseJson(value);
|
|
1729
1702
|
if (!result.ok)
|
|
1730
|
-
return
|
|
1731
|
-
type: "Json",
|
|
1732
|
-
value,
|
|
1733
|
-
message: result.error.message,
|
|
1734
|
-
});
|
|
1703
|
+
return result;
|
|
1735
1704
|
return ok(value);
|
|
1736
1705
|
});
|
|
1737
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
|
+
};
|
|
1738
1760
|
/**
|
|
1739
1761
|
* Optional {@link Type}.
|
|
1740
1762
|
*
|
|
@@ -1759,9 +1781,7 @@ export const formatJsonError = createTypeErrorFormatter((error) => `Invalid JSON
|
|
|
1759
1781
|
export const optional = (type) => ({
|
|
1760
1782
|
...createType("Optional", {
|
|
1761
1783
|
fromUnknown: type.fromUnknown,
|
|
1762
|
-
to: type.to,
|
|
1763
1784
|
fromParent: type.fromParent,
|
|
1764
|
-
toParent: type.toParent,
|
|
1765
1785
|
}),
|
|
1766
1786
|
parent: type,
|
|
1767
1787
|
});
|
|
@@ -1770,7 +1790,7 @@ export const isOptionalType = (x) => typeof x === "object" && x != null && "name
|
|
|
1770
1790
|
/**
|
|
1771
1791
|
* Creates a partial object type where all properties are optional.
|
|
1772
1792
|
*
|
|
1773
|
-
* This is useful when
|
|
1793
|
+
* This is useful when we want to validate an object in which none of the keys
|
|
1774
1794
|
* are required, but if they are present they must conform to their
|
|
1775
1795
|
* corresponding Types.
|
|
1776
1796
|
*
|
|
@@ -1841,114 +1861,86 @@ export function omit(objectType, ...keys) {
|
|
|
1841
1861
|
}
|
|
1842
1862
|
return object(newProps);
|
|
1843
1863
|
}
|
|
1864
|
+
export const maxMutationSize = 655360;
|
|
1844
1865
|
/**
|
|
1845
|
-
*
|
|
1846
|
-
*
|
|
1847
|
-
*
|
|
1848
|
-
*
|
|
1849
|
-
* ### Example
|
|
1850
|
-
*
|
|
1851
|
-
* ```ts
|
|
1852
|
-
* const Person = object({
|
|
1853
|
-
* name: NonEmptyString50,
|
|
1854
|
-
* age: FiniteNumber,
|
|
1855
|
-
* });
|
|
1856
|
-
* type Person = typeof Person.Type;
|
|
1857
|
-
*
|
|
1858
|
-
* const PersonJson = json(Person, "PersonJson");
|
|
1859
|
-
* // string & Brand<"PersonJson">
|
|
1860
|
-
* type PersonJson = typeof PersonJson.Type;
|
|
1861
|
-
*
|
|
1862
|
-
* // Person -> string & Brand<"PersonJson">
|
|
1863
|
-
* const personJson = PersonJson.from({ name: "Alice", age: 30 });
|
|
1864
|
-
* expect(personJson).toEqual(ok('{"name":"Alice","age":30}'));
|
|
1865
|
-
*
|
|
1866
|
-
* // string & Brand<"PersonJson"> -> Person
|
|
1867
|
-
* const person = PersonJson.to(personJson);
|
|
1868
|
-
*
|
|
1869
|
-
* // serialize/parse any JSON value
|
|
1870
|
-
* const AnyJson = json(JsonValue, "AnyJson");
|
|
1871
|
-
* ```
|
|
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.
|
|
1872
1870
|
*/
|
|
1873
|
-
export const
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
return jsonValue;
|
|
1878
|
-
const parsed = type.fromUnknown(jsonValue.value);
|
|
1879
|
-
if (!parsed.ok)
|
|
1880
|
-
return parsed;
|
|
1881
|
-
return ok(value);
|
|
1882
|
-
});
|
|
1883
|
-
return transform(type, BrandedJsonString, (value) => ok(JSON.stringify(value)),
|
|
1884
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
1885
|
-
(value) => JSON.parse(value));
|
|
1886
|
-
};
|
|
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.`);
|
|
1887
1875
|
/**
|
|
1888
|
-
*
|
|
1889
|
-
*
|
|
1890
|
-
*
|
|
1876
|
+
* Formats Evolu Type errors into user-friendly messages.
|
|
1877
|
+
*
|
|
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.
|
|
1891
1881
|
*
|
|
1892
|
-
*
|
|
1893
|
-
*
|
|
1882
|
+
* The `createFormatTypeError` function handles both built-in {@link TypeErrors}
|
|
1883
|
+
* and custom errors, and lets us override default formatting for specific
|
|
1884
|
+
* errors.
|
|
1894
1885
|
*
|
|
1895
|
-
* ###
|
|
1886
|
+
* ### Example
|
|
1896
1887
|
*
|
|
1897
1888
|
* ```ts
|
|
1898
|
-
* const
|
|
1899
|
-
*
|
|
1900
|
-
*
|
|
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
|
+
* });
|
|
1901
1899
|
* ```
|
|
1902
1900
|
*
|
|
1903
|
-
*
|
|
1901
|
+
* Alternatively, write a custom formatter from scratch without using
|
|
1902
|
+
* `createFormatTypeError`. This gives us full control over error formatting:
|
|
1904
1903
|
*
|
|
1905
1904
|
* ```ts
|
|
1906
|
-
*
|
|
1907
|
-
*
|
|
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 =
|
|
1908
1912
|
* | StringError
|
|
1909
|
-
* | MinLengthError
|
|
1910
1913
|
* | MaxLengthError
|
|
1911
|
-
* |
|
|
1912
|
-
* | IdError
|
|
1914
|
+
* | MinLengthError
|
|
1913
1915
|
* | TrimmedError
|
|
1914
|
-
* |
|
|
1915
|
-
* |
|
|
1916
|
-
*
|
|
1917
|
-
* |
|
|
1918
|
-
* |
|
|
1919
|
-
*
|
|
1920
|
-
* const formatTypeError: TypeErrorFormatter<
|
|
1921
|
-
* // In the real code, we would use the createTypeErrorFormatter helper
|
|
1922
|
-
* // 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) => {
|
|
1923
1923
|
* switch (error.type) {
|
|
1924
|
-
* case "Id":
|
|
1925
|
-
* return `Invalid Id on table: ${error.table}.`;
|
|
1926
|
-
* case "MaxLength":
|
|
1927
|
-
* return `Max length is ${error.max}.`;
|
|
1928
|
-
* case "MinLength":
|
|
1929
|
-
* return `Min length is ${error.min}.`;
|
|
1930
|
-
* case "Mnemonic":
|
|
1931
|
-
* return `Invalid mnemonic: ${String(error.value)}`;
|
|
1932
|
-
* case "Null":
|
|
1933
|
-
* return `Not null`;
|
|
1934
1924
|
* case "String":
|
|
1935
|
-
* // We can reuse existing formatter.
|
|
1936
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`;
|
|
1937
1932
|
* case "Trimmed":
|
|
1938
|
-
* return "
|
|
1939
|
-
* case "
|
|
1940
|
-
* return "
|
|
1941
|
-
* case "
|
|
1942
|
-
* return
|
|
1943
|
-
*
|
|
1944
|
-
*
|
|
1945
|
-
* 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";
|
|
1946
1940
|
* case "Object": {
|
|
1947
|
-
* if (
|
|
1948
|
-
*
|
|
1949
|
-
*
|
|
1950
|
-
* )
|
|
1951
|
-
* 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";
|
|
1952
1944
|
* const firstError = Object.values(error.reason.errors).find(
|
|
1953
1945
|
* (e) => e !== undefined,
|
|
1954
1946
|
* )!;
|
|
@@ -1989,8 +1981,8 @@ export const createFormatTypeError = (extraFormatter) => {
|
|
|
1989
1981
|
return formatIsTypeError(error);
|
|
1990
1982
|
case "CurrencyCode":
|
|
1991
1983
|
return formatCurrencyCodeError(error);
|
|
1992
|
-
case "
|
|
1993
|
-
return
|
|
1984
|
+
case "DateIso":
|
|
1985
|
+
return formatDateIsoError(error);
|
|
1994
1986
|
case "Trimmed":
|
|
1995
1987
|
return formatTrimmedError(error);
|
|
1996
1988
|
case "MinLength":
|
|
@@ -2005,6 +1997,8 @@ export const createFormatTypeError = (extraFormatter) => {
|
|
|
2005
1997
|
return formatRegexError(error);
|
|
2006
1998
|
case "Id":
|
|
2007
1999
|
return formatIdError(error);
|
|
2000
|
+
case "TableId":
|
|
2001
|
+
return formatTableIdError(error);
|
|
2008
2002
|
case "Positive":
|
|
2009
2003
|
return formatPositiveError(error);
|
|
2010
2004
|
case "Negative":
|
|
@@ -2035,14 +2029,12 @@ export const createFormatTypeError = (extraFormatter) => {
|
|
|
2035
2029
|
return formatLiteralError(error);
|
|
2036
2030
|
case "Int64":
|
|
2037
2031
|
return formatInt64Error(error);
|
|
2038
|
-
case "BigIntFromString":
|
|
2039
|
-
return formatBigIntFromStringError(error);
|
|
2040
2032
|
case "Int64String":
|
|
2041
2033
|
return formatInt64StringError(error);
|
|
2042
|
-
case "JsonValueFromString":
|
|
2043
|
-
return formatJsonValueFromStringError(error);
|
|
2044
2034
|
case "Json":
|
|
2045
2035
|
return formatJsonError(error);
|
|
2036
|
+
case "ValidMutationSize":
|
|
2037
|
+
return formatValidMutationSizeError(error);
|
|
2046
2038
|
// Composite Types
|
|
2047
2039
|
case "SimplePassword":
|
|
2048
2040
|
return formatSimplePasswordError(formatTypeError)(error);
|