@zudojs/config 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -118,6 +118,28 @@ const db = manager.scoped("db");
118
118
  db.string("host", "localhost");
119
119
  ```
120
120
 
121
+ `required<T>(key)` does NOT convert: `T` is an unchecked cast, so with
122
+ `DB__PORT=5432`, `scoped("db").required<number>("port")` returns the
123
+ string `"5432"`. Use the typed variants, which parse and check the value
124
+ and throw when it is missing or does not parse: `requiredString()`,
125
+ `requiredNumber()`, `requiredBoolean()` and `requiredDate()` (on the
126
+ manager, the resolver and scoped resolvers).
127
+
128
+ ```typescript
129
+ const db = manager.scoped("db");
130
+ db.requiredNumber("port"); // 5432 (a number)
131
+ ```
132
+
133
+ `store.getByPrefix(prefix)` and `store.getObjectByPrefix(prefix)` accept
134
+ the prefix with or without a trailing dot: `"db"` and `"db."` both select
135
+ `db.host` and `db.port` (and not `dbx`).
136
+
137
+ Passing a fallback narrows the return type: `manager.number("app.port")`
138
+ is `number | undefined`, while `manager.number("app.port", 3000)` is
139
+ `number`. The same holds for every typed getter and for `get(key,
140
+ fallback)`, whose literal fallback is widened (`get("mode", "dev")` is
141
+ `string`, not `"dev"`).
142
+
121
143
  `number()` accepts decimal notation only: `"0x1F90"`, `"0b11"` and `"0o17"`
122
144
  are rejected rather than silently becoming 8080, 3 and 15.
123
145
 
@@ -138,6 +160,46 @@ const config = manager.validate({
138
160
  });
139
161
  ```
140
162
 
163
+ Environment variables are always strings, so string input is coerced
164
+ before the type check when a schema's type is `NUMBER` or `BOOLEAN` (and
165
+ does not also accept `STRING`). Parsing is strict: `"8080"` becomes
166
+ `8080`, while `"80a"`, `"0x1F90"` and `""` are still rejected as
167
+ `TYPE_MISMATCH`. Booleans follow the `boolean()` convention: `true` /
168
+ `false`, `1` / `0`, `yes` / `no`, `y` / `n`, `on` / `off`. `validate` and
169
+ `transform` receive the coerced value. Set `coerce: false` on a schema to
170
+ require a real number or boolean.
171
+
172
+ ```typescript
173
+ // PORT=8080 DEBUG=true
174
+ const { port, debug } = manager.validate<{ port: number; debug: boolean }>({
175
+ properties: {
176
+ port: { type: ConfigValueType.NUMBER, min: 1, max: 65535 },
177
+ debug: { type: ConfigValueType.BOOLEAN },
178
+ },
179
+ });
180
+ ```
181
+
182
+ `resolve(key, schema)` types its schema per value type
183
+ (`TypedConfigSchema`), so the constraints the validator enforces are
184
+ accepted: `{ type: NUMBER, min: 1 }`, `{ type: STRING, minLength: 1 }`,
185
+ `{ type: ARRAY, minItems: 1 }`. A constraint that belongs to another type
186
+ (`{ type: NUMBER, minLength: 1 }`) is a compile error.
187
+
188
+ Validation runs in this order: coerce, type check, constraints,
189
+ `transform`, then `validate` on the final (transformed) value. A string
190
+ that does not have the schema's type is handed to `transform` as a
191
+ parser, and its output must then have the type and pass the constraints:
192
+
193
+ ```typescript
194
+ manager.resolve("hosts", {
195
+ type: ConfigValueType.ARRAY,
196
+ minItems: 1,
197
+ transform: (value) => String(value).split(",").map((s) => s.trim()),
198
+ }); // HOSTS="a, b" -> ["a", "b"]
199
+ ```
200
+
201
+ A non-string of the wrong type is rejected without calling `transform`.
202
+
141
203
  Object schemas nest: a property schema of type `OBJECT` that declares
142
204
  its own `properties` / `additionalProperties` is validated recursively,
143
205
  so nested constraints are enforced by `validate()`, `resolve()` and the
@@ -1,11 +1,12 @@
1
1
  import type { ConfigValue } from "../configValue/configValue.core.js";
2
- import type { ConfigSchema, AnyConfigSchema } from "../configSchema/index.js";
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
- * Gets a required configuration value.
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: ConfigSchema<T>): T | undefined;
126
+ resolve<T extends ConfigValue>(key: string, schema: TypedConfigSchema<T>): T | undefined;
107
127
  /**
108
128
  * Gets a string value.
109
129
  */
130
+ string(key: string): string | undefined;
131
+ string(key: string, fallback: string): string;
110
132
  string(key: string, fallback?: string): string | undefined;
111
133
  /**
112
134
  * Gets a number value.
113
135
  */
136
+ number(key: string): number | undefined;
137
+ number(key: string, fallback: number): number;
114
138
  number(key: string, fallback?: number): number | undefined;
115
139
  /**
116
140
  * Gets a boolean value.
117
141
  */
142
+ boolean(key: string): boolean | undefined;
143
+ boolean(key: string, fallback: boolean): boolean;
118
144
  boolean(key: string, fallback?: boolean): boolean | undefined;
119
145
  /**
120
146
  * Gets a bigint value.
121
147
  */
148
+ bigint(key: string): bigint | undefined;
149
+ bigint(key: string, fallback: bigint): bigint;
122
150
  bigint(key: string, fallback?: bigint): bigint | undefined;
123
151
  /**
124
152
  * Gets a Date value.
125
153
  */
154
+ date(key: string): Date | undefined;
155
+ date(key: string, fallback: Date): Date;
126
156
  date(key: string, fallback?: Date): Date | undefined;
127
157
  /**
128
158
  * Gets an object value.
129
159
  */
160
+ object<T extends ConfigValue = ConfigValue>(key: string): T | undefined;
161
+ object<T extends ConfigValue = ConfigValue>(key: string, fallback: T): T;
130
162
  object<T extends ConfigValue = ConfigValue>(key: string, fallback?: T): T | undefined;
131
163
  /**
132
164
  * Gets an array value.
133
165
  */
166
+ array<T extends ConfigValue = ConfigValue>(key: string): readonly T[] | undefined;
167
+ array<T extends ConfigValue = ConfigValue>(key: string, fallback: readonly T[]): readonly T[];
134
168
  array<T extends ConfigValue = ConfigValue>(key: string, fallback?: readonly T[]): readonly T[] | undefined;
135
169
  /**
136
170
  * Sets a runtime configuration value.
@@ -223,20 +223,47 @@ export class ConfigManager {
223
223
  markSecretEntries(this.store, collectSecretPaths(schema.properties));
224
224
  return cloneConfigValue(result.value);
225
225
  }
226
- /**
227
- * Gets a raw configuration value.
228
- */
229
- get(key) {
226
+ get(key, fallback) {
230
227
  this.assertActive();
231
- return this.resolver.get(key);
228
+ return this.resolver.get(key, fallback);
232
229
  }
233
230
  /**
234
- * Gets a required configuration value.
231
+ * Returns a required value WITHOUT converting it: `T` is an unchecked
232
+ * cast. An environment variable is always a string, so
233
+ * `required<number>("port")` returns `"5432"`, not `5432`. Use
234
+ * `requiredNumber`, `requiredBoolean`, `requiredString` or
235
+ * `requiredDate`, which parse and check the value.
235
236
  */
236
237
  required(key) {
237
238
  this.assertActive();
238
239
  return this.resolver.required(key);
239
240
  }
241
+ /** Gets a required string; throws when missing or not a string. */
242
+ requiredString(key) {
243
+ this.assertActive();
244
+ return this.resolver.requiredString(key);
245
+ }
246
+ /**
247
+ * Gets a required number, parsing decimal strings (`"5432"`); throws
248
+ * when missing or not a number.
249
+ */
250
+ requiredNumber(key) {
251
+ this.assertActive();
252
+ return this.resolver.requiredNumber(key);
253
+ }
254
+ /**
255
+ * Gets a required boolean, parsing `true/false`, `1/0`, `yes/no`, `y/n`
256
+ * and `on/off`; throws when missing or not a boolean.
257
+ */
258
+ requiredBoolean(key) {
259
+ this.assertActive();
260
+ return this.resolver.requiredBoolean(key);
261
+ }
262
+ /** Gets a required Date, parsing ISO strings; throws when missing. */
263
+ requiredDate(key) {
264
+ this.assertActive();
265
+ return this.resolver.requiredDate(key);
266
+ }
240
267
  /**
241
268
  * Gets a configuration value with schema validation.
242
269
  */
@@ -244,51 +271,30 @@ export class ConfigManager {
244
271
  this.assertActive();
245
272
  return this.resolver.resolve(key, schema);
246
273
  }
247
- /**
248
- * Gets a string value.
249
- */
250
274
  string(key, fallback) {
251
275
  this.assertActive();
252
276
  return this.resolver.string(key, fallback);
253
277
  }
254
- /**
255
- * Gets a number value.
256
- */
257
278
  number(key, fallback) {
258
279
  this.assertActive();
259
280
  return this.resolver.number(key, fallback);
260
281
  }
261
- /**
262
- * Gets a boolean value.
263
- */
264
282
  boolean(key, fallback) {
265
283
  this.assertActive();
266
284
  return this.resolver.boolean(key, fallback);
267
285
  }
268
- /**
269
- * Gets a bigint value.
270
- */
271
286
  bigint(key, fallback) {
272
287
  this.assertActive();
273
288
  return this.resolver.bigint(key, fallback);
274
289
  }
275
- /**
276
- * Gets a Date value.
277
- */
278
290
  date(key, fallback) {
279
291
  this.assertActive();
280
292
  return this.resolver.date(key, fallback);
281
293
  }
282
- /**
283
- * Gets an object value.
284
- */
285
294
  object(key, fallback) {
286
295
  this.assertActive();
287
296
  return this.resolver.object(key, fallback);
288
297
  }
289
- /**
290
- * Gets an array value.
291
- */
292
298
  array(key, fallback) {
293
299
  this.assertActive();
294
300
  return this.resolver.array(key, fallback);
@@ -4,6 +4,7 @@
4
4
  * ConfigurationManager error types re-exported from @zudojs/errors.
5
5
  */
6
6
  import { ConfigurationError, createConfigurationError, isConfigurationError, missingConfigurationError, invalidConfigurationError } from "@zudojs/errors";
7
+ import type { ConfigValidationIssue } from "../configSchema/configSchema.type.js";
7
8
  /**
8
9
  * Error thrown when complete configuration validation fails.
9
10
  *
@@ -11,8 +12,8 @@ import { ConfigurationError, createConfigurationError, isConfigurationError, mis
11
12
  * the base ConfigurationError from @zudojs/errors.
12
13
  */
13
14
  export declare class ConfigManagerValidationError extends ConfigurationError {
14
- readonly issues: readonly unknown[];
15
- constructor(issues: readonly unknown[]);
15
+ readonly issues: readonly ConfigValidationIssue[];
16
+ constructor(issues: readonly ConfigValidationIssue[]);
16
17
  }
17
18
  export { ConfigurationError, createConfigurationError, isConfigurationError, missingConfigurationError, invalidConfigurationError, };
18
19
  //# sourceMappingURL=configManager.error.d.ts.map
@@ -1,6 +1,7 @@
1
1
  import type { ConfigValue } from "../../configValue/configValue.core.js";
2
- import type { ConfigSchema } from "../../configSchema/index.js";
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: ConfigSchema<T>): T | undefined;
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 { ConfigSchema } from "../../configSchema/index.js";
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: ConfigSchema<T>): T | undefined;
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: ConfigSchema<T>): ConfigResolutionResult<T>;
28
+ resolveResult<T extends ConfigValue>(key: string, schema: TypedConfigSchema<T>): ConfigResolutionResult<T>;
28
29
  /**
29
- * Returns a required configuration value.
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 configuration value.
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
- * String configuration schema.
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
- * Number configuration schema.
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(value: unknown, schema: AnyConfigSchema, context?: Partial<ConfigValidationContext>): ConfigValidationResult;
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(value, schema, context) {
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
- if (!matchesConfigType(value, schema.type)) {
260
- issues.push(createConfigValidationIssue(path, `Expected ${formatExpectedType(schema.type)} but received ${getConfigValueType(value)}.`, "TYPE_MISMATCH", {
261
- expected: schema.type,
262
- received: getConfigValueType(value),
263
- }));
264
- return {
265
- valid: false,
266
- issues,
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
- 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;
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
- if (schema.validate) {
292
- const result = schema.validate(base, validationContext);
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
- transformed = schema.transform(base, validationContext);
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 ? transformed : undefined,
314
+ value: valid ? final : undefined,
313
315
  issues,
314
316
  };
315
317
  }
318
+ /** Marks a transform that threw, or a schema without one. */
319
+ const NOT_PARSED = Symbol("NOT_PARSED");
320
+ /**
321
+ * Runs `schema.transform` as a parser on a STRING that failed the type
322
+ * check (environment variables, `.env` files and CLI flags only produce
323
+ * strings). Returns {@link NOT_PARSED} for any other value, when there is
324
+ * no transform, or when it threw; the caller then reports the original
325
+ * type mismatch. A non-string of the wrong type is a real type error and
326
+ * is never handed to `transform`.
327
+ */
328
+ function parseWithTransform(value, schema, context) {
329
+ if (schema.transform === undefined || typeof value !== "string") {
330
+ return NOT_PARSED;
331
+ }
332
+ try {
333
+ return schema.transform(value, context);
334
+ }
335
+ catch {
336
+ return NOT_PARSED;
337
+ }
338
+ }
339
+ /**
340
+ * Applies the built-in constraints and, for object schemas, the nested
341
+ * property schemas. Returns the value to continue with (rebuilt when an
342
+ * item or property schema rewrote part of it).
343
+ */
344
+ function checkConstraints(value, schema, context, issues) {
345
+ const rewritten = validateBuiltInRules(value, schema, context, issues);
346
+ // An object schema carries `properties` / `additionalProperties`;
347
+ // delegate to validateConfigObject so nested constraints are enforced.
348
+ const objectSchema = schema;
349
+ if (matchesConfigType(value, ConfigValueType.OBJECT) &&
350
+ (objectSchema.properties !== undefined ||
351
+ objectSchema.additionalProperties !== undefined)) {
352
+ const nested = validateConfigObject(value, {
353
+ type: ConfigValueType.OBJECT,
354
+ properties: objectSchema.properties ?? {},
355
+ additionalProperties: objectSchema.additionalProperties,
356
+ }, context.path);
357
+ issues.push(...nested.issues);
358
+ if (nested.value !== undefined) {
359
+ return nested.value;
360
+ }
361
+ }
362
+ return rewritten ?? value;
363
+ }
316
364
  /**
317
365
  * Validates an entire configuration object.
318
366
  */
@@ -105,11 +105,13 @@ export declare class ConfigStore {
105
105
  */
106
106
  toSafeObject(): Readonly<Record<string, ConfigValue>>;
107
107
  /**
108
- * Returns entries matching a prefix.
108
+ * Returns entries matching a prefix. A trailing dot is optional:
109
+ * `"db"` and `"db."` both match `db` and `db.*` (but not `dbx`).
109
110
  */
110
111
  getByPrefix(prefix: string): readonly ConfigEntry[];
111
112
  /**
112
- * Returns configuration values matching a prefix.
113
+ * Returns configuration values matching a prefix, keyed by the rest of
114
+ * the key. A trailing dot is optional, as for {@link getByPrefix}.
113
115
  */
114
116
  getObjectByPrefix(prefix: string): Readonly<Record<string, ConfigValue>>;
115
117
  /**
@@ -14,6 +14,18 @@ export function normalizeKey(key) {
14
14
  }
15
15
  return normalized;
16
16
  }
17
+ /**
18
+ * Normalizes a key prefix: trims it and drops trailing dots, so `"db."`
19
+ * and `"db"` select the same entries. A prefix that is only dots is
20
+ * rejected like an empty key.
21
+ */
22
+ function normalizePrefix(prefix) {
23
+ const normalized = normalizeKey(prefix).replace(/\.+$/, "");
24
+ if (normalized.length === 0) {
25
+ throw new TypeError("Configuration prefix cannot be empty.");
26
+ }
27
+ return normalized;
28
+ }
17
29
  /**
18
30
  * Central in-memory configuration store.
19
31
  *
@@ -248,20 +260,22 @@ export class ConfigStore {
248
260
  return Object.freeze(result);
249
261
  }
250
262
  /**
251
- * Returns entries matching a prefix.
263
+ * Returns entries matching a prefix. A trailing dot is optional:
264
+ * `"db"` and `"db."` both match `db` and `db.*` (but not `dbx`).
252
265
  */
253
266
  getByPrefix(prefix) {
254
267
  this.assertActive();
255
- const normalizedPrefix = normalizeKey(prefix);
268
+ const normalizedPrefix = normalizePrefix(prefix);
256
269
  return Array.from(this.entries.values()).filter((entry) => entry.key === normalizedPrefix ||
257
270
  entry.key.startsWith(`${normalizedPrefix}.`));
258
271
  }
259
272
  /**
260
- * Returns configuration values matching a prefix.
273
+ * Returns configuration values matching a prefix, keyed by the rest of
274
+ * the key. A trailing dot is optional, as for {@link getByPrefix}.
261
275
  */
262
276
  getObjectByPrefix(prefix) {
263
277
  const entries = this.getByPrefix(prefix);
264
- const normalizedPrefix = normalizeKey(prefix);
278
+ const normalizedPrefix = normalizePrefix(prefix);
265
279
  const result = {};
266
280
  for (const entry of entries) {
267
281
  const key = entry.key === normalizedPrefix
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/config",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Layered configuration management with multiple sources, validation, and environment-specific overrides.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -28,12 +28,12 @@
28
28
  "node": ">=24.0.0"
29
29
  },
30
30
  "dependencies": {
31
- "@zudojs/constants": "1.1.1",
32
- "@zudojs/errors": "1.2.0"
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": "^4.1.11"
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://github.com/oyinlola-tech/zudo#readme",
47
+ "homepage": "https://zudojs.oyinlola.site/docs/packages-config",
48
48
  "bugs": {
49
49
  "url": "https://github.com/oyinlola-tech/zudo/issues"
50
50
  },