@zudojs/config 1.2.0 → 1.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/README.md +62 -0
- package/dist/configManager/configManager.core.d.ts +37 -3
- package/dist/configManager/configManager.core.js +33 -27
- package/dist/configManager/configManager.error.d.ts +3 -2
- package/dist/configResolver/accessors/configResolver.scoped.d.ts +39 -2
- package/dist/configResolver/accessors/configResolver.scoped.js +31 -2
- package/dist/configResolver/core/configResolver.core.d.ts +24 -5
- package/dist/configResolver/core/configResolver.core.js +7 -27
- package/dist/configResolver/core/configResolver.type.d.ts +8 -0
- package/dist/configSchema/configSchema.coerce.d.ts +15 -0
- package/dist/configSchema/configSchema.coerce.js +44 -0
- package/dist/configSchema/configSchema.type.d.ts +80 -10
- package/dist/configSchema/configSchema.validator.d.ts +1 -1
- package/dist/configSchema/configSchema.validator.js +88 -40
- package/dist/configStore/configStore.core.d.ts +4 -2
- package/dist/configStore/configStore.core.js +18 -4
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -118,6 +118,28 @@ const db = manager.scoped("db");
|
|
|
118
118
|
db.string("host", "localhost");
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
`required<T>(key)` does NOT convert: `T` is an unchecked cast, so with
|
|
122
|
+
`DB__PORT=5432`, `scoped("db").required<number>("port")` returns the
|
|
123
|
+
string `"5432"`. Use the typed variants, which parse and check the value
|
|
124
|
+
and throw when it is missing or does not parse: `requiredString()`,
|
|
125
|
+
`requiredNumber()`, `requiredBoolean()` and `requiredDate()` (on the
|
|
126
|
+
manager, the resolver and scoped resolvers).
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
const db = manager.scoped("db");
|
|
130
|
+
db.requiredNumber("port"); // 5432 (a number)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`store.getByPrefix(prefix)` and `store.getObjectByPrefix(prefix)` accept
|
|
134
|
+
the prefix with or without a trailing dot: `"db"` and `"db."` both select
|
|
135
|
+
`db.host` and `db.port` (and not `dbx`).
|
|
136
|
+
|
|
137
|
+
Passing a fallback narrows the return type: `manager.number("app.port")`
|
|
138
|
+
is `number | undefined`, while `manager.number("app.port", 3000)` is
|
|
139
|
+
`number`. The same holds for every typed getter and for `get(key,
|
|
140
|
+
fallback)`, whose literal fallback is widened (`get("mode", "dev")` is
|
|
141
|
+
`string`, not `"dev"`).
|
|
142
|
+
|
|
121
143
|
`number()` accepts decimal notation only: `"0x1F90"`, `"0b11"` and `"0o17"`
|
|
122
144
|
are rejected rather than silently becoming 8080, 3 and 15.
|
|
123
145
|
|
|
@@ -138,6 +160,46 @@ const config = manager.validate({
|
|
|
138
160
|
});
|
|
139
161
|
```
|
|
140
162
|
|
|
163
|
+
Environment variables are always strings, so string input is coerced
|
|
164
|
+
before the type check when a schema's type is `NUMBER` or `BOOLEAN` (and
|
|
165
|
+
does not also accept `STRING`). Parsing is strict: `"8080"` becomes
|
|
166
|
+
`8080`, while `"80a"`, `"0x1F90"` and `""` are still rejected as
|
|
167
|
+
`TYPE_MISMATCH`. Booleans follow the `boolean()` convention: `true` /
|
|
168
|
+
`false`, `1` / `0`, `yes` / `no`, `y` / `n`, `on` / `off`. `validate` and
|
|
169
|
+
`transform` receive the coerced value. Set `coerce: false` on a schema to
|
|
170
|
+
require a real number or boolean.
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// PORT=8080 DEBUG=true
|
|
174
|
+
const { port, debug } = manager.validate<{ port: number; debug: boolean }>({
|
|
175
|
+
properties: {
|
|
176
|
+
port: { type: ConfigValueType.NUMBER, min: 1, max: 65535 },
|
|
177
|
+
debug: { type: ConfigValueType.BOOLEAN },
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`resolve(key, schema)` types its schema per value type
|
|
183
|
+
(`TypedConfigSchema`), so the constraints the validator enforces are
|
|
184
|
+
accepted: `{ type: NUMBER, min: 1 }`, `{ type: STRING, minLength: 1 }`,
|
|
185
|
+
`{ type: ARRAY, minItems: 1 }`. A constraint that belongs to another type
|
|
186
|
+
(`{ type: NUMBER, minLength: 1 }`) is a compile error.
|
|
187
|
+
|
|
188
|
+
Validation runs in this order: coerce, type check, constraints,
|
|
189
|
+
`transform`, then `validate` on the final (transformed) value. A string
|
|
190
|
+
that does not have the schema's type is handed to `transform` as a
|
|
191
|
+
parser, and its output must then have the type and pass the constraints:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
manager.resolve("hosts", {
|
|
195
|
+
type: ConfigValueType.ARRAY,
|
|
196
|
+
minItems: 1,
|
|
197
|
+
transform: (value) => String(value).split(",").map((s) => s.trim()),
|
|
198
|
+
}); // HOSTS="a, b" -> ["a", "b"]
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
A non-string of the wrong type is rejected without calling `transform`.
|
|
202
|
+
|
|
141
203
|
Object schemas nest: a property schema of type `OBJECT` that declares
|
|
142
204
|
its own `properties` / `additionalProperties` is validated recursively,
|
|
143
205
|
so nested constraints are enforced by `validate()`, `resolve()` and the
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { ConfigValue } from "../configValue/configValue.core.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { AnyConfigSchema, TypedConfigSchema } from "../configSchema/index.js";
|
|
3
3
|
import type { ConfigSource } from "../configSource/configSource.core.js";
|
|
4
4
|
import type { ConfigEntry } from "../configEntry/configEntry.type.js";
|
|
5
5
|
import type { ConfigLoader, ConfigLoadResult } from "../configLoader/configLoader.core.js";
|
|
6
6
|
import type { ConfigStore } from "../configStore/configStore.core.js";
|
|
7
7
|
import type { ConfigResolver } from "../configResolver/core/configResolver.core.js";
|
|
8
8
|
import type { ScopedConfigResolver } from "../configResolver/accessors/configResolver.scoped.js";
|
|
9
|
+
import type { ConfigWiden } from "../configResolver/core/configResolver.type.js";
|
|
9
10
|
import type { ConfigManagerOptions, ConfigManagerStatus, ConfigManagerListener } from "./configManager.type.js";
|
|
10
11
|
import { ConfigManagerState } from "./configManager.type.js";
|
|
11
12
|
/**
|
|
@@ -96,41 +97,74 @@ export declare class ConfigManager {
|
|
|
96
97
|
* Gets a raw configuration value.
|
|
97
98
|
*/
|
|
98
99
|
get<T extends ConfigValue = ConfigValue>(key: string): T | undefined;
|
|
100
|
+
get<T extends ConfigValue>(key: string, fallback: T): ConfigWiden<T>;
|
|
99
101
|
/**
|
|
100
|
-
*
|
|
102
|
+
* Returns a required value WITHOUT converting it: `T` is an unchecked
|
|
103
|
+
* cast. An environment variable is always a string, so
|
|
104
|
+
* `required<number>("port")` returns `"5432"`, not `5432`. Use
|
|
105
|
+
* `requiredNumber`, `requiredBoolean`, `requiredString` or
|
|
106
|
+
* `requiredDate`, which parse and check the value.
|
|
101
107
|
*/
|
|
102
108
|
required<T extends ConfigValue = ConfigValue>(key: string): T;
|
|
109
|
+
/** Gets a required string; throws when missing or not a string. */
|
|
110
|
+
requiredString(key: string): string;
|
|
111
|
+
/**
|
|
112
|
+
* Gets a required number, parsing decimal strings (`"5432"`); throws
|
|
113
|
+
* when missing or not a number.
|
|
114
|
+
*/
|
|
115
|
+
requiredNumber(key: string): number;
|
|
116
|
+
/**
|
|
117
|
+
* Gets a required boolean, parsing `true/false`, `1/0`, `yes/no`, `y/n`
|
|
118
|
+
* and `on/off`; throws when missing or not a boolean.
|
|
119
|
+
*/
|
|
120
|
+
requiredBoolean(key: string): boolean;
|
|
121
|
+
/** Gets a required Date, parsing ISO strings; throws when missing. */
|
|
122
|
+
requiredDate(key: string): Date;
|
|
103
123
|
/**
|
|
104
124
|
* Gets a configuration value with schema validation.
|
|
105
125
|
*/
|
|
106
|
-
resolve<T extends ConfigValue>(key: string, schema:
|
|
126
|
+
resolve<T extends ConfigValue>(key: string, schema: TypedConfigSchema<T>): T | undefined;
|
|
107
127
|
/**
|
|
108
128
|
* Gets a string value.
|
|
109
129
|
*/
|
|
130
|
+
string(key: string): string | undefined;
|
|
131
|
+
string(key: string, fallback: string): string;
|
|
110
132
|
string(key: string, fallback?: string): string | undefined;
|
|
111
133
|
/**
|
|
112
134
|
* Gets a number value.
|
|
113
135
|
*/
|
|
136
|
+
number(key: string): number | undefined;
|
|
137
|
+
number(key: string, fallback: number): number;
|
|
114
138
|
number(key: string, fallback?: number): number | undefined;
|
|
115
139
|
/**
|
|
116
140
|
* Gets a boolean value.
|
|
117
141
|
*/
|
|
142
|
+
boolean(key: string): boolean | undefined;
|
|
143
|
+
boolean(key: string, fallback: boolean): boolean;
|
|
118
144
|
boolean(key: string, fallback?: boolean): boolean | undefined;
|
|
119
145
|
/**
|
|
120
146
|
* Gets a bigint value.
|
|
121
147
|
*/
|
|
148
|
+
bigint(key: string): bigint | undefined;
|
|
149
|
+
bigint(key: string, fallback: bigint): bigint;
|
|
122
150
|
bigint(key: string, fallback?: bigint): bigint | undefined;
|
|
123
151
|
/**
|
|
124
152
|
* Gets a Date value.
|
|
125
153
|
*/
|
|
154
|
+
date(key: string): Date | undefined;
|
|
155
|
+
date(key: string, fallback: Date): Date;
|
|
126
156
|
date(key: string, fallback?: Date): Date | undefined;
|
|
127
157
|
/**
|
|
128
158
|
* Gets an object value.
|
|
129
159
|
*/
|
|
160
|
+
object<T extends ConfigValue = ConfigValue>(key: string): T | undefined;
|
|
161
|
+
object<T extends ConfigValue = ConfigValue>(key: string, fallback: T): T;
|
|
130
162
|
object<T extends ConfigValue = ConfigValue>(key: string, fallback?: T): T | undefined;
|
|
131
163
|
/**
|
|
132
164
|
* Gets an array value.
|
|
133
165
|
*/
|
|
166
|
+
array<T extends ConfigValue = ConfigValue>(key: string): readonly T[] | undefined;
|
|
167
|
+
array<T extends ConfigValue = ConfigValue>(key: string, fallback: readonly T[]): readonly T[];
|
|
134
168
|
array<T extends ConfigValue = ConfigValue>(key: string, fallback?: readonly T[]): readonly T[] | undefined;
|
|
135
169
|
/**
|
|
136
170
|
* Sets a runtime configuration value.
|
|
@@ -223,20 +223,47 @@ export class ConfigManager {
|
|
|
223
223
|
markSecretEntries(this.store, collectSecretPaths(schema.properties));
|
|
224
224
|
return cloneConfigValue(result.value);
|
|
225
225
|
}
|
|
226
|
-
|
|
227
|
-
* Gets a raw configuration value.
|
|
228
|
-
*/
|
|
229
|
-
get(key) {
|
|
226
|
+
get(key, fallback) {
|
|
230
227
|
this.assertActive();
|
|
231
|
-
return this.resolver.get(key);
|
|
228
|
+
return this.resolver.get(key, fallback);
|
|
232
229
|
}
|
|
233
230
|
/**
|
|
234
|
-
*
|
|
231
|
+
* Returns a required value WITHOUT converting it: `T` is an unchecked
|
|
232
|
+
* cast. An environment variable is always a string, so
|
|
233
|
+
* `required<number>("port")` returns `"5432"`, not `5432`. Use
|
|
234
|
+
* `requiredNumber`, `requiredBoolean`, `requiredString` or
|
|
235
|
+
* `requiredDate`, which parse and check the value.
|
|
235
236
|
*/
|
|
236
237
|
required(key) {
|
|
237
238
|
this.assertActive();
|
|
238
239
|
return this.resolver.required(key);
|
|
239
240
|
}
|
|
241
|
+
/** Gets a required string; throws when missing or not a string. */
|
|
242
|
+
requiredString(key) {
|
|
243
|
+
this.assertActive();
|
|
244
|
+
return this.resolver.requiredString(key);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Gets a required number, parsing decimal strings (`"5432"`); throws
|
|
248
|
+
* when missing or not a number.
|
|
249
|
+
*/
|
|
250
|
+
requiredNumber(key) {
|
|
251
|
+
this.assertActive();
|
|
252
|
+
return this.resolver.requiredNumber(key);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Gets a required boolean, parsing `true/false`, `1/0`, `yes/no`, `y/n`
|
|
256
|
+
* and `on/off`; throws when missing or not a boolean.
|
|
257
|
+
*/
|
|
258
|
+
requiredBoolean(key) {
|
|
259
|
+
this.assertActive();
|
|
260
|
+
return this.resolver.requiredBoolean(key);
|
|
261
|
+
}
|
|
262
|
+
/** Gets a required Date, parsing ISO strings; throws when missing. */
|
|
263
|
+
requiredDate(key) {
|
|
264
|
+
this.assertActive();
|
|
265
|
+
return this.resolver.requiredDate(key);
|
|
266
|
+
}
|
|
240
267
|
/**
|
|
241
268
|
* Gets a configuration value with schema validation.
|
|
242
269
|
*/
|
|
@@ -244,51 +271,30 @@ export class ConfigManager {
|
|
|
244
271
|
this.assertActive();
|
|
245
272
|
return this.resolver.resolve(key, schema);
|
|
246
273
|
}
|
|
247
|
-
/**
|
|
248
|
-
* Gets a string value.
|
|
249
|
-
*/
|
|
250
274
|
string(key, fallback) {
|
|
251
275
|
this.assertActive();
|
|
252
276
|
return this.resolver.string(key, fallback);
|
|
253
277
|
}
|
|
254
|
-
/**
|
|
255
|
-
* Gets a number value.
|
|
256
|
-
*/
|
|
257
278
|
number(key, fallback) {
|
|
258
279
|
this.assertActive();
|
|
259
280
|
return this.resolver.number(key, fallback);
|
|
260
281
|
}
|
|
261
|
-
/**
|
|
262
|
-
* Gets a boolean value.
|
|
263
|
-
*/
|
|
264
282
|
boolean(key, fallback) {
|
|
265
283
|
this.assertActive();
|
|
266
284
|
return this.resolver.boolean(key, fallback);
|
|
267
285
|
}
|
|
268
|
-
/**
|
|
269
|
-
* Gets a bigint value.
|
|
270
|
-
*/
|
|
271
286
|
bigint(key, fallback) {
|
|
272
287
|
this.assertActive();
|
|
273
288
|
return this.resolver.bigint(key, fallback);
|
|
274
289
|
}
|
|
275
|
-
/**
|
|
276
|
-
* Gets a Date value.
|
|
277
|
-
*/
|
|
278
290
|
date(key, fallback) {
|
|
279
291
|
this.assertActive();
|
|
280
292
|
return this.resolver.date(key, fallback);
|
|
281
293
|
}
|
|
282
|
-
/**
|
|
283
|
-
* Gets an object value.
|
|
284
|
-
*/
|
|
285
294
|
object(key, fallback) {
|
|
286
295
|
this.assertActive();
|
|
287
296
|
return this.resolver.object(key, fallback);
|
|
288
297
|
}
|
|
289
|
-
/**
|
|
290
|
-
* Gets an array value.
|
|
291
|
-
*/
|
|
292
298
|
array(key, fallback) {
|
|
293
299
|
this.assertActive();
|
|
294
300
|
return this.resolver.array(key, fallback);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* ConfigurationManager error types re-exported from @zudojs/errors.
|
|
5
5
|
*/
|
|
6
6
|
import { ConfigurationError, createConfigurationError, isConfigurationError, missingConfigurationError, invalidConfigurationError } from "@zudojs/errors";
|
|
7
|
+
import type { ConfigValidationIssue } from "../configSchema/configSchema.type.js";
|
|
7
8
|
/**
|
|
8
9
|
* Error thrown when complete configuration validation fails.
|
|
9
10
|
*
|
|
@@ -11,8 +12,8 @@ import { ConfigurationError, createConfigurationError, isConfigurationError, mis
|
|
|
11
12
|
* the base ConfigurationError from @zudojs/errors.
|
|
12
13
|
*/
|
|
13
14
|
export declare class ConfigManagerValidationError extends ConfigurationError {
|
|
14
|
-
readonly issues: readonly
|
|
15
|
-
constructor(issues: readonly
|
|
15
|
+
readonly issues: readonly ConfigValidationIssue[];
|
|
16
|
+
constructor(issues: readonly ConfigValidationIssue[]);
|
|
16
17
|
}
|
|
17
18
|
export { ConfigurationError, createConfigurationError, isConfigurationError, missingConfigurationError, invalidConfigurationError, };
|
|
18
19
|
//# sourceMappingURL=configManager.error.d.ts.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ConfigValue } from "../../configValue/configValue.core.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TypedConfigSchema } from "../../configSchema/index.js";
|
|
3
3
|
import type { ConfigResolver } from "../core/configResolver.core.js";
|
|
4
|
+
import type { ConfigWiden } from "../core/configResolver.type.js";
|
|
4
5
|
/**
|
|
5
6
|
* Resolver scoped to a configuration key prefix.
|
|
6
7
|
*/
|
|
@@ -13,15 +14,51 @@ export declare class ScopedConfigResolver {
|
|
|
13
14
|
*/
|
|
14
15
|
key(key: string): string;
|
|
15
16
|
get<T extends ConfigValue = ConfigValue>(key: string): T | undefined;
|
|
17
|
+
get<T extends ConfigValue>(key: string, fallback: T): ConfigWiden<T>;
|
|
18
|
+
/**
|
|
19
|
+
* Returns a required value WITHOUT converting it: `T` is an unchecked
|
|
20
|
+
* cast. An environment variable is always a string, so
|
|
21
|
+
* `required<number>("port")` returns `"5432"`, not `5432`. Use
|
|
22
|
+
* `requiredNumber`, `requiredBoolean`, `requiredString` or
|
|
23
|
+
* `requiredDate`, which parse and check the value.
|
|
24
|
+
*/
|
|
16
25
|
required<T extends ConfigValue = ConfigValue>(key: string): T;
|
|
26
|
+
/** Returns a required string; throws when missing or not a string. */
|
|
27
|
+
requiredString(key: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Returns a required number, parsing decimal strings (`"5432"`); throws
|
|
30
|
+
* when missing or not a number.
|
|
31
|
+
*/
|
|
32
|
+
requiredNumber(key: string): number;
|
|
33
|
+
/**
|
|
34
|
+
* Returns a required boolean, parsing `true/false`, `1/0`, `yes/no`,
|
|
35
|
+
* `y/n` and `on/off`; throws when missing or not a boolean.
|
|
36
|
+
*/
|
|
37
|
+
requiredBoolean(key: string): boolean;
|
|
38
|
+
/** Returns a required Date, parsing ISO strings; throws when missing. */
|
|
39
|
+
requiredDate(key: string): Date;
|
|
40
|
+
string(key: string): string | undefined;
|
|
41
|
+
string(key: string, fallback: string): string;
|
|
17
42
|
string(key: string, fallback?: string): string | undefined;
|
|
43
|
+
number(key: string): number | undefined;
|
|
44
|
+
number(key: string, fallback: number): number;
|
|
18
45
|
number(key: string, fallback?: number): number | undefined;
|
|
46
|
+
boolean(key: string): boolean | undefined;
|
|
47
|
+
boolean(key: string, fallback: boolean): boolean;
|
|
19
48
|
boolean(key: string, fallback?: boolean): boolean | undefined;
|
|
49
|
+
bigint(key: string): bigint | undefined;
|
|
50
|
+
bigint(key: string, fallback: bigint): bigint;
|
|
20
51
|
bigint(key: string, fallback?: bigint): bigint | undefined;
|
|
52
|
+
date(key: string): Date | undefined;
|
|
53
|
+
date(key: string, fallback: Date): Date;
|
|
21
54
|
date(key: string, fallback?: Date): Date | undefined;
|
|
55
|
+
object<T extends ConfigValue = ConfigValue>(key: string): T | undefined;
|
|
56
|
+
object<T extends ConfigValue = ConfigValue>(key: string, fallback: T): T;
|
|
22
57
|
object<T extends ConfigValue = ConfigValue>(key: string, fallback?: T): T | undefined;
|
|
58
|
+
array<T extends ConfigValue = ConfigValue>(key: string): readonly T[] | undefined;
|
|
59
|
+
array<T extends ConfigValue = ConfigValue>(key: string, fallback: readonly T[]): readonly T[];
|
|
23
60
|
array<T extends ConfigValue = ConfigValue>(key: string, fallback?: readonly T[]): readonly T[] | undefined;
|
|
24
|
-
resolve<T extends ConfigValue>(key: string, schema:
|
|
61
|
+
resolve<T extends ConfigValue>(key: string, schema: TypedConfigSchema<T>): T | undefined;
|
|
25
62
|
pick(keys: readonly string[]): Readonly<Record<string, ConfigValue>>;
|
|
26
63
|
scoped(prefix: string): ScopedConfigResolver;
|
|
27
64
|
}
|
|
@@ -14,12 +14,41 @@ export class ScopedConfigResolver {
|
|
|
14
14
|
key(key) {
|
|
15
15
|
return this.prefix ? `${this.prefix}.${key}` : key;
|
|
16
16
|
}
|
|
17
|
-
get(key) {
|
|
18
|
-
return this.resolver.get(this.key(key));
|
|
17
|
+
get(key, fallback) {
|
|
18
|
+
return this.resolver.get(this.key(key), fallback);
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Returns a required value WITHOUT converting it: `T` is an unchecked
|
|
22
|
+
* cast. An environment variable is always a string, so
|
|
23
|
+
* `required<number>("port")` returns `"5432"`, not `5432`. Use
|
|
24
|
+
* `requiredNumber`, `requiredBoolean`, `requiredString` or
|
|
25
|
+
* `requiredDate`, which parse and check the value.
|
|
26
|
+
*/
|
|
20
27
|
required(key) {
|
|
21
28
|
return this.resolver.required(this.key(key));
|
|
22
29
|
}
|
|
30
|
+
/** Returns a required string; throws when missing or not a string. */
|
|
31
|
+
requiredString(key) {
|
|
32
|
+
return this.resolver.requiredString(this.key(key));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Returns a required number, parsing decimal strings (`"5432"`); throws
|
|
36
|
+
* when missing or not a number.
|
|
37
|
+
*/
|
|
38
|
+
requiredNumber(key) {
|
|
39
|
+
return this.resolver.requiredNumber(this.key(key));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Returns a required boolean, parsing `true/false`, `1/0`, `yes/no`,
|
|
43
|
+
* `y/n` and `on/off`; throws when missing or not a boolean.
|
|
44
|
+
*/
|
|
45
|
+
requiredBoolean(key) {
|
|
46
|
+
return this.resolver.requiredBoolean(this.key(key));
|
|
47
|
+
}
|
|
48
|
+
/** Returns a required Date, parsing ISO strings; throws when missing. */
|
|
49
|
+
requiredDate(key) {
|
|
50
|
+
return this.resolver.requiredDate(this.key(key));
|
|
51
|
+
}
|
|
23
52
|
string(key, fallback) {
|
|
24
53
|
return this.resolver.string(this.key(key), fallback);
|
|
25
54
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ConfigValue } from "../../configValue/configValue.core.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TypedConfigSchema } from "../../configSchema/index.js";
|
|
3
3
|
import type { ConfigStore } from "../../configStore/configStore.core.js";
|
|
4
|
-
import type { ConfigResolverOptions, ConfigResolutionResult } from "./configResolver.type.js";
|
|
4
|
+
import type { ConfigResolverOptions, ConfigResolutionResult, ConfigWiden } from "./configResolver.type.js";
|
|
5
5
|
import { ScopedConfigResolver } from "../accessors/configResolver.scoped.js";
|
|
6
6
|
/**
|
|
7
7
|
* Resolves typed configuration values from a ConfigStore.
|
|
@@ -17,21 +17,28 @@ export declare class ConfigResolver {
|
|
|
17
17
|
* Returns the raw configuration value.
|
|
18
18
|
*/
|
|
19
19
|
get<T extends ConfigValue = ConfigValue>(key: string): T | undefined;
|
|
20
|
+
get<T extends ConfigValue>(key: string, fallback: T): ConfigWiden<T>;
|
|
20
21
|
/**
|
|
21
22
|
* Resolves a value using a schema.
|
|
22
23
|
*/
|
|
23
|
-
resolve<T extends ConfigValue>(key: string, schema:
|
|
24
|
+
resolve<T extends ConfigValue>(key: string, schema: TypedConfigSchema<T>): T | undefined;
|
|
24
25
|
/**
|
|
25
26
|
* Resolves a value and returns diagnostic information.
|
|
26
27
|
*/
|
|
27
|
-
resolveResult<T extends ConfigValue>(key: string, schema:
|
|
28
|
+
resolveResult<T extends ConfigValue>(key: string, schema: TypedConfigSchema<T>): ConfigResolutionResult<T>;
|
|
28
29
|
/**
|
|
29
|
-
* Returns a required
|
|
30
|
+
* Returns a required value WITHOUT converting it: `T` is an unchecked
|
|
31
|
+
* cast. An environment variable is always a string, so
|
|
32
|
+
* `required<number>("port")` returns `"5432"`, not `5432`. Use
|
|
33
|
+
* `requiredNumber`, `requiredBoolean`, `requiredString` or
|
|
34
|
+
* `requiredDate`, which parse and check the value.
|
|
30
35
|
*/
|
|
31
36
|
required<T extends ConfigValue = ConfigValue>(key: string): T;
|
|
32
37
|
/**
|
|
33
38
|
* Returns a string configuration value.
|
|
34
39
|
*/
|
|
40
|
+
string(key: string): string | undefined;
|
|
41
|
+
string(key: string, fallback: string): string;
|
|
35
42
|
string(key: string, fallback?: string): string | undefined;
|
|
36
43
|
/**
|
|
37
44
|
* Returns a required string.
|
|
@@ -40,6 +47,8 @@ export declare class ConfigResolver {
|
|
|
40
47
|
/**
|
|
41
48
|
* Returns a number configuration value.
|
|
42
49
|
*/
|
|
50
|
+
number(key: string): number | undefined;
|
|
51
|
+
number(key: string, fallback: number): number;
|
|
43
52
|
number(key: string, fallback?: number): number | undefined;
|
|
44
53
|
/**
|
|
45
54
|
* Returns a required number.
|
|
@@ -48,6 +57,8 @@ export declare class ConfigResolver {
|
|
|
48
57
|
/**
|
|
49
58
|
* Returns a boolean configuration value.
|
|
50
59
|
*/
|
|
60
|
+
boolean(key: string): boolean | undefined;
|
|
61
|
+
boolean(key: string, fallback: boolean): boolean;
|
|
51
62
|
boolean(key: string, fallback?: boolean): boolean | undefined;
|
|
52
63
|
/**
|
|
53
64
|
* Returns a required boolean.
|
|
@@ -56,10 +67,14 @@ export declare class ConfigResolver {
|
|
|
56
67
|
/**
|
|
57
68
|
* Returns a bigint configuration value.
|
|
58
69
|
*/
|
|
70
|
+
bigint(key: string): bigint | undefined;
|
|
71
|
+
bigint(key: string, fallback: bigint): bigint;
|
|
59
72
|
bigint(key: string, fallback?: bigint): bigint | undefined;
|
|
60
73
|
/**
|
|
61
74
|
* Returns a Date configuration value.
|
|
62
75
|
*/
|
|
76
|
+
date(key: string): Date | undefined;
|
|
77
|
+
date(key: string, fallback: Date): Date;
|
|
63
78
|
date(key: string, fallback?: Date): Date | undefined;
|
|
64
79
|
/**
|
|
65
80
|
* Returns a required Date.
|
|
@@ -68,10 +83,14 @@ export declare class ConfigResolver {
|
|
|
68
83
|
/**
|
|
69
84
|
* Returns an object configuration value.
|
|
70
85
|
*/
|
|
86
|
+
object<T extends ConfigValue = ConfigValue>(key: string): T | undefined;
|
|
87
|
+
object<T extends ConfigValue = ConfigValue>(key: string, fallback: T): T;
|
|
71
88
|
object<T extends ConfigValue = ConfigValue>(key: string, fallback?: T): T | undefined;
|
|
72
89
|
/**
|
|
73
90
|
* Returns an array configuration value.
|
|
74
91
|
*/
|
|
92
|
+
array<T extends ConfigValue = ConfigValue>(key: string): readonly T[] | undefined;
|
|
93
|
+
array<T extends ConfigValue = ConfigValue>(key: string, fallback: readonly T[]): readonly T[];
|
|
75
94
|
array<T extends ConfigValue = ConfigValue>(key: string, fallback?: readonly T[]): readonly T[] | undefined;
|
|
76
95
|
/**
|
|
77
96
|
* Resolves a group of configuration keys.
|
|
@@ -18,12 +18,9 @@ export class ConfigResolver {
|
|
|
18
18
|
clone: options.clone ?? false,
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
* Returns the raw configuration value.
|
|
23
|
-
*/
|
|
24
|
-
get(key) {
|
|
21
|
+
get(key, fallback) {
|
|
25
22
|
const value = this.store.get(key);
|
|
26
|
-
return this.prepareValue(value);
|
|
23
|
+
return this.prepareValue(value === undefined ? fallback : value);
|
|
27
24
|
}
|
|
28
25
|
/**
|
|
29
26
|
* Resolves a value using a schema.
|
|
@@ -77,7 +74,11 @@ export class ConfigResolver {
|
|
|
77
74
|
};
|
|
78
75
|
}
|
|
79
76
|
/**
|
|
80
|
-
* Returns a required
|
|
77
|
+
* Returns a required value WITHOUT converting it: `T` is an unchecked
|
|
78
|
+
* cast. An environment variable is always a string, so
|
|
79
|
+
* `required<number>("port")` returns `"5432"`, not `5432`. Use
|
|
80
|
+
* `requiredNumber`, `requiredBoolean`, `requiredString` or
|
|
81
|
+
* `requiredDate`, which parse and check the value.
|
|
81
82
|
*/
|
|
82
83
|
required(key) {
|
|
83
84
|
const value = this.store.get(key);
|
|
@@ -92,9 +93,6 @@ export class ConfigResolver {
|
|
|
92
93
|
}
|
|
93
94
|
return this.prepareValue(value);
|
|
94
95
|
}
|
|
95
|
-
/**
|
|
96
|
-
* Returns a string configuration value.
|
|
97
|
-
*/
|
|
98
96
|
string(key, fallback) {
|
|
99
97
|
const value = this.store.get(key);
|
|
100
98
|
if (value === undefined) {
|
|
@@ -121,9 +119,6 @@ export class ConfigResolver {
|
|
|
121
119
|
}
|
|
122
120
|
return value;
|
|
123
121
|
}
|
|
124
|
-
/**
|
|
125
|
-
* Returns a number configuration value.
|
|
126
|
-
*/
|
|
127
122
|
number(key, fallback) {
|
|
128
123
|
const value = this.store.get(key);
|
|
129
124
|
if (value === undefined) {
|
|
@@ -159,9 +154,6 @@ export class ConfigResolver {
|
|
|
159
154
|
}
|
|
160
155
|
return value;
|
|
161
156
|
}
|
|
162
|
-
/**
|
|
163
|
-
* Returns a boolean configuration value.
|
|
164
|
-
*/
|
|
165
157
|
boolean(key, fallback) {
|
|
166
158
|
const value = this.store.get(key);
|
|
167
159
|
if (value === undefined) {
|
|
@@ -194,9 +186,6 @@ export class ConfigResolver {
|
|
|
194
186
|
}
|
|
195
187
|
return value;
|
|
196
188
|
}
|
|
197
|
-
/**
|
|
198
|
-
* Returns a bigint configuration value.
|
|
199
|
-
*/
|
|
200
189
|
bigint(key, fallback) {
|
|
201
190
|
const value = this.store.get(key);
|
|
202
191
|
if (value === undefined) {
|
|
@@ -213,9 +202,6 @@ export class ConfigResolver {
|
|
|
213
202
|
}
|
|
214
203
|
return this.invalidType(key, "bigint", value, fallback);
|
|
215
204
|
}
|
|
216
|
-
/**
|
|
217
|
-
* Returns a Date configuration value.
|
|
218
|
-
*/
|
|
219
205
|
date(key, fallback) {
|
|
220
206
|
const value = this.store.get(key);
|
|
221
207
|
if (value === undefined) {
|
|
@@ -254,9 +240,6 @@ export class ConfigResolver {
|
|
|
254
240
|
}
|
|
255
241
|
return value;
|
|
256
242
|
}
|
|
257
|
-
/**
|
|
258
|
-
* Returns an object configuration value.
|
|
259
|
-
*/
|
|
260
243
|
object(key, fallback) {
|
|
261
244
|
const value = this.store.get(key);
|
|
262
245
|
if (value === undefined) {
|
|
@@ -270,9 +253,6 @@ export class ConfigResolver {
|
|
|
270
253
|
}
|
|
271
254
|
return this.prepareValue(value);
|
|
272
255
|
}
|
|
273
|
-
/**
|
|
274
|
-
* Returns an array configuration value.
|
|
275
|
-
*/
|
|
276
256
|
array(key, fallback) {
|
|
277
257
|
const value = this.store.get(key);
|
|
278
258
|
if (value === undefined) {
|
|
@@ -17,4 +17,12 @@ export interface ConfigResolutionResult<T extends ConfigValue = ConfigValue> {
|
|
|
17
17
|
readonly valid: boolean;
|
|
18
18
|
readonly issues: readonly unknown[];
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Widens a literal fallback to its primitive type.
|
|
22
|
+
*
|
|
23
|
+
* `get(key, fallback)` returns the stored value when one exists, so a
|
|
24
|
+
* fallback of `false` or `"dev"` must not narrow the result to the literal
|
|
25
|
+
* `false` or `"dev"`: the result is `boolean` or `string`.
|
|
26
|
+
*/
|
|
27
|
+
export type ConfigWiden<T> = T extends string ? string : T extends number ? number : T extends boolean ? boolean : T extends bigint ? bigint : T;
|
|
20
28
|
//# sourceMappingURL=configResolver.type.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AnyConfigSchema } from "./configSchema.type.js";
|
|
2
|
+
/**
|
|
3
|
+
* Strictly coerces string input for `NUMBER` and `BOOLEAN` schemas.
|
|
4
|
+
*
|
|
5
|
+
* Environment-style sources only ever produce strings. The value is
|
|
6
|
+
* returned unchanged when it is not a string, when the schema opted out
|
|
7
|
+
* with `coerce: false`, when the schema also accepts strings (or any
|
|
8
|
+
* value), or when the string does not parse; the type check that follows
|
|
9
|
+
* then reports the mismatch as before.
|
|
10
|
+
*
|
|
11
|
+
* `NUMBER` is tried before `BOOLEAN`, so `"1"` against
|
|
12
|
+
* `[NUMBER, BOOLEAN]` becomes `1`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function coerceConfigInput(value: unknown, schema: AnyConfigSchema): unknown;
|
|
15
|
+
//# sourceMappingURL=configSchema.coerce.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { parseConfigBoolean, parseConfigNumber, } from "../configValue/configValue.core.js";
|
|
2
|
+
import { ConfigValueType } from "./configSchema.type.js";
|
|
3
|
+
/**
|
|
4
|
+
* Returns whether a schema type (single or union) includes `candidate`.
|
|
5
|
+
*/
|
|
6
|
+
function includesType(type, candidate) {
|
|
7
|
+
return Array.isArray(type) ? type.includes(candidate) : type === candidate;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Strictly coerces string input for `NUMBER` and `BOOLEAN` schemas.
|
|
11
|
+
*
|
|
12
|
+
* Environment-style sources only ever produce strings. The value is
|
|
13
|
+
* returned unchanged when it is not a string, when the schema opted out
|
|
14
|
+
* with `coerce: false`, when the schema also accepts strings (or any
|
|
15
|
+
* value), or when the string does not parse; the type check that follows
|
|
16
|
+
* then reports the mismatch as before.
|
|
17
|
+
*
|
|
18
|
+
* `NUMBER` is tried before `BOOLEAN`, so `"1"` against
|
|
19
|
+
* `[NUMBER, BOOLEAN]` becomes `1`.
|
|
20
|
+
*/
|
|
21
|
+
export function coerceConfigInput(value, schema) {
|
|
22
|
+
if (typeof value !== "string" || schema.coerce === false) {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
const { type } = schema;
|
|
26
|
+
if (includesType(type, ConfigValueType.STRING) ||
|
|
27
|
+
includesType(type, ConfigValueType.ANY)) {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
if (includesType(type, ConfigValueType.NUMBER)) {
|
|
31
|
+
const parsed = parseConfigNumber(value);
|
|
32
|
+
if (parsed !== undefined) {
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (includesType(type, ConfigValueType.BOOLEAN)) {
|
|
37
|
+
const parsed = parseConfigBoolean(value);
|
|
38
|
+
if (parsed !== undefined) {
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=configSchema.coerce.js.map
|
|
@@ -64,7 +64,34 @@ export interface ConfigSchema<T extends ConfigValue = ConfigValue> {
|
|
|
64
64
|
* serialization redact them.
|
|
65
65
|
*/
|
|
66
66
|
readonly secret?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Coerces string input before the type check. Defaults to `true`.
|
|
69
|
+
*
|
|
70
|
+
* Environment variables, `.env` files and CLI flags are always
|
|
71
|
+
* strings, so a `NUMBER` or `BOOLEAN` schema could never pass on them.
|
|
72
|
+
* When the value is a string, the schema does not itself accept
|
|
73
|
+
* strings, and the schema type includes `NUMBER` or `BOOLEAN`, the
|
|
74
|
+
* string is parsed strictly first: decimal numbers only (`"8080"`
|
|
75
|
+
* passes, `"80a"` and `"0x1F90"` do not), and booleans by the
|
|
76
|
+
* `parseConfigBoolean` convention (`true/false`, `1/0`, `yes/no`,
|
|
77
|
+
* `y/n`, `on/off`). A string that does not parse is still reported
|
|
78
|
+
* as `TYPE_MISMATCH`. `validate` and `transform` receive the coerced
|
|
79
|
+
* value. Set `false` to require a real number or boolean.
|
|
80
|
+
*/
|
|
81
|
+
readonly coerce?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Custom check on the FINAL value, after coercion and `transform`.
|
|
84
|
+
* Returning `false`, a message or issues fails validation.
|
|
85
|
+
*/
|
|
67
86
|
readonly validate?: (value: T, context: ConfigValidationContext) => boolean | string | ConfigValidationIssue | readonly ConfigValidationIssue[];
|
|
87
|
+
/**
|
|
88
|
+
* Converts the value. A value that already has the schema's `type` is
|
|
89
|
+
* transformed after its constraints pass. A STRING that does not have
|
|
90
|
+
* the type is passed to `transform` as a parser, and the output must
|
|
91
|
+
* then have the type and satisfy the constraints (so `{ type: ARRAY,
|
|
92
|
+
* transform: (s) => String(s).split(",") }` accepts `"a,b"`). A
|
|
93
|
+
* non-string of the wrong type is rejected without calling it.
|
|
94
|
+
*/
|
|
68
95
|
readonly transform?: (value: ConfigValue, context: ConfigValidationContext) => T;
|
|
69
96
|
}
|
|
70
97
|
/**
|
|
@@ -119,30 +146,73 @@ export interface ConfigArraySchema<T extends ConfigValue = ConfigValue> extends
|
|
|
119
146
|
readonly minItems?: number;
|
|
120
147
|
readonly maxItems?: number;
|
|
121
148
|
}
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
*/
|
|
125
|
-
export interface ConfigStringSchema extends ConfigSchema<string> {
|
|
126
|
-
readonly type: ConfigValueType.STRING;
|
|
149
|
+
/** Constraints enforced on a string value. */
|
|
150
|
+
export interface ConfigStringConstraints {
|
|
127
151
|
readonly minLength?: number;
|
|
128
152
|
readonly maxLength?: number;
|
|
129
153
|
readonly pattern?: string | RegExp;
|
|
130
154
|
readonly enum?: readonly string[];
|
|
131
155
|
}
|
|
132
|
-
/**
|
|
133
|
-
|
|
134
|
-
*/
|
|
135
|
-
export interface ConfigNumberSchema extends ConfigSchema<number> {
|
|
136
|
-
readonly type: ConfigValueType.NUMBER;
|
|
156
|
+
/** Constraints enforced on a number value. */
|
|
157
|
+
export interface ConfigNumberConstraints {
|
|
137
158
|
readonly min?: number;
|
|
138
159
|
readonly max?: number;
|
|
139
160
|
readonly integer?: boolean;
|
|
140
161
|
readonly positive?: boolean;
|
|
141
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* String configuration schema.
|
|
165
|
+
*/
|
|
166
|
+
export interface ConfigStringSchema extends ConfigSchema<string>, ConfigStringConstraints {
|
|
167
|
+
readonly type: ConfigValueType.STRING;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Number configuration schema.
|
|
171
|
+
*/
|
|
172
|
+
export interface ConfigNumberSchema extends ConfigSchema<number>, ConfigNumberConstraints {
|
|
173
|
+
readonly type: ConfigValueType.NUMBER;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* A schema accepted by `resolve()` / `resolveResult()`: a union keyed on
|
|
177
|
+
* `type`, so each value type carries exactly the constraints the
|
|
178
|
+
* validator enforces for it. `{ type: NUMBER, min: 1 }` and
|
|
179
|
+
* `{ type: STRING, minLength: 1 }` type-check; `{ type: NUMBER,
|
|
180
|
+
* minLength: 1 }` does not. `T` is the resolved (post-`transform`) type.
|
|
181
|
+
*/
|
|
182
|
+
export type TypedConfigSchema<T extends ConfigValue = ConfigValue> = (ConfigSchemaBase<T> & ConfigStringConstraints & {
|
|
183
|
+
readonly type: ConfigValueType.STRING;
|
|
184
|
+
}) | (ConfigSchemaBase<T> & ConfigNumberConstraints & {
|
|
185
|
+
readonly type: ConfigValueType.NUMBER;
|
|
186
|
+
}) | (ConfigSchemaBase<T> & ConfigArrayConstraints & {
|
|
187
|
+
readonly type: ConfigValueType.ARRAY;
|
|
188
|
+
}) | (ConfigSchemaBase<T> & ConfigObjectConstraints & {
|
|
189
|
+
readonly type: ConfigValueType.OBJECT;
|
|
190
|
+
}) | (ConfigSchemaBase<T> & {
|
|
191
|
+
readonly type: ConfigValueType.BOOLEAN | ConfigValueType.BIGINT | ConfigValueType.DATE | ConfigValueType.NULL | ConfigValueType.ANY;
|
|
192
|
+
}) | (ConfigSchemaBase<T> & ConfigStringConstraints & ConfigNumberConstraints & ConfigArrayConstraints & ConfigObjectConstraints & {
|
|
193
|
+
readonly type: readonly ConfigValueType[];
|
|
194
|
+
});
|
|
195
|
+
/**
|
|
196
|
+
* {@link ConfigSchema} without `type`, so each {@link TypedConfigSchema}
|
|
197
|
+
* member can declare a single `type` that TypeScript discriminates on.
|
|
198
|
+
*/
|
|
199
|
+
type ConfigSchemaBase<T extends ConfigValue> = Omit<ConfigSchema<T>, "type">;
|
|
200
|
+
/** Constraints enforced on an array value. */
|
|
201
|
+
export interface ConfigArrayConstraints {
|
|
202
|
+
readonly items?: AnyConfigSchema;
|
|
203
|
+
readonly minItems?: number;
|
|
204
|
+
readonly maxItems?: number;
|
|
205
|
+
}
|
|
206
|
+
/** Nested property schemas enforced on an object value. */
|
|
207
|
+
export interface ConfigObjectConstraints {
|
|
208
|
+
readonly properties?: Readonly<Record<string, AnyConfigSchema>>;
|
|
209
|
+
readonly additionalProperties?: boolean | AnyConfigSchema;
|
|
210
|
+
}
|
|
142
211
|
/**
|
|
143
212
|
* Boolean configuration schema.
|
|
144
213
|
*/
|
|
145
214
|
export interface ConfigBooleanSchema extends ConfigSchema<boolean> {
|
|
146
215
|
readonly type: ConfigValueType.BOOLEAN;
|
|
147
216
|
}
|
|
217
|
+
export {};
|
|
148
218
|
//# sourceMappingURL=configSchema.type.d.ts.map
|
|
@@ -17,7 +17,7 @@ export declare function createConfigValidationIssue(path: string, message: strin
|
|
|
17
17
|
/**
|
|
18
18
|
* Validates a value against a schema.
|
|
19
19
|
*/
|
|
20
|
-
export declare function validateConfigValue(
|
|
20
|
+
export declare function validateConfigValue(input: unknown, schema: AnyConfigSchema, context?: Partial<ConfigValidationContext>): ConfigValidationResult;
|
|
21
21
|
/**
|
|
22
22
|
* Validates an entire configuration object.
|
|
23
23
|
*/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineConfigProperty, readOwnConfigProperty, } from "../configValue/configValue.core.js";
|
|
2
2
|
import { ConfigValidationSeverity, ConfigValueType, } from "./configSchema.type.js";
|
|
3
|
+
import { coerceConfigInput } from "./configSchema.coerce.js";
|
|
3
4
|
/**
|
|
4
5
|
* Returns the runtime configuration value type.
|
|
5
6
|
*/
|
|
@@ -218,8 +219,11 @@ function validateBuiltInRules(value, schema, context, issues) {
|
|
|
218
219
|
/**
|
|
219
220
|
* Validates a value against a schema.
|
|
220
221
|
*/
|
|
221
|
-
export function validateConfigValue(
|
|
222
|
+
export function validateConfigValue(input, schema, context) {
|
|
222
223
|
const path = context?.path ?? "$";
|
|
224
|
+
// Env-style strings are parsed for NUMBER/BOOLEAN schemas before the
|
|
225
|
+
// type check; otherwise "8080" could never satisfy a NUMBER schema.
|
|
226
|
+
const value = coerceConfigInput(input, schema);
|
|
223
227
|
const validationContext = {
|
|
224
228
|
path,
|
|
225
229
|
root: context?.root ?? value,
|
|
@@ -256,63 +260,107 @@ export function validateConfigValue(value, schema, context) {
|
|
|
256
260
|
issues,
|
|
257
261
|
};
|
|
258
262
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
263
|
+
const hasErrors = () => issues.some((issue) => issue.severity === ConfigValidationSeverity.ERROR);
|
|
264
|
+
// Order: coerce -> (transform) -> type check -> constraints -> validate.
|
|
265
|
+
// A value that already has the schema's type is constrained first and
|
|
266
|
+
// transformed afterwards (so a type-changing transform such as
|
|
267
|
+
// `STRING -> Number(v)` keeps working). A string that does NOT have the
|
|
268
|
+
// type is handed to `transform` as a parser, and its OUTPUT must have
|
|
269
|
+
// the type and satisfy the constraints. Either way `validate` receives
|
|
270
|
+
// the final value, matching its `(value: T)` signature.
|
|
271
|
+
let base;
|
|
272
|
+
let transformed = false;
|
|
273
|
+
if (matchesConfigType(value, schema.type)) {
|
|
274
|
+
base = checkConstraints(value, schema, validationContext, issues);
|
|
268
275
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
objectSchema.additionalProperties !== undefined)) {
|
|
281
|
-
const nested = validateConfigObject(value, {
|
|
282
|
-
type: ConfigValueType.OBJECT,
|
|
283
|
-
properties: objectSchema.properties ?? {},
|
|
284
|
-
additionalProperties: objectSchema.additionalProperties,
|
|
285
|
-
}, path);
|
|
286
|
-
issues.push(...nested.issues);
|
|
287
|
-
if (nested.value !== undefined) {
|
|
288
|
-
base = nested.value;
|
|
276
|
+
else {
|
|
277
|
+
const parsed = parseWithTransform(value, schema, validationContext);
|
|
278
|
+
if (parsed === NOT_PARSED || !matchesConfigType(parsed, schema.type)) {
|
|
279
|
+
issues.push(createConfigValidationIssue(path, `Expected ${formatExpectedType(schema.type)} but received ${getConfigValueType(value)}.`, "TYPE_MISMATCH", {
|
|
280
|
+
expected: schema.type,
|
|
281
|
+
received: getConfigValueType(value),
|
|
282
|
+
}));
|
|
283
|
+
return {
|
|
284
|
+
valid: false,
|
|
285
|
+
issues,
|
|
286
|
+
};
|
|
289
287
|
}
|
|
288
|
+
transformed = true;
|
|
289
|
+
base = checkConstraints(parsed, schema, validationContext, issues);
|
|
290
290
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
appendCustomValidationResult(result, path, issues);
|
|
294
|
-
}
|
|
295
|
-
let transformed = base;
|
|
296
|
-
const hasErrors = () => issues.some((issue) => issue.severity === ConfigValidationSeverity.ERROR);
|
|
297
|
-
// Transforms only run on values that passed validation; running
|
|
291
|
+
let final = base;
|
|
292
|
+
// Transforms only run on values that passed their constraints; running
|
|
298
293
|
// them on invalid input would surface invalid values to callers.
|
|
299
|
-
if (schema.transform && !hasErrors()) {
|
|
294
|
+
if (!transformed && schema.transform && !hasErrors()) {
|
|
300
295
|
try {
|
|
301
|
-
|
|
296
|
+
final = schema.transform(base, validationContext);
|
|
297
|
+
transformed = true;
|
|
302
298
|
}
|
|
303
299
|
catch (error) {
|
|
304
300
|
issues.push(createConfigValidationIssue(path, error instanceof Error ? error.message : String(error), "TRANSFORM_FAILED"));
|
|
305
301
|
}
|
|
306
302
|
}
|
|
303
|
+
// `validate` sees the final value. It is skipped only when a transform
|
|
304
|
+
// should have produced that value but did not run.
|
|
305
|
+
if (schema.validate && (transformed || schema.transform === undefined)) {
|
|
306
|
+
const result = schema.validate(final, validationContext);
|
|
307
|
+
appendCustomValidationResult(result, path, issues);
|
|
308
|
+
}
|
|
307
309
|
const valid = !hasErrors();
|
|
308
310
|
return {
|
|
309
311
|
valid,
|
|
310
312
|
// Invalid values are never returned; callers fall back to the
|
|
311
313
|
// schema default or undefined instead.
|
|
312
|
-
value: valid ?
|
|
314
|
+
value: valid ? final : undefined,
|
|
313
315
|
issues,
|
|
314
316
|
};
|
|
315
317
|
}
|
|
318
|
+
/** Marks a transform that threw, or a schema without one. */
|
|
319
|
+
const NOT_PARSED = Symbol("NOT_PARSED");
|
|
320
|
+
/**
|
|
321
|
+
* Runs `schema.transform` as a parser on a STRING that failed the type
|
|
322
|
+
* check (environment variables, `.env` files and CLI flags only produce
|
|
323
|
+
* strings). Returns {@link NOT_PARSED} for any other value, when there is
|
|
324
|
+
* no transform, or when it threw; the caller then reports the original
|
|
325
|
+
* type mismatch. A non-string of the wrong type is a real type error and
|
|
326
|
+
* is never handed to `transform`.
|
|
327
|
+
*/
|
|
328
|
+
function parseWithTransform(value, schema, context) {
|
|
329
|
+
if (schema.transform === undefined || typeof value !== "string") {
|
|
330
|
+
return NOT_PARSED;
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
return schema.transform(value, context);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return NOT_PARSED;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Applies the built-in constraints and, for object schemas, the nested
|
|
341
|
+
* property schemas. Returns the value to continue with (rebuilt when an
|
|
342
|
+
* item or property schema rewrote part of it).
|
|
343
|
+
*/
|
|
344
|
+
function checkConstraints(value, schema, context, issues) {
|
|
345
|
+
const rewritten = validateBuiltInRules(value, schema, context, issues);
|
|
346
|
+
// An object schema carries `properties` / `additionalProperties`;
|
|
347
|
+
// delegate to validateConfigObject so nested constraints are enforced.
|
|
348
|
+
const objectSchema = schema;
|
|
349
|
+
if (matchesConfigType(value, ConfigValueType.OBJECT) &&
|
|
350
|
+
(objectSchema.properties !== undefined ||
|
|
351
|
+
objectSchema.additionalProperties !== undefined)) {
|
|
352
|
+
const nested = validateConfigObject(value, {
|
|
353
|
+
type: ConfigValueType.OBJECT,
|
|
354
|
+
properties: objectSchema.properties ?? {},
|
|
355
|
+
additionalProperties: objectSchema.additionalProperties,
|
|
356
|
+
}, context.path);
|
|
357
|
+
issues.push(...nested.issues);
|
|
358
|
+
if (nested.value !== undefined) {
|
|
359
|
+
return nested.value;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return rewritten ?? value;
|
|
363
|
+
}
|
|
316
364
|
/**
|
|
317
365
|
* Validates an entire configuration object.
|
|
318
366
|
*/
|
|
@@ -105,11 +105,13 @@ export declare class ConfigStore {
|
|
|
105
105
|
*/
|
|
106
106
|
toSafeObject(): Readonly<Record<string, ConfigValue>>;
|
|
107
107
|
/**
|
|
108
|
-
* Returns entries matching a prefix.
|
|
108
|
+
* Returns entries matching a prefix. A trailing dot is optional:
|
|
109
|
+
* `"db"` and `"db."` both match `db` and `db.*` (but not `dbx`).
|
|
109
110
|
*/
|
|
110
111
|
getByPrefix(prefix: string): readonly ConfigEntry[];
|
|
111
112
|
/**
|
|
112
|
-
* Returns configuration values matching a prefix
|
|
113
|
+
* Returns configuration values matching a prefix, keyed by the rest of
|
|
114
|
+
* the key. A trailing dot is optional, as for {@link getByPrefix}.
|
|
113
115
|
*/
|
|
114
116
|
getObjectByPrefix(prefix: string): Readonly<Record<string, ConfigValue>>;
|
|
115
117
|
/**
|
|
@@ -14,6 +14,18 @@ export function normalizeKey(key) {
|
|
|
14
14
|
}
|
|
15
15
|
return normalized;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Normalizes a key prefix: trims it and drops trailing dots, so `"db."`
|
|
19
|
+
* and `"db"` select the same entries. A prefix that is only dots is
|
|
20
|
+
* rejected like an empty key.
|
|
21
|
+
*/
|
|
22
|
+
function normalizePrefix(prefix) {
|
|
23
|
+
const normalized = normalizeKey(prefix).replace(/\.+$/, "");
|
|
24
|
+
if (normalized.length === 0) {
|
|
25
|
+
throw new TypeError("Configuration prefix cannot be empty.");
|
|
26
|
+
}
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
17
29
|
/**
|
|
18
30
|
* Central in-memory configuration store.
|
|
19
31
|
*
|
|
@@ -248,20 +260,22 @@ export class ConfigStore {
|
|
|
248
260
|
return Object.freeze(result);
|
|
249
261
|
}
|
|
250
262
|
/**
|
|
251
|
-
* Returns entries matching a prefix.
|
|
263
|
+
* Returns entries matching a prefix. A trailing dot is optional:
|
|
264
|
+
* `"db"` and `"db."` both match `db` and `db.*` (but not `dbx`).
|
|
252
265
|
*/
|
|
253
266
|
getByPrefix(prefix) {
|
|
254
267
|
this.assertActive();
|
|
255
|
-
const normalizedPrefix =
|
|
268
|
+
const normalizedPrefix = normalizePrefix(prefix);
|
|
256
269
|
return Array.from(this.entries.values()).filter((entry) => entry.key === normalizedPrefix ||
|
|
257
270
|
entry.key.startsWith(`${normalizedPrefix}.`));
|
|
258
271
|
}
|
|
259
272
|
/**
|
|
260
|
-
* Returns configuration values matching a prefix
|
|
273
|
+
* Returns configuration values matching a prefix, keyed by the rest of
|
|
274
|
+
* the key. A trailing dot is optional, as for {@link getByPrefix}.
|
|
261
275
|
*/
|
|
262
276
|
getObjectByPrefix(prefix) {
|
|
263
277
|
const entries = this.getByPrefix(prefix);
|
|
264
|
-
const normalizedPrefix =
|
|
278
|
+
const normalizedPrefix = normalizePrefix(prefix);
|
|
265
279
|
const result = {};
|
|
266
280
|
for (const entry of entries) {
|
|
267
281
|
const key = entry.key === normalizedPrefix
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/config",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Layered configuration management with multiple sources, validation, and environment-specific overrides.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
"node": ">=24.0.0"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@zudojs/constants": "1.1.
|
|
32
|
-
"@zudojs/errors": "1.
|
|
31
|
+
"@zudojs/constants": "1.1.2",
|
|
32
|
+
"@zudojs/errors": "1.3.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"typescript": "7.0.2",
|
|
36
|
-
"vitest": "^
|
|
36
|
+
"vitest": "^5.0.1"
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"environment",
|
|
45
45
|
"settings"
|
|
46
46
|
],
|
|
47
|
-
"homepage": "https://
|
|
47
|
+
"homepage": "https://zudojs.oyinlola.site/docs/packages-config",
|
|
48
48
|
"bugs": {
|
|
49
49
|
"url": "https://github.com/oyinlola-tech/zudo/issues"
|
|
50
50
|
},
|