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