@bymax-one/nest-config 1.0.0

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.
@@ -0,0 +1,88 @@
1
+ import { EnvShape, EnvOutput, EnvSchema } from '@bymax-one/nest-config/internal';
2
+ import { DynamicModule } from '@nestjs/common';
3
+
4
+ /**
5
+ * @fileoverview createTestConfig: the public test-config builder of the testing
6
+ * subpath. It synthesizes a complete valid source from a `defineEnv` schema,
7
+ * overlays selective nested partial overrides onto the targeted leaves through
8
+ * the shared source mapping, then runs the exact production pipeline (validate,
9
+ * deep-freeze) so a test exercises the same code path a running app does. An
10
+ * override that violates a declared constraint fails through the production
11
+ * validation error, and the returned configuration is deeply immutable. No
12
+ * `process.env` is ever read.
13
+ * @layer Utility
14
+ */
15
+
16
+ /**
17
+ * Selective nested partial overrides for a synthesized test configuration.
18
+ *
19
+ * Mirrors the two-level `namespace.leaf` shape of the inferred configuration,
20
+ * with every namespace and leaf optional, so a test pins only the values it
21
+ * cares about and lets synthesis and schema defaults resolve the rest.
22
+ *
23
+ * @typeParam TShape - The two-level schema shape.
24
+ */
25
+ type ConfigOverrides<TShape extends EnvShape> = {
26
+ readonly [Namespace in keyof EnvOutput<TShape>]?: {
27
+ readonly [Leaf in keyof EnvOutput<TShape>[Namespace]]?: EnvOutput<TShape>[Namespace][Leaf];
28
+ };
29
+ };
30
+ /**
31
+ * Build a validated, frozen test configuration from a schema and overrides.
32
+ *
33
+ * Synthesizes a complete constraint-compliant source, applies the selective
34
+ * overrides, and runs the production validation and deep-freeze pipeline, so the
35
+ * returned object is exactly what a running application would receive. A test
36
+ * never touches `process.env`, yet an override that breaks a declared constraint
37
+ * still throws the production {@link BymaxConfigValidationError}.
38
+ *
39
+ * @typeParam TShape - The two-level schema shape.
40
+ * @param schema - A schema produced by `defineEnv`.
41
+ * @param overrides - Optional selective nested partial overrides.
42
+ * @returns The validated, deep-frozen configuration typed by the schema.
43
+ * @throws {BymaxConfigValidationError} When an override violates a constraint.
44
+ * @example
45
+ * ```typescript
46
+ * const config = createTestConfig(envSchema, {
47
+ * database: { url: 'postgres://localhost:5432/test' },
48
+ * });
49
+ * config.database.url; // typed, validated, frozen
50
+ * ```
51
+ */
52
+ declare function createTestConfig<TShape extends EnvShape>(schema: EnvSchema<TShape>, overrides?: ConfigOverrides<TShape>): Readonly<EnvOutput<TShape>>;
53
+
54
+ /**
55
+ * @fileoverview configTestingModule: a thin testing wrapper that registers the
56
+ * production BymaxConfigModule with a fully synthesized (and optionally
57
+ * overridden) source. It delegates to `BymaxConfigModule.forRoot`, so a Nest
58
+ * `TestingModule` graph exercises the exact production registration path, the
59
+ * same validation, freezing, and provider wiring an application boots with,
60
+ * without touching `process.env`. This is the module-shaped counterpart to
61
+ * `createTestConfig`.
62
+ * @layer Module
63
+ */
64
+
65
+ /**
66
+ * Build an importable testing module preconfigured with a synthesized source.
67
+ *
68
+ * Synthesizes a complete valid source for the schema, applies the selective
69
+ * overrides, and registers the production {@link BymaxConfigModule} with it, so
70
+ * the returned module provides and exports the frozen `BYMAX_CONFIG` and the
71
+ * typed `ConfigService` exactly as in production. Import it into a Nest
72
+ * `TestingModule` to inject configuration without a process environment.
73
+ *
74
+ * @typeParam TShape - The two-level schema shape.
75
+ * @param schema - A schema produced by `defineEnv`.
76
+ * @param overrides - Optional selective nested partial overrides.
77
+ * @returns A dynamic module registered through the production `forRoot` path.
78
+ * @example
79
+ * ```typescript
80
+ * const moduleRef = await Test.createTestingModule({
81
+ * imports: [configTestingModule(envSchema, { server: { port: 0 } })],
82
+ * providers: [InvoiceService],
83
+ * }).compile();
84
+ * ```
85
+ */
86
+ declare function configTestingModule<TShape extends EnvShape>(schema: EnvSchema<TShape>, overrides?: ConfigOverrides<TShape>): DynamicModule;
87
+
88
+ export { configTestingModule, createTestConfig };
@@ -0,0 +1,168 @@
1
+ import { createValidatedConfig, BymaxConfigModule, resolveSourceNames } from '@bymax-one/nest-config/internal';
2
+
3
+ // src/testing/create-test-config.ts
4
+ var FILLER_CHARACTER = "a";
5
+ var PLACEHOLDER_URL = "https://placeholder.local";
6
+ var PLACEHOLDER_EMAIL_DOMAIN = "@placeholder.local";
7
+ var DEFAULT_NUMBER = 1;
8
+ var DEFAULT_STRING_LENGTH = 1;
9
+ var PLACEHOLDER_BOOLEAN = "true";
10
+ function readDef(leaf) {
11
+ return leaf._zod.def;
12
+ }
13
+ function checksOf(def) {
14
+ return def.checks ?? [];
15
+ }
16
+ function readStringLengths(def) {
17
+ let min = 0;
18
+ let max = Number.POSITIVE_INFINITY;
19
+ let exact;
20
+ for (const wrapper of checksOf(def)) {
21
+ const check = wrapper._zod.def;
22
+ if (check.check === "min_length") min = check.minimum;
23
+ if (check.check === "max_length") max = check.maximum;
24
+ if (check.check === "length_equals") exact = check.length;
25
+ }
26
+ return exact === void 0 ? { min, max } : { min, max, exact };
27
+ }
28
+ var URL_PREFIX = "http://";
29
+ var EMAIL_SUFFIX = "@a.lo";
30
+ function clampLength(preferred, min, max) {
31
+ return Math.min(Math.max(preferred, min), max);
32
+ }
33
+ function urlOfLength(target) {
34
+ const hostFill = Math.max(target - URL_PREFIX.length, 1);
35
+ return `${URL_PREFIX}${FILLER_CHARACTER.repeat(hostFill)}`;
36
+ }
37
+ function emailOfLength(target) {
38
+ const localFill = Math.max(target - EMAIL_SUFFIX.length, 1);
39
+ return `${FILLER_CHARACTER.repeat(localFill)}${EMAIL_SUFFIX}`;
40
+ }
41
+ function synthesizeUrl(lengths) {
42
+ if (lengths.exact === void 0 && PLACEHOLDER_URL.length >= lengths.min && PLACEHOLDER_URL.length <= lengths.max) {
43
+ return PLACEHOLDER_URL;
44
+ }
45
+ return urlOfLength(lengths.exact ?? clampLength(PLACEHOLDER_URL.length, lengths.min, lengths.max));
46
+ }
47
+ function synthesizeEmail(lengths) {
48
+ const canonical = `${FILLER_CHARACTER}${PLACEHOLDER_EMAIL_DOMAIN}`;
49
+ if (lengths.exact === void 0 && canonical.length >= lengths.min && canonical.length <= lengths.max) {
50
+ return canonical;
51
+ }
52
+ return emailOfLength(lengths.exact ?? clampLength(canonical.length, lengths.min, lengths.max));
53
+ }
54
+ function synthesizePlainString(lengths) {
55
+ if (lengths.exact !== void 0) return FILLER_CHARACTER.repeat(lengths.exact);
56
+ const target = Math.max(lengths.min, DEFAULT_STRING_LENGTH);
57
+ const bounded = target > lengths.max ? lengths.max : target;
58
+ return FILLER_CHARACTER.repeat(bounded);
59
+ }
60
+ function synthesizeString(def) {
61
+ const lengths = readStringLengths(def);
62
+ if (def.format === "url") return synthesizeUrl(lengths);
63
+ if (def.format === "email") return synthesizeEmail(lengths);
64
+ return synthesizePlainString(lengths);
65
+ }
66
+ function selectNumber(bounds) {
67
+ const { lower, upper, integer } = bounds;
68
+ if (lower !== void 0 && upper !== void 0) {
69
+ if (integer) {
70
+ const lo = bounds.lowerExclusive ? Math.floor(lower) + 1 : Math.ceil(lower);
71
+ const hi = bounds.upperExclusive ? Math.ceil(upper) - 1 : Math.floor(upper);
72
+ return Math.floor((lo + hi) / 2);
73
+ }
74
+ return (lower + upper) / 2;
75
+ }
76
+ if (lower !== void 0) return bounds.lowerExclusive ? lower + (integer ? 1 : 0.5) : lower;
77
+ if (upper !== void 0) return bounds.upperExclusive ? upper - (integer ? 1 : 0.5) : upper;
78
+ return DEFAULT_NUMBER;
79
+ }
80
+ function synthesizeNumber(def) {
81
+ const bounds = { lowerExclusive: false, upperExclusive: false, integer: false };
82
+ for (const wrapper of checksOf(def)) {
83
+ const check = wrapper._zod.def;
84
+ if (check.check === "greater_than") {
85
+ bounds.lower = check.value;
86
+ bounds.lowerExclusive = check.inclusive !== true;
87
+ }
88
+ if (check.check === "less_than") {
89
+ bounds.upper = check.value;
90
+ bounds.upperExclusive = check.inclusive !== true;
91
+ }
92
+ if (check.check === "number_format") bounds.integer = true;
93
+ }
94
+ return String(selectNumber(bounds));
95
+ }
96
+ function synthesizeEnum(def) {
97
+ const [first] = Object.values(def.entries);
98
+ return String(first);
99
+ }
100
+ function synthesizeLeaf(leaf) {
101
+ const def = readDef(leaf);
102
+ switch (def.type) {
103
+ case "default":
104
+ case "optional":
105
+ return void 0;
106
+ case "nullable":
107
+ return synthesizeLeaf(def.innerType);
108
+ case "string":
109
+ return synthesizeString(def);
110
+ case "number":
111
+ return synthesizeNumber(def);
112
+ case "boolean":
113
+ return PLACEHOLDER_BOOLEAN;
114
+ case "enum":
115
+ return synthesizeEnum(def);
116
+ default:
117
+ return FILLER_CHARACTER;
118
+ }
119
+ }
120
+ function synthesizePlaceholderSource(schema) {
121
+ const valueByPath = /* @__PURE__ */ new Map();
122
+ for (const [namespace, namespaceSchema] of Object.entries(schema.shape)) {
123
+ for (const [leafKey, leafSchema] of Object.entries(namespaceSchema.shape)) {
124
+ const value = synthesizeLeaf(leafSchema);
125
+ if (value !== void 0) valueByPath.set(`${namespace}.${leafKey}`, value);
126
+ }
127
+ }
128
+ const source = /* @__PURE__ */ Object.create(null);
129
+ for (const binding of resolveSourceNames(schema)) {
130
+ const value = valueByPath.get(binding.path);
131
+ if (value !== void 0) source[binding.variable] = value;
132
+ }
133
+ return source;
134
+ }
135
+
136
+ // src/testing/create-test-config.ts
137
+ function flattenOverrides(overrides) {
138
+ const flat = /* @__PURE__ */ new Map();
139
+ for (const [namespace, leaves] of Object.entries(overrides)) {
140
+ for (const [leaf, value] of Object.entries(leaves)) {
141
+ flat.set(`${namespace}.${leaf}`, value);
142
+ }
143
+ }
144
+ return flat;
145
+ }
146
+ function applyOverrides(schema, base, overrides) {
147
+ const flat = flattenOverrides(overrides ?? {});
148
+ const entries = new Map(Object.entries(base));
149
+ for (const binding of resolveSourceNames(schema)) {
150
+ const value = flat.get(binding.path);
151
+ if (value !== void 0) entries.set(binding.variable, String(value));
152
+ }
153
+ const source = /* @__PURE__ */ Object.create(null);
154
+ for (const [variable, value] of entries) source[variable] = value;
155
+ return source;
156
+ }
157
+ function buildTestSource(schema, overrides) {
158
+ return applyOverrides(schema, synthesizePlaceholderSource(schema), overrides);
159
+ }
160
+ function createTestConfig(schema, overrides) {
161
+ return createValidatedConfig({ schema, source: buildTestSource(schema, overrides) });
162
+ }
163
+ function configTestingModule(schema, overrides) {
164
+ const source = buildTestSource(schema, overrides);
165
+ return BymaxConfigModule.forRoot({ schema, source });
166
+ }
167
+
168
+ export { configTestingModule, createTestConfig };
package/package.json ADDED
@@ -0,0 +1,161 @@
1
+ {
2
+ "name": "@bymax-one/nest-config",
3
+ "version": "1.0.0",
4
+ "description": "Typed and validated environment configuration entry point for NestJS 11 applications, built on Zod v4.",
5
+ "author": "Bymax One <support@bymax.one>",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/bymaxone/nest-config#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/bymaxone/nest-config.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/bymaxone/nest-config/issues"
14
+ },
15
+ "type": "module",
16
+ "sideEffects": false,
17
+ "files": [
18
+ "dist",
19
+ "LICENSE",
20
+ "README.md",
21
+ "CHANGELOG.md"
22
+ ],
23
+ "exports": {
24
+ ".": {
25
+ "import": {
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.mjs"
28
+ },
29
+ "require": {
30
+ "types": "./dist/index.d.cts",
31
+ "default": "./dist/index.cjs"
32
+ }
33
+ },
34
+ "./testing": {
35
+ "import": {
36
+ "types": "./dist/testing/index.d.ts",
37
+ "default": "./dist/testing/index.mjs"
38
+ },
39
+ "require": {
40
+ "types": "./dist/testing/index.d.cts",
41
+ "default": "./dist/testing/index.cjs"
42
+ }
43
+ },
44
+ "./internal": {
45
+ "import": {
46
+ "types": "./dist/internal/index.d.ts",
47
+ "default": "./dist/internal/index.mjs"
48
+ },
49
+ "require": {
50
+ "types": "./dist/internal/index.d.cts",
51
+ "default": "./dist/internal/index.cjs"
52
+ }
53
+ },
54
+ "./package.json": "./package.json"
55
+ },
56
+ "lint-staged": {
57
+ "*.{ts,tsx,js,mjs,cjs}": [
58
+ "eslint --fix",
59
+ "prettier --write"
60
+ ],
61
+ "*.{json,md,yml,yaml}": [
62
+ "prettier --write"
63
+ ]
64
+ },
65
+ "dependencies": {},
66
+ "peerDependencies": {
67
+ "@nestjs/common": "^11.0.16",
68
+ "@nestjs/core": "^11.1.18",
69
+ "reflect-metadata": "^0.2.0",
70
+ "zod": "^4.0.0"
71
+ },
72
+ "devDependencies": {
73
+ "@arethetypeswrong/cli": "^0.18.2",
74
+ "@commitlint/cli": "^21.0.2",
75
+ "@commitlint/config-conventional": "^21.0.2",
76
+ "@eslint/js": "^9.39.4",
77
+ "@nestjs/common": "^11.1.20",
78
+ "@nestjs/core": "^11.1.20",
79
+ "@nestjs/platform-express": "^11.1.20",
80
+ "@nestjs/testing": "^11.1.20",
81
+ "@stryker-mutator/core": "^9",
82
+ "@stryker-mutator/jest-runner": "^9",
83
+ "@stryker-mutator/typescript-checker": "^9",
84
+ "@types/express": "^5.0.6",
85
+ "@types/jest": "^30.0.0",
86
+ "@types/node": "^25",
87
+ "@typescript-eslint/eslint-plugin": "^8.59.3",
88
+ "@typescript-eslint/parser": "^8.59.3",
89
+ "eslint": "^9.39.4",
90
+ "eslint-config-prettier": "^10.1.8",
91
+ "eslint-import-resolver-typescript": "^4.4.4",
92
+ "eslint-plugin-import": "^2.32.0",
93
+ "eslint-plugin-prettier": "^5.5.5",
94
+ "eslint-plugin-security": "^4.0.0",
95
+ "globals": "^17.6.0",
96
+ "husky": "^9.1.7",
97
+ "jest": "^30.4.2",
98
+ "lint-staged": "^17.0.7",
99
+ "prettier": "^3.8.3",
100
+ "reflect-metadata": "^0.2.2",
101
+ "ts-jest": "^29.4.9",
102
+ "ts-node": "^10.9.2",
103
+ "tsup": "^8.5.1",
104
+ "typescript": "^5.9.3",
105
+ "zod": "^4.0.0"
106
+ },
107
+ "keywords": [
108
+ "nestjs",
109
+ "config",
110
+ "environment",
111
+ "env",
112
+ "zod",
113
+ "validation",
114
+ "typed-config",
115
+ "dependency-injection",
116
+ "fail-fast",
117
+ "configuration"
118
+ ],
119
+ "engines": {
120
+ "node": ">=24.0.0"
121
+ },
122
+ "publishConfig": {
123
+ "access": "public",
124
+ "provenance": true,
125
+ "registry": "https://registry.npmjs.org/"
126
+ },
127
+ "main": "./dist/index.cjs",
128
+ "module": "./dist/index.mjs",
129
+ "types": "./dist/index.d.cts",
130
+ "typesVersions": {
131
+ "*": {
132
+ "testing": [
133
+ "./dist/testing/index.d.cts"
134
+ ],
135
+ "internal": [
136
+ "./dist/internal/index.d.cts"
137
+ ]
138
+ }
139
+ },
140
+ "scripts": {
141
+ "build": "pnpm clean && tsup",
142
+ "check:exports": "attw --pack . --profile strict",
143
+ "check:published": "node scripts/check-published-surface.mjs",
144
+ "check:runtime": "node scripts/check-consumer-runtime.mjs",
145
+ "clean": "node -e \"const fs=require('node:fs');for(const d of ['dist','coverage'])fs.rmSync(d,{recursive:true,force:true})\"",
146
+ "dogfood": "node scripts/dogfood-smoke-test.mjs",
147
+ "lint": "eslint src",
148
+ "lint:fix": "eslint src --fix",
149
+ "mutation": "stryker run",
150
+ "mutation:dry-run": "stryker run --dryRunOnly",
151
+ "mutation:incremental": "stryker run --incremental",
152
+ "release": "npm publish --provenance --access public",
153
+ "size": "node scripts/check-size.mjs",
154
+ "test": "jest",
155
+ "test:cov": "jest --coverage",
156
+ "test:cov:all": "jest --config jest.coverage.config.ts --coverage",
157
+ "test:e2e": "jest --config jest.e2e.config.ts",
158
+ "test:watch": "jest --watch",
159
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.build.json"
160
+ }
161
+ }