@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,415 @@
1
+ import { ConfigurableModuleBuilder, Injectable, Inject, Module } from '@nestjs/common';
2
+ import { z } from 'zod';
3
+
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __decorateClass = (decorators, target, key, kind) => {
6
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
7
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
8
+ if (decorator = decorators[i])
9
+ result = (decorator(result)) || result;
10
+ return result;
11
+ };
12
+ var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
13
+
14
+ // src/config.tokens.ts
15
+ var BYMAX_CONFIG_OPTIONS = /* @__PURE__ */ Symbol("BYMAX_CONFIG_OPTIONS");
16
+ var BYMAX_CONFIG = /* @__PURE__ */ Symbol("BYMAX_CONFIG");
17
+
18
+ // src/config.module-definition.ts
19
+ var { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = new ConfigurableModuleBuilder({
20
+ optionsInjectionToken: BYMAX_CONFIG_OPTIONS
21
+ }).setClassMethodName("forRoot").setExtras({ isGlobal: true }, (definition, extras) => ({
22
+ ...definition,
23
+ global: extras.isGlobal
24
+ })).build();
25
+
26
+ // src/deep-freeze.ts
27
+ function isFreezable(value) {
28
+ return typeof value === "object" && value !== null;
29
+ }
30
+ function deepFreeze(value) {
31
+ if (!isFreezable(value) || Object.isFrozen(value)) {
32
+ return value;
33
+ }
34
+ Object.freeze(value);
35
+ for (const nested of Object.values(value)) {
36
+ deepFreeze(nested);
37
+ }
38
+ return value;
39
+ }
40
+
41
+ // src/report-formatter.ts
42
+ var REPORT_INDENT = " ";
43
+ var MIN_VARIABLE_COLUMN = 20;
44
+ var VARIABLE_GUTTER = 2;
45
+ var REPORT_HEADER = "environment validation failed";
46
+ var REPORT_FOOTER = "Fix the variables above and restart the process.";
47
+ function pluralizeIssues(count) {
48
+ const noun = count === 1 ? "issue" : "issues";
49
+ return `${count} ${noun}`;
50
+ }
51
+ function computeColumnWidth(issues) {
52
+ let longest = MIN_VARIABLE_COLUMN;
53
+ for (const issue of issues) {
54
+ longest = Math.max(longest, issue.variable.length);
55
+ }
56
+ return longest + VARIABLE_GUTTER;
57
+ }
58
+ function formatIssueLine(issue, columnWidth) {
59
+ return `${REPORT_INDENT}${issue.variable.padEnd(columnWidth)}${issue.message}`;
60
+ }
61
+ function formatIssueReport(issues) {
62
+ const columnWidth = computeColumnWidth(issues);
63
+ const lines = issues.map((issue) => formatIssueLine(issue, columnWidth));
64
+ const header = `${REPORT_HEADER} (${pluralizeIssues(issues.length)})`;
65
+ return [header, "", ...lines, "", REPORT_FOOTER].join("\n");
66
+ }
67
+
68
+ // src/errors.ts
69
+ var ConfigErrorCode = Object.freeze({
70
+ /** Top-level error code: one or more schema violations were collected. */
71
+ VALIDATION: "BYMAX_CONFIG_VALIDATION",
72
+ /** Issue code: a required variable is absent from the source. */
73
+ MISSING: "BYMAX_CONFIG_MISSING",
74
+ /** Issue code: a variable is present but violates its constraint. */
75
+ INVALID: "BYMAX_CONFIG_INVALID",
76
+ /** Issue code: strict mode found a source variable matching no declared leaf. */
77
+ UNKNOWN_KEY: "BYMAX_CONFIG_UNKNOWN_KEY"
78
+ });
79
+ var BymaxConfigValidationError = class _BymaxConfigValidationError extends Error {
80
+ /**
81
+ * Build the aggregated error from a list of value-free issues.
82
+ *
83
+ * @param issues - Every collected violation; copied and frozen so neither the
84
+ * caller nor consumer code can mutate the reported list.
85
+ */
86
+ constructor(issues) {
87
+ const normalized = issues.map(
88
+ (issue) => Object.freeze({
89
+ path: issue.path,
90
+ variable: issue.variable,
91
+ code: issue.code,
92
+ message: issue.message
93
+ })
94
+ );
95
+ super(formatIssueReport(normalized));
96
+ /** The stable top-level error code. */
97
+ this.code = ConfigErrorCode.VALIDATION;
98
+ Object.defineProperty(this, "name", {
99
+ value: "BymaxConfigValidationError",
100
+ enumerable: false,
101
+ writable: false,
102
+ configurable: false
103
+ });
104
+ this.issues = Object.freeze(normalized);
105
+ Object.setPrototypeOf(this, _BymaxConfigValidationError.prototype);
106
+ Object.defineProperty(this, "code", { writable: false, configurable: false });
107
+ Object.defineProperty(this, "issues", { writable: false, configurable: false });
108
+ }
109
+ };
110
+
111
+ // src/source-mapping.ts
112
+ function toScreamingSnake(segment) {
113
+ return segment.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").toUpperCase();
114
+ }
115
+ function readEnvOverride(leaf) {
116
+ const override = leaf.meta()?.env;
117
+ return typeof override === "string" && override.length > 0 ? override : void 0;
118
+ }
119
+ function deriveVariable(namespace, leafKey) {
120
+ return `${toScreamingSnake(namespace)}_${toScreamingSnake(leafKey)}`;
121
+ }
122
+ function resolveSourceNames(schema) {
123
+ return Object.entries(schema.shape).flatMap(
124
+ ([namespace, namespaceSchema]) => Object.entries(namespaceSchema.shape).map(([leafKey, leafSchema]) => ({
125
+ path: `${namespace}.${leafKey}`,
126
+ variable: readEnvOverride(leafSchema) ?? deriveVariable(namespace, leafKey)
127
+ }))
128
+ );
129
+ }
130
+ function resolveNamespacePrefixes(schema) {
131
+ return Object.keys(schema.shape).map((namespace) => ({
132
+ namespace,
133
+ prefix: `${toScreamingSnake(namespace)}_`
134
+ }));
135
+ }
136
+
137
+ // src/env-validator.ts
138
+ var UNKNOWN_KEY_MESSAGE = "unknown variable not declared in the schema";
139
+ var MISSING_MESSAGE = "missing required value";
140
+ function collectPresentLeaves(bindings, source) {
141
+ const present = [];
142
+ for (const binding of bindings) {
143
+ const value = source.get(binding.variable);
144
+ if (value === void 0) continue;
145
+ const separator = binding.path.indexOf(".");
146
+ present.push({
147
+ namespace: binding.path.slice(0, separator),
148
+ leaf: binding.path.slice(separator + 1),
149
+ value
150
+ });
151
+ }
152
+ return present;
153
+ }
154
+ function buildCandidate(namespaces, present) {
155
+ return Object.fromEntries(
156
+ namespaces.map((namespace) => [
157
+ namespace,
158
+ Object.fromEntries(
159
+ present.filter((leaf) => leaf.namespace === namespace).map((leaf) => [leaf.leaf, leaf.value])
160
+ )
161
+ ])
162
+ );
163
+ }
164
+ function formatEnumValues(values) {
165
+ return values.map(String).join(", ");
166
+ }
167
+ function describeConstraint(issue) {
168
+ switch (issue.code) {
169
+ case "invalid_type":
170
+ return `invalid value (expected: ${issue.expected})`;
171
+ case "invalid_format":
172
+ return `invalid format (expected: ${issue.format})`;
173
+ case "invalid_value":
174
+ return `invalid value (expected one of: ${formatEnumValues(issue.values)})`;
175
+ case "too_small":
176
+ return issue.origin === "string" ? `too short (expected: string, minimum ${issue.minimum} characters)` : `out of range (expected: ${issue.origin} >= ${issue.minimum})`;
177
+ case "too_big":
178
+ return issue.origin === "string" ? `too long (expected: string, maximum ${issue.maximum} characters)` : `out of range (expected: ${issue.origin} <= ${issue.maximum})`;
179
+ default:
180
+ return "invalid value";
181
+ }
182
+ }
183
+ function toConfigIssue(binding, issue, source) {
184
+ const isMissing = source.get(binding.variable) === void 0;
185
+ return {
186
+ path: binding.path,
187
+ variable: binding.variable,
188
+ code: isMissing ? ConfigErrorCode.MISSING : ConfigErrorCode.INVALID,
189
+ message: isMissing ? MISSING_MESSAGE : describeConstraint(issue)
190
+ };
191
+ }
192
+ function translateIssues(zodIssues, bindings, source) {
193
+ const issueByPath = /* @__PURE__ */ new Map();
194
+ for (const issue of zodIssues) {
195
+ const path = issue.path.slice(0, 2).join(".");
196
+ if (!issueByPath.has(path)) {
197
+ issueByPath.set(path, issue);
198
+ }
199
+ }
200
+ const result = [];
201
+ for (const binding of bindings) {
202
+ const issue = issueByPath.get(binding.path);
203
+ if (issue === void 0) continue;
204
+ result.push(toConfigIssue(binding, issue, source));
205
+ }
206
+ return result;
207
+ }
208
+ function longestMatchingPrefix(prefixes, key) {
209
+ let match;
210
+ for (const entry of prefixes) {
211
+ if (!key.startsWith(entry.prefix)) continue;
212
+ if (match === void 0 || entry.prefix.length > match.prefix.length) {
213
+ match = entry;
214
+ }
215
+ }
216
+ return match;
217
+ }
218
+ function detectUnknownKeys(prefixes, bindings, source) {
219
+ const declared = new Set(bindings.map((binding) => binding.variable));
220
+ const issues = [];
221
+ for (const key of source.keys()) {
222
+ if (declared.has(key)) continue;
223
+ if (source.get(key) === void 0) continue;
224
+ const match = longestMatchingPrefix(prefixes, key);
225
+ if (match === void 0) continue;
226
+ issues.push({
227
+ path: match.namespace,
228
+ variable: key,
229
+ code: ConfigErrorCode.UNKNOWN_KEY,
230
+ message: UNKNOWN_KEY_MESSAGE
231
+ });
232
+ }
233
+ return issues.sort(
234
+ (left, right) => Number(left.variable > right.variable) - Number(left.variable < right.variable)
235
+ );
236
+ }
237
+ function validateEnv(schema, source, options) {
238
+ const bindings = resolveSourceNames(schema);
239
+ const sourceMap = new Map(Object.entries(source));
240
+ const namespaces = Object.keys(schema.shape);
241
+ const candidate = buildCandidate(namespaces, collectPresentLeaves(bindings, sourceMap));
242
+ const result = schema.safeParse(candidate);
243
+ const unknown = options?.strict === true ? detectUnknownKeys(resolveNamespacePrefixes(schema), bindings, sourceMap) : [];
244
+ if (result.success) {
245
+ if (unknown.length === 0) return result.data;
246
+ throw new BymaxConfigValidationError(unknown);
247
+ }
248
+ const issues = translateIssues(result.error.issues, bindings, sourceMap);
249
+ throw new BymaxConfigValidationError([...issues, ...unknown]);
250
+ }
251
+
252
+ // src/config.providers.ts
253
+ function reportIssues(hook, issues) {
254
+ if (hook === void 0) return;
255
+ try {
256
+ hook(issues);
257
+ } catch {
258
+ }
259
+ }
260
+ function createValidatedConfig(options) {
261
+ const source = options.source ?? process.env;
262
+ try {
263
+ return deepFreeze(validateEnv(options.schema, source, { strict: options.strict === true }));
264
+ } catch (error) {
265
+ if (error instanceof BymaxConfigValidationError) {
266
+ reportIssues(options.onValidationError, error.issues);
267
+ }
268
+ throw error;
269
+ }
270
+ }
271
+ var bymaxConfigProvider = {
272
+ provide: BYMAX_CONFIG,
273
+ useFactory: createValidatedConfig,
274
+ inject: [BYMAX_CONFIG_OPTIONS]
275
+ };
276
+ var ConfigService = class {
277
+ /**
278
+ * Bind the accessor to the frozen configuration object.
279
+ *
280
+ * Builds a flat `namespace.leaf` lookup once so `get`/`has` are constant-time
281
+ * and never rebuild a map per call. Iteration over entries (rather than dynamic
282
+ * bracket indexing) avoids introducing a non-literal property sink.
283
+ *
284
+ * @param config - The validated, deep-frozen config injected via {@link BYMAX_CONFIG}.
285
+ */
286
+ constructor(config) {
287
+ this.config = config;
288
+ const leaves = /* @__PURE__ */ new Map();
289
+ for (const [namespace, value] of Object.entries(config)) {
290
+ for (const [leaf, leafValue] of Object.entries(value)) {
291
+ leaves.set(`${namespace}.${leaf}`, leafValue);
292
+ }
293
+ }
294
+ this.leaves = leaves;
295
+ }
296
+ /**
297
+ * Read a leaf value by its `namespace.leaf` dot-path.
298
+ *
299
+ * The return type is inferred from the path, so `get('server.port')` is typed
300
+ * `number` with no cast. Never throws for a declared path: validation ran once
301
+ * at bootstrap. An optional leaf without a default may still resolve to
302
+ * `undefined` when its type includes it.
303
+ *
304
+ * @typeParam TPath - A declared path drawn from {@link Path}<TConfig>.
305
+ * @param path - The two-level dot-path to resolve.
306
+ * @returns The leaf value, typed as {@link PathValue}<TConfig, TPath>.
307
+ * @example
308
+ * ```typescript
309
+ * const port = config.get('server.port'); // number
310
+ * ```
311
+ */
312
+ get(path) {
313
+ return this.resolve(path);
314
+ }
315
+ /**
316
+ * Return the whole validated configuration as its deep-frozen root.
317
+ *
318
+ * Yields the same frozen reference the module registered, fully typed for
319
+ * arbitrary depth, which is the escape hatch for nesting deeper than the
320
+ * two-level dot-path inference covers.
321
+ *
322
+ * @returns The frozen configuration root.
323
+ * @example
324
+ * ```typescript
325
+ * const everything = config.getAll(); // Readonly<AppConfig>
326
+ * ```
327
+ */
328
+ getAll() {
329
+ return this.config;
330
+ }
331
+ /**
332
+ * Report whether a declared path resolves to a defined value.
333
+ *
334
+ * Returns `false` when the leaf resolved to `undefined` (an optional leaf
335
+ * without a default), distinguishing "declared but unset" from "declared and
336
+ * set" without a truthiness pitfall on falsy values such as `0` or `''`.
337
+ *
338
+ * @param path - The two-level dot-path to check.
339
+ * @returns True when the resolved value is not `undefined`.
340
+ * @example
341
+ * ```typescript
342
+ * if (config.has('redis.url')) connectRedis(config.get('redis.url'));
343
+ * ```
344
+ */
345
+ has(path) {
346
+ return this.resolve(path) !== void 0;
347
+ }
348
+ /**
349
+ * Resolve a two-level dot-path to its raw leaf value.
350
+ *
351
+ * Looks the path up in the precomputed `namespace.leaf` map, so an unset leaf
352
+ * (or, defensively, an unexpected path) yields `undefined` instead of throwing.
353
+ *
354
+ * @param path - The declared two-level dot-path.
355
+ * @returns The leaf value as `unknown`, or `undefined` when the leaf is unset.
356
+ */
357
+ resolve(path) {
358
+ return this.leaves.get(String(path));
359
+ }
360
+ };
361
+ ConfigService = __decorateClass([
362
+ Injectable(),
363
+ __decorateParam(0, Inject(BYMAX_CONFIG))
364
+ ], ConfigService);
365
+
366
+ // src/config.module.ts
367
+ function appendTo(list, ...items) {
368
+ return list === void 0 ? [...items] : [...list, ...items];
369
+ }
370
+ var BymaxConfigModule = class extends ConfigurableModuleClass {
371
+ /**
372
+ * Register the module synchronously from a static schema.
373
+ *
374
+ * @param options - The module options plus the optional `isGlobal` extra.
375
+ * @returns The dynamic module augmented with the frozen config provider.
376
+ */
377
+ static forRoot(options) {
378
+ return BymaxConfigModule.withConfigProvider(super.forRoot(options));
379
+ }
380
+ /**
381
+ * Register the module asynchronously, resolving options through a factory.
382
+ *
383
+ * @param options - The async options with `useFactory` and explicit `inject`.
384
+ * @returns The dynamic module augmented with the frozen config provider.
385
+ */
386
+ static forRootAsync(options) {
387
+ return BymaxConfigModule.withConfigProvider(super.forRootAsync(options));
388
+ }
389
+ /**
390
+ * Append the config provider and typed accessor to a generated definition.
391
+ *
392
+ * Registers the frozen {@link BYMAX_CONFIG} provider and the {@link ConfigService}
393
+ * accessor (which injects that token), then exports both so consumers can
394
+ * inject either the raw frozen object or the typed service.
395
+ *
396
+ * @param definition - The dynamic module produced by the builder base class.
397
+ * @returns A definition that provides and exports {@link BYMAX_CONFIG} and
398
+ * {@link ConfigService}.
399
+ */
400
+ static withConfigProvider(definition) {
401
+ return {
402
+ ...definition,
403
+ providers: appendTo(definition.providers, bymaxConfigProvider, ConfigService),
404
+ exports: appendTo(definition.exports, BYMAX_CONFIG, ConfigService)
405
+ };
406
+ }
407
+ };
408
+ BymaxConfigModule = __decorateClass([
409
+ Module({})
410
+ ], BymaxConfigModule);
411
+ function defineEnv(shape) {
412
+ return z.object(shape);
413
+ }
414
+
415
+ export { BYMAX_CONFIG, BYMAX_CONFIG_OPTIONS, BymaxConfigModule, BymaxConfigValidationError, ConfigErrorCode, ConfigService, createValidatedConfig, defineEnv, resolveSourceNames };
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ var internal = require('@bymax-one/nest-config/internal');
4
+
5
+ // src/testing/create-test-config.ts
6
+ var FILLER_CHARACTER = "a";
7
+ var PLACEHOLDER_URL = "https://placeholder.local";
8
+ var PLACEHOLDER_EMAIL_DOMAIN = "@placeholder.local";
9
+ var DEFAULT_NUMBER = 1;
10
+ var DEFAULT_STRING_LENGTH = 1;
11
+ var PLACEHOLDER_BOOLEAN = "true";
12
+ function readDef(leaf) {
13
+ return leaf._zod.def;
14
+ }
15
+ function checksOf(def) {
16
+ return def.checks ?? [];
17
+ }
18
+ function readStringLengths(def) {
19
+ let min = 0;
20
+ let max = Number.POSITIVE_INFINITY;
21
+ let exact;
22
+ for (const wrapper of checksOf(def)) {
23
+ const check = wrapper._zod.def;
24
+ if (check.check === "min_length") min = check.minimum;
25
+ if (check.check === "max_length") max = check.maximum;
26
+ if (check.check === "length_equals") exact = check.length;
27
+ }
28
+ return exact === void 0 ? { min, max } : { min, max, exact };
29
+ }
30
+ var URL_PREFIX = "http://";
31
+ var EMAIL_SUFFIX = "@a.lo";
32
+ function clampLength(preferred, min, max) {
33
+ return Math.min(Math.max(preferred, min), max);
34
+ }
35
+ function urlOfLength(target) {
36
+ const hostFill = Math.max(target - URL_PREFIX.length, 1);
37
+ return `${URL_PREFIX}${FILLER_CHARACTER.repeat(hostFill)}`;
38
+ }
39
+ function emailOfLength(target) {
40
+ const localFill = Math.max(target - EMAIL_SUFFIX.length, 1);
41
+ return `${FILLER_CHARACTER.repeat(localFill)}${EMAIL_SUFFIX}`;
42
+ }
43
+ function synthesizeUrl(lengths) {
44
+ if (lengths.exact === void 0 && PLACEHOLDER_URL.length >= lengths.min && PLACEHOLDER_URL.length <= lengths.max) {
45
+ return PLACEHOLDER_URL;
46
+ }
47
+ return urlOfLength(lengths.exact ?? clampLength(PLACEHOLDER_URL.length, lengths.min, lengths.max));
48
+ }
49
+ function synthesizeEmail(lengths) {
50
+ const canonical = `${FILLER_CHARACTER}${PLACEHOLDER_EMAIL_DOMAIN}`;
51
+ if (lengths.exact === void 0 && canonical.length >= lengths.min && canonical.length <= lengths.max) {
52
+ return canonical;
53
+ }
54
+ return emailOfLength(lengths.exact ?? clampLength(canonical.length, lengths.min, lengths.max));
55
+ }
56
+ function synthesizePlainString(lengths) {
57
+ if (lengths.exact !== void 0) return FILLER_CHARACTER.repeat(lengths.exact);
58
+ const target = Math.max(lengths.min, DEFAULT_STRING_LENGTH);
59
+ const bounded = target > lengths.max ? lengths.max : target;
60
+ return FILLER_CHARACTER.repeat(bounded);
61
+ }
62
+ function synthesizeString(def) {
63
+ const lengths = readStringLengths(def);
64
+ if (def.format === "url") return synthesizeUrl(lengths);
65
+ if (def.format === "email") return synthesizeEmail(lengths);
66
+ return synthesizePlainString(lengths);
67
+ }
68
+ function selectNumber(bounds) {
69
+ const { lower, upper, integer } = bounds;
70
+ if (lower !== void 0 && upper !== void 0) {
71
+ if (integer) {
72
+ const lo = bounds.lowerExclusive ? Math.floor(lower) + 1 : Math.ceil(lower);
73
+ const hi = bounds.upperExclusive ? Math.ceil(upper) - 1 : Math.floor(upper);
74
+ return Math.floor((lo + hi) / 2);
75
+ }
76
+ return (lower + upper) / 2;
77
+ }
78
+ if (lower !== void 0) return bounds.lowerExclusive ? lower + (integer ? 1 : 0.5) : lower;
79
+ if (upper !== void 0) return bounds.upperExclusive ? upper - (integer ? 1 : 0.5) : upper;
80
+ return DEFAULT_NUMBER;
81
+ }
82
+ function synthesizeNumber(def) {
83
+ const bounds = { lowerExclusive: false, upperExclusive: false, integer: false };
84
+ for (const wrapper of checksOf(def)) {
85
+ const check = wrapper._zod.def;
86
+ if (check.check === "greater_than") {
87
+ bounds.lower = check.value;
88
+ bounds.lowerExclusive = check.inclusive !== true;
89
+ }
90
+ if (check.check === "less_than") {
91
+ bounds.upper = check.value;
92
+ bounds.upperExclusive = check.inclusive !== true;
93
+ }
94
+ if (check.check === "number_format") bounds.integer = true;
95
+ }
96
+ return String(selectNumber(bounds));
97
+ }
98
+ function synthesizeEnum(def) {
99
+ const [first] = Object.values(def.entries);
100
+ return String(first);
101
+ }
102
+ function synthesizeLeaf(leaf) {
103
+ const def = readDef(leaf);
104
+ switch (def.type) {
105
+ case "default":
106
+ case "optional":
107
+ return void 0;
108
+ case "nullable":
109
+ return synthesizeLeaf(def.innerType);
110
+ case "string":
111
+ return synthesizeString(def);
112
+ case "number":
113
+ return synthesizeNumber(def);
114
+ case "boolean":
115
+ return PLACEHOLDER_BOOLEAN;
116
+ case "enum":
117
+ return synthesizeEnum(def);
118
+ default:
119
+ return FILLER_CHARACTER;
120
+ }
121
+ }
122
+ function synthesizePlaceholderSource(schema) {
123
+ const valueByPath = /* @__PURE__ */ new Map();
124
+ for (const [namespace, namespaceSchema] of Object.entries(schema.shape)) {
125
+ for (const [leafKey, leafSchema] of Object.entries(namespaceSchema.shape)) {
126
+ const value = synthesizeLeaf(leafSchema);
127
+ if (value !== void 0) valueByPath.set(`${namespace}.${leafKey}`, value);
128
+ }
129
+ }
130
+ const source = /* @__PURE__ */ Object.create(null);
131
+ for (const binding of internal.resolveSourceNames(schema)) {
132
+ const value = valueByPath.get(binding.path);
133
+ if (value !== void 0) source[binding.variable] = value;
134
+ }
135
+ return source;
136
+ }
137
+
138
+ // src/testing/create-test-config.ts
139
+ function flattenOverrides(overrides) {
140
+ const flat = /* @__PURE__ */ new Map();
141
+ for (const [namespace, leaves] of Object.entries(overrides)) {
142
+ for (const [leaf, value] of Object.entries(leaves)) {
143
+ flat.set(`${namespace}.${leaf}`, value);
144
+ }
145
+ }
146
+ return flat;
147
+ }
148
+ function applyOverrides(schema, base, overrides) {
149
+ const flat = flattenOverrides(overrides ?? {});
150
+ const entries = new Map(Object.entries(base));
151
+ for (const binding of internal.resolveSourceNames(schema)) {
152
+ const value = flat.get(binding.path);
153
+ if (value !== void 0) entries.set(binding.variable, String(value));
154
+ }
155
+ const source = /* @__PURE__ */ Object.create(null);
156
+ for (const [variable, value] of entries) source[variable] = value;
157
+ return source;
158
+ }
159
+ function buildTestSource(schema, overrides) {
160
+ return applyOverrides(schema, synthesizePlaceholderSource(schema), overrides);
161
+ }
162
+ function createTestConfig(schema, overrides) {
163
+ return internal.createValidatedConfig({ schema, source: buildTestSource(schema, overrides) });
164
+ }
165
+ function configTestingModule(schema, overrides) {
166
+ const source = buildTestSource(schema, overrides);
167
+ return internal.BymaxConfigModule.forRoot({ schema, source });
168
+ }
169
+
170
+ exports.configTestingModule = configTestingModule;
171
+ exports.createTestConfig = createTestConfig;
@@ -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 };