@zudojs/config 0.1.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- 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 +89 -20
- 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 +23 -12
- 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,10 +122,28 @@ function appendCustomValidationResult(result, path, issues) {
|
|
|
121
122
|
issues.push(...result);
|
|
122
123
|
return;
|
|
123
124
|
}
|
|
124
|
-
issues.push(
|
|
125
|
+
issues.push(result);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Compiles a string schema pattern for a one-shot `test`.
|
|
129
|
+
*
|
|
130
|
+
* A RegExp carrying the `g` or `y` flag keeps `lastIndex` between calls, so
|
|
131
|
+
* the same schema object alternated between accepting and rejecting an
|
|
132
|
+
* identical value on successive validations. Both flags are dropped; they
|
|
133
|
+
* have no meaning for a whole-value match.
|
|
134
|
+
*/
|
|
135
|
+
function compilePattern(pattern) {
|
|
136
|
+
if (typeof pattern === "string") {
|
|
137
|
+
return new RegExp(pattern);
|
|
138
|
+
}
|
|
139
|
+
const flags = pattern.flags.replace(/[gy]/g, "");
|
|
140
|
+
return flags === pattern.flags ? pattern : new RegExp(pattern.source, flags);
|
|
125
141
|
}
|
|
126
142
|
/**
|
|
127
143
|
* Built-in schema validation rules.
|
|
144
|
+
*
|
|
145
|
+
* Returns a replacement value when a rule rewrote it (an array whose item
|
|
146
|
+
* schema applied `transform` or `default`), otherwise undefined.
|
|
128
147
|
*/
|
|
129
148
|
function validateBuiltInRules(value, schema, context, issues) {
|
|
130
149
|
if (typeof value === "string") {
|
|
@@ -138,9 +157,7 @@ function validateBuiltInRules(value, schema, context, issues) {
|
|
|
138
157
|
issues.push(createConfigValidationIssue(context.path, `Value must contain at most ${stringSchema.maxLength} characters.`, "MAX_LENGTH"));
|
|
139
158
|
}
|
|
140
159
|
if (stringSchema.pattern) {
|
|
141
|
-
const pattern =
|
|
142
|
-
? new RegExp(stringSchema.pattern)
|
|
143
|
-
: stringSchema.pattern;
|
|
160
|
+
const pattern = compilePattern(stringSchema.pattern);
|
|
144
161
|
if (!pattern.test(value)) {
|
|
145
162
|
issues.push(createConfigValidationIssue(context.path, "Value does not match the required pattern.", "PATTERN"));
|
|
146
163
|
}
|
|
@@ -175,6 +192,11 @@ function validateBuiltInRules(value, schema, context, issues) {
|
|
|
175
192
|
issues.push(createConfigValidationIssue(context.path, `Array must contain at most ${arraySchema.maxItems} items.`, "MAX_ITEMS"));
|
|
176
193
|
}
|
|
177
194
|
if (arraySchema.items) {
|
|
195
|
+
// Item results used to be consulted for issues only, so an item
|
|
196
|
+
// schema's `transform` (or `default`) ran and its output was thrown
|
|
197
|
+
// away: the caller got the untransformed array back as "valid".
|
|
198
|
+
const rebuilt = [];
|
|
199
|
+
let allValid = true;
|
|
178
200
|
value.forEach((item, index) => {
|
|
179
201
|
const result = validateConfigValue(item, arraySchema.items, {
|
|
180
202
|
path: `${context.path}[${index}]`,
|
|
@@ -183,9 +205,15 @@ function validateBuiltInRules(value, schema, context, issues) {
|
|
|
183
205
|
key: index,
|
|
184
206
|
});
|
|
185
207
|
issues.push(...result.issues);
|
|
208
|
+
if (!result.valid) {
|
|
209
|
+
allValid = false;
|
|
210
|
+
}
|
|
211
|
+
rebuilt.push(result.value === undefined ? item : result.value);
|
|
186
212
|
});
|
|
213
|
+
return allValid ? rebuilt : undefined;
|
|
187
214
|
}
|
|
188
215
|
}
|
|
216
|
+
return undefined;
|
|
189
217
|
}
|
|
190
218
|
/**
|
|
191
219
|
* Validates a value against a schema.
|
|
@@ -209,11 +237,17 @@ export function validateConfigValue(value, schema, context) {
|
|
|
209
237
|
issues,
|
|
210
238
|
};
|
|
211
239
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
240
|
+
const defaultValue = resolveDefaultValue(schema);
|
|
241
|
+
if (defaultValue === undefined) {
|
|
242
|
+
return {
|
|
243
|
+
valid: true,
|
|
244
|
+
issues,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
// Applied defaults run through the same validation and transform
|
|
248
|
+
// pipeline as supplied values (defaultValue is defined here, so
|
|
249
|
+
// this recursion terminates immediately).
|
|
250
|
+
return validateConfigValue(defaultValue, schema, context);
|
|
217
251
|
}
|
|
218
252
|
if (value === null && schema.nullable) {
|
|
219
253
|
return {
|
|
@@ -232,23 +266,50 @@ export function validateConfigValue(value, schema, context) {
|
|
|
232
266
|
issues,
|
|
233
267
|
};
|
|
234
268
|
}
|
|
235
|
-
validateBuiltInRules(value, schema, validationContext, issues);
|
|
269
|
+
const rewritten = validateBuiltInRules(value, schema, validationContext, issues);
|
|
270
|
+
// An object schema carries `properties` / `additionalProperties`.
|
|
271
|
+
// Those were declared on ConfigObjectSchema but read nowhere in this
|
|
272
|
+
// function, so every nested object schema — including the ones used
|
|
273
|
+
// by ConfigResolver.resolve() and ConfigManager.resolve() — passed
|
|
274
|
+
// validation unconditionally. Delegate to validateConfigObject so
|
|
275
|
+
// nested constraints are actually enforced.
|
|
276
|
+
const objectSchema = schema;
|
|
277
|
+
let base = rewritten ?? value;
|
|
278
|
+
if (matchesConfigType(value, ConfigValueType.OBJECT) &&
|
|
279
|
+
(objectSchema.properties !== undefined ||
|
|
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;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
236
291
|
if (schema.validate) {
|
|
237
|
-
const result = schema.validate(
|
|
292
|
+
const result = schema.validate(base, validationContext);
|
|
238
293
|
appendCustomValidationResult(result, path, issues);
|
|
239
294
|
}
|
|
240
|
-
let transformed =
|
|
241
|
-
|
|
295
|
+
let transformed = base;
|
|
296
|
+
const hasErrors = () => issues.some((issue) => issue.severity === ConfigValidationSeverity.ERROR);
|
|
297
|
+
// Transforms only run on values that passed validation; running
|
|
298
|
+
// them on invalid input would surface invalid values to callers.
|
|
299
|
+
if (schema.transform && !hasErrors()) {
|
|
242
300
|
try {
|
|
243
|
-
transformed = schema.transform(
|
|
301
|
+
transformed = schema.transform(base, validationContext);
|
|
244
302
|
}
|
|
245
303
|
catch (error) {
|
|
246
304
|
issues.push(createConfigValidationIssue(path, error instanceof Error ? error.message : String(error), "TRANSFORM_FAILED"));
|
|
247
305
|
}
|
|
248
306
|
}
|
|
307
|
+
const valid = !hasErrors();
|
|
249
308
|
return {
|
|
250
|
-
valid
|
|
251
|
-
|
|
309
|
+
valid,
|
|
310
|
+
// Invalid values are never returned; callers fall back to the
|
|
311
|
+
// schema default or undefined instead.
|
|
312
|
+
value: valid ? transformed : undefined,
|
|
252
313
|
issues,
|
|
253
314
|
};
|
|
254
315
|
}
|
|
@@ -271,7 +332,11 @@ export function validateConfigObject(value, schema, path = "$") {
|
|
|
271
332
|
const result = {};
|
|
272
333
|
for (const [key, propertySchema] of Object.entries(schema.properties)) {
|
|
273
334
|
const propertyPath = `${path}.${key}`;
|
|
274
|
-
|
|
335
|
+
// Own-property read only: bracket access would walk the prototype
|
|
336
|
+
// chain, so a schema property named "constructor" or "toString"
|
|
337
|
+
// would validate an inherited function instead of reporting a
|
|
338
|
+
// missing value.
|
|
339
|
+
const propertyResult = validateConfigValue(readOwnConfigProperty(value, key), propertySchema, {
|
|
275
340
|
path: propertyPath,
|
|
276
341
|
root: value,
|
|
277
342
|
parent: value,
|
|
@@ -279,7 +344,7 @@ export function validateConfigObject(value, schema, path = "$") {
|
|
|
279
344
|
});
|
|
280
345
|
issues.push(...propertyResult.issues);
|
|
281
346
|
if (propertyResult.value !== undefined) {
|
|
282
|
-
result
|
|
347
|
+
defineConfigProperty(result, key, propertyResult.value);
|
|
283
348
|
}
|
|
284
349
|
}
|
|
285
350
|
for (const [key, child] of Object.entries(value)) {
|
|
@@ -300,11 +365,15 @@ export function validateConfigObject(value, schema, path = "$") {
|
|
|
300
365
|
});
|
|
301
366
|
issues.push(...propertyResult.issues);
|
|
302
367
|
if (propertyResult.value !== undefined) {
|
|
303
|
-
result
|
|
368
|
+
defineConfigProperty(result, key, propertyResult.value);
|
|
304
369
|
}
|
|
305
370
|
}
|
|
306
371
|
else {
|
|
307
|
-
|
|
372
|
+
// Own "__proto__"/"constructor"/"prototype" keys (e.g. from
|
|
373
|
+
// JSON.parse of untrusted input) are kept as own data
|
|
374
|
+
// properties: plain assignment would reach the inherited
|
|
375
|
+
// "__proto__" setter and mutate the result's prototype.
|
|
376
|
+
defineConfigProperty(result, key, child);
|
|
308
377
|
}
|
|
309
378
|
}
|
|
310
379
|
return {
|