@zudojs/config 1.1.0 → 1.3.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/README.md +82 -1
- package/dist/configLoader/configLoader.core.js +6 -2
- package/dist/configManager/configManager.core.d.ts +37 -3
- package/dist/configManager/configManager.core.js +47 -32
- 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/configSource/configSource.core.d.ts +10 -0
- package/dist/configSource/configSource.core.js +11 -1
- package/dist/configStore/configStore.core.d.ts +4 -2
- package/dist/configStore/configStore.core.js +25 -5
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -58,6 +58,16 @@ await `manager.ready()` before reading values.
|
|
|
58
58
|
A source provides values plus a priority. Higher priority wins; equal
|
|
59
59
|
priorities are applied in registration order (last registered wins).
|
|
60
60
|
|
|
61
|
+
A source that declares no priority gets `DEFAULT_CONFIG_SOURCE_PRIORITY`
|
|
62
|
+
(`-1`), which is strictly below the priority `initialValues` is seeded
|
|
63
|
+
at (`0`), so an undeclared source layers UNDER the values a manager was
|
|
64
|
+
seeded with. Declare `priority: 0` (or higher) on the source when it
|
|
65
|
+
should override them.
|
|
66
|
+
|
|
67
|
+
Sources are deduplicated by name — the first occurrence of a name wins,
|
|
68
|
+
both when they are passed to the constructor and through `addSource()`
|
|
69
|
+
(which rejects a duplicate outright).
|
|
70
|
+
|
|
61
71
|
- `createDefaultsConfigSource(values, name?)` — low-priority defaults.
|
|
62
72
|
- `createMemoryConfigSource(values, { name, priority?, optional? })` —
|
|
63
73
|
in-memory values.
|
|
@@ -108,6 +118,28 @@ const db = manager.scoped("db");
|
|
|
108
118
|
db.string("host", "localhost");
|
|
109
119
|
```
|
|
110
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
|
+
|
|
111
143
|
`number()` accepts decimal notation only: `"0x1F90"`, `"0b11"` and `"0o17"`
|
|
112
144
|
are rejected rather than silently becoming 8080, 3 and 15.
|
|
113
145
|
|
|
@@ -128,6 +160,46 @@ const config = manager.validate({
|
|
|
128
160
|
});
|
|
129
161
|
```
|
|
130
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
|
+
|
|
131
203
|
Object schemas nest: a property schema of type `OBJECT` that declares
|
|
132
204
|
its own `properties` / `additionalProperties` is validated recursively,
|
|
133
205
|
so nested constraints are enforced by `validate()`, `resolve()` and the
|
|
@@ -147,12 +219,21 @@ entry is redacted). Individual values can be validated with
|
|
|
147
219
|
including secrets.
|
|
148
220
|
- `manager.toSafeObject()` / `store.toSafeObject()` replace sensitive
|
|
149
221
|
values with `"[REDACTED]"` — use these for logging and diagnostics.
|
|
222
|
+
- An entry is marked sensitive automatically by `ConfigStore.set()`, so
|
|
223
|
+
the same key is redacted however it was written: from a source, from
|
|
224
|
+
`initialValues`, from `set()` / `setMany()` / `replace()`, or from
|
|
225
|
+
`manager.set()`. Detection is `isSensitiveConfigEntry` — the key name
|
|
226
|
+
(`password`, `secret`, `token`, `*_key`, `auth`, `dsn`,
|
|
227
|
+
`database_url`, …) or a value that is a URL with embedded
|
|
228
|
+
`user:password@` credentials. Pass `sensitive: false` explicitly to
|
|
229
|
+
opt a key out.
|
|
150
230
|
|
|
151
231
|
## Reloading
|
|
152
232
|
|
|
153
233
|
`manager.reload()` re-runs all sources. Values set at runtime via
|
|
154
234
|
`manager.set()` and `initialValues` survive loads; sources only
|
|
155
|
-
overwrite entries at equal or higher priority
|
|
235
|
+
overwrite entries at equal or higher priority, and a source that
|
|
236
|
+
declares no priority ranks below both. A reload rebuilds every
|
|
156
237
|
source-provided value from scratch, so a value a source no longer
|
|
157
238
|
provides disappears (letting a lower-priority default show through), and
|
|
158
239
|
the store is only updated once every source has loaded — a failing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cloneConfigValue } from "../configValue/configValue.core.js";
|
|
2
2
|
import { createConfigEntry } from "../configEntry/configEntry.type.js";
|
|
3
|
-
import { loadConfigSourceStrict, sortConfigSources, } from "../configSource/configSource.core.js";
|
|
3
|
+
import { deduplicateConfigSources, loadConfigSourceStrict, sortConfigSources, } from "../configSource/configSource.core.js";
|
|
4
4
|
import { isSensitiveConfigEntry } from "../configSource/configSource.sensitive.js";
|
|
5
5
|
import { commitReloadStaging, createReloadStaging, } from "./configLoader.reload.js";
|
|
6
6
|
import { createConfigStore } from "../configStore/configStore.factory.js";
|
|
@@ -23,7 +23,11 @@ export class ConfigLoader {
|
|
|
23
23
|
disposed = false;
|
|
24
24
|
lastResult;
|
|
25
25
|
constructor(options = {}) {
|
|
26
|
-
|
|
26
|
+
// addSource() rejects a name that is already registered, but the
|
|
27
|
+
// constructor used to accept duplicates silently — and because a
|
|
28
|
+
// later duplicate overwrote the earlier one, the LAST occurrence
|
|
29
|
+
// won, the opposite of the documented "first occurrence wins".
|
|
30
|
+
this.sources = [...deduplicateConfigSources(options.sources ?? [])];
|
|
27
31
|
this.context = options.context ?? {};
|
|
28
32
|
this.store =
|
|
29
33
|
options.store ??
|
|
@@ -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.
|
|
@@ -38,15 +38,21 @@ export class ConfigManager {
|
|
|
38
38
|
this.store =
|
|
39
39
|
suppliedStore ??
|
|
40
40
|
createConfigStore({
|
|
41
|
-
initialValues: options.initialValues,
|
|
42
41
|
freeze: options.freeze ?? true,
|
|
43
42
|
});
|
|
44
43
|
// initialValues used to be wired ONLY into a store the manager
|
|
45
44
|
// created itself: passing `store` or `loader` silently discarded
|
|
46
|
-
// them.
|
|
47
|
-
|
|
45
|
+
// them. Both paths are seeded here instead, at the baseline
|
|
46
|
+
// priority 0. A source that declares no priority now ranks below
|
|
47
|
+
// that (DEFAULT_CONFIG_SOURCE_PRIORITY), so it can no longer wipe
|
|
48
|
+
// the seeds; a source that declares a priority of 0 or above
|
|
49
|
+
// still overrides them.
|
|
50
|
+
if (options.initialValues) {
|
|
48
51
|
for (const [key, value] of Object.entries(options.initialValues)) {
|
|
49
|
-
|
|
52
|
+
this.store.set(key, value, {
|
|
53
|
+
source: "initialValues",
|
|
54
|
+
priority: 0,
|
|
55
|
+
});
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
58
|
this.name = options.name ?? "config";
|
|
@@ -217,20 +223,47 @@ export class ConfigManager {
|
|
|
217
223
|
markSecretEntries(this.store, collectSecretPaths(schema.properties));
|
|
218
224
|
return cloneConfigValue(result.value);
|
|
219
225
|
}
|
|
220
|
-
|
|
221
|
-
* Gets a raw configuration value.
|
|
222
|
-
*/
|
|
223
|
-
get(key) {
|
|
226
|
+
get(key, fallback) {
|
|
224
227
|
this.assertActive();
|
|
225
|
-
return this.resolver.get(key);
|
|
228
|
+
return this.resolver.get(key, fallback);
|
|
226
229
|
}
|
|
227
230
|
/**
|
|
228
|
-
*
|
|
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.
|
|
229
236
|
*/
|
|
230
237
|
required(key) {
|
|
231
238
|
this.assertActive();
|
|
232
239
|
return this.resolver.required(key);
|
|
233
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
|
+
}
|
|
234
267
|
/**
|
|
235
268
|
* Gets a configuration value with schema validation.
|
|
236
269
|
*/
|
|
@@ -238,51 +271,30 @@ export class ConfigManager {
|
|
|
238
271
|
this.assertActive();
|
|
239
272
|
return this.resolver.resolve(key, schema);
|
|
240
273
|
}
|
|
241
|
-
/**
|
|
242
|
-
* Gets a string value.
|
|
243
|
-
*/
|
|
244
274
|
string(key, fallback) {
|
|
245
275
|
this.assertActive();
|
|
246
276
|
return this.resolver.string(key, fallback);
|
|
247
277
|
}
|
|
248
|
-
/**
|
|
249
|
-
* Gets a number value.
|
|
250
|
-
*/
|
|
251
278
|
number(key, fallback) {
|
|
252
279
|
this.assertActive();
|
|
253
280
|
return this.resolver.number(key, fallback);
|
|
254
281
|
}
|
|
255
|
-
/**
|
|
256
|
-
* Gets a boolean value.
|
|
257
|
-
*/
|
|
258
282
|
boolean(key, fallback) {
|
|
259
283
|
this.assertActive();
|
|
260
284
|
return this.resolver.boolean(key, fallback);
|
|
261
285
|
}
|
|
262
|
-
/**
|
|
263
|
-
* Gets a bigint value.
|
|
264
|
-
*/
|
|
265
286
|
bigint(key, fallback) {
|
|
266
287
|
this.assertActive();
|
|
267
288
|
return this.resolver.bigint(key, fallback);
|
|
268
289
|
}
|
|
269
|
-
/**
|
|
270
|
-
* Gets a Date value.
|
|
271
|
-
*/
|
|
272
290
|
date(key, fallback) {
|
|
273
291
|
this.assertActive();
|
|
274
292
|
return this.resolver.date(key, fallback);
|
|
275
293
|
}
|
|
276
|
-
/**
|
|
277
|
-
* Gets an object value.
|
|
278
|
-
*/
|
|
279
294
|
object(key, fallback) {
|
|
280
295
|
this.assertActive();
|
|
281
296
|
return this.resolver.object(key, fallback);
|
|
282
297
|
}
|
|
283
|
-
/**
|
|
284
|
-
* Gets an array value.
|
|
285
|
-
*/
|
|
286
298
|
array(key, fallback) {
|
|
287
299
|
this.assertActive();
|
|
288
300
|
return this.resolver.array(key, fallback);
|
|
@@ -295,7 +307,10 @@ export class ConfigManager {
|
|
|
295
307
|
return this.store.set(key, value, {
|
|
296
308
|
source: options.source ?? "runtime",
|
|
297
309
|
priority: options.priority ?? Number.MAX_SAFE_INTEGER,
|
|
298
|
-
|
|
310
|
+
// Left undefined so the store's own secret detection runs; an
|
|
311
|
+
// explicit `false` here would have disabled it for every runtime
|
|
312
|
+
// write.
|
|
313
|
+
sensitive: options.sensitive,
|
|
299
314
|
});
|
|
300
315
|
}
|
|
301
316
|
/**
|
|
@@ -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
|
*/
|
|
@@ -77,6 +77,16 @@ export interface ConfigSourceOptions {
|
|
|
77
77
|
* Checks whether a value is a configuration source.
|
|
78
78
|
*/
|
|
79
79
|
export declare function isConfigSource(value: unknown): value is ConfigSource;
|
|
80
|
+
/**
|
|
81
|
+
* Priority given to a source that declares none.
|
|
82
|
+
*
|
|
83
|
+
* Strictly below the priority `initialValues` and other baseline store
|
|
84
|
+
* writes use (0). A source used to default to 0 as well, and because
|
|
85
|
+
* `applySource` overwrites on EQUAL priority, any undeclared source
|
|
86
|
+
* silently wiped the values a manager was seeded with. Declaring a
|
|
87
|
+
* priority of 0 or above still overrides them.
|
|
88
|
+
*/
|
|
89
|
+
export declare const DEFAULT_CONFIG_SOURCE_PRIORITY = -1;
|
|
80
90
|
/**
|
|
81
91
|
* Creates a function-based configuration source.
|
|
82
92
|
*/
|
|
@@ -32,12 +32,22 @@ export function isConfigSource(value) {
|
|
|
32
32
|
typeof source.optional === "boolean" &&
|
|
33
33
|
typeof source.load === "function");
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Priority given to a source that declares none.
|
|
37
|
+
*
|
|
38
|
+
* Strictly below the priority `initialValues` and other baseline store
|
|
39
|
+
* writes use (0). A source used to default to 0 as well, and because
|
|
40
|
+
* `applySource` overwrites on EQUAL priority, any undeclared source
|
|
41
|
+
* silently wiped the values a manager was seeded with. Declaring a
|
|
42
|
+
* priority of 0 or above still overrides them.
|
|
43
|
+
*/
|
|
44
|
+
export const DEFAULT_CONFIG_SOURCE_PRIORITY = -1;
|
|
35
45
|
/**
|
|
36
46
|
* Creates a function-based configuration source.
|
|
37
47
|
*/
|
|
38
48
|
export function createConfigSource(options, loader) {
|
|
39
49
|
const type = options.type ?? ConfigSourceType.CUSTOM;
|
|
40
|
-
const priority = options.priority ??
|
|
50
|
+
const priority = options.priority ?? DEFAULT_CONFIG_SOURCE_PRIORITY;
|
|
41
51
|
if (options.name.trim().length === 0) {
|
|
42
52
|
throw new TypeError("Configuration source name cannot be empty.");
|
|
43
53
|
}
|
|
@@ -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
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cloneConfigValue, configValuesEqual, defineConfigProperty, freezeConfigValue, } from "../configValue/configValue.core.js";
|
|
2
2
|
import { createConfigEntry, toSafeConfigEntry, } from "../configEntry/configEntry.type.js";
|
|
3
|
+
import { isSensitiveConfigEntry } from "../configSource/configSource.sensitive.js";
|
|
3
4
|
/**
|
|
4
5
|
* Normalizes a configuration key.
|
|
5
6
|
*/
|
|
@@ -13,6 +14,18 @@ export function normalizeKey(key) {
|
|
|
13
14
|
}
|
|
14
15
|
return normalized;
|
|
15
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
|
+
}
|
|
16
29
|
/**
|
|
17
30
|
* Central in-memory configuration store.
|
|
18
31
|
*
|
|
@@ -97,7 +110,12 @@ export class ConfigStore {
|
|
|
97
110
|
source: options.source ?? "runtime",
|
|
98
111
|
sourceType: options.sourceType,
|
|
99
112
|
priority: options.priority ?? 0,
|
|
100
|
-
|
|
113
|
+
// Secret detection belongs HERE, not at a single caller. It used
|
|
114
|
+
// to run only inside ConfigLoader.applySource, so the very same
|
|
115
|
+
// key was redacted when it arrived from a source and printed in
|
|
116
|
+
// clear when it arrived through initialValues, set(), setMany()
|
|
117
|
+
// or replace(). Pass `sensitive: false` explicitly to opt out.
|
|
118
|
+
sensitive: options.sensitive ?? isSensitiveConfigEntry(normalizedKey, value),
|
|
101
119
|
resolved: options.resolved ?? true,
|
|
102
120
|
});
|
|
103
121
|
if (previous &&
|
|
@@ -242,20 +260,22 @@ export class ConfigStore {
|
|
|
242
260
|
return Object.freeze(result);
|
|
243
261
|
}
|
|
244
262
|
/**
|
|
245
|
-
* 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`).
|
|
246
265
|
*/
|
|
247
266
|
getByPrefix(prefix) {
|
|
248
267
|
this.assertActive();
|
|
249
|
-
const normalizedPrefix =
|
|
268
|
+
const normalizedPrefix = normalizePrefix(prefix);
|
|
250
269
|
return Array.from(this.entries.values()).filter((entry) => entry.key === normalizedPrefix ||
|
|
251
270
|
entry.key.startsWith(`${normalizedPrefix}.`));
|
|
252
271
|
}
|
|
253
272
|
/**
|
|
254
|
-
* 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}.
|
|
255
275
|
*/
|
|
256
276
|
getObjectByPrefix(prefix) {
|
|
257
277
|
const entries = this.getByPrefix(prefix);
|
|
258
|
-
const normalizedPrefix =
|
|
278
|
+
const normalizedPrefix = normalizePrefix(prefix);
|
|
259
279
|
const result = {};
|
|
260
280
|
for (const entry of entries) {
|
|
261
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.0",
|
|
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
|
},
|