@evolu/common 8.2.0 → 8.3.1
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/Function.d.ts +1 -0
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Http.d.ts +9 -0
- package/dist/src/Http.d.ts.map +1 -1
- package/dist/src/Object.d.ts +1 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/RefCount.d.ts +1 -0
- package/dist/src/RefCount.d.ts.map +1 -1
- package/dist/src/Relation.d.ts +2 -0
- package/dist/src/Relation.d.ts.map +1 -1
- package/dist/src/Resource.d.ts +1 -0
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Result.d.ts +383 -548
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +296 -46
- package/dist/src/StackTrace.d.ts +1 -0
- package/dist/src/StackTrace.d.ts.map +1 -1
- package/dist/src/Task.d.ts +985 -1069
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1007 -156
- package/dist/src/Time.d.ts +1 -0
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Type.d.ts +1322 -793
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +1192 -244
- package/dist/src/intl/_en.d.ts +126 -0
- package/dist/src/intl/_en.d.ts.map +1 -0
- package/dist/src/intl/_en.js +267 -0
- package/dist/src/intl/ar.d.ts +117 -0
- package/dist/src/intl/ar.d.ts.map +1 -0
- package/dist/src/intl/ar.js +245 -0
- package/dist/src/intl/bn.d.ts +117 -0
- package/dist/src/intl/bn.d.ts.map +1 -0
- package/dist/src/intl/bn.js +258 -0
- package/dist/src/intl/ca.d.ts +117 -0
- package/dist/src/intl/ca.d.ts.map +1 -0
- package/dist/src/intl/ca.js +258 -0
- package/dist/src/intl/cs.d.ts +20 -7
- package/dist/src/intl/cs.d.ts.map +1 -1
- package/dist/src/intl/cs.js +54 -27
- package/dist/src/intl/da.d.ts +117 -0
- package/dist/src/intl/da.d.ts.map +1 -0
- package/dist/src/intl/da.js +258 -0
- package/dist/src/intl/de.d.ts +117 -0
- package/dist/src/intl/de.d.ts.map +1 -0
- package/dist/src/intl/de.js +258 -0
- package/dist/src/intl/el.d.ts +117 -0
- package/dist/src/intl/el.d.ts.map +1 -0
- package/dist/src/intl/el.js +265 -0
- package/dist/src/intl/es.d.ts +117 -0
- package/dist/src/intl/es.d.ts.map +1 -0
- package/dist/src/intl/es.js +258 -0
- package/dist/src/intl/fa.d.ts +117 -0
- package/dist/src/intl/fa.d.ts.map +1 -0
- package/dist/src/intl/fa.js +258 -0
- package/dist/src/intl/fi.d.ts +117 -0
- package/dist/src/intl/fi.d.ts.map +1 -0
- package/dist/src/intl/fi.js +258 -0
- package/dist/src/intl/fil.d.ts +117 -0
- package/dist/src/intl/fil.d.ts.map +1 -0
- package/dist/src/intl/fil.js +258 -0
- package/dist/src/intl/fr.d.ts +117 -0
- package/dist/src/intl/fr.d.ts.map +1 -0
- package/dist/src/intl/fr.js +258 -0
- package/dist/src/intl/he.d.ts +117 -0
- package/dist/src/intl/he.d.ts.map +1 -0
- package/dist/src/intl/he.js +258 -0
- package/dist/src/intl/hi.d.ts +117 -0
- package/dist/src/intl/hi.d.ts.map +1 -0
- package/dist/src/intl/hi.js +258 -0
- package/dist/src/intl/hr.d.ts +117 -0
- package/dist/src/intl/hr.d.ts.map +1 -0
- package/dist/src/intl/hr.js +265 -0
- package/dist/src/intl/hu.d.ts +63 -0
- package/dist/src/intl/hu.d.ts.map +1 -0
- package/dist/src/intl/hu.js +204 -0
- package/dist/src/intl/id.d.ts +117 -0
- package/dist/src/intl/id.d.ts.map +1 -0
- package/dist/src/intl/id.js +258 -0
- package/dist/src/intl/index.d.ts +42 -0
- package/dist/src/intl/index.d.ts.map +1 -1
- package/dist/src/intl/index.js +42 -0
- package/dist/src/intl/it.d.ts +117 -0
- package/dist/src/intl/it.d.ts.map +1 -0
- package/dist/src/intl/it.js +258 -0
- package/dist/src/intl/ja.d.ts +117 -0
- package/dist/src/intl/ja.d.ts.map +1 -0
- package/dist/src/intl/ja.js +258 -0
- package/dist/src/intl/ko.d.ts +117 -0
- package/dist/src/intl/ko.d.ts.map +1 -0
- package/dist/src/intl/ko.js +258 -0
- package/dist/src/intl/ml.d.ts +117 -0
- package/dist/src/intl/ml.d.ts.map +1 -0
- package/dist/src/intl/ml.js +258 -0
- package/dist/src/intl/mr.d.ts +117 -0
- package/dist/src/intl/mr.d.ts.map +1 -0
- package/dist/src/intl/mr.js +258 -0
- package/dist/src/intl/ms.d.ts +117 -0
- package/dist/src/intl/ms.d.ts.map +1 -0
- package/dist/src/intl/ms.js +245 -0
- package/dist/src/intl/nb.d.ts +63 -0
- package/dist/src/intl/nb.d.ts.map +1 -0
- package/dist/src/intl/nb.js +191 -0
- package/dist/src/intl/nl.d.ts +117 -0
- package/dist/src/intl/nl.d.ts.map +1 -0
- package/dist/src/intl/nl.js +258 -0
- package/dist/src/intl/pa.d.ts +117 -0
- package/dist/src/intl/pa.d.ts.map +1 -0
- package/dist/src/intl/pa.js +258 -0
- package/dist/src/intl/pl.d.ts +117 -0
- package/dist/src/intl/pl.d.ts.map +1 -0
- package/dist/src/intl/pl.js +242 -0
- package/dist/src/intl/pt-BR.d.ts +117 -0
- package/dist/src/intl/pt-BR.d.ts.map +1 -0
- package/dist/src/intl/pt-BR.js +258 -0
- package/dist/src/intl/pt.d.ts +117 -0
- package/dist/src/intl/pt.d.ts.map +1 -0
- package/dist/src/intl/pt.js +246 -0
- package/dist/src/intl/ro.d.ts +117 -0
- package/dist/src/intl/ro.d.ts.map +1 -0
- package/dist/src/intl/ro.js +258 -0
- package/dist/src/intl/sk.d.ts +117 -0
- package/dist/src/intl/sk.d.ts.map +1 -0
- package/dist/src/intl/sk.js +244 -0
- package/dist/src/intl/sl.d.ts +117 -0
- package/dist/src/intl/sl.d.ts.map +1 -0
- package/dist/src/intl/sl.js +258 -0
- package/dist/src/intl/sv.d.ts +117 -0
- package/dist/src/intl/sv.d.ts.map +1 -0
- package/dist/src/intl/sv.js +258 -0
- package/dist/src/intl/sw.d.ts +63 -0
- package/dist/src/intl/sw.d.ts.map +1 -0
- package/dist/src/intl/sw.js +191 -0
- package/dist/src/intl/ta.d.ts +117 -0
- package/dist/src/intl/ta.d.ts.map +1 -0
- package/dist/src/intl/ta.js +258 -0
- package/dist/src/intl/te.d.ts +117 -0
- package/dist/src/intl/te.d.ts.map +1 -0
- package/dist/src/intl/te.js +258 -0
- package/dist/src/intl/th.d.ts +117 -0
- package/dist/src/intl/th.d.ts.map +1 -0
- package/dist/src/intl/th.js +258 -0
- package/dist/src/intl/tr.d.ts +117 -0
- package/dist/src/intl/tr.d.ts.map +1 -0
- package/dist/src/intl/tr.js +255 -0
- package/dist/src/intl/uk.d.ts +117 -0
- package/dist/src/intl/uk.d.ts.map +1 -0
- package/dist/src/intl/uk.js +258 -0
- package/dist/src/intl/ur.d.ts +117 -0
- package/dist/src/intl/ur.d.ts.map +1 -0
- package/dist/src/intl/ur.js +245 -0
- package/dist/src/intl/vi.d.ts +117 -0
- package/dist/src/intl/vi.d.ts.map +1 -0
- package/dist/src/intl/vi.js +255 -0
- package/dist/src/intl/zh-CN.d.ts +117 -0
- package/dist/src/intl/zh-CN.d.ts.map +1 -0
- package/dist/src/intl/zh-CN.js +258 -0
- package/dist/src/intl/zh-TW.d.ts +117 -0
- package/dist/src/intl/zh-TW.d.ts.map +1 -0
- package/dist/src/intl/zh-TW.js +258 -0
- package/dist/src/local-first/Owner.d.ts +156 -62
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +119 -20
- package/package.json +1 -1
- package/src/Function.ts +2 -0
- package/src/Http.ts +9 -0
- package/src/Object.ts +2 -0
- package/src/RefCount.ts +2 -0
- package/src/Relation.ts +4 -0
- package/src/Resource.ts +2 -0
- package/src/Result.ts +397 -560
- package/src/StackTrace.ts +2 -0
- package/src/Task.ts +1143 -1222
- package/src/Time.ts +2 -0
- package/src/Type.ts +1759 -794
- package/src/intl/_en.ts +497 -0
- package/src/intl/ar.ts +417 -0
- package/src/intl/bn.ts +486 -0
- package/src/intl/ca.ts +488 -0
- package/src/intl/cs.ts +95 -31
- package/src/intl/da.ts +488 -0
- package/src/intl/de.ts +488 -0
- package/src/intl/el.ts +494 -0
- package/src/intl/es.ts +488 -0
- package/src/intl/fa.ts +487 -0
- package/src/intl/fi.ts +487 -0
- package/src/intl/fil.ts +488 -0
- package/src/intl/fr.ts +488 -0
- package/src/intl/he.ts +486 -0
- package/src/intl/hi.ts +487 -0
- package/src/intl/hr.ts +494 -0
- package/src/intl/hu.ts +387 -0
- package/src/intl/id.ts +487 -0
- package/src/intl/index.ts +42 -0
- package/src/intl/it.ts +488 -0
- package/src/intl/ja.ts +488 -0
- package/src/intl/ko.ts +487 -0
- package/src/intl/ml.ts +487 -0
- package/src/intl/mr.ts +487 -0
- package/src/intl/ms.ts +410 -0
- package/src/intl/nb.ts +364 -0
- package/src/intl/nl.ts +488 -0
- package/src/intl/pa.ts +487 -0
- package/src/intl/pl.ts +450 -0
- package/src/intl/pt-BR.ts +487 -0
- package/src/intl/pt.ts +424 -0
- package/src/intl/ro.ts +488 -0
- package/src/intl/sk.ts +412 -0
- package/src/intl/sl.ts +487 -0
- package/src/intl/sv.ts +488 -0
- package/src/intl/sw.ts +324 -0
- package/src/intl/ta.ts +487 -0
- package/src/intl/te.ts +487 -0
- package/src/intl/th.ts +484 -0
- package/src/intl/tr.ts +487 -0
- package/src/intl/uk.ts +488 -0
- package/src/intl/ur.ts +410 -0
- package/src/intl/vi.ts +435 -0
- package/src/intl/zh-CN.ts +482 -0
- package/src/intl/zh-TW.ts +482 -0
- package/src/local-first/Owner.ts +156 -62
package/dist/src/Type.js
CHANGED
|
@@ -1,5 +1,429 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* ## Intro
|
|
3
|
+
*
|
|
4
|
+
* Runtime validation with precise TypeScript types and structured errors.
|
|
5
|
+
*
|
|
6
|
+
* Evolu {@link Type} is a pure, synchronous codec for defining semantic domains.
|
|
7
|
+
* It partially decodes an `Input` into an `Output` and totally encodes every
|
|
8
|
+
* valid `Output` into a `CanonicalInput`. Types can validate, refine,
|
|
9
|
+
* transform, and compose without losing the contracts TypeScript can express.
|
|
10
|
+
*
|
|
11
|
+
* Decoding failures are explicit {@link Result} values, and their structured
|
|
12
|
+
* errors preserve the exact error types each Type can return.
|
|
13
|
+
*
|
|
14
|
+
* Evolu Type is designed to make correct code the easiest code to write:
|
|
15
|
+
*
|
|
16
|
+
* - Predefined constraints add a {@link Brand} to their Output.
|
|
17
|
+
* - Invalid declarations produce readable {@link CompileTimeError} types when the
|
|
18
|
+
* compiler can detect them.
|
|
19
|
+
* - Evolu Type uses runtime {@link assert | assertions} to detect developer errors
|
|
20
|
+
* that TypeScript cannot express, such as excess properties and sparse
|
|
21
|
+
* arrays.
|
|
22
|
+
* - Typed `from` boundaries allow connecting value producers to domain fields
|
|
23
|
+
* through their exact TypeScript types, so incompatible contract changes are
|
|
24
|
+
* compile-time errors rather than runtime validation errors.
|
|
25
|
+
* - Lawful codecs compose without creating unencodable values: every valid Output
|
|
26
|
+
* has a canonical Input representation and round-trips to the same semantic
|
|
27
|
+
* value.
|
|
28
|
+
* - Type-safe localization infers the required error formatters from selected
|
|
29
|
+
* Types, so missing validation messages are compile-time errors.
|
|
30
|
+
*
|
|
31
|
+
* Correctness is especially important for local-first data: application authors
|
|
32
|
+
* cannot inspect or repair a user's data.
|
|
33
|
+
*
|
|
34
|
+
* Evolu Type is optimized for small real-world bundles: composed Types share
|
|
35
|
+
* runtime code, while unused validators and formatters are tree-shaken. It
|
|
36
|
+
* could be smaller with less descriptive assertion messages, but Evolu favors
|
|
37
|
+
* actionable diagnostics over micro-optimizing isolated Types.
|
|
38
|
+
*
|
|
39
|
+
* Predefined Types use the names of corresponding JavaScript built-ins. When a
|
|
40
|
+
* Type shadows one, access the JavaScript built-in through `globalThis`, such
|
|
41
|
+
* as `globalThis.String` or `globalThis.Date`.
|
|
42
|
+
*
|
|
43
|
+
* Evolu Type supports [Standard Schema](https://standardschema.dev/) and
|
|
44
|
+
* requires TypeScript 7+ with `exactOptionalPropertyTypes` enabled.
|
|
45
|
+
*
|
|
46
|
+
* ## Examples
|
|
47
|
+
*
|
|
48
|
+
* Define a domain object with a custom `Age` Type, then validate unknown input:
|
|
49
|
+
*
|
|
50
|
+
* ```ts
|
|
51
|
+
* import {
|
|
52
|
+
* Number,
|
|
53
|
+
* NonEmptyTrimmedString100,
|
|
54
|
+
* brand,
|
|
55
|
+
* finite,
|
|
56
|
+
* int,
|
|
57
|
+
* lessThan,
|
|
58
|
+
* nonNaN,
|
|
59
|
+
* nonNegative,
|
|
60
|
+
* object,
|
|
61
|
+
* type Brand,
|
|
62
|
+
* type InferErrors,
|
|
63
|
+
* type InferType,
|
|
64
|
+
* } from "@evolu/common";
|
|
65
|
+
*
|
|
66
|
+
* // Age and its parent Types are predefined by Evolu. They are reconstructed
|
|
67
|
+
* // here to reveal every constraint behind a seemingly simple domain value.
|
|
68
|
+
* const NonNaNNumber = nonNaN(Number);
|
|
69
|
+
* const FiniteNumber = finite(NonNaNNumber);
|
|
70
|
+
* const Int = int(FiniteNumber);
|
|
71
|
+
* const NonNegativeInt = nonNegative(Int);
|
|
72
|
+
*
|
|
73
|
+
* const Age = brand("Age", lessThan(200)(NonNegativeInt));
|
|
74
|
+
* type Age = typeof Age.Output;
|
|
75
|
+
*
|
|
76
|
+
* expectTypeOf<Age>().toEqualTypeOf<
|
|
77
|
+
* number &
|
|
78
|
+
* Brand<"NonNaN"> &
|
|
79
|
+
* Brand<"Finite"> &
|
|
80
|
+
* Brand<"Int"> &
|
|
81
|
+
* Brand<"NonNegative"> &
|
|
82
|
+
* Brand<"LessThan200"> &
|
|
83
|
+
* Brand<"Age">
|
|
84
|
+
* >();
|
|
85
|
+
*
|
|
86
|
+
* const User = object({
|
|
87
|
+
* name: NonEmptyTrimmedString100,
|
|
88
|
+
* age: Age,
|
|
89
|
+
* });
|
|
90
|
+
* interface User extends InferType<typeof User> {}
|
|
91
|
+
*
|
|
92
|
+
* const value: unknown = { name: "Ada", age: 37 };
|
|
93
|
+
* const user = User.fromUnknown(value);
|
|
94
|
+
*
|
|
95
|
+
* expectOk(user, { name: "Ada", age: 37 });
|
|
96
|
+
* expectTypeOf(user.value).toExtend<User>();
|
|
97
|
+
*
|
|
98
|
+
* const invalidUser = User.fromUnknown({ name: "Ada", age: 37.5 });
|
|
99
|
+
*
|
|
100
|
+
* expectErr(invalidUser, {
|
|
101
|
+
* type: "Object",
|
|
102
|
+
* reason: {
|
|
103
|
+
* kind: "Properties",
|
|
104
|
+
* errors: {
|
|
105
|
+
* age: { type: "Int", value: 37.5 },
|
|
106
|
+
* },
|
|
107
|
+
* },
|
|
108
|
+
* });
|
|
109
|
+
*
|
|
110
|
+
* // InferErrors includes every structured error User.fromUnknown can return.
|
|
111
|
+
* expectTypeOf(invalidUser.error).toEqualTypeOf<
|
|
112
|
+
* InferErrors<typeof User>
|
|
113
|
+
* >();
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* A Type can format its structured errors into user-facing messages:
|
|
117
|
+
*
|
|
118
|
+
* ```ts
|
|
119
|
+
* import { Age } from "@evolu/common";
|
|
120
|
+
*
|
|
121
|
+
* const age = Age.fromUnknown(37.5);
|
|
122
|
+
*
|
|
123
|
+
* expectErr(age, { type: "Int", value: 37.5 });
|
|
124
|
+
* expect(Age.formatError(age.error)).toBe(
|
|
125
|
+
* "The value 37.5 must be a safe integer.",
|
|
126
|
+
* );
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* Use {@link localizeTypes} to derive Types with localized messages without
|
|
130
|
+
* changing validation behavior.
|
|
131
|
+
*
|
|
132
|
+
* One of Evolu Type's strongest features is typed `from` boundaries. A value
|
|
133
|
+
* producer, such as a form input, carries the precise constraints it
|
|
134
|
+
* guarantees, and TypeScript checks them against the consuming domain field.
|
|
135
|
+
* Unlike validation from `unknown` or `string`, this checks the contract
|
|
136
|
+
* between the producer and consumer, not merely whether the current value
|
|
137
|
+
* passes:
|
|
138
|
+
*
|
|
139
|
+
* ```ts
|
|
140
|
+
* import {
|
|
141
|
+
* NonEmptyTrimmedString100,
|
|
142
|
+
* NonEmptyTrimmedString1000,
|
|
143
|
+
* object,
|
|
144
|
+
* trim,
|
|
145
|
+
* type MaxLengthError,
|
|
146
|
+
* type MinLengthError,
|
|
147
|
+
* type Result,
|
|
148
|
+
* type TrimmedString,
|
|
149
|
+
* } from "@evolu/common";
|
|
150
|
+
*
|
|
151
|
+
* const Todo = object({ title: NonEmptyTrimmedString100 });
|
|
152
|
+
*
|
|
153
|
+
* // This is type-checked: Todo.from expects NonEmptyTrimmedString100.
|
|
154
|
+
* const title = NonEmptyTrimmedString100.orThrow("Buy milk");
|
|
155
|
+
* expectOk(Todo.from({ title }), { title });
|
|
156
|
+
*
|
|
157
|
+
* // Imagine the UI input component is changed to allow longer titles.
|
|
158
|
+
* // TypeScript rejects the mismatch, so users never see a save error
|
|
159
|
+
* // for a title the UI accepts but the domain cannot save.
|
|
160
|
+
* const longerTitle = NonEmptyTrimmedString1000.orThrow("Buy milk");
|
|
161
|
+
* // @ts-expect-error MaxLength1000 does not guarantee MaxLength100.
|
|
162
|
+
* Todo.from({ title: longerTitle });
|
|
163
|
+
*
|
|
164
|
+
* // Imagine a UI input component that returns TrimmedString.
|
|
165
|
+
* // from.parent.parent connects it to the domain field and validates the
|
|
166
|
+
* // remaining constraints.
|
|
167
|
+
* const titleFromTrimmingInput: TrimmedString = trim(" Buy milk ");
|
|
168
|
+
* const validatedTitle = Todo.props.title.from.parent.parent(
|
|
169
|
+
* titleFromTrimmingInput,
|
|
170
|
+
* );
|
|
171
|
+
*
|
|
172
|
+
* // No "not a string" or "not trimmed" errors: the input guarantees both.
|
|
173
|
+
* expectTypeOf(validatedTitle).toEqualTypeOf<
|
|
174
|
+
* Result<
|
|
175
|
+
* NonEmptyTrimmedString100,
|
|
176
|
+
* MaxLengthError<100> | MinLengthError<1>
|
|
177
|
+
* >
|
|
178
|
+
* >();
|
|
179
|
+
* expectOk(validatedTitle, "Buy milk");
|
|
180
|
+
* ```
|
|
181
|
+
*
|
|
182
|
+
* Evolu includes dozens of predefined Types and Type factories. Use Types such
|
|
183
|
+
* as {@link Age}, {@link PositiveInt}, {@link DateIso},
|
|
184
|
+
* {@link NonEmptyTrimmedString100}, {@link Base64Url}, and {@link Json} directly.
|
|
185
|
+
* Build domain Types with factories such as {@link brand}, {@link typed},
|
|
186
|
+
* {@link minLength}, {@link maxLength}, {@link array}, {@link object},
|
|
187
|
+
* {@link union}, {@link templateLiteral}, {@link transform},
|
|
188
|
+
* {@link discriminatedUnion}, and {@link json}.
|
|
189
|
+
*
|
|
190
|
+
* ## Guarantees
|
|
191
|
+
*
|
|
192
|
+
* Evolu Type validates values; it does not defend against adversarial
|
|
193
|
+
* JavaScript such as malicious Proxies, mutation during validation, throwing
|
|
194
|
+
* traps, forged built-ins, or code deliberately bypassing TypeScript with `any`
|
|
195
|
+
* or casts.
|
|
196
|
+
*
|
|
197
|
+
* Evolu Type trusts application code and audited dependencies. Untrusted code
|
|
198
|
+
* can cause harm far beyond validation and must not run in the application.
|
|
199
|
+
* Defending against it would add complexity without creating a meaningful
|
|
200
|
+
* security boundary.
|
|
201
|
+
*
|
|
202
|
+
* Runtime assertions still detect accidental developer errors that TypeScript
|
|
203
|
+
* cannot express. They are correctness checks, not defenses against malicious
|
|
204
|
+
* code.
|
|
205
|
+
*
|
|
206
|
+
* ## FAQ
|
|
207
|
+
*
|
|
208
|
+
* ### What does a Type represent?
|
|
209
|
+
*
|
|
210
|
+
* A Type is a lawful, pure codec for an exact semantic domain:
|
|
211
|
+
*
|
|
212
|
+
* ```text
|
|
213
|
+
* Input ── partial decode ──▶ Output
|
|
214
|
+
* CanonicalInput ◀─── total encode ─── Output
|
|
215
|
+
*
|
|
216
|
+
* CanonicalInput ⊆ Input
|
|
217
|
+
* ```
|
|
218
|
+
*
|
|
219
|
+
* Read each line in the direction of its arrowhead. `Input` is the complete
|
|
220
|
+
* typed decoding boundary, including candidates that validation can reject and
|
|
221
|
+
* noncanonical representations that decoding can normalize. `Output` is the
|
|
222
|
+
* validated semantic value. `CanonicalInput` is the statically known subtype of
|
|
223
|
+
* `Input` returned by the complete `to` operation. It can be wider than the
|
|
224
|
+
* values actually emitted when a refinement follows an arbitrary transformation
|
|
225
|
+
* because TypeScript cannot determine which values its encoder returns for the
|
|
226
|
+
* narrowed Output. `fromUnknown` and the `from` operations decode; `to`
|
|
227
|
+
* encodes.
|
|
228
|
+
*
|
|
229
|
+
* A lawful Type round-trips every Output:
|
|
230
|
+
*
|
|
231
|
+
* ```text
|
|
232
|
+
* fromUnknown(to(output)) ≈ ok(output)
|
|
233
|
+
* ```
|
|
234
|
+
*
|
|
235
|
+
* Encoding can canonicalize a valid Input:
|
|
236
|
+
*
|
|
237
|
+
* ```text
|
|
238
|
+
* "0042" ──decode──▶ 42 ──encode──▶ "42"
|
|
239
|
+
* ```
|
|
240
|
+
*
|
|
241
|
+
* Once canonicalized, repeating the decode-encode cycle must preserve that
|
|
242
|
+
* representation:
|
|
243
|
+
*
|
|
244
|
+
* ```text
|
|
245
|
+
* "42" ──decode──▶ 42 ──encode──▶ "42"
|
|
246
|
+
* ```
|
|
247
|
+
*
|
|
248
|
+
* Here `decode` means running the complete decoding pipeline, as `fromUnknown`
|
|
249
|
+
* does, and `≈` means equality appropriate for the semantic domain. Validation
|
|
250
|
+
* refinements, {@link ArrayType | Array Types}, and
|
|
251
|
+
* {@link ObjectType | Object Types} preserve these laws when their contained
|
|
252
|
+
* Types do. A {@link union} additionally requires compatible dispatch: it
|
|
253
|
+
* encodes through the first member matching the Output and decodes through the
|
|
254
|
+
* first member accepting the Input. Member ordering is lawful only when those
|
|
255
|
+
* choices agree semantically. Encoded representations can overlap even when
|
|
256
|
+
* member Output types are disjoint.
|
|
257
|
+
*
|
|
258
|
+
* When encoding returns a refined value unchanged, the refinement can narrow
|
|
259
|
+
* `CanonicalInput` without changing its JavaScript representation. For example,
|
|
260
|
+
* {@link FiniteNumber} has `number` as its Input, while its Output and
|
|
261
|
+
* CanonicalInput are `FiniteNumber`: decoding can reject non-finite number
|
|
262
|
+
* candidates, and encoding only receives validated finite Outputs. A
|
|
263
|
+
* transformation can change the representation entirely. For
|
|
264
|
+
* {@link Int64FromInt64String}, Input is `string`, Output is `Int64`, and
|
|
265
|
+
* CanonicalInput is `Int64String`. Structural Type factories derive their
|
|
266
|
+
* CanonicalInput recursively from their contained Types.
|
|
267
|
+
*
|
|
268
|
+
* ### Why is to total?
|
|
269
|
+
*
|
|
270
|
+
* Suppose a Type accepts only strings containing decimal digits and decodes
|
|
271
|
+
* them to JavaScript numbers. Parsing `"42"` is possible, but the Type cannot
|
|
272
|
+
* lawfully declare its Output as `number`:
|
|
273
|
+
*
|
|
274
|
+
* ```text
|
|
275
|
+
* digits-only string ──partial decode──▶ number
|
|
276
|
+
* digits-only string ◀─── total encode ── number // impossible
|
|
277
|
+
* ```
|
|
278
|
+
*
|
|
279
|
+
* `number` also contains negative and fractional numbers, `NaN`, positive and
|
|
280
|
+
* negative infinity, and `-0`. None of those values has a digits-only
|
|
281
|
+
* representation, so `to` could not encode every valid Output.
|
|
282
|
+
*
|
|
283
|
+
* One lawful design narrows the Output to the exact representable domain:
|
|
284
|
+
*
|
|
285
|
+
* ```text
|
|
286
|
+
* digits-only string ──partial decode──▶ NonNegativeSafeInteger
|
|
287
|
+
* digits-only string ◀─── total encode ── NonNegativeSafeInteger
|
|
288
|
+
*
|
|
289
|
+
* "0042" ──decode──▶ 42 ──encode──▶ "42"
|
|
290
|
+
* ```
|
|
291
|
+
*
|
|
292
|
+
* Another lawful design keeps `number` as the Output but expands the Input
|
|
293
|
+
* representation to include a canonical string for every number, including
|
|
294
|
+
* `"NaN"`, `"Infinity"`, `"-Infinity"`, and `"-0"`, as well as negative and
|
|
295
|
+
* fractional numbers.
|
|
296
|
+
*
|
|
297
|
+
* The same principle applies when converting between two representations. Give
|
|
298
|
+
* each representation its own Type with the same exact Output. For example, a
|
|
299
|
+
* string representation and a number representation can both decode to the
|
|
300
|
+
* shared `SafeInteger` domain:
|
|
301
|
+
*
|
|
302
|
+
* ```text
|
|
303
|
+
* string ──partial decode──▶ SafeInteger
|
|
304
|
+
* string ◀─── total encode ── SafeInteger
|
|
305
|
+
*
|
|
306
|
+
* number ──partial decode──▶ SafeInteger
|
|
307
|
+
* number ◀─── total encode ── SafeInteger
|
|
308
|
+
* ```
|
|
309
|
+
*
|
|
310
|
+
* Conversion decodes the source representation, then total-encodes the shared
|
|
311
|
+
* Output into the target representation. If no lossless shared domain exists,
|
|
312
|
+
* the operation is a partial conversion, migration, or policy decision and
|
|
313
|
+
* should be an explicit function returning Result, not a Type transformation.
|
|
314
|
+
*
|
|
315
|
+
* ### Why can a typed operation throw?
|
|
316
|
+
*
|
|
317
|
+
* TypeScript proves structural assignability, but it cannot describe every
|
|
318
|
+
* runtime invariant. For example, it cannot express whether an object property
|
|
319
|
+
* is own, enumerable, or a data property. It also permits a wider object with
|
|
320
|
+
* excess properties where a narrower object type is expected.
|
|
321
|
+
*
|
|
322
|
+
* `fromUnknown` treats such invalid external values as input data and returns a
|
|
323
|
+
* typed error. Typed boundaries instead assert the domain promised by their
|
|
324
|
+
* parameter type. If application code claims an accessor-backed object or an
|
|
325
|
+
* object with excess properties is an Object Output, the assertion throws
|
|
326
|
+
* because the application contract is broken. `orThrow` and `orNull` preserve
|
|
327
|
+
* the assertion at their typed `Input` boundary, then apply {@link getOrThrow}
|
|
328
|
+
* or {@link getOrNull} only to validation failures returned by the remaining
|
|
329
|
+
* pipeline.
|
|
330
|
+
*
|
|
331
|
+
* Consequently, structural representation errors such as sparse Arrays,
|
|
332
|
+
* accessors, and excess properties normally do not enter user-facing validation
|
|
333
|
+
* in typed application flows. They violate the producer's declared contract and
|
|
334
|
+
* throw as developer errors. At a genuinely unknown boundary, such as a
|
|
335
|
+
* schema-authoring tool, import, or external protocol, the same issues are
|
|
336
|
+
* legitimate typed validation errors and their formatter messages are useful.
|
|
337
|
+
*
|
|
338
|
+
* This distinction applies to data failures. Any Type operation, including
|
|
339
|
+
* `fromUnknown`, can throw when trusted Type-declaration code, such as a
|
|
340
|
+
* successful transformation callback, violates its declared contract.
|
|
341
|
+
*
|
|
342
|
+
* Materialize accessor values into plain data, remove properties the Type does
|
|
343
|
+
* not represent, or use a different Type. Silently discarding excess data would
|
|
344
|
+
* make the code constructing it dead while appearing to encode it successfully.
|
|
345
|
+
* One exact Object policy also keeps Output membership independent of parsing
|
|
346
|
+
* configuration. Exact structural policies also keep Output membership
|
|
347
|
+
* independent of whether a transformation happens to allocate a new value.
|
|
348
|
+
* Evolu Type therefore does not invoke accessors, discard excess properties, or
|
|
349
|
+
* make `to` fallible. This keeps `to` total for every legitimate Output and
|
|
350
|
+
* lets transformations compose without an encoding-error channel.
|
|
351
|
+
*
|
|
352
|
+
* ### How should values from another realm be handled?
|
|
353
|
+
*
|
|
354
|
+
* Values returned by legacy code or another realm can still be uncertain and
|
|
355
|
+
* should be validated. Realm-neutral Types accept an otherwise legitimate
|
|
356
|
+
* representation without requiring conversion merely because its JavaScript
|
|
357
|
+
* built-ins belong to another realm.
|
|
358
|
+
*
|
|
359
|
+
* When an application trusts both the producer and its return contract, expose
|
|
360
|
+
* that contract as an accurate TypeScript type and use the typed value
|
|
361
|
+
* directly. If the boundary returns `unknown`, validate it instead of bypassing
|
|
362
|
+
* the boundary with a cast. Use a specialized Type or explicit transformation
|
|
363
|
+
* when the producer uses a different representation that needs adaptation or
|
|
364
|
+
* normalization.
|
|
365
|
+
*
|
|
366
|
+
* ### Why doesn't Evolu Type extract data from rich objects?
|
|
367
|
+
*
|
|
368
|
+
* Some validation libraries parse an object's data projection. An imaginary
|
|
369
|
+
* validation library can enumerate own enumerable string properties and decode
|
|
370
|
+
* them into a fresh plain object. That lets a class instance decode as plain
|
|
371
|
+
* data while its prototype and methods are ignored. The same general policy can
|
|
372
|
+
* treat a `Date` or `Map` as an empty Record and can invoke enumerable getters.
|
|
373
|
+
* This is a coherent but intentionally forgiving normalization model.
|
|
374
|
+
*
|
|
375
|
+
* Evolu Type validates exactly the runtime representation defined by each Type;
|
|
376
|
+
* it does not implicitly project one representation into another. The
|
|
377
|
+
* predefined {@link Object} defines an open plain-object representation with
|
|
378
|
+
* unknown values, {@link object} defines a closed plain-object representation,
|
|
379
|
+
* and {@link record} defines a plain-dictionary representation whose complete
|
|
380
|
+
* set of own properties are its entries. Their realm-neutral plain-object rule
|
|
381
|
+
* accepts a `null` prototype or an immediate root prototype whose own prototype
|
|
382
|
+
* is `null`; ordinary class instances and deeper prototype chains are rejected.
|
|
383
|
+
* Every property must be an enumerable data property; inherited members are not
|
|
384
|
+
* entries, while accessors and hidden properties are invalid instead of being
|
|
385
|
+
* invoked or ignored. {@link array} similarly defines a dense sequence whose
|
|
386
|
+
* only own properties are `length` and its indexed data properties;
|
|
387
|
+
* {@link tuple} applies the same representation rules with a fixed length and a
|
|
388
|
+
* distinct Type for each position. Only an explicit {@link transform} changes
|
|
389
|
+
* the representation. Consequently, `is` tests exact Output membership and `to`
|
|
390
|
+
* stays total for valid Outputs.
|
|
391
|
+
*
|
|
392
|
+
* ### Why is JsonValue stricter than JSON.stringify?
|
|
393
|
+
*
|
|
394
|
+
* `JSON.stringify` is a forgiving data projection. It can invoke `toJSON` and
|
|
395
|
+
* accessors, discard object properties, replace unsupported array elements and
|
|
396
|
+
* non-finite numbers with `null`, and normalize `-0` to `0`. Those rules are
|
|
397
|
+
* useful for ordinary serialization, but they do not preserve an exact value.
|
|
398
|
+
*
|
|
399
|
+
* {@link JsonValue} instead defines data that is already represented as data.
|
|
400
|
+
* Invalid runtime behavior and values are rejected rather than interpreted or
|
|
401
|
+
* silently discarded. Its encoder is total and stack-safe for every valid
|
|
402
|
+
* Output, and {@link JsonValueFromJson} preserves the semantic value when it is
|
|
403
|
+
* encoded and decoded, including JavaScript's distinction between `-0` and `0`.
|
|
404
|
+
* Use an explicit transformation before this boundary when a projection or
|
|
405
|
+
* other normalization is desired.
|
|
406
|
+
*
|
|
407
|
+
* ### Why are Types pure and synchronous?
|
|
408
|
+
*
|
|
409
|
+
* A Type describes data meaning, not work. Time, I/O, dependencies, external
|
|
410
|
+
* state, authorization, and other contextual decisions belong in a {@link Task}.
|
|
411
|
+
* Use a Type to decode the data required by that work, then pass the decoded
|
|
412
|
+
* value to a Task. A pure synchronous conversion that can fail can be an
|
|
413
|
+
* ordinary function returning Result.
|
|
414
|
+
*
|
|
415
|
+
* Keeping those responsibilities separate prevents Evolu Type from becoming a
|
|
416
|
+
* hidden application workflow. It also keeps validation deterministic,
|
|
417
|
+
* dependency-free, immediately composable, and straightforward to test.
|
|
418
|
+
*
|
|
419
|
+
* ### What if only decoding is needed?
|
|
420
|
+
*
|
|
421
|
+
* Use `fromUnknown` for unknown data. For typed application data, call `from`
|
|
422
|
+
* at the boundary its input type proves, or use `orThrow` or `orNull` for a
|
|
423
|
+
* flat conversion from `Input`. The canonical `to` encoder still keeps the Type
|
|
424
|
+
* lawful and composable with transformations and structural Types. A genuinely
|
|
425
|
+
* irreversible operation is a separate function or Task, not a Type
|
|
426
|
+
* transformation.
|
|
3
427
|
*
|
|
4
428
|
* @module
|
|
5
429
|
*/
|
|
@@ -50,7 +474,7 @@ const formatDefaultRuntimeTypeIssue = (issue) => issue.formatError(issue.error);
|
|
|
50
474
|
* Asserts that a value belongs to a {@link Type} Output domain.
|
|
51
475
|
*
|
|
52
476
|
* Use this for internal invariants, not external input. Validate external input
|
|
53
|
-
* with `Type.fromUnknown` so
|
|
477
|
+
* with `Type.fromUnknown` so validation failures remain typed values. A failed
|
|
54
478
|
* assertion uses the Type name for its message and preserves the exact Output
|
|
55
479
|
* validation error as the thrown Error's cause.
|
|
56
480
|
*
|
|
@@ -70,7 +494,7 @@ const formatDefaultRuntimeTypeIssue = (issue) => issue.formatError(issue.error);
|
|
|
70
494
|
* >();
|
|
71
495
|
* ```
|
|
72
496
|
*
|
|
73
|
-
* @group Core
|
|
497
|
+
* @group Core
|
|
74
498
|
*/
|
|
75
499
|
export const assertType = (type, value) => {
|
|
76
500
|
// TODO: Make assert prepend "Expected " and accept an optional third cause
|
|
@@ -90,34 +514,32 @@ const assertTypeOutput = (name, is, validateOutput, value, options = firstValida
|
|
|
90
514
|
throw new Error(`Expected ${name}.`, { cause: error });
|
|
91
515
|
};
|
|
92
516
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* network connection. Different localized Type sets can coexist on one page or
|
|
120
|
-
* in separate dependency-injection scopes.
|
|
517
|
+
* Creates localized copies of selected {@link Type} declarations.
|
|
518
|
+
*
|
|
519
|
+
* Pass the Types used together in one localization scope and formatter maps
|
|
520
|
+
* keyed by locale. TypeScript infers every formatter required by the selected
|
|
521
|
+
* Types, including errors from nested structural Types and recursive Lazy
|
|
522
|
+
* Types. Every locale must provide the complete inferred formatter set; missing
|
|
523
|
+
* and unrelated formatters are compile-time errors.
|
|
524
|
+
*
|
|
525
|
+
* The result preserves the locale names, selected Type names, and exact
|
|
526
|
+
* TypeScript types. A localized Type validates exactly like its source Type;
|
|
527
|
+
* only its human-readable `formatError` and Standard Schema messages change.
|
|
528
|
+
* The source Types remain unchanged.
|
|
529
|
+
*
|
|
530
|
+
* Parents and reflected child Types are localized with the same formatter set.
|
|
531
|
+
* Structural Types retain error paths and delegate nested messages to the Type
|
|
532
|
+
* that produced them. Different localized Type sets can coexist in separate
|
|
533
|
+
* application or dependency-injection scopes.
|
|
534
|
+
*
|
|
535
|
+
* Localization is scoped to the selected Types instead of a package-wide
|
|
536
|
+
* translation registry. Static imports give bundlers an explicit dependency
|
|
537
|
+
* graph, so unrelated Types, locales, and formatters can be removed. Bundling
|
|
538
|
+
* every locale an app supports also allows language changes without a network
|
|
539
|
+
* connection.
|
|
540
|
+
*
|
|
541
|
+
* The selected Type map, locale map, and formatter maps must be plain objects
|
|
542
|
+
* with own enumerable string-keyed data properties.
|
|
121
543
|
*
|
|
122
544
|
* ### Example
|
|
123
545
|
*
|
|
@@ -127,7 +549,7 @@ const assertTypeOutput = (name, is, validateOutput, value, options = firstValida
|
|
|
127
549
|
*
|
|
128
550
|
* const Label = minLength(1)(String);
|
|
129
551
|
*
|
|
130
|
-
* const
|
|
552
|
+
* const typesByLocale = localizeTypes(
|
|
131
553
|
* { Label },
|
|
132
554
|
* {
|
|
133
555
|
* cs: {
|
|
@@ -137,11 +559,66 @@ const assertTypeOutput = (name, is, validateOutput, value, options = firstValida
|
|
|
137
559
|
* },
|
|
138
560
|
* );
|
|
139
561
|
*
|
|
140
|
-
* expectTypeOf<typeof
|
|
562
|
+
* expectTypeOf<typeof typesByLocale.cs.Label>().toEqualTypeOf<
|
|
141
563
|
* typeof Label
|
|
142
564
|
* >();
|
|
565
|
+
*
|
|
566
|
+
* const result = typesByLocale.cs.Label.fromUnknown("");
|
|
567
|
+
* expectErr(result, { type: "MinLength1", min: 1, value: "" });
|
|
568
|
+
* expect(typesByLocale.cs.Label.formatError(result.error)).toBe(
|
|
569
|
+
* "Text nesmí být prázdný.",
|
|
570
|
+
* );
|
|
143
571
|
* ```
|
|
144
572
|
*
|
|
573
|
+
* ### Supported locales
|
|
574
|
+
*
|
|
575
|
+
* English is built in; use {@link Type} directly for its default formatters. The
|
|
576
|
+
* following additional locales are available:
|
|
577
|
+
*
|
|
578
|
+
* - Arabic (`ar`)
|
|
579
|
+
* - Bengali (`bn`)
|
|
580
|
+
* - Catalan (`ca`)
|
|
581
|
+
* - Chinese, Simplified (`zhCN`)
|
|
582
|
+
* - Chinese, Traditional (`zhTW`)
|
|
583
|
+
* - Croatian (`hr`)
|
|
584
|
+
* - Czech (`cs`)
|
|
585
|
+
* - Danish (`da`)
|
|
586
|
+
* - Dutch (`nl`)
|
|
587
|
+
* - Filipino (`fil`)
|
|
588
|
+
* - Finnish (`fi`)
|
|
589
|
+
* - French (`fr`)
|
|
590
|
+
* - German (`de`)
|
|
591
|
+
* - Greek (`el`)
|
|
592
|
+
* - Hebrew (`he`)
|
|
593
|
+
* - Hindi (`hi`)
|
|
594
|
+
* - Hungarian (`hu`)
|
|
595
|
+
* - Indonesian (`id`)
|
|
596
|
+
* - Italian (`it`)
|
|
597
|
+
* - Japanese (`ja`)
|
|
598
|
+
* - Korean (`ko`)
|
|
599
|
+
* - Malay (`ms`)
|
|
600
|
+
* - Malayalam (`ml`)
|
|
601
|
+
* - Marathi (`mr`)
|
|
602
|
+
* - Norwegian Bokmål (`nb`)
|
|
603
|
+
* - Persian (`fa`)
|
|
604
|
+
* - Polish (`pl`)
|
|
605
|
+
* - Portuguese (`pt`)
|
|
606
|
+
* - Portuguese, Brazilian (`ptBR`)
|
|
607
|
+
* - Punjabi (`pa`)
|
|
608
|
+
* - Romanian (`ro`)
|
|
609
|
+
* - Slovak (`sk`)
|
|
610
|
+
* - Slovenian (`sl`)
|
|
611
|
+
* - Spanish (`es`)
|
|
612
|
+
* - Swahili (`sw`)
|
|
613
|
+
* - Swedish (`sv`)
|
|
614
|
+
* - Tamil (`ta`)
|
|
615
|
+
* - Telugu (`te`)
|
|
616
|
+
* - Thai (`th`)
|
|
617
|
+
* - Turkish (`tr`)
|
|
618
|
+
* - Ukrainian (`uk`)
|
|
619
|
+
* - Urdu (`ur`)
|
|
620
|
+
* - Vietnamese (`vi`)
|
|
621
|
+
*
|
|
145
622
|
* @group Localization
|
|
146
623
|
*/
|
|
147
624
|
export const localizeTypes = ((typesByName, formatErrorByTypeByLocale) => {
|
|
@@ -435,9 +912,17 @@ const addRuntimeAssertions = (name, is, validateOutput, parent, operation) => {
|
|
|
435
912
|
}
|
|
436
913
|
return asserted;
|
|
437
914
|
};
|
|
438
|
-
/**
|
|
915
|
+
/**
|
|
916
|
+
* An infallible {@link Type} accepting every value.
|
|
917
|
+
*
|
|
918
|
+
* @group Base
|
|
919
|
+
*/
|
|
439
920
|
export const Unknown = /*#__PURE__*/ createRootType("Unknown", ok, identity);
|
|
440
|
-
/**
|
|
921
|
+
/**
|
|
922
|
+
* A {@link Type} rejecting every value.
|
|
923
|
+
*
|
|
924
|
+
* @group Base
|
|
925
|
+
*/
|
|
441
926
|
export const Never = /*#__PURE__*/ createRootType("Never", (value) => err({ type: "Never", value }), (error) => `A value ${safelyStringifyUnknownValue(error.value)} is not valid for type Never.`);
|
|
442
927
|
const createTypeOfType = (name) => {
|
|
443
928
|
const typeOf = name.toLowerCase();
|
|
@@ -468,6 +953,8 @@ const createTypeOfType = (name) => {
|
|
|
468
953
|
* expectTypeOf<WireValue100>().toEqualTypeOf<
|
|
469
954
|
* string & Brand<"MaxLength100">
|
|
470
955
|
* >();
|
|
956
|
+
* expectOk(WireValue100.fromUnknown(""), "");
|
|
957
|
+
* expectOk(WireValue100.fromUnknown(" value "), " value ");
|
|
471
958
|
* ```
|
|
472
959
|
*
|
|
473
960
|
* @group String
|
|
@@ -542,36 +1029,52 @@ export const String = /*#__PURE__*/ createTypeOfType("String");
|
|
|
542
1029
|
* @group Number
|
|
543
1030
|
*/
|
|
544
1031
|
export const Number = /*#__PURE__*/ createTypeOfType("Number");
|
|
545
|
-
/**
|
|
1032
|
+
/**
|
|
1033
|
+
* A JavaScript bigint {@link Type}.
|
|
1034
|
+
*
|
|
1035
|
+
* @group Base
|
|
1036
|
+
*/
|
|
546
1037
|
export const BigInt = /*#__PURE__*/ createTypeOfType("BigInt");
|
|
547
|
-
/**
|
|
1038
|
+
/**
|
|
1039
|
+
* A JavaScript boolean {@link Type}.
|
|
1040
|
+
*
|
|
1041
|
+
* @group Base
|
|
1042
|
+
*/
|
|
548
1043
|
export const Boolean = /*#__PURE__*/ createTypeOfType("Boolean");
|
|
549
|
-
/**
|
|
1044
|
+
/**
|
|
1045
|
+
* A JavaScript symbol {@link Type}.
|
|
1046
|
+
*
|
|
1047
|
+
* @group Base
|
|
1048
|
+
*/
|
|
550
1049
|
export const Symbol = /*#__PURE__*/ createTypeOfType("Symbol");
|
|
551
|
-
/**
|
|
1050
|
+
/**
|
|
1051
|
+
* A JavaScript function {@link Type}.
|
|
1052
|
+
*
|
|
1053
|
+
* @group Base
|
|
1054
|
+
*/
|
|
552
1055
|
export const Function = /*#__PURE__*/ createTypeOfType("Function");
|
|
553
1056
|
/**
|
|
554
|
-
*
|
|
1057
|
+
* A {@link Type} validating Evolu Type declarations.
|
|
555
1058
|
*
|
|
556
1059
|
* This is useful when a Type itself crosses an unknown boundary or must be
|
|
557
1060
|
* asserted with {@link assertType}.
|
|
558
1061
|
*
|
|
559
|
-
* @group Core
|
|
1062
|
+
* @group Core
|
|
560
1063
|
*/
|
|
561
1064
|
export const EvoluType = /*#__PURE__*/ createType("EvoluType", (value) => isInstance("Type")(value)
|
|
562
1065
|
? ok(value)
|
|
563
1066
|
: err({ type: "EvoluType", value }), (error) => `A value ${safelyStringifyUnknownValue(error.value)} is not an Evolu Type.`);
|
|
564
1067
|
export function objectTag(name, outputType) {
|
|
1068
|
+
const formatError = (error) => `A value ${safelyStringifyUnknownValue(error.value)} does not have the expected object tag ${safelyStringifyUnknownValue(error.expected)}.`;
|
|
565
1069
|
if (outputType === undefined) {
|
|
566
1070
|
return createRootType(name, (value) => hasObjectTag(value, name)
|
|
567
1071
|
? ok(value)
|
|
568
|
-
: err({ type: "ObjectTag", expected: name, value }),
|
|
1072
|
+
: err({ type: "ObjectTag", expected: name, value }), formatError);
|
|
569
1073
|
}
|
|
570
1074
|
return globalThis.Object.assign(createChildType("ObjectTag", outputType, (value) => hasObjectTag(value, name)
|
|
571
1075
|
? ok(value)
|
|
572
|
-
: err({ type: "ObjectTag", expected: name, value }),
|
|
1076
|
+
: err({ type: "ObjectTag", expected: name, value }), formatError), { expected: name });
|
|
573
1077
|
}
|
|
574
|
-
const formatObjectTagError = (error) => `A value ${safelyStringifyUnknownValue(error.value)} does not have the object tag ${safelyStringifyUnknownValue(error.expected)}.`;
|
|
575
1078
|
const hasObjectTag = (value, expected) => value !== null &&
|
|
576
1079
|
(typeof value === "object" || typeof value === "function") &&
|
|
577
1080
|
globalThis.Object.prototype.toString.call(value) === `[object ${expected}]`;
|
|
@@ -580,7 +1083,7 @@ const hasObjectTag = (value, expected) => value !== null &&
|
|
|
580
1083
|
*
|
|
581
1084
|
* It trusts the reported object tag and does not verify Date internal slots.
|
|
582
1085
|
*
|
|
583
|
-
* @group Base
|
|
1086
|
+
* @group Base
|
|
584
1087
|
*/
|
|
585
1088
|
export const Date = /*#__PURE__*/ objectTag("Date");
|
|
586
1089
|
/**
|
|
@@ -589,7 +1092,7 @@ export const Date = /*#__PURE__*/ objectTag("Date");
|
|
|
589
1092
|
* It trusts the reported object tag and does not verify Uint8Array internal
|
|
590
1093
|
* slots.
|
|
591
1094
|
*
|
|
592
|
-
* @group Base
|
|
1095
|
+
* @group Base
|
|
593
1096
|
*/
|
|
594
1097
|
export const Uint8Array = /*#__PURE__*/ objectTag("Uint8Array");
|
|
595
1098
|
/**
|
|
@@ -598,7 +1101,7 @@ export const Uint8Array = /*#__PURE__*/ objectTag("Uint8Array");
|
|
|
598
1101
|
* It trusts the reported object tag and does not verify ArrayBuffer internal
|
|
599
1102
|
* slots.
|
|
600
1103
|
*
|
|
601
|
-
* @group Base
|
|
1104
|
+
* @group Base
|
|
602
1105
|
*/
|
|
603
1106
|
export const ArrayBuffer = /*#__PURE__*/ objectTag("ArrayBuffer");
|
|
604
1107
|
/**
|
|
@@ -629,7 +1132,7 @@ export const ArrayBuffer = /*#__PURE__*/ objectTag("ArrayBuffer");
|
|
|
629
1132
|
* assert(!UserInstance.is({ name: "Ada" }));
|
|
630
1133
|
* ```
|
|
631
1134
|
*
|
|
632
|
-
* @group Base
|
|
1135
|
+
* @group Base
|
|
633
1136
|
*/
|
|
634
1137
|
export const instanceOf = (constructor) => {
|
|
635
1138
|
const concreteConstructor = constructor;
|
|
@@ -690,9 +1193,17 @@ export const literal = (expected) => {
|
|
|
690
1193
|
[templateLiteralSyntaxSymbol]: true,
|
|
691
1194
|
});
|
|
692
1195
|
};
|
|
693
|
-
/**
|
|
1196
|
+
/**
|
|
1197
|
+
* Literal {@link Type} accepting only `undefined`.
|
|
1198
|
+
*
|
|
1199
|
+
* @group Unions
|
|
1200
|
+
*/
|
|
694
1201
|
export const Undefined = /*#__PURE__*/ literal(undefined);
|
|
695
|
-
/**
|
|
1202
|
+
/**
|
|
1203
|
+
* Literal {@link Type} accepting only `null`.
|
|
1204
|
+
*
|
|
1205
|
+
* @group Unions
|
|
1206
|
+
*/
|
|
696
1207
|
export const Null = /*#__PURE__*/ literal(null);
|
|
697
1208
|
export function union(...typesOrLiterals) {
|
|
698
1209
|
const members = typesOrLiterals.map((typeOrLiteral) => typeOrLiteral !== null && typeof typeOrLiteral === "object"
|
|
@@ -701,7 +1212,7 @@ export function union(...typesOrLiterals) {
|
|
|
701
1212
|
const inputMembers = members.map(getTerminalRuntimeNode);
|
|
702
1213
|
const inputFrom = createUnionValidation(inputMembers, (member, value, options) => member.fromUnknown(value, options));
|
|
703
1214
|
const inputValidateOutput = createUnionValidation(inputMembers, (member, value, options) => member[outputValidationSymbol](value, options));
|
|
704
|
-
const defaultFormatter =
|
|
1215
|
+
const defaultFormatter = (() => "A value does not match any allowed variant.");
|
|
705
1216
|
const getTypeIssues = (error) => singleRuntimeTypeIssue("Union", error, defaultFormatter);
|
|
706
1217
|
const input = createTypeNode("Union", null, inputFrom, (value) => inputMembers.some((member) => member.is(value)), inputValidateOutput, ok, identity, getTypeIssues);
|
|
707
1218
|
const fromUnknown = createUnionValidation(members, (member, value, options) => member.fromUnknown(value, options));
|
|
@@ -719,7 +1230,6 @@ export function union(...typesOrLiterals) {
|
|
|
719
1230
|
};
|
|
720
1231
|
return createTypeNode("Union", input, fromUnknown, (value) => members.some((member) => member.is(value)), validateOutput, from, to, getTypeIssues, { members, [templateLiteralSyntaxSymbol]: true });
|
|
721
1232
|
}
|
|
722
|
-
const formatUnionError = () => "A value does not match any union member.";
|
|
723
1233
|
const createUnionValidation = (members, validateMember) => (value, options = firstValidationOptions) => {
|
|
724
1234
|
let errors;
|
|
725
1235
|
for (let index = 0; index < members.length; index++) {
|
|
@@ -744,18 +1254,49 @@ const createUnionValidation = (members, validateMember) => (value, options = fir
|
|
|
744
1254
|
* This does not make an object property optional. It changes only the values
|
|
745
1255
|
* accepted when the property is present.
|
|
746
1256
|
*
|
|
1257
|
+
* ### Example
|
|
1258
|
+
*
|
|
1259
|
+
* ```ts
|
|
1260
|
+
* import { String, undefinedOr } from "@evolu/common";
|
|
1261
|
+
*
|
|
1262
|
+
* const StringOrUndefined = undefinedOr(String);
|
|
1263
|
+
*
|
|
1264
|
+
* expectOk(StringOrUndefined.fromUnknown(undefined), undefined);
|
|
1265
|
+
* ```
|
|
1266
|
+
*
|
|
747
1267
|
* @group Unions
|
|
748
1268
|
*/
|
|
749
1269
|
export const undefinedOr = (type) => union(type, Undefined);
|
|
750
1270
|
/**
|
|
751
1271
|
* Union {@link Type} containing the supplied Type and `null`.
|
|
752
1272
|
*
|
|
1273
|
+
* ### Example
|
|
1274
|
+
*
|
|
1275
|
+
* ```ts
|
|
1276
|
+
* import { String, nullOr } from "@evolu/common";
|
|
1277
|
+
*
|
|
1278
|
+
* const NullableString = nullOr(String);
|
|
1279
|
+
*
|
|
1280
|
+
* expectOk(NullableString.fromUnknown(null), null);
|
|
1281
|
+
* ```
|
|
1282
|
+
*
|
|
753
1283
|
* @group Unions
|
|
754
1284
|
*/
|
|
755
1285
|
export const nullOr = (type) => union(type, Null);
|
|
756
1286
|
/**
|
|
757
1287
|
* Union {@link Type} containing the supplied Type, `null`, and `undefined`.
|
|
758
1288
|
*
|
|
1289
|
+
* ### Example
|
|
1290
|
+
*
|
|
1291
|
+
* ```ts
|
|
1292
|
+
* import { String, nullishOr } from "@evolu/common";
|
|
1293
|
+
*
|
|
1294
|
+
* const NullishString = nullishOr(String);
|
|
1295
|
+
*
|
|
1296
|
+
* expectOk(NullishString.fromUnknown(undefined), undefined);
|
|
1297
|
+
* expectOk(NullishString.fromUnknown(null), null);
|
|
1298
|
+
* ```
|
|
1299
|
+
*
|
|
759
1300
|
* @group Unions
|
|
760
1301
|
*/
|
|
761
1302
|
export const nullishOr = (type) => union(type, Null, Undefined);
|
|
@@ -763,9 +1304,7 @@ const isRuntimeUnionTypeNode = (type) => type.name === "Union" &&
|
|
|
763
1304
|
"members" in type &&
|
|
764
1305
|
globalThis.Array.isArray(type.members);
|
|
765
1306
|
/**
|
|
766
|
-
* Template literal {@link Type}
|
|
767
|
-
*
|
|
768
|
-
* Parses and creates structured strings.
|
|
1307
|
+
* Template literal {@link Type} that parses canonical strings into Tuples.
|
|
769
1308
|
*
|
|
770
1309
|
* Accepts the same template parts as {@link templateLiteral}: fixed string
|
|
771
1310
|
* literals and Types canonically encoded as strings. Instead of keeping Output
|
|
@@ -781,10 +1320,10 @@ const isRuntimeUnionTypeNode = (type) => type.name === "Union" &&
|
|
|
781
1320
|
* reversibility and keeps capture boundaries unambiguous. Different capture
|
|
782
1321
|
* Tuples must never encode to the same string. The parser provides predictable
|
|
783
1322
|
* parsing without pathological backtracking and decodes each capture once, so
|
|
784
|
-
* adversarial input cannot trigger exponential parser work. Fixed-width
|
|
785
|
-
* may be adjacent, but only one variable-width capture is allowed.
|
|
786
|
-
* that could join UTF-16 surrogate halves across parts are
|
|
787
|
-
* construction.
|
|
1323
|
+
* adversarial input cannot trigger exponential parser work. Fixed-width
|
|
1324
|
+
* captures may be adjacent, but only one variable-width capture is allowed.
|
|
1325
|
+
* Declarations that could join UTF-16 surrogate halves across parts are
|
|
1326
|
+
* rejected during construction.
|
|
788
1327
|
*
|
|
789
1328
|
* Keep capture unions reasonably small to avoid excessive compiler work.
|
|
790
1329
|
*
|
|
@@ -821,10 +1360,9 @@ const isRuntimeUnionTypeNode = (type) => type.name === "Union" &&
|
|
|
821
1360
|
*
|
|
822
1361
|
* // Parse an unknown string into structured data.
|
|
823
1362
|
* const result = SupportedLocale.fromUnknown("cs-CZ");
|
|
824
|
-
*
|
|
1363
|
+
* expectOk(result, ["cs", "CZ"]);
|
|
825
1364
|
* const locale = result.value;
|
|
826
1365
|
* expectTypeOf(locale).toEqualTypeOf<SupportedLocale>();
|
|
827
|
-
* expect(locale).toEqual(["cs", "CZ"]);
|
|
828
1366
|
* expectErr(SupportedLocale.fromUnknown("cs/CZ"), {
|
|
829
1367
|
* type: "TemplateLiteral",
|
|
830
1368
|
* value: "cs/CZ",
|
|
@@ -883,10 +1421,9 @@ const isRuntimeUnionTypeNode = (type) => type.name === "Union" &&
|
|
|
883
1421
|
*
|
|
884
1422
|
* // Decode the string into structured data.
|
|
885
1423
|
* const result = ItemId.fromUnknown("item-42");
|
|
886
|
-
*
|
|
1424
|
+
* expectOk(result, [42n]);
|
|
887
1425
|
* const itemId = result.value;
|
|
888
1426
|
* expectTypeOf(itemId).toEqualTypeOf<ItemId>();
|
|
889
|
-
* expect(itemId).toEqual([42n]);
|
|
890
1427
|
*
|
|
891
1428
|
* // Encode the structured data into its canonical string.
|
|
892
1429
|
* const itemIdLiteral = ItemId.to(itemId);
|
|
@@ -989,7 +1526,7 @@ const createTemplateLiteralParserType = (templateParts) => {
|
|
|
989
1526
|
if ("outputError" in error) {
|
|
990
1527
|
return runtimeOutput[getRuntimeTypeIssuesSymbol](error.outputError, mode);
|
|
991
1528
|
}
|
|
992
|
-
return singleRuntimeTypeIssue("TemplateLiteral", error,
|
|
1529
|
+
return singleRuntimeTypeIssue("TemplateLiteral", error, ((error) => `The value ${safelyStringifyUnknownValue(error.value)} does not match the template literal.`));
|
|
993
1530
|
};
|
|
994
1531
|
const canonicalStringFromUnknown = (value, options = firstValidationOptions) => {
|
|
995
1532
|
const stringResult = String.fromUnknown(value, options);
|
|
@@ -1039,8 +1576,8 @@ const createTemplateLiteralParserType = (templateParts) => {
|
|
|
1039
1576
|
* Creates a canonical string Type from fixed strings and string-encoded Types.
|
|
1040
1577
|
*
|
|
1041
1578
|
* Use this factory when Output should remain a string. Switch to
|
|
1042
|
-
* {@link templateLiteralParser} when the individual Type parts should be
|
|
1043
|
-
*
|
|
1579
|
+
* {@link templateLiteralParser} when the individual Type parts should be decoded
|
|
1580
|
+
* into a Tuple.
|
|
1044
1581
|
*
|
|
1045
1582
|
* ### Example
|
|
1046
1583
|
*
|
|
@@ -1061,7 +1598,6 @@ const createTemplateLiteralParserType = (templateParts) => {
|
|
|
1061
1598
|
* @group Template literals
|
|
1062
1599
|
*/
|
|
1063
1600
|
export const templateLiteral = (...parts) => createTemplateLiteralParserType(parts).parent;
|
|
1064
|
-
const formatTemplateLiteralError = (error) => `The value ${safelyStringifyUnknownValue(error.value)} does not match the template literal.`;
|
|
1065
1601
|
const compileTemplateLiteralParser = (parts) => {
|
|
1066
1602
|
let framing = emptyTemplateLiteralFraming;
|
|
1067
1603
|
let fixedPartsWidth = 0;
|
|
@@ -1202,6 +1738,18 @@ export const DateIso = /*#__PURE__*/ brand("DateIso", String, (value) => value.l
|
|
|
1202
1738
|
/**
|
|
1203
1739
|
* Safely transforms a {@link Date} into a canonical {@link DateIso}.
|
|
1204
1740
|
*
|
|
1741
|
+
* ### Example
|
|
1742
|
+
*
|
|
1743
|
+
* ```ts
|
|
1744
|
+
* import { DateIsoFromDate } from "@evolu/common";
|
|
1745
|
+
*
|
|
1746
|
+
* const date = new globalThis.Date("2025-01-01T12:00:00.000Z");
|
|
1747
|
+
* const result = DateIsoFromDate.fromUnknown(date);
|
|
1748
|
+
*
|
|
1749
|
+
* expectOk(result, "2025-01-01T12:00:00.000Z");
|
|
1750
|
+
* expect(DateIsoFromDate.to(result.value)).toEqual(date);
|
|
1751
|
+
* ```
|
|
1752
|
+
*
|
|
1205
1753
|
* @group String
|
|
1206
1754
|
*/
|
|
1207
1755
|
export const DateIsoFromDate = /*#__PURE__*/ transform("DateIsoFromDate", Date, DateIso, {
|
|
@@ -1265,6 +1813,16 @@ export const CapitalizedString = /*#__PURE__*/ capitalized(String);
|
|
|
1265
1813
|
/**
|
|
1266
1814
|
* String {@link Brand} without surrounding whitespace.
|
|
1267
1815
|
*
|
|
1816
|
+
* ### Example
|
|
1817
|
+
*
|
|
1818
|
+
* ```ts
|
|
1819
|
+
* import { String, trimmed } from "@evolu/common";
|
|
1820
|
+
*
|
|
1821
|
+
* const Trimmed = trimmed(String);
|
|
1822
|
+
*
|
|
1823
|
+
* expectOk(Trimmed.fromUnknown("Evolu"), "Evolu");
|
|
1824
|
+
* ```
|
|
1825
|
+
*
|
|
1268
1826
|
* @group String
|
|
1269
1827
|
*/
|
|
1270
1828
|
export const trimmed = (parent) => brand("Trimmed", parent, (value) => value === value.trim()
|
|
@@ -1285,11 +1843,31 @@ export const TrimmedString = /*#__PURE__*/ trimmed(String);
|
|
|
1285
1843
|
/**
|
|
1286
1844
|
* Trims a string and returns a {@link TrimmedString}.
|
|
1287
1845
|
*
|
|
1846
|
+
* ### Example
|
|
1847
|
+
*
|
|
1848
|
+
* ```ts
|
|
1849
|
+
* import { trim } from "@evolu/common";
|
|
1850
|
+
*
|
|
1851
|
+
* expect(trim(" Evolu ")).toBe("Evolu");
|
|
1852
|
+
* ```
|
|
1853
|
+
*
|
|
1288
1854
|
* @group String
|
|
1289
1855
|
*/
|
|
1290
1856
|
export const trim = (value) => value.trim();
|
|
1291
1857
|
/**
|
|
1292
|
-
* Minimum-length {@link Brand}
|
|
1858
|
+
* Minimum-length {@link Brand} for values whose `length` is at least `min`.
|
|
1859
|
+
*
|
|
1860
|
+
* ### Example
|
|
1861
|
+
*
|
|
1862
|
+
* ```ts
|
|
1863
|
+
* import { String, array, minLength } from "@evolu/common";
|
|
1864
|
+
*
|
|
1865
|
+
* const AtLeastThreeCharacters = minLength(3)(String);
|
|
1866
|
+
* const AtLeastTwoItems = minLength(2)(array(String));
|
|
1867
|
+
*
|
|
1868
|
+
* expectOk(AtLeastThreeCharacters.fromUnknown("abc"), "abc");
|
|
1869
|
+
* expectOk(AtLeastTwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
|
|
1870
|
+
* ```
|
|
1293
1871
|
*
|
|
1294
1872
|
* @group String
|
|
1295
1873
|
* @group Collection
|
|
@@ -1316,7 +1894,19 @@ export const minLength = (min) => (parent) => {
|
|
|
1316
1894
|
*/
|
|
1317
1895
|
export const NonEmptyTrimmedString = /*#__PURE__*/ minLength(1)(TrimmedString);
|
|
1318
1896
|
/**
|
|
1319
|
-
* Maximum-length {@link Brand}
|
|
1897
|
+
* Maximum-length {@link Brand} for values whose `length` is at most `max`.
|
|
1898
|
+
*
|
|
1899
|
+
* ### Example
|
|
1900
|
+
*
|
|
1901
|
+
* ```ts
|
|
1902
|
+
* import { String, array, maxLength } from "@evolu/common";
|
|
1903
|
+
*
|
|
1904
|
+
* const AtMostThreeCharacters = maxLength(3)(String);
|
|
1905
|
+
* const AtMostTwoItems = maxLength(2)(array(String));
|
|
1906
|
+
*
|
|
1907
|
+
* expectOk(AtMostThreeCharacters.fromUnknown("abc"), "abc");
|
|
1908
|
+
* expectOk(AtMostTwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
|
|
1909
|
+
* ```
|
|
1320
1910
|
*
|
|
1321
1911
|
* @group String
|
|
1322
1912
|
* @group Collection
|
|
@@ -1340,7 +1930,19 @@ export const NonEmptyTrimmedString100 = /*#__PURE__*/ maxLength(100)(NonEmptyTri
|
|
|
1340
1930
|
*/
|
|
1341
1931
|
export const NonEmptyTrimmedString1000 = /*#__PURE__*/ maxLength(1000)(NonEmptyTrimmedString);
|
|
1342
1932
|
/**
|
|
1343
|
-
* Exact-length {@link Brand}
|
|
1933
|
+
* Exact-length {@link Brand} for values whose `length` equals `exact`.
|
|
1934
|
+
*
|
|
1935
|
+
* ### Example
|
|
1936
|
+
*
|
|
1937
|
+
* ```ts
|
|
1938
|
+
* import { String, array, length } from "@evolu/common";
|
|
1939
|
+
*
|
|
1940
|
+
* const ThreeCharacters = length(3)(String);
|
|
1941
|
+
* const TwoItems = length(2)(array(String));
|
|
1942
|
+
*
|
|
1943
|
+
* expectOk(ThreeCharacters.fromUnknown("abc"), "abc");
|
|
1944
|
+
* expectOk(TwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
|
|
1945
|
+
* ```
|
|
1344
1946
|
*
|
|
1345
1947
|
* @group String
|
|
1346
1948
|
* @group Collection
|
|
@@ -1440,8 +2042,8 @@ const base64UrlStringToUint8Array = (value) => {
|
|
|
1440
2042
|
/**
|
|
1441
2043
|
* Base64Url text without padding.
|
|
1442
2044
|
*
|
|
1443
|
-
*
|
|
1444
|
-
* {@link base64UrlToUint8Array}.
|
|
2045
|
+
* Convert bytes to Base64Url with {@link uint8ArrayToBase64Url} and convert
|
|
2046
|
+
* Base64Url to bytes with {@link base64UrlToUint8Array}.
|
|
1445
2047
|
*
|
|
1446
2048
|
* @group String
|
|
1447
2049
|
*/
|
|
@@ -1452,13 +2054,35 @@ export const Base64Url = /*#__PURE__*/ brand("Base64Url", String, (value) => {
|
|
|
1452
2054
|
: err({ type: "Base64Url", value });
|
|
1453
2055
|
}, (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a valid Base64Url string.`);
|
|
1454
2056
|
/**
|
|
1455
|
-
*
|
|
2057
|
+
* Converts bytes to {@link Base64Url}.
|
|
2058
|
+
*
|
|
2059
|
+
* ### Example
|
|
2060
|
+
*
|
|
2061
|
+
* ```ts
|
|
2062
|
+
* import { uint8ArrayToBase64Url } from "@evolu/common";
|
|
2063
|
+
*
|
|
2064
|
+
* expect(uint8ArrayToBase64Url(new Uint8Array([0, 1, 2, 255]))).toBe(
|
|
2065
|
+
* "AAEC_w",
|
|
2066
|
+
* );
|
|
2067
|
+
* ```
|
|
1456
2068
|
*
|
|
1457
2069
|
* @group String
|
|
1458
2070
|
*/
|
|
1459
2071
|
export const uint8ArrayToBase64Url = (bytes) => uint8ArrayToBase64UrlString(bytes);
|
|
1460
2072
|
/**
|
|
1461
|
-
*
|
|
2073
|
+
* Converts {@link Base64Url} to bytes.
|
|
2074
|
+
*
|
|
2075
|
+
* ### Example
|
|
2076
|
+
*
|
|
2077
|
+
* ```ts
|
|
2078
|
+
* import { Base64Url, base64UrlToUint8Array } from "@evolu/common";
|
|
2079
|
+
*
|
|
2080
|
+
* const value = Base64Url.orThrow("AAEC_w");
|
|
2081
|
+
*
|
|
2082
|
+
* expect(base64UrlToUint8Array(value)).toEqual(
|
|
2083
|
+
* new Uint8Array([0, 1, 2, 255]),
|
|
2084
|
+
* );
|
|
2085
|
+
* ```
|
|
1462
2086
|
*
|
|
1463
2087
|
* @group String
|
|
1464
2088
|
*/
|
|
@@ -1489,7 +2113,7 @@ export const SimplePassword = /*#__PURE__*/ brand("SimplePassword",
|
|
|
1489
2113
|
*/
|
|
1490
2114
|
export const Mnemonic = /*#__PURE__*/ brand("Mnemonic", NonEmptyTrimmedString, (value) => bip39.validateMnemonic(value, wordlist)
|
|
1491
2115
|
? ok()
|
|
1492
|
-
: err({ type: "Mnemonic", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a valid BIP39 mnemonic.`);
|
|
2116
|
+
: err({ type: "Mnemonic", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a valid English BIP39 mnemonic.`);
|
|
1493
2117
|
/**
|
|
1494
2118
|
* Evolu Id: 16 bytes encoded as a 22-character {@link Base64Url}.
|
|
1495
2119
|
*
|
|
@@ -1509,6 +2133,22 @@ export const Id = /*#__PURE__*/ brand("Id", String, (value) => value.length ===
|
|
|
1509
2133
|
* This is the recommended default because it does not encode creation time.
|
|
1510
2134
|
* Pass a Brand name when the returned Id belongs to one domain entity.
|
|
1511
2135
|
*
|
|
2136
|
+
* ### Example
|
|
2137
|
+
*
|
|
2138
|
+
* ```ts
|
|
2139
|
+
* import {
|
|
2140
|
+
* Id,
|
|
2141
|
+
* createId,
|
|
2142
|
+
* createRandomBytes,
|
|
2143
|
+
* type Brand,
|
|
2144
|
+
* } from "@evolu/common";
|
|
2145
|
+
*
|
|
2146
|
+
* const userId = createId<"User">({ randomBytes: createRandomBytes() });
|
|
2147
|
+
*
|
|
2148
|
+
* expect(Id.is(userId)).toBe(true);
|
|
2149
|
+
* expectTypeOf(userId).toEqualTypeOf<Id & Brand<"User">>();
|
|
2150
|
+
* ```
|
|
2151
|
+
*
|
|
1512
2152
|
* @group String
|
|
1513
2153
|
*/
|
|
1514
2154
|
export const createId = (deps, ..._validation) => uint8ArrayToBase64Url(deps.randomBytes.create(16));
|
|
@@ -1543,6 +2183,26 @@ export const createIdFromString = (value, ..._validation) => idBytesToId(sha256(
|
|
|
1543
2183
|
* logs, URLs, and exports. Prefer {@link createId} unless that tradeoff is
|
|
1544
2184
|
* deliberate.
|
|
1545
2185
|
*
|
|
2186
|
+
* ### Example
|
|
2187
|
+
*
|
|
2188
|
+
* ```ts
|
|
2189
|
+
* import {
|
|
2190
|
+
* createIdAsUuidv7,
|
|
2191
|
+
* createRandomBytes,
|
|
2192
|
+
* createTime,
|
|
2193
|
+
* idToIdBytes,
|
|
2194
|
+
* } from "@evolu/common";
|
|
2195
|
+
*
|
|
2196
|
+
* const value = createIdAsUuidv7({
|
|
2197
|
+
* randomBytes: createRandomBytes(),
|
|
2198
|
+
* time: createTime(),
|
|
2199
|
+
* });
|
|
2200
|
+
* const bytes = idToIdBytes(value);
|
|
2201
|
+
*
|
|
2202
|
+
* expect(bytes[6] >> 4).toBe(0x7);
|
|
2203
|
+
* expect(bytes[8] & 0xc0).toBe(0x80);
|
|
2204
|
+
* ```
|
|
2205
|
+
*
|
|
1546
2206
|
* @group String
|
|
1547
2207
|
*/
|
|
1548
2208
|
export const createIdAsUuidv7 = (deps, ..._validation) => {
|
|
@@ -1561,6 +2221,22 @@ export const createIdAsUuidv7 = (deps, ..._validation) => {
|
|
|
1561
2221
|
/**
|
|
1562
2222
|
* Table-specific {@link Id} Type.
|
|
1563
2223
|
*
|
|
2224
|
+
* ### Example
|
|
2225
|
+
*
|
|
2226
|
+
* ```ts
|
|
2227
|
+
* import {
|
|
2228
|
+
* createIdFromString,
|
|
2229
|
+
* id,
|
|
2230
|
+
* type Brand,
|
|
2231
|
+
* type Id,
|
|
2232
|
+
* } from "@evolu/common";
|
|
2233
|
+
*
|
|
2234
|
+
* const TodoId = id("Todo");
|
|
2235
|
+
* const todoId = TodoId.orThrow(createIdFromString("todo"));
|
|
2236
|
+
*
|
|
2237
|
+
* expectTypeOf(todoId).toEqualTypeOf<Id & Brand<"Todo">>();
|
|
2238
|
+
* ```
|
|
2239
|
+
*
|
|
1564
2240
|
* @group String
|
|
1565
2241
|
*/
|
|
1566
2242
|
export const id = (table) => {
|
|
@@ -1576,11 +2252,47 @@ export const id = (table) => {
|
|
|
1576
2252
|
*/
|
|
1577
2253
|
export const IdBytes = /*#__PURE__*/ brand("IdBytes",
|
|
1578
2254
|
/*#__PURE__*/ length(16)(Uint8Array));
|
|
1579
|
-
/**
|
|
2255
|
+
/**
|
|
2256
|
+
* Byte length of an {@link IdBytes} value.
|
|
2257
|
+
*
|
|
2258
|
+
* @group String
|
|
2259
|
+
*/
|
|
1580
2260
|
export const idBytesTypeValueLength = 16;
|
|
1581
|
-
/**
|
|
2261
|
+
/**
|
|
2262
|
+
* Converts an {@link Id} to {@link IdBytes}.
|
|
2263
|
+
*
|
|
2264
|
+
* ### Example
|
|
2265
|
+
*
|
|
2266
|
+
* ```ts
|
|
2267
|
+
* import { createIdFromString, idToIdBytes } from "@evolu/common";
|
|
2268
|
+
*
|
|
2269
|
+
* const bytes = idToIdBytes(createIdFromString("todo"));
|
|
2270
|
+
*
|
|
2271
|
+
* expect(bytes).toHaveLength(16);
|
|
2272
|
+
* ```
|
|
2273
|
+
*
|
|
2274
|
+
* @group String
|
|
2275
|
+
*/
|
|
1582
2276
|
export const idToIdBytes = (value) => base64UrlToUint8Array(value);
|
|
1583
|
-
/**
|
|
2277
|
+
/**
|
|
2278
|
+
* Converts {@link IdBytes} to an {@link Id}.
|
|
2279
|
+
*
|
|
2280
|
+
* ### Example
|
|
2281
|
+
*
|
|
2282
|
+
* ```ts
|
|
2283
|
+
* import {
|
|
2284
|
+
* createIdFromString,
|
|
2285
|
+
* idBytesToId,
|
|
2286
|
+
* idToIdBytes,
|
|
2287
|
+
* } from "@evolu/common";
|
|
2288
|
+
*
|
|
2289
|
+
* const value = createIdFromString("todo");
|
|
2290
|
+
*
|
|
2291
|
+
* expect(idBytesToId(idToIdBytes(value))).toBe(value);
|
|
2292
|
+
* ```
|
|
2293
|
+
*
|
|
2294
|
+
* @group String
|
|
2295
|
+
*/
|
|
1584
2296
|
export const idBytesToId = (value) => uint8ArrayToBase64Url(value);
|
|
1585
2297
|
/**
|
|
1586
2298
|
* Decimal string representation of a signed {@link Int64}.
|
|
@@ -1606,6 +2318,19 @@ export const Int64String = /*#__PURE__*/ brand("Int64String", NonEmptyTrimmedStr
|
|
|
1606
2318
|
* text, including SQLite queries that cast INTEGER values to TEXT to avoid a
|
|
1607
2319
|
* lossy JavaScript number conversion.
|
|
1608
2320
|
*
|
|
2321
|
+
* ### Example
|
|
2322
|
+
*
|
|
2323
|
+
* ```ts
|
|
2324
|
+
* import { Int64FromInt64String } from "@evolu/common";
|
|
2325
|
+
*
|
|
2326
|
+
* const result = Int64FromInt64String.fromUnknown("9223372036854775807");
|
|
2327
|
+
*
|
|
2328
|
+
* expectOk(result, 9223372036854775807n);
|
|
2329
|
+
* expect(Int64FromInt64String.to(result.value)).toBe(
|
|
2330
|
+
* "9223372036854775807",
|
|
2331
|
+
* );
|
|
2332
|
+
* ```
|
|
2333
|
+
*
|
|
1609
2334
|
* @group Number
|
|
1610
2335
|
*/
|
|
1611
2336
|
export const Int64FromInt64String = /*#__PURE__*/ transform("Int64FromInt64String", Int64String, Int64, {
|
|
@@ -1615,6 +2340,16 @@ export const Int64FromInt64String = /*#__PURE__*/ transform("Int64FromInt64Strin
|
|
|
1615
2340
|
/**
|
|
1616
2341
|
* Number {@link Brand} requiring a value greater than or equal to zero.
|
|
1617
2342
|
*
|
|
2343
|
+
* ### Example
|
|
2344
|
+
*
|
|
2345
|
+
* ```ts
|
|
2346
|
+
* import { Number, nonNegative } from "@evolu/common";
|
|
2347
|
+
*
|
|
2348
|
+
* const NonNegative = nonNegative(Number);
|
|
2349
|
+
*
|
|
2350
|
+
* expectOk(NonNegative.fromUnknown(0), 0);
|
|
2351
|
+
* ```
|
|
2352
|
+
*
|
|
1618
2353
|
* @group Number
|
|
1619
2354
|
*/
|
|
1620
2355
|
export const nonNegative = (parent) => brand("NonNegative", parent, (value) => value >= 0 ? ok() : err({ type: "NonNegative", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be non-negative (>= 0).`);
|
|
@@ -1627,6 +2362,16 @@ export const NonNegativeNumber = /*#__PURE__*/ nonNegative(Number);
|
|
|
1627
2362
|
/**
|
|
1628
2363
|
* Number {@link Brand} requiring a value greater than zero.
|
|
1629
2364
|
*
|
|
2365
|
+
* ### Example
|
|
2366
|
+
*
|
|
2367
|
+
* ```ts
|
|
2368
|
+
* import { Number, positive } from "@evolu/common";
|
|
2369
|
+
*
|
|
2370
|
+
* const Positive = positive(Number);
|
|
2371
|
+
*
|
|
2372
|
+
* expectOk(Positive.fromUnknown(1), 1);
|
|
2373
|
+
* ```
|
|
2374
|
+
*
|
|
1630
2375
|
* @group Number
|
|
1631
2376
|
*/
|
|
1632
2377
|
export const positive = (parent) => brand("Positive", parent, (value) => value > 0 ? ok() : err({ type: "Positive", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be positive (> 0).`);
|
|
@@ -1642,6 +2387,16 @@ export const PositiveNumber = /*#__PURE__*/ positive(NonNegativeNumber);
|
|
|
1642
2387
|
/**
|
|
1643
2388
|
* Number {@link Brand} requiring a value less than or equal to zero.
|
|
1644
2389
|
*
|
|
2390
|
+
* ### Example
|
|
2391
|
+
*
|
|
2392
|
+
* ```ts
|
|
2393
|
+
* import { Number, nonPositive } from "@evolu/common";
|
|
2394
|
+
*
|
|
2395
|
+
* const NonPositive = nonPositive(Number);
|
|
2396
|
+
*
|
|
2397
|
+
* expectOk(NonPositive.fromUnknown(0), 0);
|
|
2398
|
+
* ```
|
|
2399
|
+
*
|
|
1645
2400
|
* @group Number
|
|
1646
2401
|
*/
|
|
1647
2402
|
export const nonPositive = (parent) => brand("NonPositive", parent, (value) => value <= 0 ? ok() : err({ type: "NonPositive", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be non-positive (<= 0).`);
|
|
@@ -1654,6 +2409,16 @@ export const NonPositiveNumber = /*#__PURE__*/ nonPositive(Number);
|
|
|
1654
2409
|
/**
|
|
1655
2410
|
* Number {@link Brand} requiring a value less than zero.
|
|
1656
2411
|
*
|
|
2412
|
+
* ### Example
|
|
2413
|
+
*
|
|
2414
|
+
* ```ts
|
|
2415
|
+
* import { Number, negative } from "@evolu/common";
|
|
2416
|
+
*
|
|
2417
|
+
* const Negative = negative(Number);
|
|
2418
|
+
*
|
|
2419
|
+
* expectOk(Negative.fromUnknown(-1), -1);
|
|
2420
|
+
* ```
|
|
2421
|
+
*
|
|
1657
2422
|
* @group Number
|
|
1658
2423
|
*/
|
|
1659
2424
|
export const negative = (parent) => brand("Negative", parent, (value) => value < 0 ? ok() : err({ type: "Negative", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be negative (< 0).`);
|
|
@@ -1669,6 +2434,16 @@ export const NegativeNumber = /*#__PURE__*/ negative(NonPositiveNumber);
|
|
|
1669
2434
|
/**
|
|
1670
2435
|
* Number {@link Brand} requiring a value other than `NaN`.
|
|
1671
2436
|
*
|
|
2437
|
+
* ### Example
|
|
2438
|
+
*
|
|
2439
|
+
* ```ts
|
|
2440
|
+
* import { Number, nonNaN } from "@evolu/common";
|
|
2441
|
+
*
|
|
2442
|
+
* const NonNaN = nonNaN(Number);
|
|
2443
|
+
*
|
|
2444
|
+
* expectOk(NonNaN.fromUnknown(Infinity), Infinity);
|
|
2445
|
+
* ```
|
|
2446
|
+
*
|
|
1672
2447
|
* @group Number
|
|
1673
2448
|
*/
|
|
1674
2449
|
export const nonNaN = (parent) => brand("NonNaN", parent, (value) => globalThis.Number.isNaN(value)
|
|
@@ -1688,6 +2463,16 @@ export const NonNaNNumber = /*#__PURE__*/ nonNaN(Number);
|
|
|
1688
2463
|
/**
|
|
1689
2464
|
* Number {@link Brand} requiring a finite value.
|
|
1690
2465
|
*
|
|
2466
|
+
* ### Example
|
|
2467
|
+
*
|
|
2468
|
+
* ```ts
|
|
2469
|
+
* import { Number, finite } from "@evolu/common";
|
|
2470
|
+
*
|
|
2471
|
+
* const Finite = finite(Number);
|
|
2472
|
+
*
|
|
2473
|
+
* expectOk(Finite.fromUnknown(42), 42);
|
|
2474
|
+
* ```
|
|
2475
|
+
*
|
|
1691
2476
|
* @group Number
|
|
1692
2477
|
*/
|
|
1693
2478
|
export const finite = (parent) => brand("Finite", parent, (value) => globalThis.Number.isFinite(value)
|
|
@@ -1794,6 +2579,16 @@ export const NegativeInt = /*#__PURE__*/ negative(NonPositiveInt);
|
|
|
1794
2579
|
/**
|
|
1795
2580
|
* Number {@link Brand} requiring a value greater than `min`.
|
|
1796
2581
|
*
|
|
2582
|
+
* ### Example
|
|
2583
|
+
*
|
|
2584
|
+
* ```ts
|
|
2585
|
+
* import { Number, greaterThan } from "@evolu/common";
|
|
2586
|
+
*
|
|
2587
|
+
* const GreaterThanTen = greaterThan(10)(Number);
|
|
2588
|
+
*
|
|
2589
|
+
* expectOk(GreaterThanTen.fromUnknown(11), 11);
|
|
2590
|
+
* ```
|
|
2591
|
+
*
|
|
1797
2592
|
* @group Number
|
|
1798
2593
|
*/
|
|
1799
2594
|
export const greaterThan = (min) => (parent) => {
|
|
@@ -1805,6 +2600,16 @@ export const greaterThan = (min) => (parent) => {
|
|
|
1805
2600
|
/**
|
|
1806
2601
|
* Number {@link Brand} requiring a value greater than or equal to `min`.
|
|
1807
2602
|
*
|
|
2603
|
+
* ### Example
|
|
2604
|
+
*
|
|
2605
|
+
* ```ts
|
|
2606
|
+
* import { Number, greaterThanOrEqualTo } from "@evolu/common";
|
|
2607
|
+
*
|
|
2608
|
+
* const AtLeastTen = greaterThanOrEqualTo(10)(Number);
|
|
2609
|
+
*
|
|
2610
|
+
* expectOk(AtLeastTen.fromUnknown(10), 10);
|
|
2611
|
+
* ```
|
|
2612
|
+
*
|
|
1808
2613
|
* @group Number
|
|
1809
2614
|
*/
|
|
1810
2615
|
export const greaterThanOrEqualTo = (min) => (parent) => {
|
|
@@ -1820,6 +2625,16 @@ export const greaterThanOrEqualTo = (min) => (parent) => {
|
|
|
1820
2625
|
/**
|
|
1821
2626
|
* Number {@link Brand} requiring a value less than `max`.
|
|
1822
2627
|
*
|
|
2628
|
+
* ### Example
|
|
2629
|
+
*
|
|
2630
|
+
* ```ts
|
|
2631
|
+
* import { Number, lessThan } from "@evolu/common";
|
|
2632
|
+
*
|
|
2633
|
+
* const LessThanTen = lessThan(10)(Number);
|
|
2634
|
+
*
|
|
2635
|
+
* expectOk(LessThanTen.fromUnknown(9), 9);
|
|
2636
|
+
* ```
|
|
2637
|
+
*
|
|
1823
2638
|
* @group Number
|
|
1824
2639
|
*/
|
|
1825
2640
|
export const lessThan = (max) => (parent) => {
|
|
@@ -1838,6 +2653,16 @@ export const Age = /*#__PURE__*/ brand("Age",
|
|
|
1838
2653
|
/**
|
|
1839
2654
|
* Number {@link Brand} requiring a value less than or equal to `max`.
|
|
1840
2655
|
*
|
|
2656
|
+
* ### Example
|
|
2657
|
+
*
|
|
2658
|
+
* ```ts
|
|
2659
|
+
* import { Number, lessThanOrEqualTo } from "@evolu/common";
|
|
2660
|
+
*
|
|
2661
|
+
* const AtMostTen = lessThanOrEqualTo(10)(Number);
|
|
2662
|
+
*
|
|
2663
|
+
* expectOk(AtMostTen.fromUnknown(10), 10);
|
|
2664
|
+
* ```
|
|
2665
|
+
*
|
|
1841
2666
|
* @group Number
|
|
1842
2667
|
*/
|
|
1843
2668
|
export const lessThanOrEqualTo = (max) => (parent) => {
|
|
@@ -1902,6 +2727,16 @@ export const DecimalString = /*#__PURE__*/ brand("DecimalString", String, (value
|
|
|
1902
2727
|
/**
|
|
1903
2728
|
* {@link DecimalString} Brand requiring a value greater than or equal to zero.
|
|
1904
2729
|
*
|
|
2730
|
+
* ### Example
|
|
2731
|
+
*
|
|
2732
|
+
* ```ts
|
|
2733
|
+
* import { DecimalString, nonNegativeDecimalString } from "@evolu/common";
|
|
2734
|
+
*
|
|
2735
|
+
* const NonNegative = nonNegativeDecimalString(DecimalString);
|
|
2736
|
+
*
|
|
2737
|
+
* expectOk(NonNegative.fromUnknown("0.5"), "0.5");
|
|
2738
|
+
* ```
|
|
2739
|
+
*
|
|
1905
2740
|
* @group Number
|
|
1906
2741
|
*/
|
|
1907
2742
|
export const nonNegativeDecimalString = (parent) => brand("NonNegativeDecimalString", parent, (value) => value[0] !== "-"
|
|
@@ -1920,6 +2755,16 @@ export const NonNegativeDecimalString =
|
|
|
1920
2755
|
/**
|
|
1921
2756
|
* {@link DecimalString} Brand requiring a value greater than zero.
|
|
1922
2757
|
*
|
|
2758
|
+
* ### Example
|
|
2759
|
+
*
|
|
2760
|
+
* ```ts
|
|
2761
|
+
* import { DecimalString, positiveDecimalString } from "@evolu/common";
|
|
2762
|
+
*
|
|
2763
|
+
* const Positive = positiveDecimalString(DecimalString);
|
|
2764
|
+
*
|
|
2765
|
+
* expectOk(Positive.fromUnknown("0.5"), "0.5");
|
|
2766
|
+
* ```
|
|
2767
|
+
*
|
|
1923
2768
|
* @group Number
|
|
1924
2769
|
*/
|
|
1925
2770
|
export const positiveDecimalString = (parent) => brand("PositiveDecimalString", parent, (value) => value !== "0" && value[0] !== "-"
|
|
@@ -1940,6 +2785,16 @@ export const PositiveDecimalString = /*#__PURE__*/ positiveDecimalString(NonNega
|
|
|
1940
2785
|
/**
|
|
1941
2786
|
* {@link DecimalString} Brand requiring a value less than or equal to zero.
|
|
1942
2787
|
*
|
|
2788
|
+
* ### Example
|
|
2789
|
+
*
|
|
2790
|
+
* ```ts
|
|
2791
|
+
* import { DecimalString, nonPositiveDecimalString } from "@evolu/common";
|
|
2792
|
+
*
|
|
2793
|
+
* const NonPositive = nonPositiveDecimalString(DecimalString);
|
|
2794
|
+
*
|
|
2795
|
+
* expectOk(NonPositive.fromUnknown("-0.5"), "-0.5");
|
|
2796
|
+
* ```
|
|
2797
|
+
*
|
|
1943
2798
|
* @group Number
|
|
1944
2799
|
*/
|
|
1945
2800
|
export const nonPositiveDecimalString = (parent) => brand("NonPositiveDecimalString", parent, (value) => value === "0" || value[0] === "-"
|
|
@@ -1958,6 +2813,16 @@ export const NonPositiveDecimalString =
|
|
|
1958
2813
|
/**
|
|
1959
2814
|
* {@link DecimalString} Brand requiring a value less than zero.
|
|
1960
2815
|
*
|
|
2816
|
+
* ### Example
|
|
2817
|
+
*
|
|
2818
|
+
* ```ts
|
|
2819
|
+
* import { DecimalString, negativeDecimalString } from "@evolu/common";
|
|
2820
|
+
*
|
|
2821
|
+
* const Negative = negativeDecimalString(DecimalString);
|
|
2822
|
+
*
|
|
2823
|
+
* expectOk(Negative.fromUnknown("-0.5"), "-0.5");
|
|
2824
|
+
* ```
|
|
2825
|
+
*
|
|
1961
2826
|
* @group Number
|
|
1962
2827
|
*/
|
|
1963
2828
|
export const negativeDecimalString = (parent) => brand("NegativeDecimalString", parent, (value) => value[0] === "-"
|
|
@@ -2051,6 +2916,16 @@ const decimalStringToParts = (value) => {
|
|
|
2051
2916
|
/**
|
|
2052
2917
|
* Number {@link Brand} requiring a value within an inclusive range.
|
|
2053
2918
|
*
|
|
2919
|
+
* ### Example
|
|
2920
|
+
*
|
|
2921
|
+
* ```ts
|
|
2922
|
+
* import { Number, between } from "@evolu/common";
|
|
2923
|
+
*
|
|
2924
|
+
* const Percentage = between(0, 100)(Number);
|
|
2925
|
+
*
|
|
2926
|
+
* expectOk(Percentage.fromUnknown(75), 75);
|
|
2927
|
+
* ```
|
|
2928
|
+
*
|
|
2054
2929
|
* @group Number
|
|
2055
2930
|
*/
|
|
2056
2931
|
export const between = (min, max) => (parent) => {
|
|
@@ -2152,20 +3027,6 @@ const isArrayCollection = (value, isElement) => {
|
|
|
2152
3027
|
}
|
|
2153
3028
|
return true;
|
|
2154
3029
|
};
|
|
2155
|
-
const formatArrayError = (error) => {
|
|
2156
|
-
if (error.reason.kind === "NotArray") {
|
|
2157
|
-
return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not an array.`;
|
|
2158
|
-
}
|
|
2159
|
-
const issue = error.reason.issues[0];
|
|
2160
|
-
switch (issue.kind) {
|
|
2161
|
-
case "Hole":
|
|
2162
|
-
return `An array element at index ${issue.index} is missing.`;
|
|
2163
|
-
case "Accessor":
|
|
2164
|
-
return `An array element at index ${issue.index} must be a data property.`;
|
|
2165
|
-
case "ExcessProperty":
|
|
2166
|
-
return "An excess Array property is not allowed. Remove it or use a different Type.";
|
|
2167
|
-
}
|
|
2168
|
-
};
|
|
2169
3030
|
const arrayRuntimeConfig = {
|
|
2170
3031
|
name: "Array",
|
|
2171
3032
|
typeByElement: arrayTypeByElement,
|
|
@@ -2173,7 +3034,22 @@ const arrayRuntimeConfig = {
|
|
|
2173
3034
|
validateItems: (value, validateElement, options) => validateArrayItems(value, validateElement, options, false),
|
|
2174
3035
|
encode: encodeArrayCollection,
|
|
2175
3036
|
is: isArrayCollection,
|
|
2176
|
-
formatError:
|
|
3037
|
+
formatError: ((error) => {
|
|
3038
|
+
if (error.reason.kind === "NotArray") {
|
|
3039
|
+
return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not an array.`;
|
|
3040
|
+
}
|
|
3041
|
+
const issue = error.reason.issues[0];
|
|
3042
|
+
switch (issue.kind) {
|
|
3043
|
+
case "Hole":
|
|
3044
|
+
return `An array element at index ${issue.index} is missing.`;
|
|
3045
|
+
case "Accessor":
|
|
3046
|
+
return `An array element at index ${issue.index} must be a data property.`;
|
|
3047
|
+
case "ExcessProperty":
|
|
3048
|
+
return "An excess Array property is not allowed. Remove it or use a different Type.";
|
|
3049
|
+
case "Element":
|
|
3050
|
+
return `An array element at index ${issue.index} is invalid.`;
|
|
3051
|
+
}
|
|
3052
|
+
}),
|
|
2177
3053
|
};
|
|
2178
3054
|
const validateArrayItems = (value, validate, options, checkStructure) => validateIndexedArrayItems("Array", value, validate, options, checkStructure);
|
|
2179
3055
|
// Array and Tuple share their structural and element validation traversal.
|
|
@@ -2258,6 +3134,17 @@ const copyArrayPrefix = (value, endIndex) => {
|
|
|
2258
3134
|
* iteration order. Classification uses the realm-neutral object tag and
|
|
2259
3135
|
* prototype structure under Evolu Type's trusted JavaScript policy.
|
|
2260
3136
|
*
|
|
3137
|
+
* ### Example
|
|
3138
|
+
*
|
|
3139
|
+
* ```ts
|
|
3140
|
+
* import { String, set } from "@evolu/common";
|
|
3141
|
+
*
|
|
3142
|
+
* const Tags = set(String);
|
|
3143
|
+
* const tags = new Set(["local-first", "offline"]);
|
|
3144
|
+
*
|
|
3145
|
+
* expectOk(Tags.fromUnknown(tags), tags);
|
|
3146
|
+
* ```
|
|
3147
|
+
*
|
|
2261
3148
|
* @group Collection
|
|
2262
3149
|
*/
|
|
2263
3150
|
export const set = (element) => createHomogeneousCollectionType(element, setRuntimeConfig);
|
|
@@ -2331,16 +3218,6 @@ const isSetCollection = (value, isElement) => {
|
|
|
2331
3218
|
}
|
|
2332
3219
|
return true;
|
|
2333
3220
|
};
|
|
2334
|
-
const formatSetError = (error) => {
|
|
2335
|
-
if (error.reason.kind === "NotSet") {
|
|
2336
|
-
return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not a Set.`;
|
|
2337
|
-
}
|
|
2338
|
-
if (error.reason.kind === "UnexpectedPrototype") {
|
|
2339
|
-
return "The value is a Set subclass, but a Set Output must be a direct Set.";
|
|
2340
|
-
}
|
|
2341
|
-
const issue = error.reason.issues[0];
|
|
2342
|
-
return `An excess Set property ${safelyStringifyUnknownValue(issue.key)} is not allowed.`;
|
|
2343
|
-
};
|
|
2344
3221
|
const hasDirectSetPrototype = (value) => {
|
|
2345
3222
|
const prototype = globalThis.Object.getPrototypeOf(value);
|
|
2346
3223
|
if (prototype === null)
|
|
@@ -2356,7 +3233,19 @@ const setRuntimeConfig = {
|
|
|
2356
3233
|
validateItems: (value, validateElement, options) => validateSetItems(value, validateElement, options, false),
|
|
2357
3234
|
encode: encodeSetCollection,
|
|
2358
3235
|
is: isSetCollection,
|
|
2359
|
-
formatError:
|
|
3236
|
+
formatError: ((error) => {
|
|
3237
|
+
if (error.reason.kind === "NotSet")
|
|
3238
|
+
return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not a Set.`;
|
|
3239
|
+
if (error.reason.kind === "UnexpectedPrototype")
|
|
3240
|
+
return "The value is an instance of a Set subclass, but a Set Output must be a direct Set instance.";
|
|
3241
|
+
const issue = error.reason.issues[0];
|
|
3242
|
+
switch (issue.kind) {
|
|
3243
|
+
case "ExcessProperty":
|
|
3244
|
+
return `An excess Set property ${safelyStringifyUnknownValue(issue.key)} is not allowed.`;
|
|
3245
|
+
case "Element":
|
|
3246
|
+
return `A Set element at index ${issue.index} is invalid.`;
|
|
3247
|
+
}
|
|
3248
|
+
}),
|
|
2360
3249
|
};
|
|
2361
3250
|
const validateSetItems = (value, validate, options, checkStructure) => {
|
|
2362
3251
|
let issues;
|
|
@@ -2421,12 +3310,10 @@ const createTupleType = (typeElements) => {
|
|
|
2421
3310
|
const fromUnknown = (value, options = firstValidationOptions) => validate(value, (element, item, elementOptions) => element.fromUnknown(item, elementOptions), options);
|
|
2422
3311
|
const validateOutput = (value, options = firstValidationOptions) => validate(value, (element, item, elementOptions) => element[outputValidationSymbol](item, elementOptions), options);
|
|
2423
3312
|
const formatError = (error) => {
|
|
2424
|
-
if (error.reason.kind === "NotArray")
|
|
3313
|
+
if (error.reason.kind === "NotArray")
|
|
2425
3314
|
return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not a tuple.`;
|
|
2426
|
-
|
|
2427
|
-
if (error.reason.kind === "InvalidLength") {
|
|
3315
|
+
if (error.reason.kind === "InvalidLength")
|
|
2428
3316
|
return `A Tuple must contain exactly ${error.reason.expected} elements, but the value contains ${error.reason.actual}.`;
|
|
2429
|
-
}
|
|
2430
3317
|
const issue = error.reason.issues[0];
|
|
2431
3318
|
switch (issue.kind) {
|
|
2432
3319
|
case "Hole":
|
|
@@ -2435,6 +3322,8 @@ const createTupleType = (typeElements) => {
|
|
|
2435
3322
|
return `A Tuple element at index ${issue.index} must be a data property.`;
|
|
2436
3323
|
case "ExcessProperty":
|
|
2437
3324
|
return "An excess Tuple property is not allowed. Remove it or use a different Type.";
|
|
3325
|
+
case "Element":
|
|
3326
|
+
return `A Tuple element at index ${issue.index} is invalid.`;
|
|
2438
3327
|
}
|
|
2439
3328
|
};
|
|
2440
3329
|
const rootElements = typeElements.map(getTerminalRuntimeNode);
|
|
@@ -2506,16 +3395,14 @@ export const Digit1To6 = /*#__PURE__*/ union("1", "2", "3", "4", "5", "6");
|
|
|
2506
3395
|
*/
|
|
2507
3396
|
export const Digit1To23 = /*#__PURE__*/ union(Digit1To9,
|
|
2508
3397
|
/*#__PURE__*/ templateLiteral("1", Digit),
|
|
2509
|
-
/*#__PURE__*/ templateLiteral("2",
|
|
2510
|
-
/*#__PURE__*/ union("0", "1", "2", "3")));
|
|
3398
|
+
/*#__PURE__*/ templateLiteral("2", /*#__PURE__*/ union("0", "1", "2", "3")));
|
|
2511
3399
|
/**
|
|
2512
3400
|
* Decimal string from `"1"` to `"51"`.
|
|
2513
3401
|
*
|
|
2514
3402
|
* @group String
|
|
2515
3403
|
*/
|
|
2516
3404
|
export const Digit1To51 = /*#__PURE__*/ union(Digit1To9,
|
|
2517
|
-
/*#__PURE__*/ templateLiteral(
|
|
2518
|
-
/*#__PURE__*/ union("1", "2", "3", "4"), Digit),
|
|
3405
|
+
/*#__PURE__*/ templateLiteral(/*#__PURE__*/ union("1", "2", "3", "4"), Digit),
|
|
2519
3406
|
/*#__PURE__*/ templateLiteral("5", /*#__PURE__*/ union("0", "1")));
|
|
2520
3407
|
/**
|
|
2521
3408
|
* Decimal string from `"1"` to `"99"`.
|
|
@@ -2572,21 +3459,9 @@ const createObjectRuntimeTypeIssues = (defaultFormatter, props, recordType) => (
|
|
|
2572
3459
|
return recordType[getRuntimeTypeIssuesSymbol](propertyError, mode);
|
|
2573
3460
|
});
|
|
2574
3461
|
};
|
|
2575
|
-
const
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
}
|
|
2579
|
-
const key = Reflect.ownKeys(error.reason.errors).at(0);
|
|
2580
|
-
assertNonNullable(key);
|
|
2581
|
-
const propertyError = error.reason.errors[key];
|
|
2582
|
-
assertNonNullable(propertyError);
|
|
2583
|
-
if (propertyError.type === "ObjectPropertyAccess") {
|
|
2584
|
-
return propertyError.reason === "Accessor"
|
|
2585
|
-
? "An Object property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type."
|
|
2586
|
-
: "An Object property must be enumerable. Make it enumerable or use a different Type.";
|
|
2587
|
-
}
|
|
2588
|
-
return "An Object property key must be a string. Remove it or use a different Type.";
|
|
2589
|
-
};
|
|
3462
|
+
const formatPlainObjectRootError = (reason) => reason.kind === "NotObject"
|
|
3463
|
+
? `A value ${safelyStringifyUnknownValue(reason.value)} is not an object.`
|
|
3464
|
+
: "The value is an object, but an Object Output must be a plain object or have a null prototype.";
|
|
2590
3465
|
/**
|
|
2591
3466
|
* A {@link Type} for readonly plain objects with unknown property values.
|
|
2592
3467
|
*
|
|
@@ -2601,7 +3476,7 @@ const formatPlainObjectError = (error) => {
|
|
|
2601
3476
|
* values have their own Types, and {@link instanceOf} when an instance belongs
|
|
2602
3477
|
* to the domain.
|
|
2603
3478
|
*
|
|
2604
|
-
* @group Base
|
|
3479
|
+
* @group Base
|
|
2605
3480
|
*/
|
|
2606
3481
|
export const Object = /*#__PURE__*/ createRootType("Object", (value, options = firstValidationOptions) => {
|
|
2607
3482
|
if (value === null || typeof value !== "object") {
|
|
@@ -2651,15 +3526,56 @@ export const Object = /*#__PURE__*/ createRootType("Object", (value, options = f
|
|
|
2651
3526
|
type: "Object",
|
|
2652
3527
|
reason: { kind: "Properties", errors },
|
|
2653
3528
|
});
|
|
2654
|
-
},
|
|
2655
|
-
|
|
3529
|
+
}, (error) => {
|
|
3530
|
+
if (error.reason.kind !== "Properties")
|
|
3531
|
+
return formatPlainObjectRootError(error.reason);
|
|
3532
|
+
const key = Reflect.ownKeys(error.reason.errors).at(0);
|
|
3533
|
+
assertNonNullable(key);
|
|
3534
|
+
const propertyError = error.reason.errors[key];
|
|
3535
|
+
assertNonNullable(propertyError);
|
|
3536
|
+
if (propertyError.type === "ObjectPropertyAccess") {
|
|
3537
|
+
switch (propertyError.reason) {
|
|
3538
|
+
case "Accessor":
|
|
3539
|
+
return "An Object property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type.";
|
|
3540
|
+
case "NonEnumerable":
|
|
3541
|
+
return "An Object property must be enumerable. Make it enumerable or use a different Type.";
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
if (propertyError.type === "ObjectMissingProperty")
|
|
3545
|
+
return `The required property ${safelyStringifyUnknownValue(key)} is missing.`;
|
|
3546
|
+
if (typeof key === "symbol")
|
|
3547
|
+
return "An Object property key must be a string. Remove the symbol property or use a different Type.";
|
|
3548
|
+
if (propertyError.type === "ObjectExcessProperty")
|
|
3549
|
+
return `The property ${safelyStringifyUnknownValue(key)} is not allowed. Remove it or use a different Type.`;
|
|
3550
|
+
return `The property ${safelyStringifyUnknownValue(key)} is invalid.`;
|
|
3551
|
+
},
|
|
3552
|
+
/*#__PURE__*/ createObjectRuntimeTypeIssues(((error) => {
|
|
3553
|
+
if (error.reason.kind !== "Properties")
|
|
3554
|
+
return formatPlainObjectRootError(error.reason);
|
|
3555
|
+
const key = Reflect.ownKeys(error.reason.errors).at(0);
|
|
3556
|
+
assertNonNullable(key);
|
|
3557
|
+
const propertyError = error.reason.errors[key];
|
|
3558
|
+
assertNonNullable(propertyError);
|
|
3559
|
+
if (propertyError.type === "ObjectPropertyAccess") {
|
|
3560
|
+
switch (propertyError.reason) {
|
|
3561
|
+
case "Accessor":
|
|
3562
|
+
return "An Object property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type.";
|
|
3563
|
+
case "NonEnumerable":
|
|
3564
|
+
return "An Object property must be enumerable. Make it enumerable or use a different Type.";
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
if (propertyError.type === "ObjectMissingProperty")
|
|
3568
|
+
return `The required property ${safelyStringifyUnknownValue(key)} is missing.`;
|
|
3569
|
+
if (typeof key === "symbol")
|
|
3570
|
+
return "An Object property key must be a string. Remove the symbol property or use a different Type.";
|
|
3571
|
+
if (propertyError.type === "ObjectExcessProperty")
|
|
3572
|
+
return `The property ${safelyStringifyUnknownValue(key)} is not allowed. Remove it or use a different Type.`;
|
|
3573
|
+
return `The property ${safelyStringifyUnknownValue(key)} is invalid.`;
|
|
3574
|
+
})));
|
|
2656
3575
|
const isPlainObject = (value) => {
|
|
2657
3576
|
const prototype = globalThis.Object.getPrototypeOf(value);
|
|
2658
3577
|
return (prototype === null || globalThis.Object.getPrototypeOf(prototype) === null);
|
|
2659
3578
|
};
|
|
2660
|
-
const formatPlainObjectRootError = (reason) => reason.kind === "NotObject"
|
|
2661
|
-
? `A value ${safelyStringifyUnknownValue(reason.value)} is not an object.`
|
|
2662
|
-
: "The value is an object, but an Object Output must be a plain object or have a null prototype.";
|
|
2663
3579
|
/**
|
|
2664
3580
|
* Record {@link Type}.
|
|
2665
3581
|
*
|
|
@@ -2690,20 +3606,13 @@ const formatPlainObjectRootError = (reason) => reason.kind === "NotObject"
|
|
|
2690
3606
|
*
|
|
2691
3607
|
* ```ts
|
|
2692
3608
|
* import {
|
|
2693
|
-
*
|
|
3609
|
+
* Int64FromInt64String,
|
|
2694
3610
|
* String,
|
|
2695
|
-
* ok,
|
|
2696
3611
|
* record,
|
|
2697
|
-
*
|
|
2698
|
-
* type Result,
|
|
3612
|
+
* type Int64,
|
|
2699
3613
|
* } from "@evolu/common";
|
|
2700
3614
|
*
|
|
2701
|
-
* const
|
|
2702
|
-
* from: (value) => ok(globalThis.Number(value)),
|
|
2703
|
-
* to: globalThis.String,
|
|
2704
|
-
* });
|
|
2705
|
-
*
|
|
2706
|
-
* const ScoresByUser = record(String, ScoreFromString);
|
|
3615
|
+
* const ScoresByUser = record(String, Int64FromInt64String);
|
|
2707
3616
|
* type ScoresByUser = typeof ScoresByUser.Output;
|
|
2708
3617
|
*
|
|
2709
3618
|
* // Validate an unknown value.
|
|
@@ -2712,7 +3621,7 @@ const formatPlainObjectRootError = (reason) => reason.kind === "NotObject"
|
|
|
2712
3621
|
* grace: "20",
|
|
2713
3622
|
* });
|
|
2714
3623
|
*
|
|
2715
|
-
* expectOk(scoresFromUnknown, { ada:
|
|
3624
|
+
* expectOk(scoresFromUnknown, { ada: 10n, grace: 20n });
|
|
2716
3625
|
*
|
|
2717
3626
|
* // Validate keys and values with their root Types.
|
|
2718
3627
|
* const scoresInput = ScoresByUser.parent.fromUnknown({
|
|
@@ -2724,11 +3633,10 @@ const formatPlainObjectRootError = (reason) => reason.kind === "NotObject"
|
|
|
2724
3633
|
* // Run the remaining key and value stages.
|
|
2725
3634
|
* const scoresFromInput = ScoresByUser.from.parent(scoresInput.value);
|
|
2726
3635
|
*
|
|
2727
|
-
*
|
|
2728
|
-
*
|
|
3636
|
+
* expectOk(scoresFromInput, { ada: 10n, grace: 20n });
|
|
3637
|
+
* expectTypeOf(scoresFromInput.value).toEqualTypeOf<
|
|
3638
|
+
* Readonly<Partial<Record<string, Int64>>>
|
|
2729
3639
|
* >();
|
|
2730
|
-
*
|
|
2731
|
-
* expectOk(scoresFromInput, { ada: 10, grace: 20 });
|
|
2732
3640
|
* ```
|
|
2733
3641
|
*
|
|
2734
3642
|
* Note that TypeScript does not model an object's runtime prototype. This can
|
|
@@ -2792,18 +3700,20 @@ export const record = (key, value) => {
|
|
|
2792
3700
|
const fromUnknown = (input, options = firstValidationOptions) => validate(input, typeKey.fromUnknown, typeValue.fromUnknown, options);
|
|
2793
3701
|
const validateOutput = (input, options = firstValidationOptions) => validate(input, typeKey[outputValidationSymbol], typeValue[outputValidationSymbol], options);
|
|
2794
3702
|
const formatError = (error) => {
|
|
2795
|
-
if (error.reason.kind === "NotRecord")
|
|
2796
|
-
return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not a
|
|
2797
|
-
|
|
2798
|
-
if (error.reason.kind === "NotPlainRecord") {
|
|
3703
|
+
if (error.reason.kind === "NotRecord")
|
|
3704
|
+
return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not a Record.`;
|
|
3705
|
+
if (error.reason.kind === "NotPlainRecord")
|
|
2799
3706
|
return "The value is an object, but a Record Output must be a plain object or have a null prototype.";
|
|
2800
|
-
}
|
|
2801
3707
|
const issue = error.reason.issues[0];
|
|
2802
3708
|
switch (issue.kind) {
|
|
3709
|
+
case "Key":
|
|
3710
|
+
return `Property key ${safelyStringifyUnknownValue(issue.key)} is invalid.`;
|
|
3711
|
+
case "Value":
|
|
3712
|
+
return `The value of property ${safelyStringifyUnknownValue(issue.key)} is invalid.`;
|
|
2803
3713
|
case "Accessor":
|
|
2804
|
-
return `A
|
|
3714
|
+
return `A Record property ${safelyStringifyUnknownValue(issue.key)} must be a data property.`;
|
|
2805
3715
|
case "NonEnumerable":
|
|
2806
|
-
return `A
|
|
3716
|
+
return `A Record property ${safelyStringifyUnknownValue(issue.key)} must be enumerable.`;
|
|
2807
3717
|
case "Collision":
|
|
2808
3718
|
return `Record keys ${safelyStringifyUnknownValue(issue.previousKey)} and ${safelyStringifyUnknownValue(issue.key)} decode to the same key ${safelyStringifyUnknownValue(issue.outputKey)}.`;
|
|
2809
3719
|
}
|
|
@@ -3115,25 +4025,27 @@ const createObjectType = (props, recordType) => {
|
|
|
3115
4025
|
const fromUnknown = (value, options = firstValidationOptions) => validate(value, options, false);
|
|
3116
4026
|
const validateOutput = (value, options = firstValidationOptions) => validate(value, options, true);
|
|
3117
4027
|
const formatError = (error) => {
|
|
3118
|
-
if (error.reason.kind !== "Properties")
|
|
4028
|
+
if (error.reason.kind !== "Properties")
|
|
3119
4029
|
return formatPlainObjectRootError(error.reason);
|
|
3120
|
-
}
|
|
3121
4030
|
const key = Reflect.ownKeys(error.reason.errors).at(0);
|
|
3122
4031
|
assertNonNullable(key);
|
|
3123
4032
|
const propertyError = error.reason.errors[key];
|
|
4033
|
+
assertNonNullable(propertyError);
|
|
3124
4034
|
if (propertyError.type === "ObjectPropertyAccess") {
|
|
3125
4035
|
switch (propertyError.reason) {
|
|
3126
4036
|
case "Accessor":
|
|
3127
4037
|
return "An Object property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type.";
|
|
3128
4038
|
case "NonEnumerable":
|
|
3129
|
-
return "An Object property must be enumerable.";
|
|
4039
|
+
return "An Object property must be enumerable. Make it enumerable or use a different Type.";
|
|
3130
4040
|
}
|
|
3131
4041
|
}
|
|
3132
|
-
if (
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
4042
|
+
if (propertyError.type === "ObjectMissingProperty")
|
|
4043
|
+
return `The required property ${safelyStringifyUnknownValue(key)} is missing.`;
|
|
4044
|
+
if (typeof key === "symbol")
|
|
4045
|
+
return "An Object property key must be a string. Remove the symbol property or use a different Type.";
|
|
4046
|
+
if (propertyError.type === "ObjectExcessProperty")
|
|
4047
|
+
return `The property ${safelyStringifyUnknownValue(key)} is not allowed. Remove it or use a different Type.`;
|
|
4048
|
+
return `The property ${safelyStringifyUnknownValue(key)} is invalid.`;
|
|
3137
4049
|
};
|
|
3138
4050
|
const rootProps = createMutableRecord();
|
|
3139
4051
|
let hasNonRootType = false;
|
|
@@ -3383,6 +4295,17 @@ export const nullableToOptional = (props, ..._validation) => {
|
|
|
3383
4295
|
/**
|
|
3384
4296
|
* Object {@link Type} without the selected declared properties.
|
|
3385
4297
|
*
|
|
4298
|
+
* ### Example
|
|
4299
|
+
*
|
|
4300
|
+
* ```ts
|
|
4301
|
+
* import { String, object, omit } from "@evolu/common";
|
|
4302
|
+
*
|
|
4303
|
+
* const User = object({ name: String, password: String });
|
|
4304
|
+
* const PublicUser = omit(User, "password");
|
|
4305
|
+
*
|
|
4306
|
+
* expectOk(PublicUser.fromUnknown({ name: "Ada" }), { name: "Ada" });
|
|
4307
|
+
* ```
|
|
4308
|
+
*
|
|
3386
4309
|
* @group Objects
|
|
3387
4310
|
*/
|
|
3388
4311
|
export const omit = (objectType, ...keys) => {
|
|
@@ -3535,22 +4458,21 @@ export function discriminatedUnion(...keyOrMembers) {
|
|
|
3535
4458
|
};
|
|
3536
4459
|
const route = (value) => membersByDiscriminator.get(Reflect.get(value, key));
|
|
3537
4460
|
const formatError = (error) => {
|
|
3538
|
-
|
|
3539
|
-
switch (reason.kind) {
|
|
4461
|
+
switch (error.reason.kind) {
|
|
3540
4462
|
case "Object":
|
|
3541
|
-
return formatPlainObjectRootError(reason.error.reason);
|
|
4463
|
+
return formatPlainObjectRootError(error.reason.error.reason);
|
|
3542
4464
|
case "PropertyAccess": {
|
|
3543
|
-
const property = `The discriminator property ${safelyStringifyUnknownValue(reason.key)}`;
|
|
3544
|
-
if (reason.reason === "Accessor")
|
|
4465
|
+
const property = `The discriminator property ${safelyStringifyUnknownValue(error.reason.key)}`;
|
|
4466
|
+
if (error.reason.reason === "Accessor")
|
|
3545
4467
|
return `${property} must be a data property.`;
|
|
3546
|
-
|
|
3547
|
-
if (reason.reason === "Inherited") {
|
|
4468
|
+
if (error.reason.reason === "Inherited")
|
|
3548
4469
|
return `${property} must be an own property.`;
|
|
3549
|
-
}
|
|
3550
4470
|
return `${property} must be enumerable.`;
|
|
3551
4471
|
}
|
|
3552
4472
|
case "Discriminator":
|
|
3553
|
-
return `The discriminator property ${safelyStringifyUnknownValue(reason.key)} has an unexpected value ${safelyStringifyUnknownValue(reason.value)}.`;
|
|
4473
|
+
return `The discriminator property ${safelyStringifyUnknownValue(error.reason.key)} has an unexpected value ${safelyStringifyUnknownValue(error.reason.value)}.`;
|
|
4474
|
+
case "Member":
|
|
4475
|
+
return `The selected variant ${safelyStringifyUnknownValue(error.reason.discriminator)} is invalid.`;
|
|
3554
4476
|
}
|
|
3555
4477
|
};
|
|
3556
4478
|
const defaultFormatter = formatError;
|
|
@@ -3831,29 +4753,6 @@ const validateJsonValue = (value, options = firstValidationOptions) => {
|
|
|
3831
4753
|
},
|
|
3832
4754
|
});
|
|
3833
4755
|
};
|
|
3834
|
-
const formatJsonValueError = (error) => {
|
|
3835
|
-
const issue = error.reason.issues[0];
|
|
3836
|
-
switch (issue.kind) {
|
|
3837
|
-
case "InvalidType":
|
|
3838
|
-
return `A value ${safelyStringifyUnknownValue(issue.value)} is not a JSON value.`;
|
|
3839
|
-
case "NonFiniteNumber":
|
|
3840
|
-
return "A JSON number must be finite.";
|
|
3841
|
-
case "UnexpectedPrototype":
|
|
3842
|
-
return "The value is an object, but a JsonValue Object must be a plain object or have a null prototype.";
|
|
3843
|
-
case "Accessor":
|
|
3844
|
-
return "A JSON property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type.";
|
|
3845
|
-
case "NonEnumerable":
|
|
3846
|
-
return "A JSON Object property must be enumerable. Remove it or use a different Type.";
|
|
3847
|
-
case "SymbolProperty":
|
|
3848
|
-
return "A JSON Object property key must be a string. Remove the symbol property or use a different Type.";
|
|
3849
|
-
case "Hole":
|
|
3850
|
-
return "A JSON Array element is missing.";
|
|
3851
|
-
case "ExcessProperty":
|
|
3852
|
-
return "An excess JSON Array property is not allowed. Remove it or use a different Type.";
|
|
3853
|
-
case "CircularReference":
|
|
3854
|
-
return "A JsonValue must not contain circular references.";
|
|
3855
|
-
}
|
|
3856
|
-
};
|
|
3857
4756
|
const getJsonValueRuntimeTypeIssues = (error, mode) => {
|
|
3858
4757
|
const jsonValueError = error;
|
|
3859
4758
|
const issues = mode === "first"
|
|
@@ -3868,7 +4767,29 @@ const getJsonValueRuntimeTypeIssues = (error, mode) => {
|
|
|
3868
4767
|
reason: { kind: "Issues", issues: [issue] },
|
|
3869
4768
|
},
|
|
3870
4769
|
path: issue.path,
|
|
3871
|
-
formatError:
|
|
4770
|
+
formatError: ((error) => {
|
|
4771
|
+
const issue = error.reason.issues[0];
|
|
4772
|
+
switch (issue.kind) {
|
|
4773
|
+
case "InvalidType":
|
|
4774
|
+
return `A value ${safelyStringifyUnknownValue(issue.value)} is not a JSON value.`;
|
|
4775
|
+
case "NonFiniteNumber":
|
|
4776
|
+
return "A JSON number must be finite.";
|
|
4777
|
+
case "UnexpectedPrototype":
|
|
4778
|
+
return "The value is an object, but a JsonValue object must be a plain object or have a null prototype.";
|
|
4779
|
+
case "Accessor":
|
|
4780
|
+
return "A JSON property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type.";
|
|
4781
|
+
case "NonEnumerable":
|
|
4782
|
+
return "A JSON object property must be enumerable. Remove it or use a different Type.";
|
|
4783
|
+
case "SymbolProperty":
|
|
4784
|
+
return "A JSON object property key must be a string. Remove the symbol property or use a different Type.";
|
|
4785
|
+
case "Hole":
|
|
4786
|
+
return "A JSON array element is missing.";
|
|
4787
|
+
case "ExcessProperty":
|
|
4788
|
+
return "An excess JSON array property is not allowed. Remove it or use a different Type.";
|
|
4789
|
+
case "CircularReference":
|
|
4790
|
+
return "A JsonValue must not contain circular references.";
|
|
4791
|
+
}
|
|
4792
|
+
}),
|
|
3872
4793
|
}));
|
|
3873
4794
|
};
|
|
3874
4795
|
const parseJson = (value) => globalThis.JSON.parse(value);
|
|
@@ -3938,6 +4859,21 @@ const stringifyJsonValue = (value) => {
|
|
|
3938
4859
|
/**
|
|
3939
4860
|
* Exact root Type for {@link JsonValue} data trees.
|
|
3940
4861
|
*
|
|
4862
|
+
* ### Example
|
|
4863
|
+
*
|
|
4864
|
+
* ```ts
|
|
4865
|
+
* import { JsonValue, type JsonValueInput } from "@evolu/common";
|
|
4866
|
+
*
|
|
4867
|
+
* const input: JsonValueInput = {
|
|
4868
|
+
* name: "Ada",
|
|
4869
|
+
* scores: [10, 20],
|
|
4870
|
+
* };
|
|
4871
|
+
* const result = JsonValue.fromUnknown(input);
|
|
4872
|
+
*
|
|
4873
|
+
* expectOk(result, input);
|
|
4874
|
+
* expectTypeOf(result.value).toEqualTypeOf<JsonValue>();
|
|
4875
|
+
* ```
|
|
4876
|
+
*
|
|
3941
4877
|
* @group JSON
|
|
3942
4878
|
*/
|
|
3943
4879
|
export const JsonValue =
|
|
@@ -3958,7 +4894,7 @@ export const JsonObject = /*#__PURE__*/ record(String, JsonValue);
|
|
|
3958
4894
|
* A {@link String} Brand proving that its exact text parses to {@link JsonValue}.
|
|
3959
4895
|
*
|
|
3960
4896
|
* The Brand preserves whitespace, property order, and number spelling. Convert
|
|
3961
|
-
* it
|
|
4897
|
+
* it to {@link JsonValue} through {@link JsonValueFromJson} or
|
|
3962
4898
|
* {@link jsonToJsonValue}.
|
|
3963
4899
|
*
|
|
3964
4900
|
* @group JSON
|
|
@@ -3968,23 +4904,54 @@ export const Json = /*#__PURE__*/ brand("Json", String, (value) => {
|
|
|
3968
4904
|
return result.ok ? ok() : result;
|
|
3969
4905
|
}, (error) => `The value ${safelyStringifyUnknownValue(error.value)} cannot be parsed into a JsonValue.`);
|
|
3970
4906
|
/**
|
|
3971
|
-
*
|
|
4907
|
+
* Converts proven {@link Json} text to an exact {@link JsonValue}.
|
|
4908
|
+
*
|
|
4909
|
+
* ### Example
|
|
4910
|
+
*
|
|
4911
|
+
* ```ts
|
|
4912
|
+
* import { Json, jsonToJsonValue } from "@evolu/common";
|
|
4913
|
+
*
|
|
4914
|
+
* const value = jsonToJsonValue(Json.orThrow('{"name":"Ada"}'));
|
|
4915
|
+
*
|
|
4916
|
+
* expect(value).toEqual({ name: "Ada" });
|
|
4917
|
+
* ```
|
|
3972
4918
|
*
|
|
3973
4919
|
* @group JSON
|
|
3974
4920
|
*/
|
|
3975
4921
|
export const jsonToJsonValue = (value) => parseJson(value);
|
|
3976
4922
|
/**
|
|
3977
|
-
*
|
|
4923
|
+
* Converts an exact {@link JsonValue} to canonical {@link Json} text.
|
|
4924
|
+
*
|
|
4925
|
+
* ### Example
|
|
4926
|
+
*
|
|
4927
|
+
* ```ts
|
|
4928
|
+
* import { jsonValueToJson, type JsonValue } from "@evolu/common";
|
|
4929
|
+
*
|
|
4930
|
+
* const value: JsonValue = { name: "Ada" };
|
|
4931
|
+
*
|
|
4932
|
+
* expect(jsonValueToJson(value)).toBe('{"name":"Ada"}');
|
|
4933
|
+
* ```
|
|
3978
4934
|
*
|
|
3979
4935
|
* @group JSON
|
|
3980
4936
|
*/
|
|
3981
4937
|
export const jsonValueToJson = (value) => stringifyJsonValue(JsonValue.to(value));
|
|
3982
4938
|
/**
|
|
3983
|
-
* {@link Json}
|
|
4939
|
+
* Transformation {@link Type} that parses {@link Json} into {@link JsonValue}.
|
|
3984
4940
|
*
|
|
3985
4941
|
* Decoding unknown input first validates the Json Brand. Starting from the
|
|
3986
4942
|
* typed Json parent is infallible. Encoding canonicalizes JSON text.
|
|
3987
4943
|
*
|
|
4944
|
+
* ### Example
|
|
4945
|
+
*
|
|
4946
|
+
* ```ts
|
|
4947
|
+
* import { JsonValueFromJson } from "@evolu/common";
|
|
4948
|
+
*
|
|
4949
|
+
* const result = JsonValueFromJson.fromUnknown('{ "name": "Ada" }');
|
|
4950
|
+
*
|
|
4951
|
+
* expectOk(result, { name: "Ada" });
|
|
4952
|
+
* expect(JsonValueFromJson.to(result.value)).toBe('{"name":"Ada"}');
|
|
4953
|
+
* ```
|
|
4954
|
+
*
|
|
3988
4955
|
* @group JSON
|
|
3989
4956
|
*/
|
|
3990
4957
|
export const JsonValueFromJson = /*#__PURE__*/ transform("JsonValueFromJson", Json, JsonValue, {
|
|
@@ -3992,67 +4959,48 @@ export const JsonValueFromJson = /*#__PURE__*/ transform("JsonValueFromJson", Js
|
|
|
3992
4959
|
to: stringifyJsonValue,
|
|
3993
4960
|
});
|
|
3994
4961
|
/**
|
|
3995
|
-
* Branded {@link Json} Type and
|
|
3996
|
-
*
|
|
3997
|
-
* Use this
|
|
3998
|
-
*
|
|
3999
|
-
*
|
|
4000
|
-
*
|
|
4001
|
-
* Output.
|
|
4002
|
-
*
|
|
4003
|
-
* The supplied Type's `CanonicalInput` must be JSON-compatible. The encoder
|
|
4004
|
-
* first uses the Type's canonical `to` operation, then encodes that
|
|
4005
|
-
* representation as canonical Json. Runtime representation constraints
|
|
4006
|
-
* TypeScript cannot prove, such as dense Arrays and enumerable data properties,
|
|
4007
|
-
* are asserted as developer errors.
|
|
4008
|
-
*
|
|
4009
|
-
* The branded Json Type is the validation boundary for unknown JSON text. It
|
|
4010
|
-
* grants its {@link Brand} only when the text is valid Json and decoding it
|
|
4011
|
-
* through the supplied Type succeeds. The supplied Type is responsible for
|
|
4012
|
-
* preserving semantic Outputs across canonical JSON encoding and decoding. This
|
|
4013
|
-
* law cannot be checked generically because Types do not define semantic
|
|
4014
|
-
* equality. Before granting the Brand, the encoder asserts the weaker runtime
|
|
4015
|
-
* guarantee that the final Json successfully decodes through the supplied Type.
|
|
4016
|
-
* Failed decodability therefore throws as a developer error.
|
|
4017
|
-
*
|
|
4018
|
-
* Consequently, the two typed conversions return their values directly without
|
|
4019
|
-
* exposing a validation {@link Result}: an Output satisfying the JSON
|
|
4020
|
-
* representation contract of a correctly declared Type can always be encoded,
|
|
4021
|
-
* and the branded Json proves decoding will succeed. Decoding still runs the
|
|
4022
|
-
* Type pipeline because transformations may need to construct different Output
|
|
4023
|
-
* values.
|
|
4962
|
+
* Branded {@link Json} Type and conversions for another {@link Type}.
|
|
4963
|
+
*
|
|
4964
|
+
* Use this when a value must be stored as JSON text, such as in a JSON column
|
|
4965
|
+
* in an Evolu Schema. It returns a branded Json Type and functions for
|
|
4966
|
+
* converting the supplied Type's Output to and from that branded JSON
|
|
4967
|
+
* representation.
|
|
4024
4968
|
*
|
|
4025
4969
|
* ### Example
|
|
4026
4970
|
*
|
|
4027
4971
|
* ```ts
|
|
4028
4972
|
* import {
|
|
4029
4973
|
* Age,
|
|
4974
|
+
* NonEmptyTrimmedString100,
|
|
4030
4975
|
* json,
|
|
4031
4976
|
* object,
|
|
4032
|
-
* String,
|
|
4033
4977
|
* type Brand,
|
|
4034
|
-
* type InferType,
|
|
4035
|
-
* type Json,
|
|
4036
4978
|
* } from "@evolu/common";
|
|
4037
4979
|
*
|
|
4038
|
-
* const
|
|
4039
|
-
*
|
|
4980
|
+
* const User = object({
|
|
4981
|
+
* name: NonEmptyTrimmedString100,
|
|
4982
|
+
* age: Age,
|
|
4983
|
+
* });
|
|
4040
4984
|
*
|
|
4041
|
-
* const [
|
|
4042
|
-
*
|
|
4043
|
-
* "
|
|
4985
|
+
* const [UserJson, userToUserJson, userJsonToUser] = json(
|
|
4986
|
+
* User,
|
|
4987
|
+
* "UserJson",
|
|
4044
4988
|
* );
|
|
4045
|
-
* type PersonJson = typeof PersonJson.Output;
|
|
4046
4989
|
*
|
|
4047
|
-
*
|
|
4990
|
+
* const user = User.orThrow({ name: "Ada", age: 37 });
|
|
4991
|
+
* const userJson = userToUserJson(user);
|
|
4048
4992
|
*
|
|
4049
|
-
*
|
|
4050
|
-
*
|
|
4051
|
-
*
|
|
4052
|
-
*
|
|
4053
|
-
* expect(
|
|
4993
|
+
* expectTypeOf(userJson).toEqualTypeOf<
|
|
4994
|
+
* string & Brand<"Json"> & Brand<"UserJson">
|
|
4995
|
+
* >();
|
|
4996
|
+
* expect(userJson).toBe('{"name":"Ada","age":37}');
|
|
4997
|
+
* expect(userJsonToUser(userJson)).toEqual(user);
|
|
4054
4998
|
* ```
|
|
4055
4999
|
*
|
|
5000
|
+
* The supplied Type must have a JSON-compatible `CanonicalInput`. The branded
|
|
5001
|
+
* Json Type accepts only valid JSON text whose parsed value can be decoded by
|
|
5002
|
+
* the supplied Type.
|
|
5003
|
+
*
|
|
4056
5004
|
* @group JSON
|
|
4057
5005
|
*/
|
|
4058
5006
|
export const json = (type, name, ..._validation) => {
|