@zudojs/config 0.0.1 → 1.0.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 +127 -15
- package/dist/configEntry/configEntry.type.d.ts +5 -1
- package/dist/configEntry/configEntry.type.js +5 -10
- package/dist/configFactory/configFactory.core.d.ts +4 -6
- package/dist/configLoader/configLoader.core.d.ts +48 -4
- package/dist/configLoader/configLoader.core.js +56 -37
- package/dist/configManager/configManager.core.d.ts +35 -3
- package/dist/configManager/configManager.core.js +109 -16
- package/dist/configManager/configManager.type.d.ts +12 -0
- package/dist/configResolver/accessors/configResolver.scoped.d.ts +1 -1
- package/dist/configResolver/accessors/index.d.ts +1 -2
- package/dist/configResolver/accessors/index.js +1 -2
- package/dist/configResolver/core/configResolver.core.d.ts +1 -1
- package/dist/configResolver/core/configResolver.core.js +24 -7
- package/dist/configSchema/configSchema.type.d.ts +44 -11
- package/dist/configSchema/configSchema.validator.d.ts +3 -3
- package/dist/configSchema/configSchema.validator.js +58 -16
- package/dist/configSchema/index.d.ts +1 -1
- package/dist/configSchema/index.js +1 -1
- package/dist/configSource/configSource.core.d.ts +28 -11
- package/dist/configSource/configSource.core.js +43 -17
- package/dist/configSource/configSource.environment.d.ts +66 -0
- package/dist/configSource/configSource.environment.js +75 -0
- package/dist/configSource/index.d.ts +1 -0
- package/dist/configSource/index.js +1 -0
- package/dist/configStore/configStore.core.d.ts +15 -0
- package/dist/configStore/configStore.core.js +35 -5
- package/dist/configStore/configStore.type.d.ts +11 -0
- package/dist/configValue/configValue.core.d.ts +38 -0
- package/dist/configValue/configValue.core.js +78 -7
- package/package.json +12 -5
- package/dist/.tsbuildinfo +0 -1
- package/dist/configEntry/configEntry.type.d.ts.map +0 -1
- package/dist/configEntry/configEntry.type.js.map +0 -1
- package/dist/configEntry/index.d.ts.map +0 -1
- package/dist/configEntry/index.js.map +0 -1
- package/dist/configFactory/configFactory.core.d.ts.map +0 -1
- package/dist/configFactory/configFactory.core.js.map +0 -1
- package/dist/configFactory/index.d.ts.map +0 -1
- package/dist/configFactory/index.js.map +0 -1
- package/dist/configLoader/configLoader.core.d.ts.map +0 -1
- package/dist/configLoader/configLoader.core.js.map +0 -1
- package/dist/configLoader/index.d.ts.map +0 -1
- package/dist/configLoader/index.js.map +0 -1
- package/dist/configManager/configManager.core.d.ts.map +0 -1
- package/dist/configManager/configManager.core.js.map +0 -1
- package/dist/configManager/configManager.error.d.ts.map +0 -1
- package/dist/configManager/configManager.error.js.map +0 -1
- package/dist/configManager/configManager.factory.d.ts.map +0 -1
- package/dist/configManager/configManager.factory.js.map +0 -1
- package/dist/configManager/configManager.type.d.ts.map +0 -1
- package/dist/configManager/configManager.type.js.map +0 -1
- package/dist/configManager/index.d.ts.map +0 -1
- package/dist/configManager/index.js.map +0 -1
- package/dist/configResolver/accessors/configResolver.accessors.d.ts +0 -19
- package/dist/configResolver/accessors/configResolver.accessors.d.ts.map +0 -1
- package/dist/configResolver/accessors/configResolver.accessors.js +0 -237
- package/dist/configResolver/accessors/configResolver.accessors.js.map +0 -1
- package/dist/configResolver/accessors/configResolver.scoped.d.ts.map +0 -1
- package/dist/configResolver/accessors/configResolver.scoped.js.map +0 -1
- package/dist/configResolver/accessors/index.d.ts.map +0 -1
- package/dist/configResolver/accessors/index.js.map +0 -1
- package/dist/configResolver/core/configResolver.core.d.ts.map +0 -1
- package/dist/configResolver/core/configResolver.core.js.map +0 -1
- package/dist/configResolver/core/configResolver.error.d.ts.map +0 -1
- package/dist/configResolver/core/configResolver.error.js.map +0 -1
- package/dist/configResolver/core/configResolver.factory.d.ts.map +0 -1
- package/dist/configResolver/core/configResolver.factory.js.map +0 -1
- package/dist/configResolver/core/configResolver.type.d.ts.map +0 -1
- package/dist/configResolver/core/configResolver.type.js.map +0 -1
- package/dist/configResolver/core/index.d.ts.map +0 -1
- package/dist/configResolver/core/index.js.map +0 -1
- package/dist/configResolver/index.d.ts.map +0 -1
- package/dist/configResolver/index.js.map +0 -1
- package/dist/configSchema/configSchema.core.d.ts +0 -8
- package/dist/configSchema/configSchema.core.d.ts.map +0 -1
- package/dist/configSchema/configSchema.core.js +0 -8
- package/dist/configSchema/configSchema.core.js.map +0 -1
- package/dist/configSchema/configSchema.rules.d.ts +0 -18
- package/dist/configSchema/configSchema.rules.d.ts.map +0 -1
- package/dist/configSchema/configSchema.rules.js +0 -108
- package/dist/configSchema/configSchema.rules.js.map +0 -1
- package/dist/configSchema/configSchema.type.d.ts.map +0 -1
- package/dist/configSchema/configSchema.type.js.map +0 -1
- package/dist/configSchema/configSchema.validator.d.ts.map +0 -1
- package/dist/configSchema/configSchema.validator.js.map +0 -1
- package/dist/configSchema/index.d.ts.map +0 -1
- package/dist/configSchema/index.js.map +0 -1
- package/dist/configSource/configSource.core.d.ts.map +0 -1
- package/dist/configSource/configSource.core.js.map +0 -1
- package/dist/configSource/index.d.ts.map +0 -1
- package/dist/configSource/index.js.map +0 -1
- package/dist/configStore/configStore.core.d.ts.map +0 -1
- package/dist/configStore/configStore.core.js.map +0 -1
- package/dist/configStore/configStore.factory.d.ts.map +0 -1
- package/dist/configStore/configStore.factory.js.map +0 -1
- package/dist/configStore/configStore.type.d.ts.map +0 -1
- package/dist/configStore/configStore.type.js.map +0 -1
- package/dist/configStore/index.d.ts.map +0 -1
- package/dist/configStore/index.js.map +0 -1
- package/dist/configValue/configValue.core.d.ts.map +0 -1
- package/dist/configValue/configValue.core.js.map +0 -1
- package/dist/configValue/index.d.ts.map +0 -1
- package/dist/configValue/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cloneConfigValue } from "../configValue/configValue.core.js";
|
|
2
|
-
import { ConfigValueType, validateConfigObject, } from "../configSchema/
|
|
2
|
+
import { ConfigValueType, validateConfigObject, } from "../configSchema/index.js";
|
|
3
3
|
import { createConfigSource } from "../configSource/configSource.core.js";
|
|
4
4
|
import { createConfigLoader } from "../configLoader/configLoader.core.js";
|
|
5
5
|
import { createConfigStore } from "../configStore/configStore.factory.js";
|
|
@@ -13,6 +13,12 @@ import { ConfigManagerValidationError } from "./configManager.error.js";
|
|
|
13
13
|
* keeping their responsibilities separate.
|
|
14
14
|
*/
|
|
15
15
|
export class ConfigManager {
|
|
16
|
+
/**
|
|
17
|
+
* Human-readable name for this manager, surfaced in status
|
|
18
|
+
* snapshots so applications running several managers can tell
|
|
19
|
+
* their diagnostics apart.
|
|
20
|
+
*/
|
|
21
|
+
name;
|
|
16
22
|
store;
|
|
17
23
|
loader;
|
|
18
24
|
resolver;
|
|
@@ -21,26 +27,52 @@ export class ConfigManager {
|
|
|
21
27
|
lastLoadedAt;
|
|
22
28
|
lastError;
|
|
23
29
|
disposed = false;
|
|
30
|
+
loadPromise;
|
|
24
31
|
constructor(options = {}) {
|
|
32
|
+
// When a caller supplies a loader (and no explicit store), the
|
|
33
|
+
// manager must resolve values from the SAME store that loader
|
|
34
|
+
// writes to.
|
|
35
|
+
const providedLoader = options.loader;
|
|
36
|
+
const suppliedStore = options.store ?? providedLoader?.getStore();
|
|
25
37
|
this.store =
|
|
26
|
-
|
|
38
|
+
suppliedStore ??
|
|
27
39
|
createConfigStore({
|
|
28
40
|
initialValues: options.initialValues,
|
|
29
41
|
freeze: options.freeze ?? true,
|
|
30
42
|
});
|
|
43
|
+
// initialValues used to be wired ONLY into a store the manager
|
|
44
|
+
// created itself: passing `store` or `loader` silently discarded
|
|
45
|
+
// them. Seed a supplied store explicitly instead.
|
|
46
|
+
if (suppliedStore && options.initialValues) {
|
|
47
|
+
for (const [key, value] of Object.entries(options.initialValues)) {
|
|
48
|
+
suppliedStore.set(key, value, { source: "initialValues" });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
this.name = options.name ?? "config";
|
|
31
52
|
this.loader =
|
|
32
|
-
|
|
53
|
+
providedLoader ??
|
|
33
54
|
createConfigLoader({
|
|
34
55
|
sources: options.sources,
|
|
35
56
|
context: options.context,
|
|
36
57
|
store: this.store,
|
|
37
58
|
freeze: options.freeze ?? true,
|
|
59
|
+
// The manager seeds its store with initialValues and accepts
|
|
60
|
+
// runtime set() writes; loading must layer sources on top of
|
|
61
|
+
// those (by priority) instead of wiping them.
|
|
62
|
+
clearStore: false,
|
|
38
63
|
});
|
|
39
64
|
this.resolver = createConfigResolver(this.store, {
|
|
40
65
|
strict: options.strict ?? true,
|
|
41
66
|
allowUndefined: options.allowUndefined ?? true,
|
|
42
67
|
clone: options.clone ?? false,
|
|
43
68
|
});
|
|
69
|
+
if (options.autoLoad) {
|
|
70
|
+
// Kick off the initial load immediately; ready() (or load())
|
|
71
|
+
// exposes the in-flight promise. The catch handler prevents an
|
|
72
|
+
// unhandled rejection when nobody awaits ready(): the failure
|
|
73
|
+
// is still recorded in the FAILED state and lastError.
|
|
74
|
+
this.load().catch(() => { });
|
|
75
|
+
}
|
|
44
76
|
}
|
|
45
77
|
/**
|
|
46
78
|
* Returns the current lifecycle state.
|
|
@@ -91,25 +123,40 @@ export class ConfigManager {
|
|
|
91
123
|
}
|
|
92
124
|
/**
|
|
93
125
|
* Loads configuration.
|
|
126
|
+
*
|
|
127
|
+
* Calling load() while a load is already in flight returns the
|
|
128
|
+
* in-flight promise instead of starting a second load.
|
|
94
129
|
*/
|
|
95
130
|
async load() {
|
|
96
131
|
this.assertActive();
|
|
97
132
|
if (this.isLoading) {
|
|
133
|
+
if (this.loadPromise) {
|
|
134
|
+
return this.loadPromise;
|
|
135
|
+
}
|
|
98
136
|
throw new Error("Configuration manager is already loading.");
|
|
99
137
|
}
|
|
100
138
|
this.setState(ConfigManagerState.LOADING);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
139
|
+
const promise = this.performLoad(false);
|
|
140
|
+
this.loadPromise = promise;
|
|
141
|
+
return promise;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Waits until the initial configuration load has completed.
|
|
145
|
+
*
|
|
146
|
+
* With `autoLoad: true` this awaits the load started by the
|
|
147
|
+
* constructor (rethrowing its failure). Otherwise it starts a load
|
|
148
|
+
* when none has happened yet.
|
|
149
|
+
*/
|
|
150
|
+
async ready() {
|
|
151
|
+
this.assertActive();
|
|
152
|
+
if (this.loadPromise) {
|
|
153
|
+
await this.loadPromise;
|
|
154
|
+
return;
|
|
107
155
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
this.setState(ConfigManagerState.FAILED);
|
|
111
|
-
throw error;
|
|
156
|
+
if (this.isReady) {
|
|
157
|
+
return;
|
|
112
158
|
}
|
|
159
|
+
await this.load();
|
|
113
160
|
}
|
|
114
161
|
/**
|
|
115
162
|
* Reloads configuration.
|
|
@@ -120,16 +167,34 @@ export class ConfigManager {
|
|
|
120
167
|
throw new Error("Configuration manager is already loading.");
|
|
121
168
|
}
|
|
122
169
|
this.setState(ConfigManagerState.RELOADING);
|
|
170
|
+
const promise = this.performLoad(true);
|
|
171
|
+
this.loadPromise = promise;
|
|
172
|
+
return promise;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Executes a load or reload while keeping lifecycle state safe.
|
|
176
|
+
*
|
|
177
|
+
* If the manager is disposed while the load is in flight, no state
|
|
178
|
+
* transition happens afterwards: a disposed manager stays DISPOSED.
|
|
179
|
+
*/
|
|
180
|
+
async performLoad(reload) {
|
|
123
181
|
try {
|
|
124
|
-
const result =
|
|
182
|
+
const result = reload
|
|
183
|
+
? await this.loader.reload()
|
|
184
|
+
: await this.loader.load();
|
|
185
|
+
if (this.disposed) {
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
125
188
|
this.lastLoadedAt = result.loadedAt;
|
|
126
189
|
this.lastError = undefined;
|
|
127
190
|
this.setState(ConfigManagerState.READY);
|
|
128
191
|
return result;
|
|
129
192
|
}
|
|
130
193
|
catch (error) {
|
|
131
|
-
this.
|
|
132
|
-
|
|
194
|
+
if (!this.disposed) {
|
|
195
|
+
this.lastError = error;
|
|
196
|
+
this.setState(ConfigManagerState.FAILED);
|
|
197
|
+
}
|
|
133
198
|
throw error;
|
|
134
199
|
}
|
|
135
200
|
}
|
|
@@ -146,6 +211,23 @@ export class ConfigManager {
|
|
|
146
211
|
if (!result.valid) {
|
|
147
212
|
throw new ConfigManagerValidationError(result.issues);
|
|
148
213
|
}
|
|
214
|
+
// Property schemas flagged `secret` mark the corresponding store
|
|
215
|
+
// entries as sensitive so safe serialization redacts them.
|
|
216
|
+
for (const [key, propertySchema] of Object.entries(schema.properties)) {
|
|
217
|
+
if (!propertySchema.secret) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const entry = this.store.getEntry(key);
|
|
221
|
+
if (entry && !entry.sensitive) {
|
|
222
|
+
this.store.set(key, entry.value, {
|
|
223
|
+
source: entry.source,
|
|
224
|
+
sourceType: entry.sourceType,
|
|
225
|
+
priority: entry.priority,
|
|
226
|
+
sensitive: true,
|
|
227
|
+
resolved: entry.resolved,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
149
231
|
return cloneConfigValue(result.value);
|
|
150
232
|
}
|
|
151
233
|
/**
|
|
@@ -238,16 +320,27 @@ export class ConfigManager {
|
|
|
238
320
|
}
|
|
239
321
|
/**
|
|
240
322
|
* Returns the complete configuration object.
|
|
323
|
+
*
|
|
324
|
+
* WARNING: values are returned RAW — sensitive entries are NOT
|
|
325
|
+
* redacted. Use toSafeObject() for logging or diagnostics.
|
|
241
326
|
*/
|
|
242
327
|
toObject() {
|
|
243
328
|
this.assertActive();
|
|
244
329
|
return this.store.toObject();
|
|
245
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* Returns the configuration object with sensitive values redacted.
|
|
333
|
+
*/
|
|
334
|
+
toSafeObject() {
|
|
335
|
+
this.assertActive();
|
|
336
|
+
return this.store.toSafeObject();
|
|
337
|
+
}
|
|
246
338
|
/**
|
|
247
339
|
* Returns a manager status snapshot.
|
|
248
340
|
*/
|
|
249
341
|
getStatus() {
|
|
250
342
|
return {
|
|
343
|
+
name: this.name,
|
|
251
344
|
state: this.state,
|
|
252
345
|
loaded: this.isReady,
|
|
253
346
|
loading: this.isLoading,
|
|
@@ -18,11 +18,22 @@ export declare enum ConfigManagerState {
|
|
|
18
18
|
* Configuration manager options.
|
|
19
19
|
*/
|
|
20
20
|
export interface ConfigManagerOptions extends ConfigResolverOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Human-readable name for the manager. Surfaced by
|
|
23
|
+
* `manager.name` and in `getStatus()`.
|
|
24
|
+
*/
|
|
25
|
+
readonly name?: string;
|
|
21
26
|
readonly sources?: readonly ConfigSource[];
|
|
22
27
|
readonly initialValues?: Readonly<Record<string, ConfigValue>>;
|
|
23
28
|
readonly store?: ConfigStore;
|
|
24
29
|
readonly loader?: ConfigLoader;
|
|
25
30
|
readonly freeze?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* When true, the manager starts loading configuration immediately
|
|
33
|
+
* on construction. Await `manager.ready()` to wait for (and surface
|
|
34
|
+
* errors from) that initial load; `load()` called while the initial
|
|
35
|
+
* load is in flight reuses the same promise.
|
|
36
|
+
*/
|
|
26
37
|
readonly autoLoad?: boolean;
|
|
27
38
|
readonly context?: {
|
|
28
39
|
readonly environment?: string;
|
|
@@ -34,6 +45,7 @@ export interface ConfigManagerOptions extends ConfigResolverOptions {
|
|
|
34
45
|
* Configuration manager status.
|
|
35
46
|
*/
|
|
36
47
|
export interface ConfigManagerStatus {
|
|
48
|
+
readonly name: string;
|
|
37
49
|
readonly state: ConfigManagerState;
|
|
38
50
|
readonly loaded: boolean;
|
|
39
51
|
readonly loading: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConfigValue } from "../../configValue/configValue.core.js";
|
|
2
|
-
import type { ConfigSchema } from "../../configSchema/
|
|
2
|
+
import type { ConfigSchema } from "../../configSchema/index.js";
|
|
3
3
|
import type { ConfigResolver } from "../core/configResolver.core.js";
|
|
4
4
|
/**
|
|
5
5
|
* Resolver scoped to a configuration key prefix.
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zudojs/config/configResolver/accessors
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Scoped configuration resolver.
|
|
5
5
|
*/
|
|
6
|
-
export * from "./configResolver.accessors.js";
|
|
7
6
|
export * from "./configResolver.scoped.js";
|
|
8
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zudojs/config/configResolver/accessors
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Scoped configuration resolver.
|
|
5
5
|
*/
|
|
6
|
-
export * from "./configResolver.accessors.js";
|
|
7
6
|
export * from "./configResolver.scoped.js";
|
|
8
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConfigValue } from "../../configValue/configValue.core.js";
|
|
2
|
-
import type { ConfigSchema } from "../../configSchema/
|
|
2
|
+
import type { ConfigSchema } from "../../configSchema/index.js";
|
|
3
3
|
import type { ConfigStore } from "../../configStore/configStore.core.js";
|
|
4
4
|
import type { ConfigResolverOptions, ConfigResolutionResult } from "./configResolver.type.js";
|
|
5
5
|
import { ScopedConfigResolver } from "../accessors/configResolver.scoped.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { parseConfigBigInt, parseConfigBoolean, parseConfigDate, parseConfigNumber, } from "../../configValue/configValue.core.js";
|
|
2
|
-
import { validateConfigValue } from "../../configSchema/
|
|
1
|
+
import { defineConfigProperty, parseConfigBigInt, parseConfigBoolean, parseConfigDate, parseConfigNumber, } from "../../configValue/configValue.core.js";
|
|
2
|
+
import { validateConfigValue } from "../../configSchema/index.js";
|
|
3
3
|
import { ScopedConfigResolver } from "../accessors/configResolver.scoped.js";
|
|
4
4
|
/**
|
|
5
5
|
* Resolves typed configuration values from a ConfigStore.
|
|
@@ -34,8 +34,19 @@ export class ConfigResolver {
|
|
|
34
34
|
path: key,
|
|
35
35
|
root: value,
|
|
36
36
|
});
|
|
37
|
-
if (!result.valid
|
|
38
|
-
|
|
37
|
+
if (!result.valid) {
|
|
38
|
+
if (this.options.strict) {
|
|
39
|
+
throw new ConfigResolutionError(key, result.issues);
|
|
40
|
+
}
|
|
41
|
+
// Non-strict mode: never return an invalid value. Fall back to
|
|
42
|
+
// the schema default when present, otherwise undefined.
|
|
43
|
+
const fallback = typeof schema.default === "function"
|
|
44
|
+
? schema.default()
|
|
45
|
+
: schema.default;
|
|
46
|
+
if (fallback === undefined && !this.options.allowUndefined) {
|
|
47
|
+
throw new ConfigResolutionError(key, result.issues);
|
|
48
|
+
}
|
|
49
|
+
return this.prepareValue(fallback);
|
|
39
50
|
}
|
|
40
51
|
if (result.value === undefined && !this.options.allowUndefined) {
|
|
41
52
|
throw new ConfigResolutionError(key, [
|
|
@@ -211,7 +222,13 @@ export class ConfigResolver {
|
|
|
211
222
|
return fallback;
|
|
212
223
|
}
|
|
213
224
|
if (value instanceof Date) {
|
|
214
|
-
|
|
225
|
+
const parsed = parseConfigDate(value);
|
|
226
|
+
if (parsed !== undefined) {
|
|
227
|
+
return parsed;
|
|
228
|
+
}
|
|
229
|
+
// Invalid Date instances route through the same strict/fallback
|
|
230
|
+
// handling as any other invalid type.
|
|
231
|
+
return this.invalidType(key, "date", value, fallback);
|
|
215
232
|
}
|
|
216
233
|
if (typeof value === "string") {
|
|
217
234
|
const parsed = parseConfigDate(value);
|
|
@@ -274,7 +291,7 @@ export class ConfigResolver {
|
|
|
274
291
|
for (const key of keys) {
|
|
275
292
|
const value = this.get(key);
|
|
276
293
|
if (value !== undefined) {
|
|
277
|
-
result
|
|
294
|
+
defineConfigProperty(result, key, value);
|
|
278
295
|
}
|
|
279
296
|
}
|
|
280
297
|
return Object.freeze(result);
|
|
@@ -309,7 +326,7 @@ export class ConfigResolver {
|
|
|
309
326
|
}
|
|
310
327
|
const result = {};
|
|
311
328
|
for (const [key, child] of Object.entries(value)) {
|
|
312
|
-
result
|
|
329
|
+
defineConfigProperty(result, key, this.prepareValue(child));
|
|
313
330
|
}
|
|
314
331
|
return result;
|
|
315
332
|
}
|
|
@@ -57,24 +57,65 @@ export interface ConfigSchema<T extends ConfigValue = ConfigValue> {
|
|
|
57
57
|
readonly nullable?: boolean;
|
|
58
58
|
readonly default?: T | (() => T);
|
|
59
59
|
readonly description?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Marks the value as secret. When a ConfigManager validates its
|
|
62
|
+
* configuration, store entries whose property schema is flagged
|
|
63
|
+
* `secret` are marked sensitive, so toSafeObject() and safe entry
|
|
64
|
+
* serialization redact them.
|
|
65
|
+
*/
|
|
60
66
|
readonly secret?: boolean;
|
|
61
67
|
readonly validate?: (value: T, context: ConfigValidationContext) => boolean | string | ConfigValidationIssue | readonly ConfigValidationIssue[];
|
|
62
68
|
readonly transform?: (value: ConfigValue, context: ConfigValidationContext) => T;
|
|
63
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* A schema for a nested value whose value type the container does not
|
|
72
|
+
* know.
|
|
73
|
+
*
|
|
74
|
+
* `ConfigSchema<T>` is contravariant in `T` through `validate(value: T,
|
|
75
|
+
* ...)` and `transform`, so a `ConfigStringSchema` (which extends
|
|
76
|
+
* `ConfigSchema<string>`) is NOT assignable to `ConfigSchema<ConfigValue>`:
|
|
77
|
+
* a validator that accepts only `string` cannot stand in for one that must
|
|
78
|
+
* accept any `ConfigValue`.
|
|
79
|
+
*
|
|
80
|
+
* That made typed object schemas inexpressible. Declaring `properties` as
|
|
81
|
+
* `Record<string, ConfigSchema>` rejected every specific variant, including
|
|
82
|
+
* through a typed intermediate constant, so there was no user-side
|
|
83
|
+
* workaround short of a cast:
|
|
84
|
+
*
|
|
85
|
+
* ```ts
|
|
86
|
+
* const schema: ConfigObjectSchema = {
|
|
87
|
+
* type: ConfigValueType.OBJECT,
|
|
88
|
+
* // previously: not assignable to ConfigSchema<ConfigValue>
|
|
89
|
+
* properties: { port: { type: ConfigValueType.NUMBER, min: 1 } },
|
|
90
|
+
* };
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* A container of schemas over differing value types is an existential
|
|
94
|
+
* type, which TypeScript cannot express. `ConfigSchema<any>` is the
|
|
95
|
+
* deliberate encoding of that: `any` is assignable in both directions, so
|
|
96
|
+
* every variant is accepted and the validator can still invoke `validate`
|
|
97
|
+
* on what it reads back out.
|
|
98
|
+
*
|
|
99
|
+
* The named variants are unioned in as well so an inline literal may carry
|
|
100
|
+
* variant-specific fields (`minLength`, `min`, `max`, `enum`). Excess
|
|
101
|
+
* property checking against a union admits any property declared by some
|
|
102
|
+
* member, which a bare `ConfigSchema<any>` would reject.
|
|
103
|
+
*/
|
|
104
|
+
export type AnyConfigSchema = ConfigSchema<any> | ConfigStringSchema | ConfigNumberSchema | ConfigBooleanSchema | ConfigObjectSchema | ConfigArraySchema;
|
|
64
105
|
/**
|
|
65
106
|
* Object configuration schema.
|
|
66
107
|
*/
|
|
67
108
|
export interface ConfigObjectSchema<T extends ConfigValue = ConfigValue> extends ConfigSchema<T> {
|
|
68
109
|
readonly type: ConfigValueType.OBJECT;
|
|
69
|
-
readonly properties: Readonly<Record<string,
|
|
70
|
-
readonly additionalProperties?: boolean |
|
|
110
|
+
readonly properties: Readonly<Record<string, AnyConfigSchema>>;
|
|
111
|
+
readonly additionalProperties?: boolean | AnyConfigSchema;
|
|
71
112
|
}
|
|
72
113
|
/**
|
|
73
114
|
* Array configuration schema.
|
|
74
115
|
*/
|
|
75
116
|
export interface ConfigArraySchema<T extends ConfigValue = ConfigValue> extends ConfigSchema<T> {
|
|
76
117
|
readonly type: ConfigValueType.ARRAY;
|
|
77
|
-
readonly items?:
|
|
118
|
+
readonly items?: AnyConfigSchema;
|
|
78
119
|
readonly minItems?: number;
|
|
79
120
|
readonly maxItems?: number;
|
|
80
121
|
}
|
|
@@ -104,12 +145,4 @@ export interface ConfigNumberSchema extends ConfigSchema<number> {
|
|
|
104
145
|
export interface ConfigBooleanSchema extends ConfigSchema<boolean> {
|
|
105
146
|
readonly type: ConfigValueType.BOOLEAN;
|
|
106
147
|
}
|
|
107
|
-
/**
|
|
108
|
-
* Schema definition shorthand.
|
|
109
|
-
*/
|
|
110
|
-
export type ConfigSchemaDefinition = ConfigSchema | ConfigSchemaDefinition[] | ConfigSchemaBuilder;
|
|
111
|
-
/**
|
|
112
|
-
* Schema builder function.
|
|
113
|
-
*/
|
|
114
|
-
export type ConfigSchemaBuilder = () => ConfigSchema;
|
|
115
148
|
//# sourceMappingURL=configSchema.type.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConfigValue } from "../configValue/configValue.core.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { AnyConfigSchema, ConfigValidationContext, ConfigValidationIssue, ConfigValidationResult } from "./configSchema.type.js";
|
|
3
3
|
import { ConfigValueType } from "./configSchema.type.js";
|
|
4
4
|
import type { ConfigObjectSchema } from "./configSchema.type.js";
|
|
5
5
|
/**
|
|
@@ -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(value: unknown, schema:
|
|
20
|
+
export declare function validateConfigValue(value: unknown, schema: AnyConfigSchema, context?: Partial<ConfigValidationContext>): ConfigValidationResult;
|
|
21
21
|
/**
|
|
22
22
|
* Validates an entire configuration object.
|
|
23
23
|
*/
|
|
@@ -25,7 +25,7 @@ export declare function validateConfigObject(value: Readonly<Record<string, unkn
|
|
|
25
25
|
/**
|
|
26
26
|
* Validates an object against a schema and throws when invalid.
|
|
27
27
|
*/
|
|
28
|
-
export declare function assertValidConfig(value: unknown, schema:
|
|
28
|
+
export declare function assertValidConfig(value: unknown, schema: AnyConfigSchema): ConfigValue;
|
|
29
29
|
import { ConfigurationError } from "@zudojs/errors";
|
|
30
30
|
/**
|
|
31
31
|
* Error thrown when schema validation fails.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { defineConfigProperty, readOwnConfigProperty, } from "../configValue/configValue.core.js";
|
|
1
2
|
import { ConfigValidationSeverity, ConfigValueType, } from "./configSchema.type.js";
|
|
2
3
|
/**
|
|
3
4
|
* Returns the runtime configuration value type.
|
|
@@ -121,7 +122,7 @@ function appendCustomValidationResult(result, path, issues) {
|
|
|
121
122
|
issues.push(...result);
|
|
122
123
|
return;
|
|
123
124
|
}
|
|
124
|
-
issues.push(
|
|
125
|
+
issues.push(result);
|
|
125
126
|
}
|
|
126
127
|
/**
|
|
127
128
|
* Built-in schema validation rules.
|
|
@@ -209,11 +210,17 @@ export function validateConfigValue(value, schema, context) {
|
|
|
209
210
|
issues,
|
|
210
211
|
};
|
|
211
212
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
213
|
+
const defaultValue = resolveDefaultValue(schema);
|
|
214
|
+
if (defaultValue === undefined) {
|
|
215
|
+
return {
|
|
216
|
+
valid: true,
|
|
217
|
+
issues,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
// Applied defaults run through the same validation and transform
|
|
221
|
+
// pipeline as supplied values (defaultValue is defined here, so
|
|
222
|
+
// this recursion terminates immediately).
|
|
223
|
+
return validateConfigValue(defaultValue, schema, context);
|
|
217
224
|
}
|
|
218
225
|
if (value === null && schema.nullable) {
|
|
219
226
|
return {
|
|
@@ -233,22 +240,49 @@ export function validateConfigValue(value, schema, context) {
|
|
|
233
240
|
};
|
|
234
241
|
}
|
|
235
242
|
validateBuiltInRules(value, schema, validationContext, issues);
|
|
243
|
+
// An object schema carries `properties` / `additionalProperties`.
|
|
244
|
+
// Those were declared on ConfigObjectSchema but read nowhere in this
|
|
245
|
+
// function, so every nested object schema — including the ones used
|
|
246
|
+
// by ConfigResolver.resolve() and ConfigManager.resolve() — passed
|
|
247
|
+
// validation unconditionally. Delegate to validateConfigObject so
|
|
248
|
+
// nested constraints are actually enforced.
|
|
249
|
+
const objectSchema = schema;
|
|
250
|
+
let base = value;
|
|
251
|
+
if (matchesConfigType(value, ConfigValueType.OBJECT) &&
|
|
252
|
+
(objectSchema.properties !== undefined ||
|
|
253
|
+
objectSchema.additionalProperties !== undefined)) {
|
|
254
|
+
const nested = validateConfigObject(value, {
|
|
255
|
+
type: ConfigValueType.OBJECT,
|
|
256
|
+
properties: objectSchema.properties ?? {},
|
|
257
|
+
additionalProperties: objectSchema.additionalProperties,
|
|
258
|
+
}, path);
|
|
259
|
+
issues.push(...nested.issues);
|
|
260
|
+
if (nested.value !== undefined) {
|
|
261
|
+
base = nested.value;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
236
264
|
if (schema.validate) {
|
|
237
|
-
const result = schema.validate(
|
|
265
|
+
const result = schema.validate(base, validationContext);
|
|
238
266
|
appendCustomValidationResult(result, path, issues);
|
|
239
267
|
}
|
|
240
|
-
let transformed =
|
|
241
|
-
|
|
268
|
+
let transformed = base;
|
|
269
|
+
const hasErrors = () => issues.some((issue) => issue.severity === ConfigValidationSeverity.ERROR);
|
|
270
|
+
// Transforms only run on values that passed validation; running
|
|
271
|
+
// them on invalid input would surface invalid values to callers.
|
|
272
|
+
if (schema.transform && !hasErrors()) {
|
|
242
273
|
try {
|
|
243
|
-
transformed = schema.transform(
|
|
274
|
+
transformed = schema.transform(base, validationContext);
|
|
244
275
|
}
|
|
245
276
|
catch (error) {
|
|
246
277
|
issues.push(createConfigValidationIssue(path, error instanceof Error ? error.message : String(error), "TRANSFORM_FAILED"));
|
|
247
278
|
}
|
|
248
279
|
}
|
|
280
|
+
const valid = !hasErrors();
|
|
249
281
|
return {
|
|
250
|
-
valid
|
|
251
|
-
|
|
282
|
+
valid,
|
|
283
|
+
// Invalid values are never returned; callers fall back to the
|
|
284
|
+
// schema default or undefined instead.
|
|
285
|
+
value: valid ? transformed : undefined,
|
|
252
286
|
issues,
|
|
253
287
|
};
|
|
254
288
|
}
|
|
@@ -271,7 +305,11 @@ export function validateConfigObject(value, schema, path = "$") {
|
|
|
271
305
|
const result = {};
|
|
272
306
|
for (const [key, propertySchema] of Object.entries(schema.properties)) {
|
|
273
307
|
const propertyPath = `${path}.${key}`;
|
|
274
|
-
|
|
308
|
+
// Own-property read only: bracket access would walk the prototype
|
|
309
|
+
// chain, so a schema property named "constructor" or "toString"
|
|
310
|
+
// would validate an inherited function instead of reporting a
|
|
311
|
+
// missing value.
|
|
312
|
+
const propertyResult = validateConfigValue(readOwnConfigProperty(value, key), propertySchema, {
|
|
275
313
|
path: propertyPath,
|
|
276
314
|
root: value,
|
|
277
315
|
parent: value,
|
|
@@ -279,7 +317,7 @@ export function validateConfigObject(value, schema, path = "$") {
|
|
|
279
317
|
});
|
|
280
318
|
issues.push(...propertyResult.issues);
|
|
281
319
|
if (propertyResult.value !== undefined) {
|
|
282
|
-
result
|
|
320
|
+
defineConfigProperty(result, key, propertyResult.value);
|
|
283
321
|
}
|
|
284
322
|
}
|
|
285
323
|
for (const [key, child] of Object.entries(value)) {
|
|
@@ -300,11 +338,15 @@ export function validateConfigObject(value, schema, path = "$") {
|
|
|
300
338
|
});
|
|
301
339
|
issues.push(...propertyResult.issues);
|
|
302
340
|
if (propertyResult.value !== undefined) {
|
|
303
|
-
result
|
|
341
|
+
defineConfigProperty(result, key, propertyResult.value);
|
|
304
342
|
}
|
|
305
343
|
}
|
|
306
344
|
else {
|
|
307
|
-
|
|
345
|
+
// Own "__proto__"/"constructor"/"prototype" keys (e.g. from
|
|
346
|
+
// JSON.parse of untrusted input) are kept as own data
|
|
347
|
+
// properties: plain assignment would reach the inherited
|
|
348
|
+
// "__proto__" setter and mutate the result's prototype.
|
|
349
|
+
defineConfigProperty(result, key, child);
|
|
308
350
|
}
|
|
309
351
|
}
|
|
310
352
|
return {
|