@evolu/common 7.2.3 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Buffer.d.ts +2 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +1 -0
- package/dist/src/Callbacks.d.ts +1 -1
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +1 -1
- package/dist/src/Console.d.ts +1 -9
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +1 -41
- package/dist/src/Function.d.ts +61 -0
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +19 -0
- package/dist/src/Object.d.ts +16 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +16 -0
- package/dist/src/Redacted.d.ts +89 -0
- package/dist/src/Redacted.d.ts.map +1 -0
- package/dist/src/Redacted.js +48 -0
- package/dist/src/Result.d.ts +36 -29
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Sqlite.d.ts +9 -7
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +8 -0
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +6 -4
- package/dist/src/Type.d.ts +34 -10
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +82 -202
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/local-first/Db.d.ts +1 -5
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +109 -108
- package/dist/src/local-first/Evolu.d.ts +3 -0
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +6 -7
- package/dist/src/local-first/LocalAuth.d.ts +2 -2
- package/dist/src/local-first/Owner.d.ts +7 -0
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +10 -10
- package/dist/src/local-first/Public.d.ts +6 -1
- package/dist/src/local-first/Public.d.ts.map +1 -1
- package/dist/src/local-first/Public.js +6 -1
- package/dist/src/local-first/PublicKysely.js +1 -1
- package/dist/src/local-first/Query.d.ts +54 -6
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Query.js +130 -11
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +17 -18
- package/dist/src/local-first/Schema.d.ts +15 -14
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +43 -51
- package/dist/src/local-first/Storage.d.ts +2 -2
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +2 -6
- package/dist/src/local-first/Sync.d.ts +9 -4
- package/dist/src/local-first/Sync.d.ts.map +1 -1
- package/dist/src/local-first/Sync.js +132 -59
- package/dist/src/local-first/index.d.ts +9 -2
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +9 -2
- package/package.json +3 -4
- package/src/Buffer.ts +2 -1
- package/src/Callbacks.ts +1 -1
- package/src/Console.ts +1 -10
- package/src/Function.ts +74 -0
- package/src/Object.ts +20 -0
- package/src/Redacted.ts +118 -0
- package/src/Result.ts +38 -29
- package/src/Sqlite.ts +18 -7
- package/src/Task.ts +6 -4
- package/src/Type.ts +148 -20
- package/src/index.ts +1 -0
- package/src/local-first/Db.ts +181 -209
- package/src/local-first/Evolu.ts +9 -6
- package/src/local-first/LocalAuth.ts +2 -2
- package/src/local-first/Owner.ts +8 -0
- package/src/local-first/Protocol.ts +10 -16
- package/src/local-first/Public.ts +6 -1
- package/src/local-first/PublicKysely.ts +1 -1
- package/src/local-first/Query.ts +216 -21
- package/src/local-first/Relay.ts +20 -24
- package/src/local-first/Schema.ts +83 -70
- package/src/local-first/Storage.ts +3 -8
- package/src/local-first/Sync.ts +190 -66
- package/src/local-first/index.ts +9 -2
- package/dist/src/local-first/Diff.d.ts +0 -43
- package/dist/src/local-first/Diff.d.ts.map +0 -1
- package/dist/src/local-first/Diff.js +0 -97
- package/src/local-first/Diff.ts +0 -144
package/src/Redacted.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { assert } from "./Assert.js";
|
|
2
|
+
import type { Brand } from "./Brand.js";
|
|
3
|
+
import type { Eq } from "./Eq.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A wrapper type that prevents sensitive values from being accidentally exposed
|
|
7
|
+
* through logging, serialization, or inspection.
|
|
8
|
+
*
|
|
9
|
+
* The wrapped value is hidden and can only be accessed explicitly via
|
|
10
|
+
* {@link revealRedacted}. All standard methods (`toString`, `toJSON`, and
|
|
11
|
+
* Node.js inspect) return `<redacted>`.
|
|
12
|
+
*
|
|
13
|
+
* For type-level distinction between different secrets, use branded types.
|
|
14
|
+
*
|
|
15
|
+
* The actual value lives in a `WeakMap`, so it never appears as a property and
|
|
16
|
+
* is automatically garbage collected when the wrapper is dropped. This is
|
|
17
|
+
* better than a class with a private field because private fields are still
|
|
18
|
+
* visible in devtools. Symbols can't be used because they don't support custom
|
|
19
|
+
* `toString`.
|
|
20
|
+
*
|
|
21
|
+
* Implements `Disposable` for automatic cleanup via the `using` syntax.
|
|
22
|
+
*
|
|
23
|
+
* ### Example
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* // Define branded types for your secrets
|
|
27
|
+
* type ApiKey = string & Brand<"ApiKey">;
|
|
28
|
+
* type DbPassword = string & Brand<"DbPassword">;
|
|
29
|
+
*
|
|
30
|
+
* // Wrap them with Redacted for safe passing
|
|
31
|
+
* type RedactedApiKey = Redacted<ApiKey>;
|
|
32
|
+
* type RedactedDbPassword = Redacted<DbPassword>;
|
|
33
|
+
*
|
|
34
|
+
* // Create a redacted secret
|
|
35
|
+
* const apiKey: ApiKey = "secret-123" as ApiKey;
|
|
36
|
+
* const redactedKey: RedactedApiKey = createRedacted(apiKey);
|
|
37
|
+
*
|
|
38
|
+
* console.log(redactedKey); // <redacted>
|
|
39
|
+
* console.log(revealRedacted(redactedKey)); // secret-123
|
|
40
|
+
*
|
|
41
|
+
* // Type safety: RedactedApiKey ≠ RedactedDbPassword
|
|
42
|
+
* const fetchUser = (key: RedactedApiKey) => {
|
|
43
|
+
* const value: ApiKey = revealRedacted(key);
|
|
44
|
+
* // use value...
|
|
45
|
+
* };
|
|
46
|
+
*
|
|
47
|
+
* fetchUser(redactedKey); // ✅
|
|
48
|
+
* // fetchUser(createRedacted("x" as DbPassword)); // ❌ type error
|
|
49
|
+
*
|
|
50
|
+
* // Automatic cleanup with `using`
|
|
51
|
+
* {
|
|
52
|
+
* using secret = createRedacted("sensitive" as ApiKey);
|
|
53
|
+
* // ... use secret ...
|
|
54
|
+
* } // automatically wiped from memory
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @experimental
|
|
58
|
+
*/
|
|
59
|
+
export interface Redacted<A> extends Brand<"Redacted">, Disposable {
|
|
60
|
+
/** The inner type. Useful for inference via `typeof redacted.Type`. */
|
|
61
|
+
readonly Type: A;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Creates a {@link Redacted} wrapper for a sensitive value. */
|
|
65
|
+
export const createRedacted = <A>(value: A): Redacted<A> => {
|
|
66
|
+
const redacted = Object.create(proto) as Redacted<A>;
|
|
67
|
+
registry.set(redacted, value);
|
|
68
|
+
return redacted;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const proto = {
|
|
72
|
+
toString: () => redactedString,
|
|
73
|
+
toJSON: () => redactedString,
|
|
74
|
+
[Symbol.for("nodejs.util.inspect.custom")]: () => redactedString,
|
|
75
|
+
[Symbol.dispose](this: Redacted<unknown>) {
|
|
76
|
+
registry.delete(this);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
const redactedString = "<redacted>";
|
|
80
|
+
const registry = new WeakMap<Redacted<unknown>, unknown>();
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Reveals the original value from a {@link Redacted} wrapper.
|
|
84
|
+
*
|
|
85
|
+
* This is a separate function rather than a method on {@link Redacted} to make
|
|
86
|
+
* access visually explicit and easy to grep in code reviews. Accessing
|
|
87
|
+
* sensitive values should feel intentional, not convenient.
|
|
88
|
+
*/
|
|
89
|
+
export const revealRedacted = <A>(redacted: Redacted<A>): A => {
|
|
90
|
+
assert(registry.has(redacted), "Redacted value was not in registry");
|
|
91
|
+
return registry.get(redacted) as A;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** Checks if a value is a {@link Redacted} wrapper. */
|
|
95
|
+
export const isRedacted = (value: unknown): value is Redacted<unknown> =>
|
|
96
|
+
typeof value === "object" &&
|
|
97
|
+
value !== null &&
|
|
98
|
+
Object.getPrototypeOf(value) === proto;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Creates an {@link Eq} for {@link Redacted} values based on an equality function
|
|
102
|
+
* for the underlying type.
|
|
103
|
+
*
|
|
104
|
+
* ### Example
|
|
105
|
+
*
|
|
106
|
+
* ```ts
|
|
107
|
+
* type ApiKey = string & Brand<"ApiKey">;
|
|
108
|
+
* const eqRedactedApiKey = createEqRedacted<ApiKey>(eqString);
|
|
109
|
+
*
|
|
110
|
+
* const a = createRedacted("x" as ApiKey);
|
|
111
|
+
* const b = createRedacted("x" as ApiKey);
|
|
112
|
+
* eqRedactedApiKey(a, b); // true
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export const createEqRedacted =
|
|
116
|
+
<A>(eq: Eq<A>): Eq<Redacted<A>> =>
|
|
117
|
+
(x, y) =>
|
|
118
|
+
eq(revealRedacted(x), revealRedacted(y));
|
package/src/Result.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Task } from "./Task.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* The problem with throwing an exception in JavaScript is that the caught error
|
|
3
5
|
* is always of an unknown type. The unknown type is a problem because we can't
|
|
@@ -82,19 +84,22 @@
|
|
|
82
84
|
* };
|
|
83
85
|
* ```
|
|
84
86
|
*
|
|
87
|
+
* For lazy, cancellable async operations, see {@link Task}.
|
|
88
|
+
*
|
|
85
89
|
* ### Naming convention
|
|
86
90
|
*
|
|
87
|
-
* - For values
|
|
88
|
-
* - For
|
|
89
|
-
*
|
|
91
|
+
* - **For values you need:** use a name without Result suffix (`user`, `config`)
|
|
92
|
+
* - **For void operations:** use `result` (no value to name)
|
|
93
|
+
*
|
|
94
|
+
* For multiple void operations, use block scopes to avoid potentially long
|
|
95
|
+
* names like `createBaseTablesResult`, `createRelayTablesResult`, or counters
|
|
96
|
+
* like `result1`, `result2`:
|
|
90
97
|
*
|
|
91
98
|
* ```ts
|
|
92
99
|
* const processUser = () => {
|
|
93
|
-
* // we have a value
|
|
94
100
|
* const user = getUser();
|
|
95
101
|
* if (!user.ok) return user;
|
|
96
102
|
*
|
|
97
|
-
* // single void operation
|
|
98
103
|
* const result = saveToDatabase(user.value);
|
|
99
104
|
* if (!result.ok) return result;
|
|
100
105
|
*
|
|
@@ -102,12 +107,15 @@
|
|
|
102
107
|
* };
|
|
103
108
|
*
|
|
104
109
|
* const setupDatabase = () => {
|
|
105
|
-
* //
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
110
|
+
* // Multiple void operations - use block scopes to avoid name clash
|
|
111
|
+
* {
|
|
112
|
+
* const result = createBaseTables();
|
|
113
|
+
* if (!result.ok) return result;
|
|
114
|
+
* }
|
|
115
|
+
* {
|
|
116
|
+
* const result = createRelayTables();
|
|
117
|
+
* if (!result.ok) return result;
|
|
118
|
+
* }
|
|
111
119
|
*
|
|
112
120
|
* return ok();
|
|
113
121
|
* };
|
|
@@ -124,30 +132,31 @@
|
|
|
124
132
|
* schema, and initializes the database, stopping on the first error:
|
|
125
133
|
*
|
|
126
134
|
* ```ts
|
|
127
|
-
* const
|
|
128
|
-
* const
|
|
129
|
-
* if (!
|
|
135
|
+
* const result = deps.sqlite.transaction(() => {
|
|
136
|
+
* const result = dropAllTables(deps);
|
|
137
|
+
* if (!result.ok) return result;
|
|
130
138
|
*
|
|
131
139
|
* if (message.restore) {
|
|
132
140
|
* const dbSchema = getDbSchema(deps)();
|
|
133
141
|
* if (!dbSchema.ok) return dbSchema;
|
|
134
142
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
143
|
+
* {
|
|
144
|
+
* const result = ensureDbSchema(deps)(
|
|
145
|
+
* message.restore.dbSchema,
|
|
146
|
+
* dbSchema.value,
|
|
147
|
+
* );
|
|
148
|
+
* if (!result.ok) return result;
|
|
149
|
+
* }
|
|
150
|
+
* {
|
|
151
|
+
* const result = initializeDb(deps)(message.restore.mnemonic);
|
|
152
|
+
* if (!result.ok) return result;
|
|
153
|
+
* }
|
|
145
154
|
* }
|
|
146
155
|
* return ok();
|
|
147
156
|
* });
|
|
148
157
|
*
|
|
149
|
-
* if (!
|
|
150
|
-
* deps.postMessage({ type: "onError", error:
|
|
158
|
+
* if (!result.ok) {
|
|
159
|
+
* deps.postMessage({ type: "onError", error: result.error });
|
|
151
160
|
* return;
|
|
152
161
|
* }
|
|
153
162
|
* ```
|
|
@@ -254,13 +263,13 @@
|
|
|
254
263
|
* ```ts
|
|
255
264
|
* // ✅ Safe to return void - unsafe code is wrapped and error is handled
|
|
256
265
|
* const processData = (data: string): void => {
|
|
257
|
-
* const
|
|
266
|
+
* const result = trySync(
|
|
258
267
|
* () => JSON.parse(data),
|
|
259
268
|
* (error) => ({ type: "ParseError", message: String(error) }),
|
|
260
269
|
* );
|
|
261
270
|
*
|
|
262
|
-
* if (!
|
|
263
|
-
* logError(
|
|
271
|
+
* if (!result.ok) {
|
|
272
|
+
* logError(result.error);
|
|
264
273
|
* return;
|
|
265
274
|
* }
|
|
266
275
|
*
|
package/src/Sqlite.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Brand } from "./Brand.js";
|
|
|
2
2
|
import { createLruCache } from "./Cache.js";
|
|
3
3
|
import { ConsoleDep } from "./Console.js";
|
|
4
4
|
import { EncryptionKey } from "./Crypto.js";
|
|
5
|
+
import { Eq, eqArrayNumber } from "./Eq.js";
|
|
5
6
|
import { createTransferableError, TransferableError } from "./Error.js";
|
|
6
7
|
import { err, ok, Result, tryAsync, trySync } from "./Result.js";
|
|
7
8
|
import {
|
|
@@ -34,8 +35,8 @@ export interface CreateSqliteDriverDep {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export interface SqliteDriverOptions {
|
|
37
|
-
memory?: boolean;
|
|
38
|
-
encryptionKey?: EncryptionKey | undefined;
|
|
38
|
+
readonly memory?: boolean;
|
|
39
|
+
readonly encryptionKey?: EncryptionKey | undefined;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/**
|
|
@@ -75,7 +76,7 @@ export interface SqliteQuery {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
/** A type representing a sanitized SQL string. */
|
|
78
|
-
export type SafeSql = string & Brand<"
|
|
79
|
+
export type SafeSql = string & Brand<"SafeSql">;
|
|
79
80
|
|
|
80
81
|
/**
|
|
81
82
|
* A value that can be stored in Sqlite.
|
|
@@ -86,6 +87,16 @@ export type SafeSql = string & Brand<"TimestampString">;
|
|
|
86
87
|
export const SqliteValue = union(Null, String, Number, Uint8Array);
|
|
87
88
|
export type SqliteValue = typeof SqliteValue.Type;
|
|
88
89
|
|
|
90
|
+
export const eqSqliteValue: Eq<SqliteValue> = (x, y) => {
|
|
91
|
+
if (
|
|
92
|
+
x instanceof globalThis.Uint8Array &&
|
|
93
|
+
y instanceof globalThis.Uint8Array
|
|
94
|
+
) {
|
|
95
|
+
return eqArrayNumber(x, y);
|
|
96
|
+
}
|
|
97
|
+
return x === y;
|
|
98
|
+
};
|
|
99
|
+
|
|
89
100
|
export interface SqliteQueryOptions {
|
|
90
101
|
/**
|
|
91
102
|
* If set to `true`, logs the time taken to execute the SQL query. Useful for
|
|
@@ -292,13 +303,13 @@ export const createPreparedStatementsCache = <P>(
|
|
|
292
303
|
};
|
|
293
304
|
|
|
294
305
|
export interface SqlIdentifier {
|
|
295
|
-
type: "SqlIdentifier";
|
|
296
|
-
sql: SafeSql;
|
|
306
|
+
readonly type: "SqlIdentifier";
|
|
307
|
+
readonly sql: SafeSql;
|
|
297
308
|
}
|
|
298
309
|
|
|
299
310
|
export interface RawSql {
|
|
300
|
-
type: "RawSql";
|
|
301
|
-
sql: string;
|
|
311
|
+
readonly type: "RawSql";
|
|
312
|
+
readonly sql: string;
|
|
302
313
|
}
|
|
303
314
|
|
|
304
315
|
export type SqlTemplateParam = SqliteValue | SqlIdentifier | RawSql;
|
package/src/Task.ts
CHANGED
|
@@ -570,10 +570,12 @@ export const retry = <T, E>(
|
|
|
570
570
|
}
|
|
571
571
|
|
|
572
572
|
// Wait before retry
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
573
|
+
{
|
|
574
|
+
const result = await wait(NonNegativeInt.orThrow(delay))(context);
|
|
575
|
+
if (!result.ok) {
|
|
576
|
+
// If delay was aborted, return AbortError (will be handled by toTask)
|
|
577
|
+
return result;
|
|
578
|
+
}
|
|
577
579
|
}
|
|
578
580
|
}
|
|
579
581
|
});
|
package/src/Type.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
|
+
import { utf8ToBytes } from "@noble/ciphers/utils.js";
|
|
2
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
3
|
+
import * as bip39 from "@scure/bip39";
|
|
4
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
5
|
+
import { pack } from "msgpackr";
|
|
6
|
+
import type { Brand } from "./Brand.js";
|
|
7
|
+
import type { RandomBytesDep } from "./Crypto.js";
|
|
8
|
+
import { isPlainObject } from "./Object.js";
|
|
9
|
+
import { hasNodeBuffer } from "./Platform.js";
|
|
10
|
+
import { err, getOrNull, getOrThrow, ok, Result, trySync } from "./Result.js";
|
|
11
|
+
import { safelyStringifyUnknownValue } from "./String.js";
|
|
12
|
+
import type { TimeDep } from "./Time.js";
|
|
13
|
+
import type { Literal, Refinement, Simplify, WidenLiteral } from "./Types.js";
|
|
14
|
+
import { IntentionalNever } from "./Types.js";
|
|
15
|
+
|
|
1
16
|
/**
|
|
2
|
-
* Runtime types
|
|
3
|
-
*
|
|
4
17
|
* Evolu {@link Type} is like a type guard that returns typed errors (via
|
|
5
18
|
* {@link Result}) instead of throwing. We either get a safely typed value or a
|
|
6
19
|
* composable typed error telling us exactly why validation failed.
|
|
@@ -197,25 +210,7 @@
|
|
|
197
210
|
* ```
|
|
198
211
|
*
|
|
199
212
|
* Note `minLength` and `maxLength` are curried because they are factories.
|
|
200
|
-
*
|
|
201
|
-
* @module
|
|
202
213
|
*/
|
|
203
|
-
|
|
204
|
-
import { utf8ToBytes } from "@noble/ciphers/utils.js";
|
|
205
|
-
import { sha256 } from "@noble/hashes/sha2.js";
|
|
206
|
-
import * as bip39 from "@scure/bip39";
|
|
207
|
-
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
208
|
-
import { pack } from "msgpackr";
|
|
209
|
-
import type { Brand } from "./Brand.js";
|
|
210
|
-
import { type RandomBytesDep } from "./Crypto.js";
|
|
211
|
-
import { isPlainObject } from "./Object.js";
|
|
212
|
-
import { hasNodeBuffer } from "./Platform.js";
|
|
213
|
-
import { err, getOrNull, getOrThrow, ok, Result, trySync } from "./Result.js";
|
|
214
|
-
import { safelyStringifyUnknownValue } from "./String.js";
|
|
215
|
-
import type { TimeDep } from "./Time.js";
|
|
216
|
-
import type { Literal, Refinement, Simplify, WidenLiteral } from "./Types.js";
|
|
217
|
-
import { IntentionalNever } from "./Types.js";
|
|
218
|
-
|
|
219
214
|
export interface Type<
|
|
220
215
|
Name extends TypeName,
|
|
221
216
|
/** The type this Type resolves to. */
|
|
@@ -2391,6 +2386,124 @@ export const formatArrayError = <Error extends TypeError>(
|
|
|
2391
2386
|
}
|
|
2392
2387
|
});
|
|
2393
2388
|
|
|
2389
|
+
/**
|
|
2390
|
+
* Set of a specific {@link Type}.
|
|
2391
|
+
*
|
|
2392
|
+
* ### Example
|
|
2393
|
+
*
|
|
2394
|
+
* ```ts
|
|
2395
|
+
* const NumberSet = set(Number);
|
|
2396
|
+
*
|
|
2397
|
+
* const result1 = NumberSet.from(new Set([1, 2, 3])); // ok(Set { 1, 2, 3 })
|
|
2398
|
+
* const result2 = NumberSet.from(new Set(["a", "b"])); // err(...)
|
|
2399
|
+
* ```
|
|
2400
|
+
*
|
|
2401
|
+
* @category Base Factories
|
|
2402
|
+
*/
|
|
2403
|
+
export const set = <ElementType extends AnyType>(
|
|
2404
|
+
element: ElementType,
|
|
2405
|
+
): SetType<ElementType> => {
|
|
2406
|
+
const fromUnknown = (
|
|
2407
|
+
value: unknown,
|
|
2408
|
+
): Result<
|
|
2409
|
+
ReadonlySet<InferType<ElementType>>,
|
|
2410
|
+
SetError<InferErrors<ElementType>>
|
|
2411
|
+
> => {
|
|
2412
|
+
if (!(value instanceof globalThis.Set)) {
|
|
2413
|
+
return err<SetError<InferErrors<ElementType>>>({
|
|
2414
|
+
type: "Set",
|
|
2415
|
+
value,
|
|
2416
|
+
reason: { kind: "NotSet" },
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
let index = 0;
|
|
2421
|
+
for (const item of value) {
|
|
2422
|
+
const elementResult = element.fromUnknown(item);
|
|
2423
|
+
if (!elementResult.ok) {
|
|
2424
|
+
return err<SetError<InferErrors<ElementType>>>({
|
|
2425
|
+
type: "Set",
|
|
2426
|
+
value,
|
|
2427
|
+
reason: {
|
|
2428
|
+
kind: "Element",
|
|
2429
|
+
index,
|
|
2430
|
+
error: elementResult.error as InferErrors<ElementType>,
|
|
2431
|
+
},
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
index++;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
return ok(value as ReadonlySet<InferType<ElementType>>);
|
|
2438
|
+
};
|
|
2439
|
+
|
|
2440
|
+
const fromParent = (
|
|
2441
|
+
value: ReadonlySet<InferParent<ElementType>>,
|
|
2442
|
+
): Result<
|
|
2443
|
+
ReadonlySet<InferType<ElementType>>,
|
|
2444
|
+
SetError<InferError<ElementType>>
|
|
2445
|
+
> => {
|
|
2446
|
+
let index = 0;
|
|
2447
|
+
for (const item of value) {
|
|
2448
|
+
const elementResult = element.fromParent(item);
|
|
2449
|
+
if (!elementResult.ok) {
|
|
2450
|
+
return err({
|
|
2451
|
+
type: "Set",
|
|
2452
|
+
value,
|
|
2453
|
+
reason: {
|
|
2454
|
+
kind: "Element",
|
|
2455
|
+
index,
|
|
2456
|
+
error: elementResult.error as InferError<ElementType>,
|
|
2457
|
+
},
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
index++;
|
|
2461
|
+
}
|
|
2462
|
+
return ok(value as ReadonlySet<InferType<ElementType>>);
|
|
2463
|
+
};
|
|
2464
|
+
|
|
2465
|
+
return {
|
|
2466
|
+
...createType("Set", { fromUnknown, fromParent }),
|
|
2467
|
+
element,
|
|
2468
|
+
};
|
|
2469
|
+
};
|
|
2470
|
+
|
|
2471
|
+
/** SetType extends Type with an additional `element` property for reflection. */
|
|
2472
|
+
export interface SetType<ElementType extends AnyType>
|
|
2473
|
+
extends Type<
|
|
2474
|
+
"Set",
|
|
2475
|
+
ReadonlySet<InferType<ElementType>>,
|
|
2476
|
+
ReadonlySet<InferInput<ElementType>>,
|
|
2477
|
+
SetError<InferError<ElementType>>,
|
|
2478
|
+
ReadonlySet<InferParent<ElementType>>,
|
|
2479
|
+
SetError<InferParentError<ElementType>>
|
|
2480
|
+
> {
|
|
2481
|
+
readonly element: ElementType;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
export interface SetError<Error extends TypeError = TypeError>
|
|
2485
|
+
extends TypeErrorWithReason<
|
|
2486
|
+
"Set",
|
|
2487
|
+
| { readonly kind: "NotSet" }
|
|
2488
|
+
| {
|
|
2489
|
+
readonly kind: "Element";
|
|
2490
|
+
readonly index: number;
|
|
2491
|
+
readonly error: Error;
|
|
2492
|
+
}
|
|
2493
|
+
> {}
|
|
2494
|
+
|
|
2495
|
+
export const formatSetError = <Error extends TypeError>(
|
|
2496
|
+
formatTypeError: TypeErrorFormatter<Error>,
|
|
2497
|
+
): TypeErrorFormatter<SetError<Error>> =>
|
|
2498
|
+
createTypeErrorFormatter((error) => {
|
|
2499
|
+
switch (error.reason.kind) {
|
|
2500
|
+
case "NotSet":
|
|
2501
|
+
return `Expected a Set but received ${error.value}.`;
|
|
2502
|
+
case "Element":
|
|
2503
|
+
return `Invalid element at index ${error.reason.index}: ${formatTypeError(error.reason.error)}`;
|
|
2504
|
+
}
|
|
2505
|
+
});
|
|
2506
|
+
|
|
2394
2507
|
/**
|
|
2395
2508
|
* Record of a key {@link Type} and value {@link Type}.
|
|
2396
2509
|
*
|
|
@@ -3970,6 +4083,7 @@ export type TypeErrors<ExtraErrors extends TypeError = never> =
|
|
|
3970
4083
|
| ExtraErrors
|
|
3971
4084
|
// Composite errors
|
|
3972
4085
|
| ArrayError<TypeErrors<ExtraErrors>>
|
|
4086
|
+
| SetError<TypeErrors<ExtraErrors>>
|
|
3973
4087
|
| RecordError<TypeErrors<ExtraErrors>, TypeErrors<ExtraErrors>>
|
|
3974
4088
|
| ObjectError<Record<string, TypeErrors<ExtraErrors>>>
|
|
3975
4089
|
| ObjectWithRecordError<
|
|
@@ -4154,6 +4268,8 @@ export const createFormatTypeError = <ExtraErrors extends TypeError = never>(
|
|
|
4154
4268
|
return formatSimplePasswordError(formatTypeError)(error);
|
|
4155
4269
|
case "Array":
|
|
4156
4270
|
return formatArrayError(formatTypeError)(error);
|
|
4271
|
+
case "Set":
|
|
4272
|
+
return formatSetError(formatTypeError)(error);
|
|
4157
4273
|
case "Record":
|
|
4158
4274
|
return formatRecordError(formatTypeError)(error);
|
|
4159
4275
|
case "Object":
|
|
@@ -4202,6 +4318,18 @@ export const typeErrorToStandardSchemaIssues = <
|
|
|
4202
4318
|
);
|
|
4203
4319
|
}
|
|
4204
4320
|
|
|
4321
|
+
if (error.type === "Set") {
|
|
4322
|
+
const setError = error as SetError;
|
|
4323
|
+
if (setError.reason.kind === "NotSet") {
|
|
4324
|
+
return [{ message: formatTypeError(error), path }];
|
|
4325
|
+
}
|
|
4326
|
+
return typeErrorToStandardSchemaIssues(
|
|
4327
|
+
setError.reason.error as TypeErrors<ExtraErrors>,
|
|
4328
|
+
formatTypeError,
|
|
4329
|
+
[...path, setError.reason.index],
|
|
4330
|
+
);
|
|
4331
|
+
}
|
|
4332
|
+
|
|
4205
4333
|
if (error.type === "Object") {
|
|
4206
4334
|
const objectError = error as ObjectError;
|
|
4207
4335
|
if (
|
package/src/index.ts
CHANGED