@evolu/common 8.8.0 → 8.10.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/Assert.d.ts +21 -1
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +31 -27
- package/dist/src/Bytes.d.ts +647 -0
- package/dist/src/Bytes.d.ts.map +1 -0
- package/dist/src/{Binary.js → Bytes.js} +266 -16
- package/dist/src/Config.d.ts +142 -0
- package/dist/src/Config.d.ts.map +1 -0
- package/dist/src/Config.js +181 -0
- package/dist/src/Fs.d.ts +302 -0
- package/dist/src/Fs.d.ts.map +1 -0
- package/dist/src/Fs.js +111 -0
- package/dist/src/Number.d.ts +50 -7
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +47 -8
- package/dist/src/Object.d.ts +32 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +41 -0
- package/dist/src/Polyfills.d.ts +6 -0
- package/dist/src/Polyfills.d.ts.map +1 -1
- package/dist/src/Polyfills.js +6 -0
- package/dist/src/Sqlite.js +1 -1
- package/dist/src/Time.d.ts +74 -12
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +40 -2
- package/dist/src/Type.d.ts +1451 -79
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +1303 -96
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/intl/_en.d.ts +24 -1
- package/dist/src/intl/_en.d.ts.map +1 -1
- package/dist/src/intl/_en.js +20 -0
- package/dist/src/intl/ar.d.ts +24 -1
- package/dist/src/intl/ar.d.ts.map +1 -1
- package/dist/src/intl/ar.js +20 -0
- package/dist/src/intl/bn.d.ts +24 -1
- package/dist/src/intl/bn.d.ts.map +1 -1
- package/dist/src/intl/bn.js +20 -0
- package/dist/src/intl/ca.d.ts +24 -1
- package/dist/src/intl/ca.d.ts.map +1 -1
- package/dist/src/intl/ca.js +20 -0
- package/dist/src/intl/cs.d.ts +24 -1
- package/dist/src/intl/cs.d.ts.map +1 -1
- package/dist/src/intl/cs.js +20 -0
- package/dist/src/intl/da.d.ts +24 -1
- package/dist/src/intl/da.d.ts.map +1 -1
- package/dist/src/intl/da.js +20 -0
- package/dist/src/intl/de.d.ts +24 -1
- package/dist/src/intl/de.d.ts.map +1 -1
- package/dist/src/intl/de.js +20 -0
- package/dist/src/intl/el.d.ts +24 -1
- package/dist/src/intl/el.d.ts.map +1 -1
- package/dist/src/intl/el.js +20 -0
- package/dist/src/intl/es.d.ts +24 -1
- package/dist/src/intl/es.d.ts.map +1 -1
- package/dist/src/intl/es.js +20 -0
- package/dist/src/intl/fa.d.ts +24 -1
- package/dist/src/intl/fa.d.ts.map +1 -1
- package/dist/src/intl/fa.js +20 -0
- package/dist/src/intl/fi.d.ts +24 -1
- package/dist/src/intl/fi.d.ts.map +1 -1
- package/dist/src/intl/fi.js +20 -0
- package/dist/src/intl/fil.d.ts +24 -1
- package/dist/src/intl/fil.d.ts.map +1 -1
- package/dist/src/intl/fil.js +20 -0
- package/dist/src/intl/fr.d.ts +24 -1
- package/dist/src/intl/fr.d.ts.map +1 -1
- package/dist/src/intl/fr.js +20 -0
- package/dist/src/intl/he.d.ts +24 -1
- package/dist/src/intl/he.d.ts.map +1 -1
- package/dist/src/intl/he.js +20 -0
- package/dist/src/intl/hi.d.ts +24 -1
- package/dist/src/intl/hi.d.ts.map +1 -1
- package/dist/src/intl/hi.js +20 -0
- package/dist/src/intl/hr.d.ts +24 -1
- package/dist/src/intl/hr.d.ts.map +1 -1
- package/dist/src/intl/hr.js +20 -0
- package/dist/src/intl/hu.d.ts +22 -1
- package/dist/src/intl/hu.d.ts.map +1 -1
- package/dist/src/intl/hu.js +18 -0
- package/dist/src/intl/id.d.ts +24 -1
- package/dist/src/intl/id.d.ts.map +1 -1
- package/dist/src/intl/id.js +20 -0
- package/dist/src/intl/it.d.ts +24 -1
- package/dist/src/intl/it.d.ts.map +1 -1
- package/dist/src/intl/it.js +20 -0
- package/dist/src/intl/ja.d.ts +24 -1
- package/dist/src/intl/ja.d.ts.map +1 -1
- package/dist/src/intl/ja.js +20 -0
- package/dist/src/intl/ko.d.ts +24 -1
- package/dist/src/intl/ko.d.ts.map +1 -1
- package/dist/src/intl/ko.js +20 -0
- package/dist/src/intl/ml.d.ts +24 -1
- package/dist/src/intl/ml.d.ts.map +1 -1
- package/dist/src/intl/ml.js +20 -0
- package/dist/src/intl/mr.d.ts +24 -1
- package/dist/src/intl/mr.d.ts.map +1 -1
- package/dist/src/intl/mr.js +20 -0
- package/dist/src/intl/ms.d.ts +24 -1
- package/dist/src/intl/ms.d.ts.map +1 -1
- package/dist/src/intl/ms.js +20 -0
- package/dist/src/intl/nb.d.ts +22 -1
- package/dist/src/intl/nb.d.ts.map +1 -1
- package/dist/src/intl/nb.js +18 -0
- package/dist/src/intl/nl.d.ts +24 -1
- package/dist/src/intl/nl.d.ts.map +1 -1
- package/dist/src/intl/nl.js +20 -0
- package/dist/src/intl/pa.d.ts +24 -1
- package/dist/src/intl/pa.d.ts.map +1 -1
- package/dist/src/intl/pa.js +20 -0
- package/dist/src/intl/pl.d.ts +23 -0
- package/dist/src/intl/pl.d.ts.map +1 -1
- package/dist/src/intl/pl.js +20 -0
- package/dist/src/intl/pt-BR.d.ts +24 -1
- package/dist/src/intl/pt-BR.d.ts.map +1 -1
- package/dist/src/intl/pt-BR.js +20 -0
- package/dist/src/intl/pt.d.ts +24 -1
- package/dist/src/intl/pt.d.ts.map +1 -1
- package/dist/src/intl/pt.js +20 -0
- package/dist/src/intl/ro.d.ts +24 -1
- package/dist/src/intl/ro.d.ts.map +1 -1
- package/dist/src/intl/ro.js +20 -0
- package/dist/src/intl/sk.d.ts +24 -1
- package/dist/src/intl/sk.d.ts.map +1 -1
- package/dist/src/intl/sk.js +20 -0
- package/dist/src/intl/sl.d.ts +24 -1
- package/dist/src/intl/sl.d.ts.map +1 -1
- package/dist/src/intl/sl.js +20 -0
- package/dist/src/intl/sv.d.ts +24 -1
- package/dist/src/intl/sv.d.ts.map +1 -1
- package/dist/src/intl/sv.js +20 -0
- package/dist/src/intl/sw.d.ts +21 -0
- package/dist/src/intl/sw.d.ts.map +1 -1
- package/dist/src/intl/sw.js +18 -0
- package/dist/src/intl/ta.d.ts +24 -1
- package/dist/src/intl/ta.d.ts.map +1 -1
- package/dist/src/intl/ta.js +20 -0
- package/dist/src/intl/te.d.ts +24 -1
- package/dist/src/intl/te.d.ts.map +1 -1
- package/dist/src/intl/te.js +20 -0
- package/dist/src/intl/th.d.ts +24 -1
- package/dist/src/intl/th.d.ts.map +1 -1
- package/dist/src/intl/th.js +20 -0
- package/dist/src/intl/tr.d.ts +24 -1
- package/dist/src/intl/tr.d.ts.map +1 -1
- package/dist/src/intl/tr.js +20 -0
- package/dist/src/intl/uk.d.ts +80 -57
- package/dist/src/intl/uk.d.ts.map +1 -1
- package/dist/src/intl/uk.js +174 -149
- package/dist/src/intl/ur.d.ts +24 -1
- package/dist/src/intl/ur.d.ts.map +1 -1
- package/dist/src/intl/ur.js +20 -0
- package/dist/src/intl/vi.d.ts +24 -1
- package/dist/src/intl/vi.d.ts.map +1 -1
- package/dist/src/intl/vi.js +20 -0
- package/dist/src/intl/zh-CN.d.ts +24 -1
- package/dist/src/intl/zh-CN.d.ts.map +1 -1
- package/dist/src/intl/zh-CN.js +20 -0
- package/dist/src/intl/zh-TW.d.ts +24 -1
- package/dist/src/intl/zh-TW.d.ts.map +1 -1
- package/dist/src/intl/zh-TW.js +20 -0
- package/dist/src/local-first/Protocol.d.ts +1 -1
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +1 -1
- package/dist/src/local-first/Schema.d.ts +10 -0
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +1 -1
- package/dist/src/local-first/Timestamp.js +1 -1
- package/package.json +1 -1
- package/src/Assert.test.ts +51 -0
- package/src/Assert.ts +56 -4
- package/src/{Binary.test.ts → Bytes.test.ts} +286 -1
- package/src/{Binary.ts → Bytes.ts} +652 -21
- package/src/Config.test.ts +672 -0
- package/src/Config.ts +410 -0
- package/src/Fs.test.ts +105 -0
- package/src/Fs.ts +414 -0
- package/src/Number.test.ts +82 -18
- package/src/Number.ts +76 -8
- package/src/Object.test.ts +115 -1
- package/src/Object.ts +44 -0
- package/src/Polyfills.ts +6 -0
- package/src/Sqlite.ts +1 -1
- package/src/Time.test.ts +82 -11
- package/src/Time.ts +141 -16
- package/src/Type.test.ts +3114 -157
- package/src/Type.ts +2553 -196
- package/src/index.ts +3 -1
- package/src/intl/_en.ts +70 -0
- package/src/intl/ar.ts +71 -0
- package/src/intl/bn.ts +70 -0
- package/src/intl/ca.ts +70 -0
- package/src/intl/cs.ts +70 -0
- package/src/intl/da.ts +70 -0
- package/src/intl/de.ts +70 -0
- package/src/intl/el.ts +70 -0
- package/src/intl/es.ts +70 -0
- package/src/intl/fa.ts +70 -0
- package/src/intl/fi.ts +70 -0
- package/src/intl/fil.ts +70 -0
- package/src/intl/fr.ts +70 -0
- package/src/intl/he.ts +70 -0
- package/src/intl/hi.ts +70 -0
- package/src/intl/hr.ts +70 -0
- package/src/intl/hu.ts +69 -0
- package/src/intl/id.ts +70 -0
- package/src/intl/intl.test.ts +819 -1
- package/src/intl/it.ts +70 -0
- package/src/intl/ja.ts +70 -0
- package/src/intl/ko.ts +68 -0
- package/src/intl/ml.ts +70 -0
- package/src/intl/mr.ts +70 -0
- package/src/intl/ms.ts +71 -0
- package/src/intl/nb.ts +69 -0
- package/src/intl/nl.ts +70 -0
- package/src/intl/pa.ts +70 -0
- package/src/intl/pl.ts +63 -0
- package/src/intl/pt-BR.ts +70 -0
- package/src/intl/pt.ts +71 -0
- package/src/intl/ro.ts +70 -0
- package/src/intl/sk.ts +71 -0
- package/src/intl/sl.ts +70 -0
- package/src/intl/sv.ts +70 -0
- package/src/intl/sw.ts +62 -0
- package/src/intl/ta.ts +70 -0
- package/src/intl/te.ts +70 -0
- package/src/intl/th.ts +68 -0
- package/src/intl/tr.ts +70 -0
- package/src/intl/uk.ts +228 -155
- package/src/intl/ur.ts +70 -0
- package/src/intl/vi.ts +70 -0
- package/src/intl/zh-CN.ts +68 -0
- package/src/intl/zh-TW.ts +68 -0
- package/src/local-first/Protocol.test.ts +1 -1
- package/src/local-first/Protocol.ts +1 -1
- package/src/local-first/Schema.ts +11 -0
- package/src/local-first/Storage.ts +1 -1
- package/src/local-first/Timestamp.ts +1 -1
- package/dist/src/Binary.d.ts +0 -254
- package/dist/src/Binary.d.ts.map +0 -1
package/src/Config.ts
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict configuration Types with explicit application policy.
|
|
3
|
+
*
|
|
4
|
+
* Decode configuration at startup, then pass validated values to constructors
|
|
5
|
+
* or Tasks. The application decides how to load configuration, combine sources,
|
|
6
|
+
* and apply defaults. Declare defaults with {@link withDefault}, or use `??`
|
|
7
|
+
* where a value is consumed. Invalid supplied values still fail decoding.
|
|
8
|
+
*
|
|
9
|
+
* Resolve source precedence before replacing absence with defaults. If a source
|
|
10
|
+
* Type already provides defaults and later composition needs to distinguish
|
|
11
|
+
* them from supplied values, use `strategy: "preserve"` and inspect
|
|
12
|
+
* `defaultUsed` explicitly. Preserving this evidence does not choose a merging
|
|
13
|
+
* policy: the application still decides which source wins.
|
|
14
|
+
*
|
|
15
|
+
* {@link env} creates a pure, reversible codec. It does not read global state,
|
|
16
|
+
* load files, merge sources, or introduce a Run dependency. Tests can pass
|
|
17
|
+
* ordinary objects; a Node.js entry point can pass `process.env`.
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { assert } from "./Assert.ts";
|
|
23
|
+
import {
|
|
24
|
+
createMutableRecord,
|
|
25
|
+
filterObjectKeys,
|
|
26
|
+
isPlainObject,
|
|
27
|
+
} from "./Object.ts";
|
|
28
|
+
import { ok } from "./Result.ts";
|
|
29
|
+
import {
|
|
30
|
+
CamelCaseIdentifier,
|
|
31
|
+
camelCaseToConstantCase,
|
|
32
|
+
ConstantCaseIdentifier,
|
|
33
|
+
EvoluType,
|
|
34
|
+
maxLength,
|
|
35
|
+
object,
|
|
36
|
+
objectKeys,
|
|
37
|
+
String,
|
|
38
|
+
transform,
|
|
39
|
+
Unknown,
|
|
40
|
+
type AnyType,
|
|
41
|
+
type InferErrors,
|
|
42
|
+
type ValidateLiteral,
|
|
43
|
+
type ObjectKeysType,
|
|
44
|
+
type ObjectProps,
|
|
45
|
+
type ObjectType,
|
|
46
|
+
type TransformType,
|
|
47
|
+
type TypeNode,
|
|
48
|
+
type withDefault,
|
|
49
|
+
} from "./Type.ts";
|
|
50
|
+
import type {
|
|
51
|
+
CompileTimeError,
|
|
52
|
+
IsUnion,
|
|
53
|
+
UnionToIntersection,
|
|
54
|
+
} from "./Types.ts";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* An environment variable name in Evolu's configuration convention.
|
|
58
|
+
*
|
|
59
|
+
* Names are {@link ConstantCaseIdentifier}s of at most 255 characters, including
|
|
60
|
+
* the application prefix. This is an Evolu convention, not an operating-system
|
|
61
|
+
* limit. Double underscores do not introduce nesting.
|
|
62
|
+
*
|
|
63
|
+
* ### Example
|
|
64
|
+
*
|
|
65
|
+
* ```ts
|
|
66
|
+
* import { assertErr, assertOk, EnvName } from "@evolu/common";
|
|
67
|
+
*
|
|
68
|
+
* assertOk(
|
|
69
|
+
* EnvName.fromUnknown("APP_MAX_OWNER_BYTES"),
|
|
70
|
+
* "APP_MAX_OWNER_BYTES",
|
|
71
|
+
* );
|
|
72
|
+
* assertErr(EnvName.fromUnknown("APP_maxOwnerBytes"));
|
|
73
|
+
* assertErr(EnvName.fromUnknown("APP_DB__PORT"));
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export const EnvName = /*#__PURE__*/ maxLength(255)(ConstantCaseIdentifier);
|
|
77
|
+
export type EnvName = typeof EnvName.Output;
|
|
78
|
+
|
|
79
|
+
/** Fields and one-level namespace groups used to construct an {@link env} Type. */
|
|
80
|
+
export type EnvProps = Readonly<
|
|
81
|
+
Record<string, ObjectProps[string] | ObjectProps>
|
|
82
|
+
>;
|
|
83
|
+
|
|
84
|
+
// Classify declarations by casing; identifier Types validate the full grammar
|
|
85
|
+
// and name lengths during construction.
|
|
86
|
+
type EnvDeclarationKind<Key> = Key extends string
|
|
87
|
+
? Key extends Uppercase<Key>
|
|
88
|
+
? "group"
|
|
89
|
+
: Key extends Uncapitalize<Key>
|
|
90
|
+
? "field"
|
|
91
|
+
: "invalid"
|
|
92
|
+
: "invalid";
|
|
93
|
+
|
|
94
|
+
type EnvFlatProps<Props extends EnvProps> =
|
|
95
|
+
UnionToIntersection<
|
|
96
|
+
{
|
|
97
|
+
[Key in keyof Props]: EnvDeclarationKind<Key> extends "group"
|
|
98
|
+
? Props[Key]
|
|
99
|
+
: EnvDeclarationKind<Key> extends "field"
|
|
100
|
+
? { readonly [Field in Key]: Props[Key] }
|
|
101
|
+
: never;
|
|
102
|
+
}[keyof Props]
|
|
103
|
+
> extends infer Flat extends ObjectProps
|
|
104
|
+
? Flat
|
|
105
|
+
: {};
|
|
106
|
+
|
|
107
|
+
type EnvFieldType<Field> = Field extends TypeNode
|
|
108
|
+
? Field
|
|
109
|
+
: Field extends { readonly type: infer T extends TypeNode }
|
|
110
|
+
? T
|
|
111
|
+
: never;
|
|
112
|
+
|
|
113
|
+
type EnvKeysValidation<Props> =
|
|
114
|
+
| (string extends keyof Props
|
|
115
|
+
? CompileTimeError<
|
|
116
|
+
"Env",
|
|
117
|
+
"Environment properties must use fixed string keys."
|
|
118
|
+
>
|
|
119
|
+
: never)
|
|
120
|
+
| (IsUnion<Props> extends true
|
|
121
|
+
? CompileTimeError<
|
|
122
|
+
"Env",
|
|
123
|
+
"Environment properties must use one concrete schema."
|
|
124
|
+
>
|
|
125
|
+
: never)
|
|
126
|
+
| {
|
|
127
|
+
[Key in keyof Props]: Key extends string
|
|
128
|
+
? ValidateLiteral<Key> extends Key
|
|
129
|
+
? never
|
|
130
|
+
: CompileTimeError<
|
|
131
|
+
"Env",
|
|
132
|
+
"Environment properties must use fixed string keys."
|
|
133
|
+
>
|
|
134
|
+
: CompileTimeError<
|
|
135
|
+
"Env",
|
|
136
|
+
"Environment properties must use fixed string keys."
|
|
137
|
+
>;
|
|
138
|
+
}[keyof Props];
|
|
139
|
+
|
|
140
|
+
type EnvFieldValidation<Field> =
|
|
141
|
+
IsUnion<Field> extends true
|
|
142
|
+
? CompileTimeError<"Env", "Environment fields must use one concrete Type.">
|
|
143
|
+
: EnvFieldType<Field> extends infer T extends AnyType
|
|
144
|
+
? IsUnion<T> extends true
|
|
145
|
+
? CompileTimeError<
|
|
146
|
+
"Env",
|
|
147
|
+
"Environment fields must use one concrete Type."
|
|
148
|
+
>
|
|
149
|
+
: [T["CanonicalInput"]] extends [string]
|
|
150
|
+
? Extract<
|
|
151
|
+
| "ObjectMissingProperty"
|
|
152
|
+
| "ObjectPropertyAccess"
|
|
153
|
+
| "ObjectExcessProperty",
|
|
154
|
+
InferErrors<T>["type"]
|
|
155
|
+
> extends never
|
|
156
|
+
? never
|
|
157
|
+
: CompileTimeError<
|
|
158
|
+
"Env",
|
|
159
|
+
"Environment fields must not use error tags reserved for Object structure."
|
|
160
|
+
>
|
|
161
|
+
: CompileTimeError<
|
|
162
|
+
"Env",
|
|
163
|
+
"Environment fields must encode to strings."
|
|
164
|
+
>
|
|
165
|
+
: CompileTimeError<
|
|
166
|
+
"Env",
|
|
167
|
+
"Environment fields must use one concrete Type."
|
|
168
|
+
>;
|
|
169
|
+
|
|
170
|
+
type EnvFieldsValidation<Props extends ObjectProps> =
|
|
171
|
+
| EnvKeysValidation<Props>
|
|
172
|
+
| {
|
|
173
|
+
[Key in keyof Props]: EnvDeclarationKind<Key> extends "field"
|
|
174
|
+
? EnvFieldValidation<Props[Key]>
|
|
175
|
+
: CompileTimeError<
|
|
176
|
+
"Env",
|
|
177
|
+
"Environment fields must use camelCase names."
|
|
178
|
+
>;
|
|
179
|
+
}[keyof Props];
|
|
180
|
+
|
|
181
|
+
type EnvValidation<Props extends EnvProps> =
|
|
182
|
+
| EnvKeysValidation<Props>
|
|
183
|
+
| {
|
|
184
|
+
[Key in keyof Props]: EnvDeclarationKind<Key> extends "group"
|
|
185
|
+
? Props[Key] extends ObjectProps
|
|
186
|
+
? EnvFieldsValidation<Props[Key]>
|
|
187
|
+
: CompileTimeError<
|
|
188
|
+
"Env",
|
|
189
|
+
"Environment namespaces must contain a group of fields."
|
|
190
|
+
>
|
|
191
|
+
: EnvDeclarationKind<Key> extends "field"
|
|
192
|
+
? Props[Key] extends ObjectProps[string]
|
|
193
|
+
? EnvFieldValidation<Props[Key]>
|
|
194
|
+
: CompileTimeError<
|
|
195
|
+
"Env",
|
|
196
|
+
"Environment fields must be Types, optional properties, or defaulted properties."
|
|
197
|
+
>
|
|
198
|
+
: CompileTimeError<
|
|
199
|
+
"Env",
|
|
200
|
+
"Environment declarations must use camelCase field names or CONSTANT_CASE namespace names."
|
|
201
|
+
>;
|
|
202
|
+
}[keyof Props];
|
|
203
|
+
|
|
204
|
+
type EnvKeyType = TransformType<
|
|
205
|
+
typeof String,
|
|
206
|
+
typeof CamelCaseIdentifier,
|
|
207
|
+
"EnvKey",
|
|
208
|
+
never,
|
|
209
|
+
string
|
|
210
|
+
>;
|
|
211
|
+
|
|
212
|
+
/** The configuration codec returned by {@link env}. */
|
|
213
|
+
export interface EnvType<Props extends EnvProps> extends TransformType<
|
|
214
|
+
typeof Unknown,
|
|
215
|
+
ObjectKeysType<EnvKeyType, ObjectType<EnvFlatProps<Props>>>,
|
|
216
|
+
"Env",
|
|
217
|
+
never,
|
|
218
|
+
Readonly<
|
|
219
|
+
Record<
|
|
220
|
+
string,
|
|
221
|
+
ObjectType<EnvFlatProps<Props>>["CanonicalInput"][keyof ObjectType<
|
|
222
|
+
EnvFlatProps<Props>
|
|
223
|
+
>["CanonicalInput"]]
|
|
224
|
+
>
|
|
225
|
+
>
|
|
226
|
+
> {}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Creates a reversible environment-variable codec with a flat decoded output.
|
|
230
|
+
*
|
|
231
|
+
* Declare camelCase fields directly, or put them in one-level CONSTANT_CASE
|
|
232
|
+
* namespace groups. A direct `port` field reads `PORT`; a `maxOwnerBytes` field
|
|
233
|
+
* in an `EVOLU_RELAY` group reads `EVOLU_RELAY_MAX_OWNER_BYTES`. Groups affect
|
|
234
|
+
* external names only. Duplicate decoded fields or encoded names are rejected
|
|
235
|
+
* during construction, even when their Types are identical.
|
|
236
|
+
*
|
|
237
|
+
* Selects exact declared unprefixed names and all keys within declared
|
|
238
|
+
* namespaces. Namespace selection ignores casing so incorrectly cased names
|
|
239
|
+
* fail validation instead of being ignored. Selected names must match their
|
|
240
|
+
* exact declared spelling. Unrelated variables are ignored; misspelled
|
|
241
|
+
* unprefixed names or namespace prefixes can therefore still look absent.
|
|
242
|
+
*
|
|
243
|
+
* Field Types must encode to strings. Optional fields may be absent, but
|
|
244
|
+
* explicit `undefined` values are rejected. Empty strings remain present and
|
|
245
|
+
* must satisfy the field Type. Compose {@link withDefault} to supply defaults.
|
|
246
|
+
* Encoding emits canonical external names and string values, preserving absence
|
|
247
|
+
* when the field codec does. Errors retain external names and nested paths.
|
|
248
|
+
*
|
|
249
|
+
* Reads own string properties from any non-null object, including
|
|
250
|
+
* `process.env`. Selected properties must be enumerable data properties;
|
|
251
|
+
* getters are never read. Prototypes and internal contents such as Map entries
|
|
252
|
+
* are ignored. Non-object inputs fail with standard object errors. Schema names
|
|
253
|
+
* must satisfy {@link EnvName}; groups cannot be nested. Loading and source
|
|
254
|
+
* precedence remain explicit application code.
|
|
255
|
+
*
|
|
256
|
+
* ### Example
|
|
257
|
+
*
|
|
258
|
+
* ```ts
|
|
259
|
+
* import {
|
|
260
|
+
* assertEqual,
|
|
261
|
+
* assertErr,
|
|
262
|
+
* assertOk,
|
|
263
|
+
* assertType,
|
|
264
|
+
* ByteSizeLiteral,
|
|
265
|
+
* env,
|
|
266
|
+
* optional,
|
|
267
|
+
* Port,
|
|
268
|
+
* PortFromString,
|
|
269
|
+
* withDefault,
|
|
270
|
+
* } from "@evolu/common";
|
|
271
|
+
*
|
|
272
|
+
* const RelayEnv = env({
|
|
273
|
+
* port: withDefault(optional(PortFromString), Port.orThrow(4000)),
|
|
274
|
+
* EVOLU_RELAY: {
|
|
275
|
+
* maxOwnerBytes: optional(ByteSizeLiteral),
|
|
276
|
+
* },
|
|
277
|
+
* });
|
|
278
|
+
*
|
|
279
|
+
* const result = RelayEnv.fromUnknown({
|
|
280
|
+
* PORT: "04000",
|
|
281
|
+
* EVOLU_RELAY_MAX_OWNER_BYTES: "512KiB",
|
|
282
|
+
* HOME: "/home/evolu",
|
|
283
|
+
* });
|
|
284
|
+
*
|
|
285
|
+
* assertOk(result, { port: 4000, maxOwnerBytes: "512KiB" });
|
|
286
|
+
* assertType<typeof result.value.port, Port>();
|
|
287
|
+
* assertEqual(RelayEnv.to(result.value), {
|
|
288
|
+
* PORT: "4000",
|
|
289
|
+
* EVOLU_RELAY_MAX_OWNER_BYTES: "512KiB",
|
|
290
|
+
* });
|
|
291
|
+
*
|
|
292
|
+
* assertOk(RelayEnv.fromUnknown({}), { port: 4000 });
|
|
293
|
+
* assertErr(RelayEnv.fromUnknown({ PORT: "" }));
|
|
294
|
+
* assertErr(RelayEnv.fromUnknown({ EVOLU_RELAY_MAX_OWENR_BYTES: "1MiB" }));
|
|
295
|
+
* assertErr(RelayEnv.fromUnknown({ evolu_relay_max_owner_bytes: "1MiB" }));
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
export const env = <const Props extends EnvProps>(
|
|
299
|
+
props: Props,
|
|
300
|
+
..._validation: [EnvValidation<Props>] extends [never]
|
|
301
|
+
? []
|
|
302
|
+
: [EnvValidation<Props>]
|
|
303
|
+
): EnvType<Props> => {
|
|
304
|
+
assert(
|
|
305
|
+
isPlainObject(props),
|
|
306
|
+
"Environment properties must be a plain object.",
|
|
307
|
+
);
|
|
308
|
+
const flat = createMutableRecord<string, ObjectProps[string]>();
|
|
309
|
+
const inputNameByOutputName = new Map<string, string>();
|
|
310
|
+
const outputNameByInputName = new Map<string, string>();
|
|
311
|
+
const prefixes: Array<string> = [];
|
|
312
|
+
const unprefixedNames = new Set<string>();
|
|
313
|
+
|
|
314
|
+
for (const rootName of Reflect.ownKeys(props)) {
|
|
315
|
+
const descriptor = Object.getOwnPropertyDescriptor(props, rootName)!;
|
|
316
|
+
assert(
|
|
317
|
+
typeof rootName === "string" &&
|
|
318
|
+
descriptor.enumerable &&
|
|
319
|
+
"value" in descriptor,
|
|
320
|
+
"Environment properties must be enumerable string data properties.",
|
|
321
|
+
);
|
|
322
|
+
const grouped = ConstantCaseIdentifier.is(rootName);
|
|
323
|
+
assert(
|
|
324
|
+
grouped || CamelCaseIdentifier.is(rootName),
|
|
325
|
+
`Invalid environment schema name ${JSON.stringify(rootName)}. Expected a camelCase field or CONSTANT_CASE namespace.`,
|
|
326
|
+
);
|
|
327
|
+
const rootValue: unknown = descriptor.value;
|
|
328
|
+
const fields = grouped ? rootValue : { [rootName]: rootValue };
|
|
329
|
+
assert(
|
|
330
|
+
isPlainObject(fields) && !EvoluType.is(fields),
|
|
331
|
+
"Environment groups must be plain objects containing fields.",
|
|
332
|
+
);
|
|
333
|
+
if (grouped) {
|
|
334
|
+
assert(
|
|
335
|
+
EnvName.is(rootName + "_A"),
|
|
336
|
+
`Invalid environment namespace ${JSON.stringify(rootName)}. Names must fit within 255 characters.`,
|
|
337
|
+
);
|
|
338
|
+
prefixes.push(rootName + "_");
|
|
339
|
+
}
|
|
340
|
+
for (const name of Reflect.ownKeys(fields)) {
|
|
341
|
+
const field = Object.getOwnPropertyDescriptor(fields, name)!;
|
|
342
|
+
assert(
|
|
343
|
+
typeof name === "string" && field.enumerable && "value" in field,
|
|
344
|
+
"Environment fields must be enumerable string data properties.",
|
|
345
|
+
);
|
|
346
|
+
assert(
|
|
347
|
+
CamelCaseIdentifier.is(name),
|
|
348
|
+
`Invalid environment field ${JSON.stringify(name)}. Expected a camelCase identifier; groups cannot be nested.`,
|
|
349
|
+
);
|
|
350
|
+
assert(
|
|
351
|
+
!inputNameByOutputName.has(name),
|
|
352
|
+
`Duplicate environment field ${JSON.stringify(name)}.`,
|
|
353
|
+
);
|
|
354
|
+
const encoded =
|
|
355
|
+
(grouped ? rootName + "_" : "") + camelCaseToConstantCase(name);
|
|
356
|
+
assert(
|
|
357
|
+
EnvName.is(encoded),
|
|
358
|
+
`Invalid environment schema name ${JSON.stringify(encoded)}. Expected a CONSTANT_CASE identifier of at most 255 characters.`,
|
|
359
|
+
);
|
|
360
|
+
assert(
|
|
361
|
+
!outputNameByInputName.has(encoded),
|
|
362
|
+
`Duplicate environment variable ${JSON.stringify(encoded)}.`,
|
|
363
|
+
);
|
|
364
|
+
const property: unknown = field.value;
|
|
365
|
+
const fieldType: unknown = EvoluType.is(property)
|
|
366
|
+
? property
|
|
367
|
+
: isPlainObject(property)
|
|
368
|
+
? Object.getOwnPropertyDescriptor(property, "type")?.value
|
|
369
|
+
: undefined;
|
|
370
|
+
assert(
|
|
371
|
+
EvoluType.is(fieldType),
|
|
372
|
+
`Invalid environment field ${JSON.stringify(name)}. Expected a Type or optional/defaulted property; groups must use CONSTANT_CASE names.`,
|
|
373
|
+
);
|
|
374
|
+
flat[name] = property as ObjectProps[string];
|
|
375
|
+
inputNameByOutputName.set(name, encoded);
|
|
376
|
+
outputNameByInputName.set(encoded, name);
|
|
377
|
+
if (!grouped) unprefixedNames.add(encoded);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Runtime construction validates the flattened fields; the public signature
|
|
382
|
+
// checks their concrete Types and string encodings before flattening.
|
|
383
|
+
const output = (
|
|
384
|
+
object as unknown as (props: ObjectProps) => ObjectType<ObjectProps>
|
|
385
|
+
)(flat);
|
|
386
|
+
const key = transform("EnvKey", String, CamelCaseIdentifier, {
|
|
387
|
+
// Identity fallback keeps this exposed codec round-trippable for other
|
|
388
|
+
// valid camelCase identifiers. objectKeys accepts only declared fields.
|
|
389
|
+
from: (value) => ok(outputNameByInputName.get(value) ?? value),
|
|
390
|
+
to: (value) => inputNameByOutputName.get(value) ?? value,
|
|
391
|
+
});
|
|
392
|
+
const withKeys = objectKeys(key)(output);
|
|
393
|
+
const type = transform("Env", Unknown, withKeys, {
|
|
394
|
+
from: (value) => {
|
|
395
|
+
if (typeof value !== "object" || value === null) return ok(value);
|
|
396
|
+
return ok(
|
|
397
|
+
filterObjectKeys(
|
|
398
|
+
value,
|
|
399
|
+
(key) =>
|
|
400
|
+
unprefixedNames.has(key) ||
|
|
401
|
+
prefixes.some((prefix) => key.toUpperCase().startsWith(prefix)),
|
|
402
|
+
),
|
|
403
|
+
);
|
|
404
|
+
},
|
|
405
|
+
to: (value) => value,
|
|
406
|
+
});
|
|
407
|
+
// The maps preserve each field's name and codec; only the static flattening
|
|
408
|
+
// cannot be expressed by the runtime loops above.
|
|
409
|
+
return type as unknown as EnvType<Props>;
|
|
410
|
+
};
|
package/src/Fs.test.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import {
|
|
3
|
+
assert,
|
|
4
|
+
assertEqual,
|
|
5
|
+
assertErr,
|
|
6
|
+
assertInstanceOf,
|
|
7
|
+
assertOk,
|
|
8
|
+
assertSame,
|
|
9
|
+
} from "./Assert.ts";
|
|
10
|
+
import { testCreateFs, type Fs, type FsDep, type FsError } from "./Fs.ts";
|
|
11
|
+
import { err, ok } from "./Result.ts";
|
|
12
|
+
import { AbortError, testCreateRun, type Task } from "./Task.ts";
|
|
13
|
+
import { assertType } from "./Type.ts";
|
|
14
|
+
|
|
15
|
+
describe("testCreateFs", () => {
|
|
16
|
+
it("accepts native copy options and rejects the former overwrite option", () => {
|
|
17
|
+
const fs = testCreateFs();
|
|
18
|
+
const task = fs.copy("source", "destination", {
|
|
19
|
+
force: false,
|
|
20
|
+
errorOnExist: true,
|
|
21
|
+
preserveTimestamps: true,
|
|
22
|
+
});
|
|
23
|
+
assertType<typeof task, Task<void, FsError>>();
|
|
24
|
+
|
|
25
|
+
// @ts-expect-error: FsCopyOptions has no overwrite property; use Node's force and errorOnExist options.
|
|
26
|
+
fs.copy("source", "destination", { overwrite: false });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
for (const [method, createTask] of [
|
|
30
|
+
["readFile", (fs: Fs) => fs.readFile("file.txt")],
|
|
31
|
+
["writeFile", (fs: Fs) => fs.writeFile("file.txt", "content")],
|
|
32
|
+
["readDirectory", (fs: Fs) => fs.readDirectory("directory")],
|
|
33
|
+
["createDirectory", (fs: Fs) => fs.createDirectory("directory")],
|
|
34
|
+
["copy", (fs: Fs) => fs.copy("source", "destination")],
|
|
35
|
+
["copyFile", (fs: Fs) => fs.copyFile("source", "destination")],
|
|
36
|
+
["rename", (fs: Fs) => fs.rename("source", "destination")],
|
|
37
|
+
["remove", (fs: Fs) => fs.remove("file.txt")],
|
|
38
|
+
["getMetadata", (fs: Fs) => fs.getMetadata("file.txt")],
|
|
39
|
+
["exists", (fs: Fs) => fs.exists("file.txt")],
|
|
40
|
+
["createTempDirectory", (fs: Fs) => fs.createTempDirectory()],
|
|
41
|
+
] as const) {
|
|
42
|
+
it(`panics when an unconfigured ${method} Task runs`, async () => {
|
|
43
|
+
const fs = testCreateFs();
|
|
44
|
+
const task = createTask(fs);
|
|
45
|
+
await using run = testCreateRun();
|
|
46
|
+
|
|
47
|
+
const result = await run.abortable<unknown, FsError>(task);
|
|
48
|
+
|
|
49
|
+
assertErr(result);
|
|
50
|
+
assert(AbortError.is(result.error), "Unexpected Fs calls must abort.");
|
|
51
|
+
assertEqual(result.error.reason.type, "PanicAbortReason");
|
|
52
|
+
const { defect } = result.error.reason;
|
|
53
|
+
assertInstanceOf(defect, Error);
|
|
54
|
+
assertEqual(defect.message, `Unexpected Fs.${method} call`);
|
|
55
|
+
assertEqual(run.deps.reportDefect.getDefects(), [result.error]);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
it("uses an injected override with the original arguments", async () => {
|
|
60
|
+
const path = new URL("file:///message.txt");
|
|
61
|
+
const data = new TextEncoder().encode("hello");
|
|
62
|
+
const options = { flag: "a" } as const;
|
|
63
|
+
let calls = 0;
|
|
64
|
+
const writeFile: Fs["writeFile"] =
|
|
65
|
+
(receivedPath, receivedData, receivedOptions) => () => {
|
|
66
|
+
calls += 1;
|
|
67
|
+
assertSame(receivedPath, path);
|
|
68
|
+
assertSame(receivedData, data);
|
|
69
|
+
assertSame(receivedOptions, options);
|
|
70
|
+
return ok();
|
|
71
|
+
};
|
|
72
|
+
const fs = testCreateFs({ writeFile });
|
|
73
|
+
assertSame(fs.writeFile, writeFile);
|
|
74
|
+
await using run = testCreateRun({ fs });
|
|
75
|
+
const saveMessage: Task<void, FsError, FsDep> = (run) =>
|
|
76
|
+
run(run.deps.fs.writeFile(path, data, options));
|
|
77
|
+
|
|
78
|
+
assertEqual(calls, 0);
|
|
79
|
+
assertOk(await run(saveMessage));
|
|
80
|
+
assertEqual(calls, 1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("preserves readFile overloads and injected errors", async () => {
|
|
84
|
+
const error: FsError = {
|
|
85
|
+
type: "FsError",
|
|
86
|
+
reason: "PermissionDenied",
|
|
87
|
+
path: "protected.txt",
|
|
88
|
+
syscall: "open",
|
|
89
|
+
message: "Permission denied",
|
|
90
|
+
};
|
|
91
|
+
const fs = testCreateFs({ readFile: () => () => err(error) });
|
|
92
|
+
await using run = testCreateRun();
|
|
93
|
+
const bytes = fs.readFile("protected.txt");
|
|
94
|
+
const text = fs.readFile("protected.txt", "utf8");
|
|
95
|
+
const encoded = fs.readFile("protected.txt", { encoding: "utf8" });
|
|
96
|
+
assertType<typeof bytes, Task<Uint8Array, FsError>>();
|
|
97
|
+
assertType<typeof text, Task<string, FsError>>();
|
|
98
|
+
assertType<typeof encoded, Task<string, FsError>>();
|
|
99
|
+
|
|
100
|
+
assertErr(await run(bytes), error);
|
|
101
|
+
assertErr(await run(text), error);
|
|
102
|
+
assertErr(await run(encoded), error);
|
|
103
|
+
assertEqual(run.deps.reportDefect.getDefects(), []);
|
|
104
|
+
});
|
|
105
|
+
});
|