@bymax-one/nest-config 1.2.0 → 1.2.2

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.
@@ -122,8 +122,12 @@ function toScreamingSnake(segment) {
122
122
  return segment.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").toUpperCase();
123
123
  }
124
124
  function readEnvOverride(leaf) {
125
- const override = leaf.meta()?.env;
126
- return typeof override === "string" && override.length > 0 ? override : void 0;
125
+ for (let node = leaf; node !== void 0; ) {
126
+ const override = node.meta()?.env;
127
+ if (typeof override === "string" && override.length > 0) return override;
128
+ node = node._zod.def.innerType ?? node._zod.def.in;
129
+ }
130
+ return void 0;
127
131
  }
128
132
  function readOpenFlag(namespace) {
129
133
  return namespace.meta()?.open === true;
@@ -210,13 +214,28 @@ function toConfigIssue(binding, issue, source) {
210
214
  message: authored ?? (isMissing ? MISSING_MESSAGE : describeConstraint(issue))
211
215
  };
212
216
  }
213
- function translateIssues(zodIssues, bindings, source) {
217
+ function toNamespaceIssue(path, variable, issue) {
218
+ return {
219
+ path,
220
+ variable,
221
+ code: ConfigErrorCode.INVALID,
222
+ message: authoredMessage(issue) ?? describeConstraint(issue)
223
+ };
224
+ }
225
+ function translateIssues(zodIssues, bindings, prefixes, source) {
226
+ const leafPaths = new Set(bindings.map((binding) => binding.path));
227
+ const namespaces = new Set(prefixes.map((entry) => entry.namespace));
214
228
  const issueByPath = /* @__PURE__ */ new Map();
229
+ const issueByNamespace = /* @__PURE__ */ new Map();
215
230
  for (const issue of zodIssues) {
216
231
  const path = issue.path.slice(0, 2).join(".");
217
- if (!issueByPath.has(path)) {
218
- issueByPath.set(path, issue);
232
+ if (leafPaths.has(path)) {
233
+ if (!issueByPath.has(path)) issueByPath.set(path, issue);
234
+ continue;
219
235
  }
236
+ const namespace = issue.path.length > 0 && namespaces.has(String(issue.path[0]));
237
+ const key = namespace ? String(issue.path[0]) : "";
238
+ if (!issueByNamespace.has(key)) issueByNamespace.set(key, issue);
220
239
  }
221
240
  const result = [];
222
241
  for (const binding of bindings) {
@@ -224,6 +243,13 @@ function translateIssues(zodIssues, bindings, source) {
224
243
  if (issue === void 0) continue;
225
244
  result.push(toConfigIssue(binding, issue, source));
226
245
  }
246
+ for (const entry of prefixes) {
247
+ const issue = issueByNamespace.get(entry.namespace);
248
+ if (issue === void 0) continue;
249
+ result.push(toNamespaceIssue(entry.namespace, `${entry.prefix}*`, issue));
250
+ }
251
+ const wholeSchema = issueByNamespace.get("");
252
+ if (wholeSchema !== void 0) result.push(toNamespaceIssue("", "(schema)", wholeSchema));
227
253
  return result;
228
254
  }
229
255
  function longestMatchingPrefix(prefixes, key) {
@@ -262,37 +288,47 @@ function validateEnv(schema, source, options) {
262
288
  const namespaces = Object.keys(schema.shape);
263
289
  const candidate = buildCandidate(namespaces, collectPresentLeaves(bindings, sourceMap));
264
290
  const result = schema.safeParse(candidate);
265
- const unknown = options?.strict === true ? detectUnknownKeys(resolveNamespacePrefixes(schema), bindings, sourceMap) : [];
291
+ const prefixes = resolveNamespacePrefixes(schema);
292
+ const unknown = options?.strict === true ? detectUnknownKeys(prefixes, bindings, sourceMap) : [];
266
293
  if (result.success) {
267
294
  if (unknown.length === 0) return result.data;
268
295
  throw new BymaxConfigValidationError(unknown);
269
296
  }
270
- const issues = translateIssues(result.error.issues, bindings, sourceMap);
297
+ const issues = translateIssues(result.error.issues, bindings, prefixes, sourceMap);
271
298
  throw new BymaxConfigValidationError([...issues, ...unknown]);
272
299
  }
273
300
 
274
301
  // src/config.providers.ts
275
- function reportIssues(hook, issues) {
302
+ async function reportIssues(hook, error) {
276
303
  if (hook === void 0) return;
277
304
  try {
278
- hook(issues);
279
- } catch {
305
+ await hook(error.issues);
306
+ } catch (hookError) {
307
+ Object.defineProperty(error, "cause", {
308
+ value: hookError,
309
+ enumerable: false,
310
+ writable: false,
311
+ configurable: false
312
+ });
280
313
  }
281
314
  }
282
315
  function createValidatedConfig(options) {
283
316
  const source = options.source ?? process.env;
317
+ return deepFreeze(validateEnv(options.schema, source, { strict: options.strict === true }));
318
+ }
319
+ async function provideValidatedConfig(options) {
284
320
  try {
285
- return deepFreeze(validateEnv(options.schema, source, { strict: options.strict === true }));
321
+ return createValidatedConfig(options);
286
322
  } catch (error) {
287
323
  if (error instanceof BymaxConfigValidationError) {
288
- reportIssues(options.onValidationError, error.issues);
324
+ await reportIssues(options.onValidationError, error);
289
325
  }
290
326
  throw error;
291
327
  }
292
328
  }
293
329
  var bymaxConfigProvider = {
294
330
  provide: BYMAX_CONFIG,
295
- useFactory: createValidatedConfig,
331
+ useFactory: provideValidatedConfig,
296
332
  inject: [BYMAX_CONFIG_OPTIONS]
297
333
  };
298
334
  var _config, _leaves;
@@ -230,13 +230,16 @@ interface BymaxConfigModuleOptions<TSchema extends EnvSchema = EnvSchema> {
230
230
  readonly source?: Record<string, string | undefined>;
231
231
  /**
232
232
  * Observability hook invoked once with the structured issue list immediately
233
- * before the validation error is thrown. The issues are value-free on the
234
- * terms stated by {@link ConfigIssue}: generated descriptions never carry a
235
- * received value, while a schema author's own `custom` message is passed
236
- * through as written. It is observability only and cannot suppress the
237
- * failure: the error still propagates even when the hook itself throws.
233
+ * before the validation error is thrown. Its return value is ignored unless it is a
234
+ * promise, which the module waits for before the boot fails; there is no timeout, so a
235
+ * hook that never settles holds the boot. The issues are value-free on the terms stated by
236
+ * {@link ConfigIssue}: generated descriptions never carry a received value, while a
237
+ * schema author's own `custom` message is passed through as written. It is
238
+ * observability only and cannot suppress the failure: the validation error still
239
+ * propagates when the hook throws or rejects, and the hook's error is attached to it
240
+ * as its non-enumerable `cause`.
238
241
  */
239
- readonly onValidationError?: (issues: ReadonlyArray<ConfigIssue>) => void;
242
+ readonly onValidationError?: (issues: ReadonlyArray<ConfigIssue>) => unknown;
240
243
  /**
241
244
  * When true, source variables that match a declared namespace prefix but no
242
245
  * declared leaf produce `BYMAX_CONFIG_UNKNOWN_KEY` issues. Defaults to false.
@@ -534,24 +537,24 @@ declare function defineEnv<TShape extends EnvShape>(shape: TShape): EnvSchema<TS
534
537
  * @fileoverview The BYMAX_CONFIG provider factory: the fail-fast heart of the
535
538
  * module. It resolves the source (defaulting to `process.env`, using a custom
536
539
  * source verbatim), runs the single-pass validator, and on success returns the
537
- * deep-frozen typed config. On failure it invokes the observability hook
538
- * exactly once before rethrowing the aggregated error; the hook is
540
+ * deep-frozen typed config. On failure the factory runs the observability hook
541
+ * and waits for it, then rethrows the aggregated error; the hook is
539
542
  * observability only and can never suppress or replace the failure, so a
540
543
  * misconfigured process is stopped before any consumer provider is constructed.
544
+ * A failing hook is recorded as the `cause` of that same error.
541
545
  * @layer Provider
542
546
  */
543
547
 
544
548
  /**
545
- * Validate the source against the schema, freeze the result, or fail fast.
549
+ * Validate the source against the schema, freeze the result, or throw.
546
550
  *
547
551
  * Resolves the source to the supplied record or `process.env`, runs exactly one
548
- * validation pass, and returns the deep-frozen typed output. On a validation
549
- * failure the hook runs once with the issues and the aggregated error is
550
- * rethrown unchanged, aborting module resolution before any consumer provider
551
- * is instantiated.
552
+ * validation pass, and returns the deep-frozen typed output. A validation failure is
553
+ * thrown as the aggregated error; the observability hook belongs to the provider factory,
554
+ * which can wait for it, and this synchronous function never calls it.
552
555
  *
553
556
  * @typeParam TShape - The two-level schema shape.
554
- * @param options - The resolved module options (schema, source, hook, strict).
557
+ * @param options - The resolved module options (schema, source, strict).
555
558
  * @returns The validated, deep-frozen configuration output.
556
559
  * @throws {BymaxConfigValidationError} When the source fails the schema.
557
560
  * @example
@@ -560,7 +563,7 @@ declare function defineEnv<TShape extends EnvShape>(shape: TShape): EnvSchema<TS
560
563
  * config.server.port; // typed, validated, frozen
561
564
  * ```
562
565
  */
563
- declare function createValidatedConfig<TShape extends EnvShape>(options: Omit<BymaxConfigModuleOptions, 'schema'> & {
566
+ declare function createValidatedConfig<TShape extends EnvShape>(options: Omit<BymaxConfigModuleOptions, 'schema' | 'onValidationError'> & {
564
567
  readonly schema: EnvSchema<TShape>;
565
568
  }): Readonly<EnvOutput<TShape>>;
566
569
 
@@ -571,13 +574,15 @@ declare function createValidatedConfig<TShape extends EnvShape>(options: Omit<By
571
574
  * unless the leaf declares a `meta({ env })` override, which wins. It also
572
575
  * resolves the shared prefix of each namespace and whether that namespace
573
576
  * declared `meta({ open: true })`, the two facts strict unknown-key detection
574
- * runs on. This is an internal module consumed by the validator; it is not part
575
- * of the public API.
577
+ * runs on. The validator and the package root share this one module:
578
+ * `resolveSourceNames` and `SourceBinding` are exported from the package root,
579
+ * while the namespace-prefix helpers stay internal.
576
580
  * @layer Utility
577
581
  */
578
582
 
579
583
  /**
580
584
  * The resolved binding between one config leaf and its source variable name.
585
+ * Part of the public API, as the element type of `resolveSourceNames`.
581
586
  */
582
587
  interface SourceBinding {
583
588
  /** Nested config path, e.g. `database.url`. */
@@ -594,6 +599,14 @@ interface SourceBinding {
594
599
  * is total: every declared leaf maps to exactly one variable, in declaration
595
600
  * order.
596
601
  *
602
+ * This is the list the validator reads, not a copy of its rules: validation
603
+ * resolves its bindings through this same function. It reads the schema only,
604
+ * never a source record. Declared is not required: a leaf with a default or an
605
+ * optional leaf is listed like any other. Two distinct paths can resolve to one
606
+ * variable (`a.bC` and `aB.c` both derive `A_B_C`); both bindings are returned
607
+ * and none is dropped. An open namespace changes nothing here, since opening
608
+ * only waives strict unknown-key detection.
609
+ *
597
610
  * @typeParam TShape - The two-level shape the schema was composed from.
598
611
  * @param schema - A schema produced by `defineEnv`.
599
612
  * @returns The ordered list of leaf-to-variable bindings, typed `readonly`.
@@ -230,13 +230,16 @@ interface BymaxConfigModuleOptions<TSchema extends EnvSchema = EnvSchema> {
230
230
  readonly source?: Record<string, string | undefined>;
231
231
  /**
232
232
  * Observability hook invoked once with the structured issue list immediately
233
- * before the validation error is thrown. The issues are value-free on the
234
- * terms stated by {@link ConfigIssue}: generated descriptions never carry a
235
- * received value, while a schema author's own `custom` message is passed
236
- * through as written. It is observability only and cannot suppress the
237
- * failure: the error still propagates even when the hook itself throws.
233
+ * before the validation error is thrown. Its return value is ignored unless it is a
234
+ * promise, which the module waits for before the boot fails; there is no timeout, so a
235
+ * hook that never settles holds the boot. The issues are value-free on the terms stated by
236
+ * {@link ConfigIssue}: generated descriptions never carry a received value, while a
237
+ * schema author's own `custom` message is passed through as written. It is
238
+ * observability only and cannot suppress the failure: the validation error still
239
+ * propagates when the hook throws or rejects, and the hook's error is attached to it
240
+ * as its non-enumerable `cause`.
238
241
  */
239
- readonly onValidationError?: (issues: ReadonlyArray<ConfigIssue>) => void;
242
+ readonly onValidationError?: (issues: ReadonlyArray<ConfigIssue>) => unknown;
240
243
  /**
241
244
  * When true, source variables that match a declared namespace prefix but no
242
245
  * declared leaf produce `BYMAX_CONFIG_UNKNOWN_KEY` issues. Defaults to false.
@@ -534,24 +537,24 @@ declare function defineEnv<TShape extends EnvShape>(shape: TShape): EnvSchema<TS
534
537
  * @fileoverview The BYMAX_CONFIG provider factory: the fail-fast heart of the
535
538
  * module. It resolves the source (defaulting to `process.env`, using a custom
536
539
  * source verbatim), runs the single-pass validator, and on success returns the
537
- * deep-frozen typed config. On failure it invokes the observability hook
538
- * exactly once before rethrowing the aggregated error; the hook is
540
+ * deep-frozen typed config. On failure the factory runs the observability hook
541
+ * and waits for it, then rethrows the aggregated error; the hook is
539
542
  * observability only and can never suppress or replace the failure, so a
540
543
  * misconfigured process is stopped before any consumer provider is constructed.
544
+ * A failing hook is recorded as the `cause` of that same error.
541
545
  * @layer Provider
542
546
  */
543
547
 
544
548
  /**
545
- * Validate the source against the schema, freeze the result, or fail fast.
549
+ * Validate the source against the schema, freeze the result, or throw.
546
550
  *
547
551
  * Resolves the source to the supplied record or `process.env`, runs exactly one
548
- * validation pass, and returns the deep-frozen typed output. On a validation
549
- * failure the hook runs once with the issues and the aggregated error is
550
- * rethrown unchanged, aborting module resolution before any consumer provider
551
- * is instantiated.
552
+ * validation pass, and returns the deep-frozen typed output. A validation failure is
553
+ * thrown as the aggregated error; the observability hook belongs to the provider factory,
554
+ * which can wait for it, and this synchronous function never calls it.
552
555
  *
553
556
  * @typeParam TShape - The two-level schema shape.
554
- * @param options - The resolved module options (schema, source, hook, strict).
557
+ * @param options - The resolved module options (schema, source, strict).
555
558
  * @returns The validated, deep-frozen configuration output.
556
559
  * @throws {BymaxConfigValidationError} When the source fails the schema.
557
560
  * @example
@@ -560,7 +563,7 @@ declare function defineEnv<TShape extends EnvShape>(shape: TShape): EnvSchema<TS
560
563
  * config.server.port; // typed, validated, frozen
561
564
  * ```
562
565
  */
563
- declare function createValidatedConfig<TShape extends EnvShape>(options: Omit<BymaxConfigModuleOptions, 'schema'> & {
566
+ declare function createValidatedConfig<TShape extends EnvShape>(options: Omit<BymaxConfigModuleOptions, 'schema' | 'onValidationError'> & {
564
567
  readonly schema: EnvSchema<TShape>;
565
568
  }): Readonly<EnvOutput<TShape>>;
566
569
 
@@ -571,13 +574,15 @@ declare function createValidatedConfig<TShape extends EnvShape>(options: Omit<By
571
574
  * unless the leaf declares a `meta({ env })` override, which wins. It also
572
575
  * resolves the shared prefix of each namespace and whether that namespace
573
576
  * declared `meta({ open: true })`, the two facts strict unknown-key detection
574
- * runs on. This is an internal module consumed by the validator; it is not part
575
- * of the public API.
577
+ * runs on. The validator and the package root share this one module:
578
+ * `resolveSourceNames` and `SourceBinding` are exported from the package root,
579
+ * while the namespace-prefix helpers stay internal.
576
580
  * @layer Utility
577
581
  */
578
582
 
579
583
  /**
580
584
  * The resolved binding between one config leaf and its source variable name.
585
+ * Part of the public API, as the element type of `resolveSourceNames`.
581
586
  */
582
587
  interface SourceBinding {
583
588
  /** Nested config path, e.g. `database.url`. */
@@ -594,6 +599,14 @@ interface SourceBinding {
594
599
  * is total: every declared leaf maps to exactly one variable, in declaration
595
600
  * order.
596
601
  *
602
+ * This is the list the validator reads, not a copy of its rules: validation
603
+ * resolves its bindings through this same function. It reads the schema only,
604
+ * never a source record. Declared is not required: a leaf with a default or an
605
+ * optional leaf is listed like any other. Two distinct paths can resolve to one
606
+ * variable (`a.bC` and `aB.c` both derive `A_B_C`); both bindings are returned
607
+ * and none is dropped. An open namespace changes nothing here, since opening
608
+ * only waives strict unknown-key detection.
609
+ *
597
610
  * @typeParam TShape - The two-level shape the schema was composed from.
598
611
  * @param schema - A schema produced by `defineEnv`.
599
612
  * @returns The ordered list of leaf-to-variable bindings, typed `readonly`.
@@ -120,8 +120,12 @@ function toScreamingSnake(segment) {
120
120
  return segment.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").toUpperCase();
121
121
  }
122
122
  function readEnvOverride(leaf) {
123
- const override = leaf.meta()?.env;
124
- return typeof override === "string" && override.length > 0 ? override : void 0;
123
+ for (let node = leaf; node !== void 0; ) {
124
+ const override = node.meta()?.env;
125
+ if (typeof override === "string" && override.length > 0) return override;
126
+ node = node._zod.def.innerType ?? node._zod.def.in;
127
+ }
128
+ return void 0;
125
129
  }
126
130
  function readOpenFlag(namespace) {
127
131
  return namespace.meta()?.open === true;
@@ -208,13 +212,28 @@ function toConfigIssue(binding, issue, source) {
208
212
  message: authored ?? (isMissing ? MISSING_MESSAGE : describeConstraint(issue))
209
213
  };
210
214
  }
211
- function translateIssues(zodIssues, bindings, source) {
215
+ function toNamespaceIssue(path, variable, issue) {
216
+ return {
217
+ path,
218
+ variable,
219
+ code: ConfigErrorCode.INVALID,
220
+ message: authoredMessage(issue) ?? describeConstraint(issue)
221
+ };
222
+ }
223
+ function translateIssues(zodIssues, bindings, prefixes, source) {
224
+ const leafPaths = new Set(bindings.map((binding) => binding.path));
225
+ const namespaces = new Set(prefixes.map((entry) => entry.namespace));
212
226
  const issueByPath = /* @__PURE__ */ new Map();
227
+ const issueByNamespace = /* @__PURE__ */ new Map();
213
228
  for (const issue of zodIssues) {
214
229
  const path = issue.path.slice(0, 2).join(".");
215
- if (!issueByPath.has(path)) {
216
- issueByPath.set(path, issue);
230
+ if (leafPaths.has(path)) {
231
+ if (!issueByPath.has(path)) issueByPath.set(path, issue);
232
+ continue;
217
233
  }
234
+ const namespace = issue.path.length > 0 && namespaces.has(String(issue.path[0]));
235
+ const key = namespace ? String(issue.path[0]) : "";
236
+ if (!issueByNamespace.has(key)) issueByNamespace.set(key, issue);
218
237
  }
219
238
  const result = [];
220
239
  for (const binding of bindings) {
@@ -222,6 +241,13 @@ function translateIssues(zodIssues, bindings, source) {
222
241
  if (issue === void 0) continue;
223
242
  result.push(toConfigIssue(binding, issue, source));
224
243
  }
244
+ for (const entry of prefixes) {
245
+ const issue = issueByNamespace.get(entry.namespace);
246
+ if (issue === void 0) continue;
247
+ result.push(toNamespaceIssue(entry.namespace, `${entry.prefix}*`, issue));
248
+ }
249
+ const wholeSchema = issueByNamespace.get("");
250
+ if (wholeSchema !== void 0) result.push(toNamespaceIssue("", "(schema)", wholeSchema));
225
251
  return result;
226
252
  }
227
253
  function longestMatchingPrefix(prefixes, key) {
@@ -260,37 +286,47 @@ function validateEnv(schema, source, options) {
260
286
  const namespaces = Object.keys(schema.shape);
261
287
  const candidate = buildCandidate(namespaces, collectPresentLeaves(bindings, sourceMap));
262
288
  const result = schema.safeParse(candidate);
263
- const unknown = options?.strict === true ? detectUnknownKeys(resolveNamespacePrefixes(schema), bindings, sourceMap) : [];
289
+ const prefixes = resolveNamespacePrefixes(schema);
290
+ const unknown = options?.strict === true ? detectUnknownKeys(prefixes, bindings, sourceMap) : [];
264
291
  if (result.success) {
265
292
  if (unknown.length === 0) return result.data;
266
293
  throw new BymaxConfigValidationError(unknown);
267
294
  }
268
- const issues = translateIssues(result.error.issues, bindings, sourceMap);
295
+ const issues = translateIssues(result.error.issues, bindings, prefixes, sourceMap);
269
296
  throw new BymaxConfigValidationError([...issues, ...unknown]);
270
297
  }
271
298
 
272
299
  // src/config.providers.ts
273
- function reportIssues(hook, issues) {
300
+ async function reportIssues(hook, error) {
274
301
  if (hook === void 0) return;
275
302
  try {
276
- hook(issues);
277
- } catch {
303
+ await hook(error.issues);
304
+ } catch (hookError) {
305
+ Object.defineProperty(error, "cause", {
306
+ value: hookError,
307
+ enumerable: false,
308
+ writable: false,
309
+ configurable: false
310
+ });
278
311
  }
279
312
  }
280
313
  function createValidatedConfig(options) {
281
314
  const source = options.source ?? process.env;
315
+ return deepFreeze(validateEnv(options.schema, source, { strict: options.strict === true }));
316
+ }
317
+ async function provideValidatedConfig(options) {
282
318
  try {
283
- return deepFreeze(validateEnv(options.schema, source, { strict: options.strict === true }));
319
+ return createValidatedConfig(options);
284
320
  } catch (error) {
285
321
  if (error instanceof BymaxConfigValidationError) {
286
- reportIssues(options.onValidationError, error.issues);
322
+ await reportIssues(options.onValidationError, error);
287
323
  }
288
324
  throw error;
289
325
  }
290
326
  }
291
327
  var bymaxConfigProvider = {
292
328
  provide: BYMAX_CONFIG,
293
- useFactory: createValidatedConfig,
329
+ useFactory: provideValidatedConfig,
294
330
  inject: [BYMAX_CONFIG_OPTIONS]
295
331
  };
296
332
  var _config, _leaves;
@@ -9,6 +9,10 @@ var PLACEHOLDER_EMAIL_DOMAIN = "@placeholder.local";
9
9
  var DEFAULT_NUMBER = 1;
10
10
  var DEFAULT_STRING_LENGTH = 1;
11
11
  var PLACEHOLDER_BOOLEAN = "true";
12
+ var PLACEHOLDER_UUID = "00000000-0000-4000-8000-000000000000";
13
+ var PLACEHOLDER_DATETIME = "2000-01-01T00:00:00.000Z";
14
+ var PLACEHOLDER_DATE = "2000-01-01";
15
+ var PLACEHOLDER_TIME = "00:00:00";
12
16
  function readDef(leaf) {
13
17
  return leaf._zod.def;
14
18
  }
@@ -59,11 +63,23 @@ function synthesizePlainString(lengths) {
59
63
  const bounded = target > lengths.max ? lengths.max : target;
60
64
  return FILLER_CHARACTER.repeat(bounded);
61
65
  }
66
+ function formatOf(def) {
67
+ const declared = checksOf(def).map((wrapper) => wrapper._zod.def).find((check) => check.check === "string_format");
68
+ return def.format ?? declared?.format;
69
+ }
70
+ var FIXED_FORMATS = /* @__PURE__ */ new Map([
71
+ ["uuid", PLACEHOLDER_UUID],
72
+ ["datetime", PLACEHOLDER_DATETIME],
73
+ ["date", PLACEHOLDER_DATE],
74
+ ["time", PLACEHOLDER_TIME]
75
+ ]);
62
76
  function synthesizeString(def) {
63
77
  const lengths = readStringLengths(def);
64
- if (def.format === "url") return synthesizeUrl(lengths);
65
- if (def.format === "email") return synthesizeEmail(lengths);
66
- return synthesizePlainString(lengths);
78
+ const format = formatOf(def);
79
+ if (format === "url") return synthesizeUrl(lengths);
80
+ if (format === "email") return synthesizeEmail(lengths);
81
+ const fixed = format === void 0 ? void 0 : FIXED_FORMATS.get(format);
82
+ return fixed ?? synthesizePlainString(lengths);
67
83
  }
68
84
  function selectNumber(bounds) {
69
85
  const { lower, upper, integer } = bounds;
@@ -99,20 +115,35 @@ function synthesizeEnum(def) {
99
115
  const [first] = Object.values(def.entries);
100
116
  return String(first);
101
117
  }
118
+ function withoutOptional(leaf) {
119
+ const def = readDef(leaf);
120
+ return def.type === "optional" ? withoutOptional(def.innerType) : leaf;
121
+ }
102
122
  function synthesizeLeaf(leaf) {
103
123
  const def = readDef(leaf);
104
124
  switch (def.type) {
105
125
  case "default":
126
+ case "prefault":
127
+ case "catch":
106
128
  case "optional":
107
129
  return void 0;
108
130
  case "nullable":
131
+ case "readonly":
109
132
  return synthesizeLeaf(def.innerType);
133
+ case "nonoptional":
134
+ return synthesizeLeaf(withoutOptional(def.innerType));
135
+ case "pipe":
136
+ return readDef(def.out).type === "boolean" && readDef(def.in).type === "string" ? PLACEHOLDER_BOOLEAN : synthesizeLeaf(def.in);
110
137
  case "string":
111
138
  return synthesizeString(def);
112
139
  case "number":
113
140
  return synthesizeNumber(def);
114
141
  case "boolean":
115
142
  return PLACEHOLDER_BOOLEAN;
143
+ case "date":
144
+ return PLACEHOLDER_DATETIME;
145
+ case "literal":
146
+ return String(def.values[0]);
116
147
  case "enum":
117
148
  return synthesizeEnum(def);
118
149
  default:
@@ -145,25 +176,43 @@ function flattenOverrides(overrides) {
145
176
  }
146
177
  return flat;
147
178
  }
148
- function applyOverrides(schema, base, overrides) {
179
+ function applyOverrides(schema, base, overrides, overridden) {
149
180
  const flat = flattenOverrides(overrides ?? {});
150
181
  const entries = new Map(Object.entries(base));
151
182
  for (const binding of internal.resolveSourceNames(schema)) {
152
183
  const value = flat.get(binding.path);
153
- if (value !== void 0) entries.set(binding.variable, String(value));
184
+ if (value === void 0) continue;
185
+ entries.set(binding.variable, String(value));
186
+ overridden.add(binding.variable);
154
187
  }
155
188
  const source = /* @__PURE__ */ Object.create(null);
156
189
  for (const [variable, value] of entries) source[variable] = value;
157
190
  return source;
158
191
  }
159
- function buildTestSource(schema, overrides) {
160
- return applyOverrides(schema, synthesizePlaceholderSource(schema), overrides);
192
+ function explainUnsatisfiedLeaves(error, overridden) {
193
+ return new internal.BymaxConfigValidationError(
194
+ error.issues.map(
195
+ (issue) => !issue.path.includes(".") || overridden.has(issue.variable) ? issue : {
196
+ ...issue,
197
+ message: `no placeholder satisfies this leaf (${issue.message}); pass an override for ${issue.path}`
198
+ }
199
+ )
200
+ );
201
+ }
202
+ function prepareTestConfig(schema, overrides) {
203
+ const overridden = /* @__PURE__ */ new Set();
204
+ const source = applyOverrides(schema, synthesizePlaceholderSource(schema), overrides, overridden);
205
+ try {
206
+ return { source, config: internal.createValidatedConfig({ schema, source }) };
207
+ } catch (error) {
208
+ throw error instanceof internal.BymaxConfigValidationError ? explainUnsatisfiedLeaves(error, overridden) : error;
209
+ }
161
210
  }
162
211
  function createTestConfig(schema, overrides) {
163
- return internal.createValidatedConfig({ schema, source: buildTestSource(schema, overrides) });
212
+ return prepareTestConfig(schema, overrides).config;
164
213
  }
165
214
  function configTestingModule(schema, overrides) {
166
- const source = buildTestSource(schema, overrides);
215
+ const { source } = prepareTestConfig(schema, overrides);
167
216
  return internal.BymaxConfigModule.forRoot({ schema, source });
168
217
  }
169
218
 
@@ -40,7 +40,8 @@ type ConfigOverrides<TShape extends EnvShape> = {
40
40
  * @param schema - A schema produced by `defineEnv`.
41
41
  * @param overrides - Optional selective nested partial overrides.
42
42
  * @returns The validated, deep-frozen configuration typed by the schema.
43
- * @throws {BymaxConfigValidationError} When an override violates a constraint.
43
+ * @throws {BymaxConfigValidationError} When an override violates a constraint, or when no
44
+ * placeholder satisfies a leaf: that issue says so and names the leaf to override.
44
45
  * @example
45
46
  * ```typescript
46
47
  * const config = createTestConfig(envSchema, {
@@ -75,6 +76,8 @@ declare function createTestConfig<TShape extends EnvShape>(schema: EnvSchema<TSh
75
76
  * @param schema - A schema produced by `defineEnv`.
76
77
  * @param overrides - Optional selective nested partial overrides.
77
78
  * @returns A dynamic module registered through the production `forRoot` path.
79
+ * @throws {BymaxConfigValidationError} When an override violates a constraint, or when no
80
+ * placeholder satisfies a leaf; the module is not built, and the issue names the leaf to override.
78
81
  * @example
79
82
  * ```typescript
80
83
  * const moduleRef = await Test.createTestingModule({
@@ -40,7 +40,8 @@ type ConfigOverrides<TShape extends EnvShape> = {
40
40
  * @param schema - A schema produced by `defineEnv`.
41
41
  * @param overrides - Optional selective nested partial overrides.
42
42
  * @returns The validated, deep-frozen configuration typed by the schema.
43
- * @throws {BymaxConfigValidationError} When an override violates a constraint.
43
+ * @throws {BymaxConfigValidationError} When an override violates a constraint, or when no
44
+ * placeholder satisfies a leaf: that issue says so and names the leaf to override.
44
45
  * @example
45
46
  * ```typescript
46
47
  * const config = createTestConfig(envSchema, {
@@ -75,6 +76,8 @@ declare function createTestConfig<TShape extends EnvShape>(schema: EnvSchema<TSh
75
76
  * @param schema - A schema produced by `defineEnv`.
76
77
  * @param overrides - Optional selective nested partial overrides.
77
78
  * @returns A dynamic module registered through the production `forRoot` path.
79
+ * @throws {BymaxConfigValidationError} When an override violates a constraint, or when no
80
+ * placeholder satisfies a leaf; the module is not built, and the issue names the leaf to override.
78
81
  * @example
79
82
  * ```typescript
80
83
  * const moduleRef = await Test.createTestingModule({