@arikajs/config 0.0.5
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 +13 -0
- package/LICENSE +21 -0
- package/README.md +253 -0
- package/dist/EnvLoader.d.ts +10 -0
- package/dist/EnvLoader.js +44 -0
- package/dist/EnvLoader.js.map +1 -0
- package/dist/Repository.d.ts +96 -0
- package/dist/Repository.js +418 -0
- package/dist/Repository.js.map +1 -0
- package/dist/helpers.d.ts +13 -0
- package/dist/helpers.js +27 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/package.json +33 -0
- package/src/EnvLoader.ts +51 -0
- package/src/Repository.ts +562 -0
- package/src/helpers.ts +34 -0
- package/src/index.ts +11 -0
- package/tests/Config.test.ts +330 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
|
|
6
|
+
const localRequire = createRequire(import.meta.url);
|
|
7
|
+
|
|
8
|
+
// ─── Schema Types ───────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export type SchemaRule = {
|
|
11
|
+
type?: 'string' | 'number' | 'boolean' | 'object' | 'array';
|
|
12
|
+
required?: boolean;
|
|
13
|
+
default?: any;
|
|
14
|
+
enum?: any[];
|
|
15
|
+
min?: number;
|
|
16
|
+
max?: number;
|
|
17
|
+
pattern?: RegExp;
|
|
18
|
+
message?: string;
|
|
19
|
+
children?: Record<string, SchemaRule>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type ConfigSchema = Record<string, SchemaRule | Record<string, SchemaRule>>;
|
|
23
|
+
|
|
24
|
+
export interface ValidationError {
|
|
25
|
+
key: string;
|
|
26
|
+
message: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── Change Listener Types ──────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export type ConfigChangeListener = (key: string, newValue: any, oldValue: any) => void;
|
|
32
|
+
|
|
33
|
+
// ─── Repository ─────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export class Repository {
|
|
36
|
+
private config: Record<string, any> = {};
|
|
37
|
+
private booted = false;
|
|
38
|
+
|
|
39
|
+
// Feature 1: Config Caching
|
|
40
|
+
private flatCache: Map<string, any> = new Map();
|
|
41
|
+
private cacheBuilt = false;
|
|
42
|
+
|
|
43
|
+
// Feature 2: Schema Validation
|
|
44
|
+
private schema: ConfigSchema | null = null;
|
|
45
|
+
|
|
46
|
+
// Feature 4: Change Listeners
|
|
47
|
+
private listeners: Map<string, ConfigChangeListener[]> = new Map();
|
|
48
|
+
private globalListeners: ConfigChangeListener[] = [];
|
|
49
|
+
|
|
50
|
+
// Feature 6: Encrypted Config
|
|
51
|
+
private decrypter: ((value: string) => string) | null = null;
|
|
52
|
+
|
|
53
|
+
constructor(initialConfig: Record<string, any> = {}) {
|
|
54
|
+
this.config = { ...initialConfig };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Loading ────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Load configuration files from the config directory.
|
|
61
|
+
*/
|
|
62
|
+
public loadConfigDirectory(configPath: string, environment?: string): void {
|
|
63
|
+
if (this.booted) {
|
|
64
|
+
throw new Error('Configuration cannot be modified after boot.');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!fs.existsSync(configPath)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const files = fs.readdirSync(configPath);
|
|
72
|
+
|
|
73
|
+
// Feature 3: Environment-based Merging
|
|
74
|
+
// First load base configs (e.g., database.js)
|
|
75
|
+
// Then overlay env-specific (e.g., database.production.js)
|
|
76
|
+
const env = environment || process.env.NODE_ENV || 'development';
|
|
77
|
+
|
|
78
|
+
const baseFiles = files.filter(
|
|
79
|
+
(file: string) =>
|
|
80
|
+
this.isConfigFile(file) && !this.isEnvSpecificFile(file),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const envFiles = files.filter(
|
|
84
|
+
(file: string) =>
|
|
85
|
+
this.isConfigFile(file) && file.includes(`.${env}.`),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Load base configs first
|
|
89
|
+
for (const file of baseFiles) {
|
|
90
|
+
this.loadFile(configPath, file);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Then overlay environment-specific configs
|
|
94
|
+
for (const file of envFiles) {
|
|
95
|
+
this.loadFile(configPath, file, true);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private isConfigFile(file: string): boolean {
|
|
100
|
+
return (
|
|
101
|
+
file.endsWith('.js') ||
|
|
102
|
+
file.endsWith('.ts') ||
|
|
103
|
+
file.endsWith('.cjs') ||
|
|
104
|
+
file.endsWith('.mjs')
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private isEnvSpecificFile(file: string): boolean {
|
|
109
|
+
const envPatterns = [
|
|
110
|
+
'.development.',
|
|
111
|
+
'.production.',
|
|
112
|
+
'.staging.',
|
|
113
|
+
'.testing.',
|
|
114
|
+
'.local.',
|
|
115
|
+
];
|
|
116
|
+
return envPatterns.some((p) => file.includes(p));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private loadFile(
|
|
120
|
+
configPath: string,
|
|
121
|
+
file: string,
|
|
122
|
+
isOverlay: boolean = false,
|
|
123
|
+
): void {
|
|
124
|
+
const filePath = path.join(configPath, file);
|
|
125
|
+
|
|
126
|
+
// Extract base config name (e.g., "database" from "database.production.js")
|
|
127
|
+
const parts = path.basename(file, path.extname(file)).split('.');
|
|
128
|
+
const configName = parts[0];
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
let configValue;
|
|
132
|
+
try {
|
|
133
|
+
const module = localRequire(filePath);
|
|
134
|
+
configValue = module.default || module;
|
|
135
|
+
} catch {
|
|
136
|
+
// ESM fallback — silently skip
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (typeof configValue === 'object' && configValue !== null) {
|
|
140
|
+
if (isOverlay && this.config[configName]) {
|
|
141
|
+
// Deep merge for env overlays
|
|
142
|
+
this.config[configName] = this.deepMerge(
|
|
143
|
+
this.config[configName],
|
|
144
|
+
configValue,
|
|
145
|
+
);
|
|
146
|
+
} else {
|
|
147
|
+
this.config[configName] = {
|
|
148
|
+
...this.config[configName],
|
|
149
|
+
...configValue,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// Silently skip files that can't be loaded
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ─── Get / Set / Has ────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get a configuration value using dot notation.
|
|
162
|
+
* Uses flat cache for O(1) lookup when booted.
|
|
163
|
+
*/
|
|
164
|
+
public get<T = any>(key: string, defaultValue?: T): T {
|
|
165
|
+
// Feature 1: Use flat cache after boot for O(1)
|
|
166
|
+
if (this.cacheBuilt) {
|
|
167
|
+
const cached = this.flatCache.get(key);
|
|
168
|
+
if (cached !== undefined) {
|
|
169
|
+
// Feature 6: Auto-decrypt encrypted values
|
|
170
|
+
if (typeof cached === 'string' && cached.startsWith('enc:')) {
|
|
171
|
+
return this.decrypt(cached) as T;
|
|
172
|
+
}
|
|
173
|
+
return cached as T;
|
|
174
|
+
}
|
|
175
|
+
return defaultValue as T;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Pre-boot: walk the tree
|
|
179
|
+
const keys = key.split('.');
|
|
180
|
+
let value: any = this.config;
|
|
181
|
+
|
|
182
|
+
for (const k of keys) {
|
|
183
|
+
if (value === undefined || value === null) {
|
|
184
|
+
return defaultValue as T;
|
|
185
|
+
}
|
|
186
|
+
value = value[k];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const result = value !== undefined ? value : defaultValue;
|
|
190
|
+
|
|
191
|
+
// Feature 6: Auto-decrypt encrypted values
|
|
192
|
+
if (typeof result === 'string' && result.startsWith('enc:')) {
|
|
193
|
+
return this.decrypt(result) as T;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return result as T;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check if a configuration key exists.
|
|
201
|
+
*/
|
|
202
|
+
public has(key: string): boolean {
|
|
203
|
+
if (this.cacheBuilt) {
|
|
204
|
+
return this.flatCache.has(key);
|
|
205
|
+
}
|
|
206
|
+
return this.get(key) !== undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Set a configuration value using dot notation.
|
|
211
|
+
*/
|
|
212
|
+
public set(key: string, value: any): void {
|
|
213
|
+
if (this.booted) {
|
|
214
|
+
throw new Error('Configuration cannot be modified after boot.');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const oldValue = this.get(key);
|
|
218
|
+
|
|
219
|
+
const keys = key.split('.');
|
|
220
|
+
let current = this.config;
|
|
221
|
+
|
|
222
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
223
|
+
const k = keys[i];
|
|
224
|
+
if (
|
|
225
|
+
!(k in current) ||
|
|
226
|
+
typeof current[k] !== 'object' ||
|
|
227
|
+
current[k] === null
|
|
228
|
+
) {
|
|
229
|
+
current[k] = {};
|
|
230
|
+
}
|
|
231
|
+
current = current[k];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
current[keys[keys.length - 1]] = value;
|
|
235
|
+
|
|
236
|
+
// Feature 4: Notify change listeners
|
|
237
|
+
this.notifyListeners(key, value, oldValue);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get all configuration.
|
|
242
|
+
*/
|
|
243
|
+
public all(): Record<string, any> {
|
|
244
|
+
return { ...this.config };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ─── Boot Lifecycle ─────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Mark the repository as booted (read-only).
|
|
251
|
+
* Builds the flat cache and deep-freezes the config for immutability.
|
|
252
|
+
*/
|
|
253
|
+
public markAsBooted(): void {
|
|
254
|
+
// Feature 2: Validate schema before boot
|
|
255
|
+
if (this.schema) {
|
|
256
|
+
const errors = this.validateSchema();
|
|
257
|
+
if (errors.length > 0) {
|
|
258
|
+
const messages = errors.map((e) => ` - ${e.key}: ${e.message}`).join('\n');
|
|
259
|
+
throw new Error(
|
|
260
|
+
`Configuration validation failed:\n${messages}`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Feature 1: Build flat cache
|
|
266
|
+
this.buildFlatCache();
|
|
267
|
+
|
|
268
|
+
// Feature 5: Deep freeze
|
|
269
|
+
this.deepFreeze(this.config);
|
|
270
|
+
|
|
271
|
+
this.booted = true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Check if the repository has been booted.
|
|
276
|
+
*/
|
|
277
|
+
public isBooted(): boolean {
|
|
278
|
+
return this.booted;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ─── Feature 1: Config Caching ──────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Build a flat Map of all dot-notation keys for O(1) lookups.
|
|
285
|
+
*/
|
|
286
|
+
private buildFlatCache(obj?: Record<string, any>, prefix?: string): void {
|
|
287
|
+
const target = obj || this.config;
|
|
288
|
+
const pre = prefix ? `${prefix}.` : '';
|
|
289
|
+
|
|
290
|
+
for (const key of Object.keys(target)) {
|
|
291
|
+
const fullKey = `${pre}${key}`;
|
|
292
|
+
const value = target[key];
|
|
293
|
+
|
|
294
|
+
this.flatCache.set(fullKey, value);
|
|
295
|
+
|
|
296
|
+
if (
|
|
297
|
+
typeof value === 'object' &&
|
|
298
|
+
value !== null &&
|
|
299
|
+
!Array.isArray(value)
|
|
300
|
+
) {
|
|
301
|
+
this.buildFlatCache(value, fullKey);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!prefix) {
|
|
306
|
+
this.cacheBuilt = true;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ─── Feature 2: Schema Validation ───────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Define a validation schema for the configuration.
|
|
314
|
+
*/
|
|
315
|
+
public defineSchema(schema: ConfigSchema): this {
|
|
316
|
+
this.schema = schema;
|
|
317
|
+
return this;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Validate the current config against the defined schema.
|
|
322
|
+
*/
|
|
323
|
+
public validate(): ValidationError[] {
|
|
324
|
+
if (!this.schema) return [];
|
|
325
|
+
return this.validateSchema();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private validateSchema(
|
|
329
|
+
schema?: Record<string, SchemaRule>,
|
|
330
|
+
prefix?: string,
|
|
331
|
+
): ValidationError[] {
|
|
332
|
+
const errors: ValidationError[] = [];
|
|
333
|
+
const rules = schema || this.schema;
|
|
334
|
+
|
|
335
|
+
if (!rules) return errors;
|
|
336
|
+
|
|
337
|
+
for (const [key, rule] of Object.entries(rules)) {
|
|
338
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
339
|
+
const value = this.get(fullKey);
|
|
340
|
+
|
|
341
|
+
// Handle nested schema via 'children'
|
|
342
|
+
if ((rule as SchemaRule).children) {
|
|
343
|
+
errors.push(
|
|
344
|
+
...this.validateSchema(
|
|
345
|
+
(rule as SchemaRule).children,
|
|
346
|
+
fullKey,
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const r = rule as SchemaRule;
|
|
353
|
+
|
|
354
|
+
// Required check
|
|
355
|
+
if (r.required && (value === undefined || value === null)) {
|
|
356
|
+
errors.push({
|
|
357
|
+
key: fullKey,
|
|
358
|
+
message:
|
|
359
|
+
r.message || `'${fullKey}' is required but not set.`,
|
|
360
|
+
});
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Skip further checks if value is undefined and not required
|
|
365
|
+
if (value === undefined || value === null) continue;
|
|
366
|
+
|
|
367
|
+
// Type check
|
|
368
|
+
if (r.type) {
|
|
369
|
+
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
370
|
+
if (actualType !== r.type) {
|
|
371
|
+
errors.push({
|
|
372
|
+
key: fullKey,
|
|
373
|
+
message:
|
|
374
|
+
r.message ||
|
|
375
|
+
`'${fullKey}' must be of type '${r.type}', got '${actualType}'.`,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Enum check
|
|
381
|
+
if (r.enum && !r.enum.includes(value)) {
|
|
382
|
+
errors.push({
|
|
383
|
+
key: fullKey,
|
|
384
|
+
message:
|
|
385
|
+
r.message ||
|
|
386
|
+
`'${fullKey}' must be one of [${r.enum.join(', ')}], got '${value}'.`,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Min/Max for numbers
|
|
391
|
+
if (r.type === 'number' && typeof value === 'number') {
|
|
392
|
+
if (r.min !== undefined && value < r.min) {
|
|
393
|
+
errors.push({
|
|
394
|
+
key: fullKey,
|
|
395
|
+
message:
|
|
396
|
+
r.message ||
|
|
397
|
+
`'${fullKey}' must be >= ${r.min}, got ${value}.`,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
if (r.max !== undefined && value > r.max) {
|
|
401
|
+
errors.push({
|
|
402
|
+
key: fullKey,
|
|
403
|
+
message:
|
|
404
|
+
r.message ||
|
|
405
|
+
`'${fullKey}' must be <= ${r.max}, got ${value}.`,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Min/Max for string length
|
|
411
|
+
if (r.type === 'string' && typeof value === 'string') {
|
|
412
|
+
if (r.min !== undefined && value.length < r.min) {
|
|
413
|
+
errors.push({
|
|
414
|
+
key: fullKey,
|
|
415
|
+
message:
|
|
416
|
+
r.message ||
|
|
417
|
+
`'${fullKey}' must have at least ${r.min} characters.`,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
if (r.max !== undefined && value.length > r.max) {
|
|
421
|
+
errors.push({
|
|
422
|
+
key: fullKey,
|
|
423
|
+
message:
|
|
424
|
+
r.message ||
|
|
425
|
+
`'${fullKey}' must have at most ${r.max} characters.`,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Pattern check
|
|
431
|
+
if (r.pattern && typeof value === 'string' && !r.pattern.test(value)) {
|
|
432
|
+
errors.push({
|
|
433
|
+
key: fullKey,
|
|
434
|
+
message:
|
|
435
|
+
r.message ||
|
|
436
|
+
`'${fullKey}' does not match the required pattern.`,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return errors;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ─── Feature 3: Environment Merging (handled in loadConfigDirectory) ────
|
|
445
|
+
|
|
446
|
+
// ─── Feature 4: Config Change Listeners ─────────────────────────────────
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Register a listener for changes to a specific config key.
|
|
450
|
+
*/
|
|
451
|
+
public onChange(key: string, listener: ConfigChangeListener): this {
|
|
452
|
+
if (!this.listeners.has(key)) {
|
|
453
|
+
this.listeners.set(key, []);
|
|
454
|
+
}
|
|
455
|
+
this.listeners.get(key)!.push(listener);
|
|
456
|
+
return this;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Register a global listener for ALL config changes.
|
|
461
|
+
*/
|
|
462
|
+
public onAnyChange(listener: ConfigChangeListener): this {
|
|
463
|
+
this.globalListeners.push(listener);
|
|
464
|
+
return this;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private notifyListeners(key: string, newValue: any, oldValue: any): void {
|
|
468
|
+
// Key-specific listeners
|
|
469
|
+
const keyListeners = this.listeners.get(key);
|
|
470
|
+
if (keyListeners) {
|
|
471
|
+
for (const listener of keyListeners) {
|
|
472
|
+
listener(key, newValue, oldValue);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Wildcard: notify listeners for parent keys too
|
|
477
|
+
// e.g. changing 'database.host' should notify 'database' listeners
|
|
478
|
+
const parts = key.split('.');
|
|
479
|
+
for (let i = parts.length - 1; i > 0; i--) {
|
|
480
|
+
const parentKey = parts.slice(0, i).join('.');
|
|
481
|
+
const parentListeners = this.listeners.get(parentKey);
|
|
482
|
+
if (parentListeners) {
|
|
483
|
+
for (const listener of parentListeners) {
|
|
484
|
+
listener(key, newValue, oldValue);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Global listeners
|
|
490
|
+
for (const listener of this.globalListeners) {
|
|
491
|
+
listener(key, newValue, oldValue);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ─── Feature 5: Deep Freeze ─────────────────────────────────────────────
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Recursively freeze an object to prevent any mutations.
|
|
499
|
+
*/
|
|
500
|
+
private deepFreeze(obj: any): any {
|
|
501
|
+
if (obj === null || typeof obj !== 'object') return obj;
|
|
502
|
+
|
|
503
|
+
Object.freeze(obj);
|
|
504
|
+
|
|
505
|
+
for (const key of Object.keys(obj)) {
|
|
506
|
+
const value = obj[key];
|
|
507
|
+
if (typeof value === 'object' && value !== null && !Object.isFrozen(value)) {
|
|
508
|
+
this.deepFreeze(value);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return obj;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ─── Feature 6: Encrypted Config Values ─────────────────────────────────
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Set a decrypter function for auto-decrypting `enc:` prefixed values.
|
|
519
|
+
*/
|
|
520
|
+
public setDecrypter(fn: (encryptedValue: string) => string): this {
|
|
521
|
+
this.decrypter = fn;
|
|
522
|
+
return this;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private decrypt(value: string): string {
|
|
526
|
+
const encrypted = value.substring(4); // Remove 'enc:' prefix
|
|
527
|
+
if (this.decrypter) {
|
|
528
|
+
return this.decrypter(encrypted);
|
|
529
|
+
}
|
|
530
|
+
// If no decrypter is set, return the raw encrypted string
|
|
531
|
+
return encrypted;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ─── Utilities ──────────────────────────────────────────────────────────
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Deep merge two objects. Source values override target values.
|
|
538
|
+
*/
|
|
539
|
+
private deepMerge(
|
|
540
|
+
target: Record<string, any>,
|
|
541
|
+
source: Record<string, any>,
|
|
542
|
+
): Record<string, any> {
|
|
543
|
+
const result = { ...target };
|
|
544
|
+
|
|
545
|
+
for (const key of Object.keys(source)) {
|
|
546
|
+
if (
|
|
547
|
+
source[key] &&
|
|
548
|
+
typeof source[key] === 'object' &&
|
|
549
|
+
!Array.isArray(source[key]) &&
|
|
550
|
+
result[key] &&
|
|
551
|
+
typeof result[key] === 'object' &&
|
|
552
|
+
!Array.isArray(result[key])
|
|
553
|
+
) {
|
|
554
|
+
result[key] = this.deepMerge(result[key], source[key]);
|
|
555
|
+
} else {
|
|
556
|
+
result[key] = source[key];
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return result;
|
|
561
|
+
}
|
|
562
|
+
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
|
|
2
|
+
import { Repository } from './Repository.js';
|
|
3
|
+
import { EnvLoader } from './EnvLoader.js';
|
|
4
|
+
|
|
5
|
+
let repository: Repository | null = null;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Set the global configuration repository.
|
|
9
|
+
*/
|
|
10
|
+
export function setConfigRepository(repo: Repository): void {
|
|
11
|
+
repository = repo;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get/Set configuration values.
|
|
16
|
+
*/
|
|
17
|
+
export function config<T = any>(key?: string, defaultValue?: T): T | Repository | any {
|
|
18
|
+
if (key === undefined) {
|
|
19
|
+
return repository;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!repository) {
|
|
23
|
+
return defaultValue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return repository.get(key, defaultValue);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get an environment variable.
|
|
31
|
+
*/
|
|
32
|
+
export function env<T = any>(key: string, defaultValue?: T): T {
|
|
33
|
+
return EnvLoader.get(key, defaultValue);
|
|
34
|
+
}
|