@bymax-one/nest-config 1.2.1 → 1.2.3
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/CHANGELOG.md +75 -1
- package/README.md +177 -31
- package/dist/index.cjs +4 -0
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/internal/index.cjs +71 -13
- package/dist/internal/index.d.cts +57 -18
- package/dist/internal/index.d.ts +57 -18
- package/dist/internal/index.mjs +71 -14
- package/dist/testing/index.cjs +59 -9
- package/dist/testing/index.d.cts +4 -1
- package/dist/testing/index.d.ts +4 -1
- package/dist/testing/index.mjs +60 -10
- package/package.json +1 -1
|
@@ -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.
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
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>) =>
|
|
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
|
|
538
|
-
*
|
|
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
|
|
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.
|
|
549
|
-
*
|
|
550
|
-
*
|
|
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,
|
|
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.
|
|
575
|
-
*
|
|
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, though validation rejects a schema that holds such a pair. 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`.
|
|
@@ -606,4 +619,30 @@ interface SourceBinding {
|
|
|
606
619
|
*/
|
|
607
620
|
declare function resolveSourceNames<TShape extends EnvShape = EnvShape>(schema: EnvSchema<TShape>): readonly SourceBinding[];
|
|
608
621
|
|
|
609
|
-
|
|
622
|
+
/**
|
|
623
|
+
* @fileoverview Single-pass environment validator. Maps a flat source record
|
|
624
|
+
* onto the nested schema through the resolved source-name bindings, runs exactly
|
|
625
|
+
* one Zod parse, and either returns the typed parsed output or throws a
|
|
626
|
+
* BymaxConfigValidationError aggregating every violation. Zod issues are
|
|
627
|
+
* translated into value-free ConfigIssue descriptions that state only the
|
|
628
|
+
* expected constraint, never the received value. The one exception is the
|
|
629
|
+
* message a schema author attaches to a `custom` issue (`.check`, `.refine`,
|
|
630
|
+
* `.superRefine`), which is reported as written: it is schema text, not input,
|
|
631
|
+
* and it is the only place a conditional or cross-field rule can explain itself.
|
|
632
|
+
* @layer Service
|
|
633
|
+
*/
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Reject a schema in which two leaves read one source variable.
|
|
637
|
+
*
|
|
638
|
+
* Two leaves that derive, or are told through `meta({ env })`, the same variable would
|
|
639
|
+
* each read the one value and neither could report on its own, so the schema is wrong
|
|
640
|
+
* before any source is looked at. One issue is produced per colliding leaf, in order of
|
|
641
|
+
* first appearance, each naming the other leaves that share its variable.
|
|
642
|
+
*
|
|
643
|
+
* @param bindings - Every leaf binding of a schema, from `resolveSourceNames`.
|
|
644
|
+
* @throws {BymaxConfigValidationError} When any variable is bound by more than one leaf.
|
|
645
|
+
*/
|
|
646
|
+
declare function assertDistinctVariables(bindings: ReadonlyArray<SourceBinding>): void;
|
|
647
|
+
|
|
648
|
+
export { BYMAX_CONFIG, BYMAX_CONFIG_OPTIONS, BymaxConfigModule, type BymaxConfigModuleOptions, BymaxConfigValidationError, ConfigErrorCode, type ConfigIssue, type ConfigIssueCode, ConfigService, type ConfigValidationCode, type EnvLeaf, type EnvNamespace, type EnvOutput, type EnvSchema, type EnvShape, type Path, type PathValue, type SourceBinding, assertDistinctVariables, createValidatedConfig, defineEnv, resolveSourceNames };
|
package/dist/internal/index.d.ts
CHANGED
|
@@ -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.
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
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>) =>
|
|
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
|
|
538
|
-
*
|
|
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
|
|
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.
|
|
549
|
-
*
|
|
550
|
-
*
|
|
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,
|
|
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.
|
|
575
|
-
*
|
|
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, though validation rejects a schema that holds such a pair. 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`.
|
|
@@ -606,4 +619,30 @@ interface SourceBinding {
|
|
|
606
619
|
*/
|
|
607
620
|
declare function resolveSourceNames<TShape extends EnvShape = EnvShape>(schema: EnvSchema<TShape>): readonly SourceBinding[];
|
|
608
621
|
|
|
609
|
-
|
|
622
|
+
/**
|
|
623
|
+
* @fileoverview Single-pass environment validator. Maps a flat source record
|
|
624
|
+
* onto the nested schema through the resolved source-name bindings, runs exactly
|
|
625
|
+
* one Zod parse, and either returns the typed parsed output or throws a
|
|
626
|
+
* BymaxConfigValidationError aggregating every violation. Zod issues are
|
|
627
|
+
* translated into value-free ConfigIssue descriptions that state only the
|
|
628
|
+
* expected constraint, never the received value. The one exception is the
|
|
629
|
+
* message a schema author attaches to a `custom` issue (`.check`, `.refine`,
|
|
630
|
+
* `.superRefine`), which is reported as written: it is schema text, not input,
|
|
631
|
+
* and it is the only place a conditional or cross-field rule can explain itself.
|
|
632
|
+
* @layer Service
|
|
633
|
+
*/
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Reject a schema in which two leaves read one source variable.
|
|
637
|
+
*
|
|
638
|
+
* Two leaves that derive, or are told through `meta({ env })`, the same variable would
|
|
639
|
+
* each read the one value and neither could report on its own, so the schema is wrong
|
|
640
|
+
* before any source is looked at. One issue is produced per colliding leaf, in order of
|
|
641
|
+
* first appearance, each naming the other leaves that share its variable.
|
|
642
|
+
*
|
|
643
|
+
* @param bindings - Every leaf binding of a schema, from `resolveSourceNames`.
|
|
644
|
+
* @throws {BymaxConfigValidationError} When any variable is bound by more than one leaf.
|
|
645
|
+
*/
|
|
646
|
+
declare function assertDistinctVariables(bindings: ReadonlyArray<SourceBinding>): void;
|
|
647
|
+
|
|
648
|
+
export { BYMAX_CONFIG, BYMAX_CONFIG_OPTIONS, BymaxConfigModule, type BymaxConfigModuleOptions, BymaxConfigValidationError, ConfigErrorCode, type ConfigIssue, type ConfigIssueCode, ConfigService, type ConfigValidationCode, type EnvLeaf, type EnvNamespace, type EnvOutput, type EnvSchema, type EnvShape, type Path, type PathValue, type SourceBinding, assertDistinctVariables, createValidatedConfig, defineEnv, resolveSourceNames };
|
package/dist/internal/index.mjs
CHANGED
|
@@ -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
|
-
|
|
124
|
-
|
|
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
|
|
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 (
|
|
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) {
|
|
@@ -254,43 +280,74 @@ function detectUnknownKeys(prefixes, bindings, source) {
|
|
|
254
280
|
(left, right) => Number(left.variable > right.variable) - Number(left.variable < right.variable)
|
|
255
281
|
);
|
|
256
282
|
}
|
|
283
|
+
function assertDistinctVariables(bindings) {
|
|
284
|
+
const byVariable = /* @__PURE__ */ new Map();
|
|
285
|
+
for (const { path, variable } of bindings) {
|
|
286
|
+
byVariable.set(variable, [...byVariable.get(variable) ?? [], path]);
|
|
287
|
+
}
|
|
288
|
+
const issues = [];
|
|
289
|
+
for (const [variable, paths] of byVariable) {
|
|
290
|
+
if (paths.length < 2) continue;
|
|
291
|
+
for (const path of paths) {
|
|
292
|
+
const others = paths.filter((other) => other !== path).join(", ");
|
|
293
|
+
issues.push({
|
|
294
|
+
path,
|
|
295
|
+
variable,
|
|
296
|
+
code: ConfigErrorCode.INVALID,
|
|
297
|
+
message: `shares this variable with ${others}; rename one with meta({ env })`
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (issues.length > 0) throw new BymaxConfigValidationError(issues);
|
|
302
|
+
}
|
|
257
303
|
function validateEnv(schema, source, options) {
|
|
258
304
|
const bindings = resolveSourceNames(schema);
|
|
305
|
+
assertDistinctVariables(bindings);
|
|
259
306
|
const sourceMap = new Map(Object.entries(source));
|
|
260
307
|
const namespaces = Object.keys(schema.shape);
|
|
261
308
|
const candidate = buildCandidate(namespaces, collectPresentLeaves(bindings, sourceMap));
|
|
262
309
|
const result = schema.safeParse(candidate);
|
|
263
|
-
const
|
|
310
|
+
const prefixes = resolveNamespacePrefixes(schema);
|
|
311
|
+
const unknown = options?.strict === true ? detectUnknownKeys(prefixes, bindings, sourceMap) : [];
|
|
264
312
|
if (result.success) {
|
|
265
313
|
if (unknown.length === 0) return result.data;
|
|
266
314
|
throw new BymaxConfigValidationError(unknown);
|
|
267
315
|
}
|
|
268
|
-
const issues = translateIssues(result.error.issues, bindings, sourceMap);
|
|
316
|
+
const issues = translateIssues(result.error.issues, bindings, prefixes, sourceMap);
|
|
269
317
|
throw new BymaxConfigValidationError([...issues, ...unknown]);
|
|
270
318
|
}
|
|
271
319
|
|
|
272
320
|
// src/config.providers.ts
|
|
273
|
-
function reportIssues(hook,
|
|
321
|
+
async function reportIssues(hook, error) {
|
|
274
322
|
if (hook === void 0) return;
|
|
275
323
|
try {
|
|
276
|
-
hook(issues);
|
|
277
|
-
} catch {
|
|
324
|
+
await hook(error.issues);
|
|
325
|
+
} catch (hookError) {
|
|
326
|
+
Object.defineProperty(error, "cause", {
|
|
327
|
+
value: hookError,
|
|
328
|
+
enumerable: false,
|
|
329
|
+
writable: false,
|
|
330
|
+
configurable: false
|
|
331
|
+
});
|
|
278
332
|
}
|
|
279
333
|
}
|
|
280
334
|
function createValidatedConfig(options) {
|
|
281
335
|
const source = options.source ?? process.env;
|
|
336
|
+
return deepFreeze(validateEnv(options.schema, source, { strict: options.strict === true }));
|
|
337
|
+
}
|
|
338
|
+
async function provideValidatedConfig(options) {
|
|
282
339
|
try {
|
|
283
|
-
return
|
|
340
|
+
return createValidatedConfig(options);
|
|
284
341
|
} catch (error) {
|
|
285
342
|
if (error instanceof BymaxConfigValidationError) {
|
|
286
|
-
reportIssues(options.onValidationError, error
|
|
343
|
+
await reportIssues(options.onValidationError, error);
|
|
287
344
|
}
|
|
288
345
|
throw error;
|
|
289
346
|
}
|
|
290
347
|
}
|
|
291
348
|
var bymaxConfigProvider = {
|
|
292
349
|
provide: BYMAX_CONFIG,
|
|
293
|
-
useFactory:
|
|
350
|
+
useFactory: provideValidatedConfig,
|
|
294
351
|
inject: [BYMAX_CONFIG_OPTIONS]
|
|
295
352
|
};
|
|
296
353
|
var _config, _leaves;
|
|
@@ -474,4 +531,4 @@ function defineEnv(shape) {
|
|
|
474
531
|
return z.object(shape);
|
|
475
532
|
}
|
|
476
533
|
|
|
477
|
-
export { BYMAX_CONFIG, BYMAX_CONFIG_OPTIONS, BymaxConfigModule, BymaxConfigValidationError, ConfigErrorCode, ConfigService, createValidatedConfig, defineEnv, resolveSourceNames };
|
|
534
|
+
export { BYMAX_CONFIG, BYMAX_CONFIG_OPTIONS, BymaxConfigModule, BymaxConfigValidationError, ConfigErrorCode, ConfigService, assertDistinctVariables, createValidatedConfig, defineEnv, resolveSourceNames };
|
package/dist/testing/index.cjs
CHANGED
|
@@ -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
|
-
|
|
65
|
-
if (
|
|
66
|
-
return
|
|
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,44 @@ 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
|
|
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
|
|
160
|
-
return
|
|
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
|
+
internal.assertDistinctVariables(internal.resolveSourceNames(schema));
|
|
204
|
+
const overridden = /* @__PURE__ */ new Set();
|
|
205
|
+
const source = applyOverrides(schema, synthesizePlaceholderSource(schema), overrides, overridden);
|
|
206
|
+
try {
|
|
207
|
+
return { source, config: internal.createValidatedConfig({ schema, source }) };
|
|
208
|
+
} catch (error) {
|
|
209
|
+
throw error instanceof internal.BymaxConfigValidationError ? explainUnsatisfiedLeaves(error, overridden) : error;
|
|
210
|
+
}
|
|
161
211
|
}
|
|
162
212
|
function createTestConfig(schema, overrides) {
|
|
163
|
-
return
|
|
213
|
+
return prepareTestConfig(schema, overrides).config;
|
|
164
214
|
}
|
|
165
215
|
function configTestingModule(schema, overrides) {
|
|
166
|
-
const source =
|
|
216
|
+
const { source } = prepareTestConfig(schema, overrides);
|
|
167
217
|
return internal.BymaxConfigModule.forRoot({ schema, source });
|
|
168
218
|
}
|
|
169
219
|
|
package/dist/testing/index.d.cts
CHANGED
|
@@ -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({
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -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({
|