@jeengbe/config 0.0.9 → 1.0.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/README.md +18 -3
- package/dist/ast.d.mts +108 -8
- package/dist/ast.d.mts.map +1 -1
- package/dist/ast.mjs +113 -7
- package/dist/ast.mjs.map +1 -1
- package/dist/env.d.mts +203 -5
- package/dist/env.d.mts.map +1 -1
- package/dist/env.mjs +75 -35
- package/dist/env.mjs.map +1 -1
- package/dist/if-enabled.d.mts +13 -2
- package/dist/if-enabled.d.mts.map +1 -1
- package/dist/if-enabled.mjs +11 -0
- package/dist/if-enabled.mjs.map +1 -1
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +2 -1
- package/dist/validation.d.mts +103 -6
- package/dist/validation.d.mts.map +1 -1
- package/dist/validation.mjs +109 -5
- package/dist/validation.mjs.map +1 -1
- package/package.json +6 -2
- package/src/ast.ts +211 -19
- package/src/env.ts +361 -73
- package/src/if-enabled.ts +15 -7
- package/src/index.ts +8 -1
- package/src/validation.ts +162 -12
package/src/env.ts
CHANGED
|
@@ -1,24 +1,194 @@
|
|
|
1
|
-
import type { EnvSpec,
|
|
1
|
+
import type { EnvSpec, ParseEnv, Pretty, ScalarValidationResultOrEither } from './ast.js';
|
|
2
2
|
import { EnvNode, ScalarEnvNode } from './ast.js';
|
|
3
|
-
import
|
|
4
|
-
import { arrayIncludes,
|
|
3
|
+
import { ValidationResult } from './validation.js';
|
|
4
|
+
import { arrayIncludes, collectValidationResults } from './validation.js';
|
|
5
5
|
import { Either } from '@jeengbe/prelude';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Type-safe configuration loader.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { env } from '@jeengbe/config';
|
|
14
|
+
*
|
|
15
|
+
* const config = env.load({
|
|
16
|
+
* port: env.number('PORT', 3000),
|
|
17
|
+
* debug: env.boolean('DEBUG', false),
|
|
18
|
+
* driver: env.discriminate('driver', env.enum('DRIVER', ['memory', 'redis']), {
|
|
19
|
+
* memory: {},
|
|
20
|
+
* redis: { url: env.string('REDIS_URL') },
|
|
21
|
+
* }),
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // -> {
|
|
25
|
+
* // port: number;
|
|
26
|
+
* // debug: boolean;
|
|
27
|
+
* // driver: { driver: 'memory' } | { driver: 'redis'; url: string };
|
|
28
|
+
* // }
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* Variables are read from `process.env` and trimmed before validation. Only a fully unset variable
|
|
32
|
+
* falls back to a default value or fails as required - an explicitly empty value is passed through to
|
|
33
|
+
* validation like any other input.
|
|
34
|
+
*/
|
|
7
35
|
export interface Env {
|
|
36
|
+
/**
|
|
37
|
+
* Reads a string environment variable.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* const res = env.load(env.string('API_KEY'));
|
|
43
|
+
* // ^? string
|
|
44
|
+
*
|
|
45
|
+
* // API_KEY=abc-test -> 'abc-test'
|
|
46
|
+
* // API_KEY=123 -> '123'
|
|
47
|
+
* // API_KEY= -> ''
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
8
50
|
string(key: string, defaultValue?: string): ScalarEnvNode<string>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Reads a numeric environment variable. Must match `/^-?\d+(?:\.\d+)?$/`, i.e. `[-]digits[.digits]`.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
*
|
|
57
|
+
* ```ts
|
|
58
|
+
* const res = env.load(env.number('PORT'));
|
|
59
|
+
* // ^? number
|
|
60
|
+
*
|
|
61
|
+
* // PORT=3000 -> 3000
|
|
62
|
+
* // PORT=-1 -> -1
|
|
63
|
+
* // PORT=abc -> error
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
9
66
|
number(key: string, defaultValue?: number): ScalarEnvNode<number>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Reads a boolean environment variable (must be 'true' or 'false').
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
*
|
|
73
|
+
* ```ts
|
|
74
|
+
* const res = env.load(env.boolean('DEBUG'));
|
|
75
|
+
* // ^? boolean
|
|
76
|
+
*
|
|
77
|
+
* // DEBUG=true -> true
|
|
78
|
+
* // DEBUG=false -> false
|
|
79
|
+
* // DEBUG=abc -> error
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
10
82
|
boolean(key: string, defaultValue?: boolean): ScalarEnvNode<boolean>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Reads an environment variable constrained to one of the provided values.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
*
|
|
89
|
+
* ```ts
|
|
90
|
+
* const res = env.load(env.enum('DRIVER', ['memory', 'redis']));
|
|
91
|
+
* // ^? 'memory' | 'redis'
|
|
92
|
+
*
|
|
93
|
+
* // DRIVER=memory -> 'memory'
|
|
94
|
+
* // DRIVER=abc -> error
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
11
97
|
enum<const T extends string>(
|
|
12
98
|
key: string,
|
|
13
99
|
values: readonly T[],
|
|
14
100
|
defaultValue?: T,
|
|
15
101
|
): ScalarEnvNode<T>;
|
|
16
|
-
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Reads a comma-separated list environment variable, validating each item against the provided item type.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
*
|
|
108
|
+
* ```ts
|
|
109
|
+
* const res = env.load(env.array(env.string('API_KEYS')));
|
|
110
|
+
* // ^? readonly string[]
|
|
111
|
+
*
|
|
112
|
+
* // API_KEYS=abc,def,ghi -> ['abc', 'def', 'ghi']
|
|
113
|
+
* // API_KEYS= -> []
|
|
114
|
+
* // API_KEYS=abc,,ghi -> error
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
*
|
|
119
|
+
* ```ts
|
|
120
|
+
* const res = env.load(env.array(env.string('API_KEYS'), ['default1', 'default2']));
|
|
121
|
+
* // ^? readonly string[]
|
|
122
|
+
*
|
|
123
|
+
* // API_KEYS=abc,def,ghi -> ['abc', 'def', 'ghi']
|
|
124
|
+
* // API_KEYS= -> []
|
|
125
|
+
* // (not set) -> ['default1', 'default2']
|
|
126
|
+
* // API_KEYS=abc,,ghi -> error
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
*
|
|
131
|
+
* ```ts
|
|
132
|
+
* const res = env.load(env.array(env.string('API_KEYS').optional()));
|
|
133
|
+
* // ^? readonly (string | undefined)[]
|
|
134
|
+
*
|
|
135
|
+
* // API_KEYS=abc,def,ghi -> ['abc', 'def', 'ghi']
|
|
136
|
+
* // API_KEYS= -> []
|
|
137
|
+
* // API_KEYS=abc,,ghi -> ['abc', undefined, 'ghi']
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
array<const T>(
|
|
141
|
+
itemType: ScalarEnvNode<T>,
|
|
142
|
+
defaultValue?: readonly T[],
|
|
143
|
+
): ScalarEnvNode<readonly T[]>;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Reads an environment variable, validating and transforming it with the provided function.
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
*
|
|
150
|
+
* ```ts
|
|
151
|
+
* const res = env.load(
|
|
152
|
+
* // ^? string
|
|
153
|
+
* env.scalar('TAG', (value, path) =>
|
|
154
|
+
* value.length === 3
|
|
155
|
+
* ? ValidationResult.success({ value, defaulted: [] })
|
|
156
|
+
* : ValidationResult.fail({
|
|
157
|
+
* errors: [{ path, key: 'TAG', message: 'must be 3 characters long', value }],
|
|
158
|
+
* }),
|
|
159
|
+
* ),
|
|
160
|
+
* );
|
|
161
|
+
*
|
|
162
|
+
* // TAG=abc -> 'abc'
|
|
163
|
+
* // TAG=abcd -> error
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
scalar<const T>(
|
|
17
167
|
key: string,
|
|
18
|
-
transform: (value: string) =>
|
|
168
|
+
transform: (value: string, path: string) => ScalarValidationResultOrEither<T>,
|
|
19
169
|
defaultValue?: T,
|
|
20
170
|
): ScalarEnvNode<T>;
|
|
21
|
-
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Reads a discriminator environment variable and resolves the nested schema mapped to its value.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
*
|
|
177
|
+
* ```ts
|
|
178
|
+
* const res = env.load(
|
|
179
|
+
* // ^? { driver: 'memory'; } | { driver: 'redis'; url: string; }
|
|
180
|
+
* env.discriminate('driver', env.enum('DRIVER', ['memory', 'redis']), {
|
|
181
|
+
* memory: {},
|
|
182
|
+
* redis: { url: env.string('REDIS_URL') },
|
|
183
|
+
* }),
|
|
184
|
+
* );
|
|
185
|
+
*
|
|
186
|
+
* // DRIVER=memory -> { driver: 'memory' }
|
|
187
|
+
* // DRIVER=redis, REDIS_URL=redis://localhost -> { driver: 'redis', url: 'redis://localhost' }
|
|
188
|
+
* // DRIVER=redis, REDIS_URL= -> { driver: 'redis', url: '' }
|
|
189
|
+
* // DRIVER=abc -> error
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
22
192
|
discriminate<
|
|
23
193
|
K extends string,
|
|
24
194
|
V extends string,
|
|
@@ -28,83 +198,186 @@ export interface Env {
|
|
|
28
198
|
discriminatorValueType: ScalarEnvNode<V>,
|
|
29
199
|
mapping: M,
|
|
30
200
|
): EnvNode<DiscriminatorResult<K, V, M>>;
|
|
31
|
-
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Validates and loads the provided schema from `process.env`. Returns a `ValidationResult` that contains either the parsed values or a list of validation errors.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
*
|
|
207
|
+
* ```ts
|
|
208
|
+
* const result = env.parse({
|
|
209
|
+
* port: env.number('PORT'),
|
|
210
|
+
* debug: env.boolean('DEBUG'),
|
|
211
|
+
* });
|
|
212
|
+
*
|
|
213
|
+
* if (result.getLeft()) {
|
|
214
|
+
* console.error('Validation failed:', result.getLeft());
|
|
215
|
+
* } else {
|
|
216
|
+
* const config = result.get()!;
|
|
217
|
+
* // -> { port: number; debug: boolean; }
|
|
218
|
+
* }
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
parse<S extends EnvSpec>(spec: S, env?: NodeJS.ProcessEnv): ValidationResult<Pretty<ParseEnv<S>>>;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Validates and loads the provided schema from `process.env`. Throws if any required variables are missing or invalid.
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
*
|
|
228
|
+
* ```ts
|
|
229
|
+
* const config = env.load({
|
|
230
|
+
* port: env.number('PORT', 3000),
|
|
231
|
+
* debug: env.boolean('DEBUG', false),
|
|
232
|
+
* }, console.log);
|
|
233
|
+
*
|
|
234
|
+
* // -> {
|
|
235
|
+
* // port: number;
|
|
236
|
+
* // debug: boolean;
|
|
237
|
+
* // }
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
load<S extends EnvSpec>(spec: S, env?: NodeJS.ProcessEnv): Pretty<ParseEnv<S>>;
|
|
32
241
|
}
|
|
33
242
|
|
|
34
243
|
export const env: Env = {
|
|
35
|
-
string(key, defaultValue) {
|
|
36
|
-
return env.
|
|
244
|
+
string(key: string, defaultValue?: string): ScalarEnvNode<string> {
|
|
245
|
+
return env.scalar(
|
|
246
|
+
key,
|
|
247
|
+
(value): ValidationResult<string> => ValidationResult.success({ value, defaulted: [] }),
|
|
248
|
+
defaultValue,
|
|
249
|
+
);
|
|
37
250
|
},
|
|
38
251
|
|
|
39
|
-
number(key, defaultValue) {
|
|
40
|
-
return env.
|
|
252
|
+
number(key: string, defaultValue?: number): ScalarEnvNode<number> {
|
|
253
|
+
return env.scalar(
|
|
41
254
|
key,
|
|
42
|
-
(value) => {
|
|
255
|
+
(value, path): ValidationResult<number> => {
|
|
43
256
|
if (/^-?\d+(?:\.\d+)?$/.test(value)) {
|
|
44
|
-
return
|
|
257
|
+
return ValidationResult.success({ value: Number(value), defaulted: [] });
|
|
45
258
|
}
|
|
46
259
|
|
|
47
|
-
return
|
|
260
|
+
return ValidationResult.fail({
|
|
261
|
+
errors: [
|
|
262
|
+
{
|
|
263
|
+
path,
|
|
264
|
+
key,
|
|
265
|
+
message: 'invalid number',
|
|
266
|
+
value,
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
});
|
|
48
270
|
},
|
|
49
271
|
defaultValue,
|
|
50
272
|
);
|
|
51
273
|
},
|
|
52
274
|
|
|
53
|
-
boolean(key, defaultValue) {
|
|
54
|
-
return env.
|
|
275
|
+
boolean(key: string, defaultValue?: boolean): ScalarEnvNode<boolean> {
|
|
276
|
+
return env.scalar(
|
|
55
277
|
key,
|
|
56
|
-
(value) => {
|
|
57
|
-
if (value.toLowerCase() === 'true')
|
|
58
|
-
|
|
278
|
+
(value, path): ValidationResult<boolean> => {
|
|
279
|
+
if (value.toLowerCase() === 'true') {
|
|
280
|
+
return ValidationResult.success({ value: true, defaulted: [] });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (value.toLowerCase() === 'false') {
|
|
284
|
+
return ValidationResult.success({ value: false, defaulted: [] });
|
|
285
|
+
}
|
|
59
286
|
|
|
60
|
-
return
|
|
287
|
+
return ValidationResult.fail({
|
|
288
|
+
errors: [
|
|
289
|
+
{
|
|
290
|
+
path,
|
|
291
|
+
key,
|
|
292
|
+
message: 'invalid boolean',
|
|
293
|
+
formatHint: "must be 'true' or 'false'",
|
|
294
|
+
value,
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
});
|
|
61
298
|
},
|
|
62
299
|
defaultValue,
|
|
63
300
|
);
|
|
64
301
|
},
|
|
65
302
|
|
|
66
|
-
enum
|
|
67
|
-
|
|
303
|
+
enum<const T extends string>(
|
|
304
|
+
key: string,
|
|
305
|
+
values: readonly T[],
|
|
306
|
+
defaultValue?: T,
|
|
307
|
+
): ScalarEnvNode<T> {
|
|
308
|
+
return env.scalar(
|
|
68
309
|
key,
|
|
69
|
-
(value) => {
|
|
70
|
-
if (arrayIncludes(values, value)) return
|
|
310
|
+
(value, path): ValidationResult<T> => {
|
|
311
|
+
if (arrayIncludes(values, value)) return ValidationResult.success({ value, defaulted: [] });
|
|
71
312
|
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
|
|
313
|
+
return ValidationResult.fail({
|
|
314
|
+
errors: [
|
|
315
|
+
{
|
|
316
|
+
path,
|
|
317
|
+
key,
|
|
318
|
+
message: 'invalid enum value',
|
|
319
|
+
formatHint: `must be one of: ${values.map((v) => `'${v}'`).join(', ')}`,
|
|
320
|
+
value,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
});
|
|
75
324
|
},
|
|
76
325
|
defaultValue,
|
|
77
326
|
);
|
|
78
327
|
},
|
|
79
328
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
329
|
+
array<const T>(
|
|
330
|
+
itemType: ScalarEnvNode<T>,
|
|
331
|
+
defaultValue?: readonly T[],
|
|
332
|
+
): ScalarEnvNode<readonly T[]> {
|
|
333
|
+
return env.scalar(
|
|
334
|
+
itemType.key,
|
|
335
|
+
(value, path): ValidationResult<readonly T[]> => {
|
|
336
|
+
// Special case because ''.split(',') returns [''] instead of [].
|
|
337
|
+
if (value === '') return ValidationResult.success({ value: [], defaulted: [] });
|
|
86
338
|
|
|
87
|
-
|
|
88
|
-
|
|
339
|
+
return collectValidationResults(
|
|
340
|
+
...value
|
|
341
|
+
.split(',')
|
|
342
|
+
.map((v) => v.trim() || undefined)
|
|
343
|
+
.map((v, i) =>
|
|
344
|
+
itemType.validate(
|
|
345
|
+
(k) =>
|
|
346
|
+
// itemType always reads its own key, so this is never false
|
|
347
|
+
k === itemType.key ? v : /* v8 ignore next */ undefined,
|
|
348
|
+
`${path}.${i}`,
|
|
349
|
+
),
|
|
350
|
+
),
|
|
351
|
+
);
|
|
352
|
+
},
|
|
353
|
+
defaultValue,
|
|
354
|
+
);
|
|
89
355
|
},
|
|
90
356
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
357
|
+
scalar<T>(
|
|
358
|
+
key: string,
|
|
359
|
+
transform: (value: string, path: string) => ScalarValidationResultOrEither<T>,
|
|
360
|
+
defaultValue?: T,
|
|
361
|
+
): ScalarEnvNode<T> {
|
|
362
|
+
return new ScalarEnvNode(key, (value, path): ScalarValidationResultOrEither<T> => {
|
|
94
363
|
if (value === undefined) {
|
|
95
|
-
if (defaultValue !== undefined)
|
|
96
|
-
|
|
364
|
+
if (defaultValue !== undefined) {
|
|
365
|
+
return ValidationResult.success({
|
|
366
|
+
value: defaultValue,
|
|
367
|
+
defaulted: [
|
|
368
|
+
{
|
|
369
|
+
path,
|
|
370
|
+
key,
|
|
371
|
+
defaultValue,
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return Either.left('required');
|
|
97
378
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return combineValidationResults(
|
|
101
|
-
...value
|
|
102
|
-
.split(',')
|
|
103
|
-
.map((v) => v.trim() || undefined)
|
|
104
|
-
.map((v, i) =>
|
|
105
|
-
itemType.validate(`${path}.${i}`, (k) => (k === itemType.key ? v : undefined)),
|
|
106
|
-
),
|
|
107
|
-
);
|
|
379
|
+
|
|
380
|
+
return transform(value, path);
|
|
108
381
|
});
|
|
109
382
|
},
|
|
110
383
|
|
|
@@ -117,28 +390,43 @@ export const env: Env = {
|
|
|
117
390
|
discriminatorValueType: ScalarEnvNode<V>,
|
|
118
391
|
mapping: M,
|
|
119
392
|
): EnvNode<DiscriminatorResult<K, V, M>> {
|
|
120
|
-
return new EnvNode((
|
|
121
|
-
return discriminatorValueType
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
393
|
+
return new EnvNode((loadValue, path): ValidationResult<DiscriminatorResult<K, V, M>> => {
|
|
394
|
+
return discriminatorValueType
|
|
395
|
+
.validate(loadValue, `${path}.${discriminatorKey}`)
|
|
396
|
+
.flatMap((discriminatorValue) => {
|
|
397
|
+
return resolveNode<NonNullable<M[keyof M]> | {}>(
|
|
398
|
+
path,
|
|
399
|
+
mapping[discriminatorValue] ?? {},
|
|
400
|
+
loadValue,
|
|
401
|
+
).map(
|
|
402
|
+
(mappingValues) =>
|
|
403
|
+
({
|
|
404
|
+
[discriminatorKey]: discriminatorValue,
|
|
405
|
+
...mappingValues,
|
|
406
|
+
}) as DiscriminatorResult<K, V, M>,
|
|
407
|
+
);
|
|
408
|
+
});
|
|
134
409
|
});
|
|
135
410
|
},
|
|
136
411
|
|
|
137
|
-
|
|
138
|
-
return resolveNode('$', spec, (key) =>
|
|
139
|
-
|
|
140
|
-
|
|
412
|
+
parse<S extends EnvSpec>(spec: S, env = process.env): ValidationResult<Pretty<ParseEnv<S>>> {
|
|
413
|
+
return resolveNode('$', spec, (key) => env[key]?.trim());
|
|
414
|
+
},
|
|
415
|
+
|
|
416
|
+
load<S extends EnvSpec>(spec: S, env = process.env): Pretty<ParseEnv<S>> {
|
|
417
|
+
return this.parse(spec, env).fold(
|
|
418
|
+
(failure) => {
|
|
419
|
+
throw new Error(
|
|
420
|
+
`Environment validation failed:\n${failure.errors
|
|
421
|
+
.map(
|
|
422
|
+
(e) =>
|
|
423
|
+
// oxlint-disable-next-line typescript/no-base-to-string
|
|
424
|
+
` ${e.path} (${e.key}): ${e.message} (${[e.formatHint, `got: '${String(e.value ?? '<not provided>')}'`].filter((x) => x).join('; ')})`,
|
|
425
|
+
)
|
|
426
|
+
.join('\n')}`,
|
|
427
|
+
);
|
|
141
428
|
},
|
|
429
|
+
({ value }) => value,
|
|
142
430
|
);
|
|
143
431
|
},
|
|
144
432
|
};
|
|
@@ -150,21 +438,21 @@ type DiscriminatorResult<
|
|
|
150
438
|
> = Pretty<
|
|
151
439
|
// This "redundant" condition is necessary to make sure that 'DiscriminatorResult' distributes over
|
|
152
440
|
// the union type V
|
|
153
|
-
V extends unknown ? Record<K, V> &
|
|
441
|
+
V extends unknown ? Record<K, V> & ParseEnv<M[V] extends EnvSpec ? M[V] : {}> : never
|
|
154
442
|
>;
|
|
155
443
|
|
|
156
444
|
function resolveNode<S extends EnvSpec>(
|
|
157
445
|
path: string,
|
|
158
446
|
spec: S,
|
|
159
447
|
loadValue: (key: string) => string | undefined,
|
|
160
|
-
): ValidationResult<Pretty<
|
|
448
|
+
): ValidationResult<Pretty<ParseEnv<S>>> {
|
|
161
449
|
if (spec instanceof EnvNode) {
|
|
162
|
-
return (spec as EnvNode<Pretty<
|
|
450
|
+
return (spec as EnvNode<Pretty<ParseEnv<S>>>).validate(loadValue, path);
|
|
163
451
|
}
|
|
164
452
|
|
|
165
|
-
return
|
|
453
|
+
return collectValidationResults(
|
|
166
454
|
...Object.entries(spec).map(([key, value]) =>
|
|
167
455
|
resolveNode(`${path}.${key}`, value, loadValue).map((v) => [key, v] as const),
|
|
168
456
|
),
|
|
169
|
-
).map((entries) => Object.fromEntries(entries) as Pretty<
|
|
457
|
+
).map((entries) => Object.fromEntries(entries) as Pretty<ParseEnv<S>>);
|
|
170
458
|
}
|
package/src/if-enabled.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type { EnvNode, EnvSpec,
|
|
1
|
+
import type { EnvNode, EnvSpec, ParseEnv } from './ast.js';
|
|
2
2
|
import { env } from './env.js';
|
|
3
|
-
import type { ValidationResult } from './validation.js';
|
|
4
3
|
import { Either } from '@jeengbe/prelude';
|
|
5
4
|
|
|
6
5
|
type IfEnabled<T> =
|
|
@@ -9,25 +8,34 @@ type IfEnabled<T> =
|
|
|
9
8
|
} & Omit<T, 'enabled'>)
|
|
10
9
|
| { enabled: false };
|
|
11
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Wraps a config schema behind an `enabled` boolean flag read from `envVar`. When disabled, none of the
|
|
13
|
+
* nested schema's environment variables are validated or required.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* ifEnabled('FEATURE_X', { apiKey: env.string('FEATURE_X_API_KEY') });
|
|
19
|
+
* // -> { enabled: true; apiKey: string } | { enabled: false }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
12
22
|
export function ifEnabled<T extends Record<string, EnvSpec>>(
|
|
13
23
|
envVar: string,
|
|
14
24
|
config: T,
|
|
15
25
|
defaultEnabled = false,
|
|
16
|
-
): EnvNode<IfEnabled<
|
|
26
|
+
): EnvNode<IfEnabled<ParseEnv<T>>> {
|
|
17
27
|
// Since discriminate only works with string values, we need to bridge 'true' -> 'enabled' -> true
|
|
18
28
|
return env
|
|
19
29
|
.discriminate(
|
|
20
30
|
'enabled',
|
|
21
31
|
env
|
|
22
32
|
.boolean(envVar, defaultEnabled)
|
|
23
|
-
.transform(
|
|
24
|
-
(v): ValidationResult<'enabled' | 'disabled'> => Either.right(v ? 'enabled' : 'disabled'),
|
|
25
|
-
),
|
|
33
|
+
.transform<'enabled' | 'disabled'>((v) => Either.right(v ? 'enabled' : 'disabled')),
|
|
26
34
|
{
|
|
27
35
|
enabled: config,
|
|
28
36
|
},
|
|
29
37
|
)
|
|
30
|
-
.transform((value):
|
|
38
|
+
.transform((value): Either<never, IfEnabled<ParseEnv<T>>> => {
|
|
31
39
|
if (value.enabled === 'enabled') {
|
|
32
40
|
return Either.right({
|
|
33
41
|
...value,
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
export type { EnvNode, EnvSpec,
|
|
1
|
+
export type { EnvNode, ScalarEnvNode, EnvSpec, ParseEnv } from './ast.js';
|
|
2
2
|
export { env } from './env.js';
|
|
3
3
|
export { ifEnabled } from './if-enabled.js';
|
|
4
|
+
export { collectValidationResults, ValidationResult } from './validation.js';
|
|
5
|
+
export type {
|
|
6
|
+
ValidationDefaulted,
|
|
7
|
+
ValidationError,
|
|
8
|
+
ValidationFailure,
|
|
9
|
+
ValidationSuccess,
|
|
10
|
+
} from './validation.js';
|