@bymax-one/nest-config 1.2.2 → 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 +17 -1
- package/README.md +11 -10
- package/dist/internal/index.cjs +22 -0
- package/dist/internal/index.d.cts +28 -2
- package/dist/internal/index.d.ts +28 -2
- package/dist/internal/index.mjs +22 -1
- package/dist/testing/index.cjs +1 -0
- package/dist/testing/index.mjs +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,21 @@ as the GitHub Release body, so each released version needs a matching
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.2.3] - 2026-10-31
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **A schema in which two leaves read one variable is rejected.** `a.bC` and `aB.c` both
|
|
19
|
+
derive `A_B_C`, and `httpServer.port` and `HTTPServer.port` both derive
|
|
20
|
+
`HTTP_SERVER_PORT`; so does giving two leaves the same `meta({ env })`. Each pair used to
|
|
21
|
+
read one value silently, so setting the variable satisfied both leaves and neither could
|
|
22
|
+
tell. `forRoot`, `createValidatedConfig` and the testing helpers now throw a
|
|
23
|
+
`BymaxConfigValidationError` before the source is read, with one `BYMAX_CONFIG_INVALID`
|
|
24
|
+
issue per colliding leaf naming the other leaves and the shared variable. There is no
|
|
25
|
+
opt-out. `resolveSourceNames` still returns every binding. **Apply to a derived
|
|
26
|
+
backend:** a colliding schema now fails at boot. Give one leaf of each pair a distinct
|
|
27
|
+
`meta({ env })`, or read one leaf and derive the other value from it in the consumer.
|
|
28
|
+
|
|
14
29
|
## [1.2.2] - 2026-10-31
|
|
15
30
|
|
|
16
31
|
### Added
|
|
@@ -468,4 +483,5 @@ have regressed from. They are kept because the reasoning is worth having.
|
|
|
468
483
|
[1.2.0]: https://github.com/bymaxone/nest-config/compare/v1.1.3...v1.2.0
|
|
469
484
|
[1.2.1]: https://github.com/bymaxone/nest-config/compare/v1.2.0...v1.2.1
|
|
470
485
|
[1.2.2]: https://github.com/bymaxone/nest-config/compare/v1.2.1...v1.2.2
|
|
471
|
-
[
|
|
486
|
+
[1.2.3]: https://github.com/bymaxone/nest-config/compare/v1.2.2...v1.2.3
|
|
487
|
+
[Unreleased]: https://github.com/bymaxone/nest-config/compare/v1.2.3...HEAD
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<a href="https://www.npmjs.com/package/@bymax-one/nest-config"><img src="https://img.shields.io/npm/dm/@bymax-one/nest-config?style=flat-square&colorA=000000&colorB=000000" alt="npm downloads" /></a>
|
|
15
15
|
<a href="https://github.com/bymaxone/nest-config/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/bymaxone/nest-config/ci.yml?branch=main&style=flat-square&colorA=000000&label=CI" alt="CI status" /></a>
|
|
16
16
|
<a href="https://github.com/bymaxone/nest-config/actions/workflows/ci.yml"><img src="https://img.shields.io/badge/coverage-100%25-brightgreen?style=flat-square&colorA=000000" alt="coverage" /></a>
|
|
17
|
-
<a href="https://github.com/bymaxone/nest-config/blob/main/docs/mutation_testing_results.md"><img src="https://img.shields.io/badge/mutation-99.
|
|
17
|
+
<a href="https://github.com/bymaxone/nest-config/blob/main/docs/mutation_testing_results.md"><img src="https://img.shields.io/badge/mutation-99.71%25-brightgreen?style=flat-square&colorA=000000" alt="mutation score" /></a>
|
|
18
18
|
<a href="https://scorecard.dev/viewer/?uri=github.com/bymaxone/nest-config"><img src="https://api.scorecard.dev/projects/github.com/bymaxone/nest-config/badge?style=flat-square" alt="OpenSSF Scorecard" /></a>
|
|
19
19
|
<a href="https://github.com/bymaxone/nest-config/blob/main/LICENSE"><img src="https://img.shields.io/github/license/bymaxone/nest-config?style=flat-square&colorA=000000&colorB=000000" alt="license" /></a>
|
|
20
20
|
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-strict-3178C6?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript" /></a>
|
|
@@ -638,8 +638,9 @@ it('documents every variable the schema reads in .env.example', () => {
|
|
|
638
638
|
the schema reads it.
|
|
639
639
|
- **Two paths can read one variable.** Names are derived from the keys, so `a.bC` and
|
|
640
640
|
`aB.c` both derive `A_B_C`, and `httpServer.port` and `HTTPServer.port` both derive
|
|
641
|
-
`HTTP_SERVER_PORT`. Every binding is returned and none is dropped
|
|
642
|
-
|
|
641
|
+
`HTTP_SERVER_PORT`. Every binding is returned and none is dropped, so a tool can see the
|
|
642
|
+
collision; validation rejects such a schema (see the error catalog). Giving two leaves
|
|
643
|
+
the same `meta({ env })` is the same collision, and there is no opt-out.
|
|
643
644
|
- **An open namespace changes nothing.** `meta({ open: true })` only waives strict
|
|
644
645
|
unknown-key detection, so the list is the same whether a namespace is open or closed. It
|
|
645
646
|
does not help to find variables in `.env.example` that no leaf reads.
|
|
@@ -648,12 +649,12 @@ it('documents every variable the schema reads in .env.example', () => {
|
|
|
648
649
|
|
|
649
650
|
## 🚨 Error Catalog
|
|
650
651
|
|
|
651
|
-
| Code | Meaning
|
|
652
|
-
| -------------------------- |
|
|
653
|
-
| `BYMAX_CONFIG_VALIDATION` | One or more schema violations (top-level error code).
|
|
654
|
-
| `BYMAX_CONFIG_MISSING` | Required variable absent from the source (issue code).
|
|
655
|
-
| `BYMAX_CONFIG_INVALID` |
|
|
656
|
-
| `BYMAX_CONFIG_UNKNOWN_KEY` | Strict mode: source variable matches no declared leaf.
|
|
652
|
+
| Code | Meaning |
|
|
653
|
+
| -------------------------- | --------------------------------------------------------- |
|
|
654
|
+
| `BYMAX_CONFIG_VALIDATION` | One or more schema violations (top-level error code). |
|
|
655
|
+
| `BYMAX_CONFIG_MISSING` | Required variable absent from the source (issue code). |
|
|
656
|
+
| `BYMAX_CONFIG_INVALID` | Failed its constraint, or shares a variable (issue code). |
|
|
657
|
+
| `BYMAX_CONFIG_UNKNOWN_KEY` | Strict mode: source variable matches no declared leaf. |
|
|
657
658
|
|
|
658
659
|
## 🏗️ Architecture
|
|
659
660
|
|
|
@@ -815,7 +816,7 @@ Configuration decides whether a process may start at all, so the suite is held t
|
|
|
815
816
|
beyond "the tests pass".
|
|
816
817
|
|
|
817
818
|
- ✅ **100% line coverage** — statements, branches, functions and lines, enforced as a gate
|
|
818
|
-
- ✅ **99.
|
|
819
|
+
- ✅ **99.71% mutation score** — verified with [Stryker](https://stryker-mutator.io/) at
|
|
819
820
|
`break: 95`; ten provable equivalents carry their reason inline, and the one survivor left
|
|
820
821
|
is an equivalent no directive can reach, so it stays counted rather than silenced
|
|
821
822
|
([report](./docs/mutation_testing_results.md))
|
package/dist/internal/index.cjs
CHANGED
|
@@ -282,8 +282,29 @@ function detectUnknownKeys(prefixes, bindings, source) {
|
|
|
282
282
|
(left, right) => Number(left.variable > right.variable) - Number(left.variable < right.variable)
|
|
283
283
|
);
|
|
284
284
|
}
|
|
285
|
+
function assertDistinctVariables(bindings) {
|
|
286
|
+
const byVariable = /* @__PURE__ */ new Map();
|
|
287
|
+
for (const { path, variable } of bindings) {
|
|
288
|
+
byVariable.set(variable, [...byVariable.get(variable) ?? [], path]);
|
|
289
|
+
}
|
|
290
|
+
const issues = [];
|
|
291
|
+
for (const [variable, paths] of byVariable) {
|
|
292
|
+
if (paths.length < 2) continue;
|
|
293
|
+
for (const path of paths) {
|
|
294
|
+
const others = paths.filter((other) => other !== path).join(", ");
|
|
295
|
+
issues.push({
|
|
296
|
+
path,
|
|
297
|
+
variable,
|
|
298
|
+
code: ConfigErrorCode.INVALID,
|
|
299
|
+
message: `shares this variable with ${others}; rename one with meta({ env })`
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (issues.length > 0) throw new BymaxConfigValidationError(issues);
|
|
304
|
+
}
|
|
285
305
|
function validateEnv(schema, source, options) {
|
|
286
306
|
const bindings = resolveSourceNames(schema);
|
|
307
|
+
assertDistinctVariables(bindings);
|
|
287
308
|
const sourceMap = new Map(Object.entries(source));
|
|
288
309
|
const namespaces = Object.keys(schema.shape);
|
|
289
310
|
const candidate = buildCandidate(namespaces, collectPresentLeaves(bindings, sourceMap));
|
|
@@ -516,6 +537,7 @@ exports.BYMAX_CONFIG = BYMAX_CONFIG;
|
|
|
516
537
|
exports.BYMAX_CONFIG_OPTIONS = BYMAX_CONFIG_OPTIONS;
|
|
517
538
|
exports.BymaxConfigValidationError = BymaxConfigValidationError;
|
|
518
539
|
exports.ConfigErrorCode = ConfigErrorCode;
|
|
540
|
+
exports.assertDistinctVariables = assertDistinctVariables;
|
|
519
541
|
exports.createValidatedConfig = createValidatedConfig;
|
|
520
542
|
exports.defineEnv = defineEnv;
|
|
521
543
|
exports.resolveSourceNames = resolveSourceNames;
|
|
@@ -604,7 +604,7 @@ interface SourceBinding {
|
|
|
604
604
|
* never a source record. Declared is not required: a leaf with a default or an
|
|
605
605
|
* optional leaf is listed like any other. Two distinct paths can resolve to one
|
|
606
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
|
|
607
|
+
* and none is dropped, though validation rejects a schema that holds such a pair. An open namespace changes nothing here, since opening
|
|
608
608
|
* only waives strict unknown-key detection.
|
|
609
609
|
*
|
|
610
610
|
* @typeParam TShape - The two-level shape the schema was composed from.
|
|
@@ -619,4 +619,30 @@ interface SourceBinding {
|
|
|
619
619
|
*/
|
|
620
620
|
declare function resolveSourceNames<TShape extends EnvShape = EnvShape>(schema: EnvSchema<TShape>): readonly SourceBinding[];
|
|
621
621
|
|
|
622
|
-
|
|
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
|
@@ -604,7 +604,7 @@ interface SourceBinding {
|
|
|
604
604
|
* never a source record. Declared is not required: a leaf with a default or an
|
|
605
605
|
* optional leaf is listed like any other. Two distinct paths can resolve to one
|
|
606
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
|
|
607
|
+
* and none is dropped, though validation rejects a schema that holds such a pair. An open namespace changes nothing here, since opening
|
|
608
608
|
* only waives strict unknown-key detection.
|
|
609
609
|
*
|
|
610
610
|
* @typeParam TShape - The two-level shape the schema was composed from.
|
|
@@ -619,4 +619,30 @@ interface SourceBinding {
|
|
|
619
619
|
*/
|
|
620
620
|
declare function resolveSourceNames<TShape extends EnvShape = EnvShape>(schema: EnvSchema<TShape>): readonly SourceBinding[];
|
|
621
621
|
|
|
622
|
-
|
|
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
|
@@ -280,8 +280,29 @@ function detectUnknownKeys(prefixes, bindings, source) {
|
|
|
280
280
|
(left, right) => Number(left.variable > right.variable) - Number(left.variable < right.variable)
|
|
281
281
|
);
|
|
282
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
|
+
}
|
|
283
303
|
function validateEnv(schema, source, options) {
|
|
284
304
|
const bindings = resolveSourceNames(schema);
|
|
305
|
+
assertDistinctVariables(bindings);
|
|
285
306
|
const sourceMap = new Map(Object.entries(source));
|
|
286
307
|
const namespaces = Object.keys(schema.shape);
|
|
287
308
|
const candidate = buildCandidate(namespaces, collectPresentLeaves(bindings, sourceMap));
|
|
@@ -510,4 +531,4 @@ function defineEnv(shape) {
|
|
|
510
531
|
return z.object(shape);
|
|
511
532
|
}
|
|
512
533
|
|
|
513
|
-
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
|
@@ -200,6 +200,7 @@ function explainUnsatisfiedLeaves(error, overridden) {
|
|
|
200
200
|
);
|
|
201
201
|
}
|
|
202
202
|
function prepareTestConfig(schema, overrides) {
|
|
203
|
+
internal.assertDistinctVariables(internal.resolveSourceNames(schema));
|
|
203
204
|
const overridden = /* @__PURE__ */ new Set();
|
|
204
205
|
const source = applyOverrides(schema, synthesizePlaceholderSource(schema), overrides, overridden);
|
|
205
206
|
try {
|
package/dist/testing/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BymaxConfigModule, createValidatedConfig, BymaxConfigValidationError
|
|
1
|
+
import { BymaxConfigModule, assertDistinctVariables, resolveSourceNames, createValidatedConfig, BymaxConfigValidationError } from '@bymax-one/nest-config/internal';
|
|
2
2
|
|
|
3
3
|
// src/testing/create-test-config.ts
|
|
4
4
|
var FILLER_CHARACTER = "a";
|
|
@@ -198,6 +198,7 @@ function explainUnsatisfiedLeaves(error, overridden) {
|
|
|
198
198
|
);
|
|
199
199
|
}
|
|
200
200
|
function prepareTestConfig(schema, overrides) {
|
|
201
|
+
assertDistinctVariables(resolveSourceNames(schema));
|
|
201
202
|
const overridden = /* @__PURE__ */ new Set();
|
|
202
203
|
const source = applyOverrides(schema, synthesizePlaceholderSource(schema), overrides, overridden);
|
|
203
204
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bymax-one/nest-config",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "Typed and validated environment configuration entry point for NestJS 11 applications, built on Zod v4.",
|
|
5
5
|
"author": "Bymax One <support@bymax.one>",
|
|
6
6
|
"license": "MIT",
|