@morojs/moro 1.5.5 → 1.5.7
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/dist/core/config/config-manager.d.ts +44 -0
- package/dist/core/config/config-manager.js +114 -0
- package/dist/core/config/config-manager.js.map +1 -0
- package/dist/core/config/config-sources.d.ts +21 -0
- package/dist/core/config/config-sources.js +314 -0
- package/dist/core/config/config-sources.js.map +1 -0
- package/dist/core/config/config-validator.d.ts +21 -0
- package/dist/core/config/config-validator.js +744 -0
- package/dist/core/config/config-validator.js.map +1 -0
- package/dist/core/config/file-loader.d.ts +0 -5
- package/dist/core/config/file-loader.js +0 -171
- package/dist/core/config/file-loader.js.map +1 -1
- package/dist/core/config/index.d.ts +39 -10
- package/dist/core/config/index.js +66 -29
- package/dist/core/config/index.js.map +1 -1
- package/dist/core/config/schema.js +29 -31
- package/dist/core/config/schema.js.map +1 -1
- package/dist/core/config/utils.d.ts +9 -2
- package/dist/core/config/utils.js +19 -32
- package/dist/core/config/utils.js.map +1 -1
- package/dist/core/framework.d.ts +4 -7
- package/dist/core/framework.js +38 -12
- package/dist/core/framework.js.map +1 -1
- package/dist/core/http/http-server.d.ts +12 -0
- package/dist/core/http/http-server.js +56 -0
- package/dist/core/http/http-server.js.map +1 -1
- package/dist/core/http/router.d.ts +12 -0
- package/dist/core/http/router.js +114 -36
- package/dist/core/http/router.js.map +1 -1
- package/dist/core/logger/index.d.ts +1 -1
- package/dist/core/logger/index.js +2 -1
- package/dist/core/logger/index.js.map +1 -1
- package/dist/core/logger/logger.d.ts +9 -1
- package/dist/core/logger/logger.js +36 -3
- package/dist/core/logger/logger.js.map +1 -1
- package/dist/core/routing/index.d.ts +20 -0
- package/dist/core/routing/index.js +109 -11
- package/dist/core/routing/index.js.map +1 -1
- package/dist/moro.d.ts +7 -20
- package/dist/moro.js +115 -200
- package/dist/moro.js.map +1 -1
- package/dist/types/config.d.ts +46 -2
- package/dist/types/core.d.ts +22 -39
- package/dist/types/logger.d.ts +4 -0
- package/package.json +1 -1
- package/src/core/config/config-manager.ts +133 -0
- package/src/core/config/config-sources.ts +384 -0
- package/src/core/config/config-validator.ts +1042 -0
- package/src/core/config/file-loader.ts +0 -233
- package/src/core/config/index.ts +77 -32
- package/src/core/config/schema.ts +29 -31
- package/src/core/config/utils.ts +22 -29
- package/src/core/framework.ts +51 -18
- package/src/core/http/http-server.ts +66 -0
- package/src/core/http/router.ts +127 -38
- package/src/core/logger/index.ts +1 -0
- package/src/core/logger/logger.ts +43 -4
- package/src/core/routing/index.ts +116 -12
- package/src/moro.ts +127 -233
- package/src/types/config.ts +47 -2
- package/src/types/core.ts +32 -43
- package/src/types/logger.ts +6 -0
- package/dist/core/config/loader.d.ts +0 -7
- package/dist/core/config/loader.js +0 -269
- package/dist/core/config/loader.js.map +0 -1
- package/dist/core/config/validation.d.ts +0 -17
- package/dist/core/config/validation.js +0 -131
- package/dist/core/config/validation.js.map +0 -1
- package/src/core/config/loader.ts +0 -633
- package/src/core/config/validation.ts +0 -140
|
@@ -0,0 +1,1042 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Validator - Type-Safe Schema Validation
|
|
3
|
+
*
|
|
4
|
+
* This module provides runtime validation for configuration objects using
|
|
5
|
+
* simple TypeScript functions that match the type definitions exactly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { AppConfig } from '../../types/config';
|
|
9
|
+
import { createFrameworkLogger } from '../logger';
|
|
10
|
+
|
|
11
|
+
const logger = createFrameworkLogger('ConfigValidator');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Configuration validation error with detailed context
|
|
15
|
+
*/
|
|
16
|
+
export class ConfigValidationError extends Error {
|
|
17
|
+
constructor(
|
|
18
|
+
public readonly field: string,
|
|
19
|
+
public readonly value: unknown,
|
|
20
|
+
public readonly expectedType: string,
|
|
21
|
+
message: string
|
|
22
|
+
) {
|
|
23
|
+
super(`Configuration validation failed for '${field}': ${message}`);
|
|
24
|
+
this.name = 'ConfigValidationError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validate and normalize a complete configuration object
|
|
30
|
+
* This ensures type safety and provides helpful error messages
|
|
31
|
+
*/
|
|
32
|
+
export function validateConfig(config: any): AppConfig {
|
|
33
|
+
logger.debug('Validating configuration');
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const validatedConfig: AppConfig = {
|
|
37
|
+
server: validateServerConfig(config.server, 'server'),
|
|
38
|
+
serviceDiscovery: validateServiceDiscoveryConfig(config.serviceDiscovery, 'serviceDiscovery'),
|
|
39
|
+
database: validateDatabaseConfig(config.database, 'database'),
|
|
40
|
+
modules: validateModuleDefaultsConfig(config.modules, 'modules'),
|
|
41
|
+
logging: validateLoggingConfig(config.logging, 'logging'),
|
|
42
|
+
security: validateSecurityConfig(config.security, 'security'),
|
|
43
|
+
external: validateExternalServicesConfig(config.external, 'external'),
|
|
44
|
+
performance: validatePerformanceConfig(config.performance, 'performance'),
|
|
45
|
+
websocket: validateWebSocketConfig(config.websocket, 'websocket'),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
logger.debug('Configuration validation successful');
|
|
49
|
+
return validatedConfig;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error instanceof ConfigValidationError) {
|
|
52
|
+
logger.error(`❌ Configuration validation failed for '${error.field}':`, error.message);
|
|
53
|
+
|
|
54
|
+
// Provide helpful hints
|
|
55
|
+
provideValidationHints(error);
|
|
56
|
+
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
logger.error('❌ Unexpected configuration validation error:', String(error));
|
|
61
|
+
throw new Error(`Configuration validation failed: ${String(error)}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validate server configuration
|
|
67
|
+
*/
|
|
68
|
+
function validateServerConfig(config: any, path: string) {
|
|
69
|
+
if (!config || typeof config !== 'object') {
|
|
70
|
+
throw new ConfigValidationError(
|
|
71
|
+
path,
|
|
72
|
+
config,
|
|
73
|
+
'object',
|
|
74
|
+
'Server configuration must be an object'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
port: validatePort(config.port, `${path}.port`),
|
|
80
|
+
host: validateString(config.host, `${path}.host`),
|
|
81
|
+
maxConnections: validateNumber(config.maxConnections, `${path}.maxConnections`, { min: 1 }),
|
|
82
|
+
timeout: validateNumber(config.timeout, `${path}.timeout`, { min: 1000 }),
|
|
83
|
+
bodySizeLimit: validateString(config.bodySizeLimit, `${path}.bodySizeLimit`),
|
|
84
|
+
requestTracking: {
|
|
85
|
+
enabled: validateBoolean(config.requestTracking?.enabled, `${path}.requestTracking.enabled`),
|
|
86
|
+
},
|
|
87
|
+
errorBoundary: {
|
|
88
|
+
enabled: validateBoolean(config.errorBoundary?.enabled, `${path}.errorBoundary.enabled`),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Validate service discovery configuration
|
|
95
|
+
*/
|
|
96
|
+
function validateServiceDiscoveryConfig(config: any, path: string) {
|
|
97
|
+
if (!config || typeof config !== 'object') {
|
|
98
|
+
throw new ConfigValidationError(
|
|
99
|
+
path,
|
|
100
|
+
config,
|
|
101
|
+
'object',
|
|
102
|
+
'Service discovery configuration must be an object'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
enabled: validateBoolean(config.enabled, `${path}.enabled`),
|
|
108
|
+
type: validateEnum(config.type, ['memory', 'consul', 'kubernetes'], `${path}.type`),
|
|
109
|
+
consulUrl: validateString(config.consulUrl, `${path}.consulUrl`),
|
|
110
|
+
kubernetesNamespace: validateString(config.kubernetesNamespace, `${path}.kubernetesNamespace`),
|
|
111
|
+
healthCheckInterval: validateNumber(config.healthCheckInterval, `${path}.healthCheckInterval`, {
|
|
112
|
+
min: 1000,
|
|
113
|
+
}),
|
|
114
|
+
retryAttempts: validateNumber(config.retryAttempts, `${path}.retryAttempts`, { min: 0 }),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Validate database configuration
|
|
120
|
+
*/
|
|
121
|
+
function validateDatabaseConfig(config: any, path: string) {
|
|
122
|
+
if (!config || typeof config !== 'object') {
|
|
123
|
+
throw new ConfigValidationError(
|
|
124
|
+
path,
|
|
125
|
+
config,
|
|
126
|
+
'object',
|
|
127
|
+
'Database configuration must be an object'
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const result: any = {};
|
|
132
|
+
|
|
133
|
+
// Optional URL
|
|
134
|
+
if (config.url !== undefined) {
|
|
135
|
+
result.url = validateString(config.url, `${path}.url`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Optional Redis - only validate if present
|
|
139
|
+
if (config.redis !== undefined) {
|
|
140
|
+
result.redis = validateRedisConfig(config.redis, `${path}.redis`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Optional MySQL - only validate if present
|
|
144
|
+
if (config.mysql !== undefined) {
|
|
145
|
+
result.mysql = validateMySQLConfig(config.mysql, `${path}.mysql`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Optional PostgreSQL - only validate if present
|
|
149
|
+
if (config.postgresql !== undefined) {
|
|
150
|
+
result.postgresql = validatePostgreSQLConfig(config.postgresql, `${path}.postgresql`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Optional SQLite - only validate if present
|
|
154
|
+
if (config.sqlite !== undefined) {
|
|
155
|
+
result.sqlite = validateSQLiteConfig(config.sqlite, `${path}.sqlite`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Optional MongoDB - only validate if present
|
|
159
|
+
if (config.mongodb !== undefined) {
|
|
160
|
+
result.mongodb = validateMongoDBConfig(config.mongodb, `${path}.mongodb`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Validate Redis configuration
|
|
168
|
+
*/
|
|
169
|
+
function validateRedisConfig(config: any, path: string) {
|
|
170
|
+
if (!config || typeof config !== 'object') {
|
|
171
|
+
throw new ConfigValidationError(
|
|
172
|
+
path,
|
|
173
|
+
config,
|
|
174
|
+
'object',
|
|
175
|
+
'Redis configuration must be an object'
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
url: validateString(config.url, `${path}.url`),
|
|
181
|
+
maxRetries: validateNumber(config.maxRetries, `${path}.maxRetries`, { min: 0 }),
|
|
182
|
+
retryDelay: validateNumber(config.retryDelay, `${path}.retryDelay`, { min: 0 }),
|
|
183
|
+
keyPrefix: validateString(config.keyPrefix, `${path}.keyPrefix`),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Validate MySQL configuration
|
|
189
|
+
*/
|
|
190
|
+
function validateMySQLConfig(config: any, path: string) {
|
|
191
|
+
if (!config || typeof config !== 'object') {
|
|
192
|
+
throw new ConfigValidationError(
|
|
193
|
+
path,
|
|
194
|
+
config,
|
|
195
|
+
'object',
|
|
196
|
+
'MySQL configuration must be an object'
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const result: any = {
|
|
201
|
+
host: validateString(config.host, `${path}.host`),
|
|
202
|
+
port: validatePort(config.port, `${path}.port`),
|
|
203
|
+
connectionLimit: validateNumber(config.connectionLimit, `${path}.connectionLimit`, { min: 1 }),
|
|
204
|
+
acquireTimeout: validateNumber(config.acquireTimeout, `${path}.acquireTimeout`, { min: 1000 }),
|
|
205
|
+
timeout: validateNumber(config.timeout, `${path}.timeout`, { min: 1000 }),
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Optional fields
|
|
209
|
+
if (config.database !== undefined) {
|
|
210
|
+
result.database = validateString(config.database, `${path}.database`);
|
|
211
|
+
}
|
|
212
|
+
if (config.username !== undefined) {
|
|
213
|
+
result.username = validateString(config.username, `${path}.username`);
|
|
214
|
+
}
|
|
215
|
+
if (config.password !== undefined) {
|
|
216
|
+
result.password = validateString(config.password, `${path}.password`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Validate PostgreSQL configuration
|
|
224
|
+
*/
|
|
225
|
+
function validatePostgreSQLConfig(config: any, path: string) {
|
|
226
|
+
if (!config || typeof config !== 'object') {
|
|
227
|
+
throw new ConfigValidationError(
|
|
228
|
+
path,
|
|
229
|
+
config,
|
|
230
|
+
'object',
|
|
231
|
+
'PostgreSQL configuration must be an object'
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const result: any = {
|
|
236
|
+
host: validateString(config.host, `${path}.host`),
|
|
237
|
+
port: validatePort(config.port, `${path}.port`),
|
|
238
|
+
connectionLimit: validateNumber(config.connectionLimit, `${path}.connectionLimit`, { min: 1 }),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Optional fields
|
|
242
|
+
if (config.database !== undefined) {
|
|
243
|
+
result.database = validateString(config.database, `${path}.database`);
|
|
244
|
+
}
|
|
245
|
+
if (config.user !== undefined) {
|
|
246
|
+
result.user = validateString(config.user, `${path}.user`);
|
|
247
|
+
}
|
|
248
|
+
if (config.password !== undefined) {
|
|
249
|
+
result.password = validateString(config.password, `${path}.password`);
|
|
250
|
+
}
|
|
251
|
+
if (config.ssl !== undefined) {
|
|
252
|
+
result.ssl = validateBoolean(config.ssl, `${path}.ssl`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Validate SQLite configuration
|
|
260
|
+
*/
|
|
261
|
+
function validateSQLiteConfig(config: any, path: string) {
|
|
262
|
+
if (!config || typeof config !== 'object') {
|
|
263
|
+
throw new ConfigValidationError(
|
|
264
|
+
path,
|
|
265
|
+
config,
|
|
266
|
+
'object',
|
|
267
|
+
'SQLite configuration must be an object'
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const result: any = {
|
|
272
|
+
filename: validateString(config.filename, `${path}.filename`),
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// Optional fields
|
|
276
|
+
if (config.memory !== undefined) {
|
|
277
|
+
result.memory = validateBoolean(config.memory, `${path}.memory`);
|
|
278
|
+
}
|
|
279
|
+
if (config.verbose !== undefined) {
|
|
280
|
+
result.verbose = validateBoolean(config.verbose, `${path}.verbose`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Validate MongoDB configuration
|
|
288
|
+
*/
|
|
289
|
+
function validateMongoDBConfig(config: any, path: string) {
|
|
290
|
+
if (!config || typeof config !== 'object') {
|
|
291
|
+
throw new ConfigValidationError(
|
|
292
|
+
path,
|
|
293
|
+
config,
|
|
294
|
+
'object',
|
|
295
|
+
'MongoDB configuration must be an object'
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const result: any = {};
|
|
300
|
+
|
|
301
|
+
// Either url or host+port
|
|
302
|
+
if (config.url !== undefined) {
|
|
303
|
+
result.url = validateString(config.url, `${path}.url`);
|
|
304
|
+
}
|
|
305
|
+
if (config.host !== undefined) {
|
|
306
|
+
result.host = validateString(config.host, `${path}.host`);
|
|
307
|
+
}
|
|
308
|
+
if (config.port !== undefined) {
|
|
309
|
+
result.port = validatePort(config.port, `${path}.port`);
|
|
310
|
+
}
|
|
311
|
+
if (config.database !== undefined) {
|
|
312
|
+
result.database = validateString(config.database, `${path}.database`);
|
|
313
|
+
}
|
|
314
|
+
if (config.username !== undefined) {
|
|
315
|
+
result.username = validateString(config.username, `${path}.username`);
|
|
316
|
+
}
|
|
317
|
+
if (config.password !== undefined) {
|
|
318
|
+
result.password = validateString(config.password, `${path}.password`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Validate module defaults configuration
|
|
326
|
+
*/
|
|
327
|
+
function validateModuleDefaultsConfig(config: any, path: string) {
|
|
328
|
+
if (!config || typeof config !== 'object') {
|
|
329
|
+
throw new ConfigValidationError(
|
|
330
|
+
path,
|
|
331
|
+
config,
|
|
332
|
+
'object',
|
|
333
|
+
'Module defaults configuration must be an object'
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
cache: validateCacheConfig(config.cache, `${path}.cache`),
|
|
339
|
+
rateLimit: validateRateLimitConfig(config.rateLimit, `${path}.rateLimit`),
|
|
340
|
+
validation: validateValidationConfig(config.validation, `${path}.validation`),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Validate cache configuration
|
|
346
|
+
*/
|
|
347
|
+
function validateCacheConfig(config: any, path: string) {
|
|
348
|
+
if (!config || typeof config !== 'object') {
|
|
349
|
+
throw new ConfigValidationError(
|
|
350
|
+
path,
|
|
351
|
+
config,
|
|
352
|
+
'object',
|
|
353
|
+
'Cache configuration must be an object'
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
enabled: validateBoolean(config.enabled, `${path}.enabled`),
|
|
359
|
+
defaultTtl: validateNumber(config.defaultTtl, `${path}.defaultTtl`, { min: 0 }),
|
|
360
|
+
maxSize: validateNumber(config.maxSize, `${path}.maxSize`, { min: 1 }),
|
|
361
|
+
strategy: validateEnum(config.strategy, ['lru', 'lfu', 'fifo'], `${path}.strategy`),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Validate rate limit configuration
|
|
367
|
+
*/
|
|
368
|
+
function validateRateLimitConfig(config: any, path: string) {
|
|
369
|
+
if (!config || typeof config !== 'object') {
|
|
370
|
+
throw new ConfigValidationError(
|
|
371
|
+
path,
|
|
372
|
+
config,
|
|
373
|
+
'object',
|
|
374
|
+
'Rate limit configuration must be an object'
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
enabled: validateBoolean(config.enabled, `${path}.enabled`),
|
|
380
|
+
defaultRequests: validateNumber(config.defaultRequests, `${path}.defaultRequests`, { min: 1 }),
|
|
381
|
+
defaultWindow: validateNumber(config.defaultWindow, `${path}.defaultWindow`, { min: 1000 }),
|
|
382
|
+
skipSuccessfulRequests: validateBoolean(
|
|
383
|
+
config.skipSuccessfulRequests,
|
|
384
|
+
`${path}.skipSuccessfulRequests`
|
|
385
|
+
),
|
|
386
|
+
skipFailedRequests: validateBoolean(config.skipFailedRequests, `${path}.skipFailedRequests`),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Validate validation configuration
|
|
392
|
+
*/
|
|
393
|
+
function validateValidationConfig(config: any, path: string) {
|
|
394
|
+
if (!config || typeof config !== 'object') {
|
|
395
|
+
throw new ConfigValidationError(
|
|
396
|
+
path,
|
|
397
|
+
config,
|
|
398
|
+
'object',
|
|
399
|
+
'Validation configuration must be an object'
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
enabled: validateBoolean(config.enabled, `${path}.enabled`),
|
|
405
|
+
stripUnknown: validateBoolean(config.stripUnknown, `${path}.stripUnknown`),
|
|
406
|
+
abortEarly: validateBoolean(config.abortEarly, `${path}.abortEarly`),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Validate logging configuration
|
|
412
|
+
*/
|
|
413
|
+
function validateLoggingConfig(config: any, path: string) {
|
|
414
|
+
if (!config || typeof config !== 'object') {
|
|
415
|
+
throw new ConfigValidationError(
|
|
416
|
+
path,
|
|
417
|
+
config,
|
|
418
|
+
'object',
|
|
419
|
+
'Logging configuration must be an object'
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
level: validateEnum(config.level, ['debug', 'info', 'warn', 'error', 'fatal'], `${path}.level`),
|
|
425
|
+
format: validateEnum(config.format, ['pretty', 'json', 'compact'], `${path}.format`),
|
|
426
|
+
enableColors: validateBoolean(config.enableColors, `${path}.enableColors`),
|
|
427
|
+
enableTimestamp: validateBoolean(config.enableTimestamp, `${path}.enableTimestamp`),
|
|
428
|
+
enableContext: validateBoolean(config.enableContext, `${path}.enableContext`),
|
|
429
|
+
outputs: validateLoggingOutputsConfig(config.outputs, `${path}.outputs`),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Validate logging outputs configuration
|
|
435
|
+
*/
|
|
436
|
+
function validateLoggingOutputsConfig(config: any, path: string) {
|
|
437
|
+
if (!config || typeof config !== 'object') {
|
|
438
|
+
throw new ConfigValidationError(
|
|
439
|
+
path,
|
|
440
|
+
config,
|
|
441
|
+
'object',
|
|
442
|
+
'Logging outputs configuration must be an object'
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
console: validateBoolean(config.console, `${path}.console`),
|
|
448
|
+
file: validateLoggingFileConfig(config.file, `${path}.file`),
|
|
449
|
+
webhook: validateLoggingWebhookConfig(config.webhook, `${path}.webhook`),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Validate logging file configuration
|
|
455
|
+
*/
|
|
456
|
+
function validateLoggingFileConfig(config: any, path: string) {
|
|
457
|
+
if (!config || typeof config !== 'object') {
|
|
458
|
+
throw new ConfigValidationError(
|
|
459
|
+
path,
|
|
460
|
+
config,
|
|
461
|
+
'object',
|
|
462
|
+
'Logging file configuration must be an object'
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
enabled: validateBoolean(config.enabled, `${path}.enabled`),
|
|
468
|
+
path: validateString(config.path, `${path}.path`),
|
|
469
|
+
maxSize: validateString(config.maxSize, `${path}.maxSize`),
|
|
470
|
+
maxFiles: validateNumber(config.maxFiles, `${path}.maxFiles`, { min: 1 }),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Validate logging webhook configuration
|
|
476
|
+
*/
|
|
477
|
+
function validateLoggingWebhookConfig(config: any, path: string) {
|
|
478
|
+
if (!config || typeof config !== 'object') {
|
|
479
|
+
throw new ConfigValidationError(
|
|
480
|
+
path,
|
|
481
|
+
config,
|
|
482
|
+
'object',
|
|
483
|
+
'Logging webhook configuration must be an object'
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const result: any = {
|
|
488
|
+
enabled: validateBoolean(config.enabled, `${path}.enabled`),
|
|
489
|
+
headers: validateObject(config.headers, `${path}.headers`),
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// Optional URL
|
|
493
|
+
if (config.url !== undefined) {
|
|
494
|
+
result.url = validateString(config.url, `${path}.url`);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return result;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Validate security configuration
|
|
502
|
+
*/
|
|
503
|
+
function validateSecurityConfig(config: any, path: string) {
|
|
504
|
+
if (!config || typeof config !== 'object') {
|
|
505
|
+
throw new ConfigValidationError(
|
|
506
|
+
path,
|
|
507
|
+
config,
|
|
508
|
+
'object',
|
|
509
|
+
'Security configuration must be an object'
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
cors: validateCorsConfig(config.cors, `${path}.cors`),
|
|
515
|
+
helmet: validateHelmetConfig(config.helmet, `${path}.helmet`),
|
|
516
|
+
rateLimit: validateSecurityRateLimitConfig(config.rateLimit, `${path}.rateLimit`),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Validate CORS configuration
|
|
522
|
+
*/
|
|
523
|
+
function validateCorsConfig(config: any, path: string) {
|
|
524
|
+
if (!config || typeof config !== 'object') {
|
|
525
|
+
throw new ConfigValidationError(path, config, 'object', 'CORS configuration must be an object');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
enabled: validateBoolean(config.enabled, `${path}.enabled`),
|
|
530
|
+
origin: validateCorsOrigin(config.origin, `${path}.origin`),
|
|
531
|
+
methods: validateStringArray(config.methods, `${path}.methods`),
|
|
532
|
+
allowedHeaders: validateStringArray(config.allowedHeaders, `${path}.allowedHeaders`),
|
|
533
|
+
credentials: validateBoolean(config.credentials, `${path}.credentials`),
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Validate CORS origin (can be string, array, or boolean)
|
|
539
|
+
*/
|
|
540
|
+
function validateCorsOrigin(value: any, path: string): string | string[] | boolean {
|
|
541
|
+
if (typeof value === 'boolean' || typeof value === 'string') {
|
|
542
|
+
return value;
|
|
543
|
+
}
|
|
544
|
+
if (Array.isArray(value)) {
|
|
545
|
+
return validateStringArray(value, path);
|
|
546
|
+
}
|
|
547
|
+
throw new ConfigValidationError(
|
|
548
|
+
path,
|
|
549
|
+
value,
|
|
550
|
+
'string | string[] | boolean',
|
|
551
|
+
'Must be a string, array of strings, or boolean'
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Validate helmet configuration
|
|
557
|
+
*/
|
|
558
|
+
function validateHelmetConfig(config: any, path: string) {
|
|
559
|
+
if (!config || typeof config !== 'object') {
|
|
560
|
+
throw new ConfigValidationError(
|
|
561
|
+
path,
|
|
562
|
+
config,
|
|
563
|
+
'object',
|
|
564
|
+
'Helmet configuration must be an object'
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
enabled: validateBoolean(config.enabled, `${path}.enabled`),
|
|
570
|
+
contentSecurityPolicy: validateBoolean(
|
|
571
|
+
config.contentSecurityPolicy,
|
|
572
|
+
`${path}.contentSecurityPolicy`
|
|
573
|
+
),
|
|
574
|
+
hsts: validateBoolean(config.hsts, `${path}.hsts`),
|
|
575
|
+
noSniff: validateBoolean(config.noSniff, `${path}.noSniff`),
|
|
576
|
+
frameguard: validateBoolean(config.frameguard, `${path}.frameguard`),
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Validate security rate limit configuration
|
|
582
|
+
*/
|
|
583
|
+
function validateSecurityRateLimitConfig(config: any, path: string) {
|
|
584
|
+
if (!config || typeof config !== 'object') {
|
|
585
|
+
throw new ConfigValidationError(
|
|
586
|
+
path,
|
|
587
|
+
config,
|
|
588
|
+
'object',
|
|
589
|
+
'Security rate limit configuration must be an object'
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return {
|
|
594
|
+
global: validateGlobalRateLimitConfig(config.global, `${path}.global`),
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Validate global rate limit configuration
|
|
600
|
+
*/
|
|
601
|
+
function validateGlobalRateLimitConfig(config: any, path: string) {
|
|
602
|
+
if (!config || typeof config !== 'object') {
|
|
603
|
+
throw new ConfigValidationError(
|
|
604
|
+
path,
|
|
605
|
+
config,
|
|
606
|
+
'object',
|
|
607
|
+
'Global rate limit configuration must be an object'
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
enabled: validateBoolean(config.enabled, `${path}.enabled`),
|
|
613
|
+
requests: validateNumber(config.requests, `${path}.requests`, { min: 1 }),
|
|
614
|
+
window: validateNumber(config.window, `${path}.window`, { min: 1000 }),
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Validate external services configuration
|
|
620
|
+
*/
|
|
621
|
+
function validateExternalServicesConfig(config: any, path: string) {
|
|
622
|
+
if (!config || typeof config !== 'object') {
|
|
623
|
+
throw new ConfigValidationError(
|
|
624
|
+
path,
|
|
625
|
+
config,
|
|
626
|
+
'object',
|
|
627
|
+
'External services configuration must be an object'
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const result: any = {};
|
|
632
|
+
|
|
633
|
+
// Optional services - only validate if present
|
|
634
|
+
if (config.stripe !== undefined) {
|
|
635
|
+
result.stripe = validateStripeConfig(config.stripe, `${path}.stripe`);
|
|
636
|
+
}
|
|
637
|
+
if (config.paypal !== undefined) {
|
|
638
|
+
result.paypal = validatePayPalConfig(config.paypal, `${path}.paypal`);
|
|
639
|
+
}
|
|
640
|
+
if (config.smtp !== undefined) {
|
|
641
|
+
result.smtp = validateSMTPConfig(config.smtp, `${path}.smtp`);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return result;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Validate Stripe configuration
|
|
649
|
+
*/
|
|
650
|
+
function validateStripeConfig(config: any, path: string) {
|
|
651
|
+
if (!config || typeof config !== 'object') {
|
|
652
|
+
throw new ConfigValidationError(
|
|
653
|
+
path,
|
|
654
|
+
config,
|
|
655
|
+
'object',
|
|
656
|
+
'Stripe configuration must be an object'
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const result: any = {};
|
|
661
|
+
|
|
662
|
+
// Optional fields
|
|
663
|
+
if (config.secretKey !== undefined) {
|
|
664
|
+
result.secretKey = validateString(config.secretKey, `${path}.secretKey`);
|
|
665
|
+
}
|
|
666
|
+
if (config.publishableKey !== undefined) {
|
|
667
|
+
result.publishableKey = validateString(config.publishableKey, `${path}.publishableKey`);
|
|
668
|
+
}
|
|
669
|
+
if (config.webhookSecret !== undefined) {
|
|
670
|
+
result.webhookSecret = validateString(config.webhookSecret, `${path}.webhookSecret`);
|
|
671
|
+
}
|
|
672
|
+
if (config.apiVersion !== undefined) {
|
|
673
|
+
result.apiVersion = validateString(config.apiVersion, `${path}.apiVersion`);
|
|
674
|
+
} else {
|
|
675
|
+
result.apiVersion = '2023-10-16'; // Default API version
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return result;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Validate PayPal configuration
|
|
683
|
+
*/
|
|
684
|
+
function validatePayPalConfig(config: any, path: string) {
|
|
685
|
+
if (!config || typeof config !== 'object') {
|
|
686
|
+
throw new ConfigValidationError(
|
|
687
|
+
path,
|
|
688
|
+
config,
|
|
689
|
+
'object',
|
|
690
|
+
'PayPal configuration must be an object'
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const result: any = {
|
|
695
|
+
environment: validateEnum(config.environment, ['sandbox', 'production'], `${path}.environment`),
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
// Optional fields
|
|
699
|
+
if (config.clientId !== undefined) {
|
|
700
|
+
result.clientId = validateString(config.clientId, `${path}.clientId`);
|
|
701
|
+
}
|
|
702
|
+
if (config.clientSecret !== undefined) {
|
|
703
|
+
result.clientSecret = validateString(config.clientSecret, `${path}.clientSecret`);
|
|
704
|
+
}
|
|
705
|
+
if (config.webhookId !== undefined) {
|
|
706
|
+
result.webhookId = validateString(config.webhookId, `${path}.webhookId`);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return result;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Validate SMTP configuration
|
|
714
|
+
*/
|
|
715
|
+
function validateSMTPConfig(config: any, path: string) {
|
|
716
|
+
if (!config || typeof config !== 'object') {
|
|
717
|
+
throw new ConfigValidationError(path, config, 'object', 'SMTP configuration must be an object');
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const result: any = {
|
|
721
|
+
port: validatePort(config.port, `${path}.port`),
|
|
722
|
+
secure: validateBoolean(config.secure, `${path}.secure`),
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
// Optional fields
|
|
726
|
+
if (config.host !== undefined) {
|
|
727
|
+
result.host = validateString(config.host, `${path}.host`);
|
|
728
|
+
}
|
|
729
|
+
if (config.username !== undefined) {
|
|
730
|
+
result.username = validateString(config.username, `${path}.username`);
|
|
731
|
+
}
|
|
732
|
+
if (config.password !== undefined) {
|
|
733
|
+
result.password = validateString(config.password, `${path}.password`);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return result;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Validate performance configuration
|
|
741
|
+
*/
|
|
742
|
+
function validatePerformanceConfig(config: any, path: string) {
|
|
743
|
+
if (!config || typeof config !== 'object') {
|
|
744
|
+
throw new ConfigValidationError(
|
|
745
|
+
path,
|
|
746
|
+
config,
|
|
747
|
+
'object',
|
|
748
|
+
'Performance configuration must be an object'
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
return {
|
|
753
|
+
compression: validateCompressionConfig(config.compression, `${path}.compression`),
|
|
754
|
+
circuitBreaker: validateCircuitBreakerConfig(config.circuitBreaker, `${path}.circuitBreaker`),
|
|
755
|
+
clustering: validateClusteringConfig(config.clustering, `${path}.clustering`),
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Validate compression configuration
|
|
761
|
+
*/
|
|
762
|
+
function validateCompressionConfig(config: any, path: string) {
|
|
763
|
+
if (!config || typeof config !== 'object') {
|
|
764
|
+
throw new ConfigValidationError(
|
|
765
|
+
path,
|
|
766
|
+
config,
|
|
767
|
+
'object',
|
|
768
|
+
'Compression configuration must be an object'
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return {
|
|
773
|
+
enabled: validateBoolean(config.enabled, `${path}.enabled`),
|
|
774
|
+
level: validateNumber(config.level, `${path}.level`, { min: 1, max: 9 }),
|
|
775
|
+
threshold: validateNumber(config.threshold, `${path}.threshold`, { min: 0 }),
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Validate circuit breaker configuration
|
|
781
|
+
*/
|
|
782
|
+
function validateCircuitBreakerConfig(config: any, path: string) {
|
|
783
|
+
if (!config || typeof config !== 'object') {
|
|
784
|
+
throw new ConfigValidationError(
|
|
785
|
+
path,
|
|
786
|
+
config,
|
|
787
|
+
'object',
|
|
788
|
+
'Circuit breaker configuration must be an object'
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return {
|
|
793
|
+
enabled: validateBoolean(config.enabled, `${path}.enabled`),
|
|
794
|
+
failureThreshold: validateNumber(config.failureThreshold, `${path}.failureThreshold`, {
|
|
795
|
+
min: 1,
|
|
796
|
+
}),
|
|
797
|
+
resetTimeout: validateNumber(config.resetTimeout, `${path}.resetTimeout`, { min: 1000 }),
|
|
798
|
+
monitoringPeriod: validateNumber(config.monitoringPeriod, `${path}.monitoringPeriod`, {
|
|
799
|
+
min: 1000,
|
|
800
|
+
}),
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Validate clustering configuration
|
|
806
|
+
*/
|
|
807
|
+
function validateClusteringConfig(config: any, path: string) {
|
|
808
|
+
if (!config || typeof config !== 'object') {
|
|
809
|
+
throw new ConfigValidationError(
|
|
810
|
+
path,
|
|
811
|
+
config,
|
|
812
|
+
'object',
|
|
813
|
+
'Clustering configuration must be an object'
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const result: any = {
|
|
818
|
+
enabled: validateBoolean(config.enabled, `${path}.enabled`),
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
// Workers can be number or 'auto'
|
|
822
|
+
if (typeof config.workers === 'string' && config.workers === 'auto') {
|
|
823
|
+
result.workers = 'auto';
|
|
824
|
+
} else {
|
|
825
|
+
result.workers = validateNumber(config.workers, `${path}.workers`, { min: 1 });
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Optional memoryPerWorkerGB
|
|
829
|
+
if (config.memoryPerWorkerGB !== undefined) {
|
|
830
|
+
result.memoryPerWorkerGB = validateNumber(
|
|
831
|
+
config.memoryPerWorkerGB,
|
|
832
|
+
`${path}.memoryPerWorkerGB`,
|
|
833
|
+
{ min: 0.1 }
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return result;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Validate WebSocket configuration
|
|
842
|
+
*/
|
|
843
|
+
function validateWebSocketConfig(config: any, path: string) {
|
|
844
|
+
if (!config || typeof config !== 'object') {
|
|
845
|
+
throw new ConfigValidationError(
|
|
846
|
+
path,
|
|
847
|
+
config,
|
|
848
|
+
'object',
|
|
849
|
+
'WebSocket configuration must be an object'
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const result: any = {
|
|
854
|
+
enabled: validateBoolean(config.enabled, `${path}.enabled`),
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
// Optional fields
|
|
858
|
+
if (config.adapter !== undefined) {
|
|
859
|
+
result.adapter = validateString(config.adapter, `${path}.adapter`);
|
|
860
|
+
}
|
|
861
|
+
if (config.compression !== undefined) {
|
|
862
|
+
result.compression = validateBoolean(config.compression, `${path}.compression`);
|
|
863
|
+
}
|
|
864
|
+
if (config.customIdGenerator !== undefined) {
|
|
865
|
+
result.customIdGenerator = config.customIdGenerator; // Function - no validation needed
|
|
866
|
+
}
|
|
867
|
+
if (config.options !== undefined) {
|
|
868
|
+
result.options = validateWebSocketOptions(config.options, `${path}.options`);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
return result;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Validate WebSocket options
|
|
876
|
+
*/
|
|
877
|
+
function validateWebSocketOptions(config: any, path: string) {
|
|
878
|
+
if (!config || typeof config !== 'object') {
|
|
879
|
+
throw new ConfigValidationError(path, config, 'object', 'WebSocket options must be an object');
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const result: any = {};
|
|
883
|
+
|
|
884
|
+
if (config.cors !== undefined) {
|
|
885
|
+
result.cors = validateWebSocketCorsOptions(config.cors, `${path}.cors`);
|
|
886
|
+
}
|
|
887
|
+
if (config.path !== undefined) {
|
|
888
|
+
result.path = validateString(config.path, `${path}.path`);
|
|
889
|
+
}
|
|
890
|
+
if (config.maxPayloadLength !== undefined) {
|
|
891
|
+
result.maxPayloadLength = validateNumber(config.maxPayloadLength, `${path}.maxPayloadLength`, {
|
|
892
|
+
min: 1024,
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return result;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Validate WebSocket CORS options
|
|
901
|
+
*/
|
|
902
|
+
function validateWebSocketCorsOptions(config: any, path: string) {
|
|
903
|
+
if (!config || typeof config !== 'object') {
|
|
904
|
+
throw new ConfigValidationError(
|
|
905
|
+
path,
|
|
906
|
+
config,
|
|
907
|
+
'object',
|
|
908
|
+
'WebSocket CORS options must be an object'
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const result: any = {};
|
|
913
|
+
|
|
914
|
+
if (config.origin !== undefined) {
|
|
915
|
+
result.origin = validateCorsOrigin(config.origin, `${path}.origin`);
|
|
916
|
+
}
|
|
917
|
+
if (config.credentials !== undefined) {
|
|
918
|
+
result.credentials = validateBoolean(config.credentials, `${path}.credentials`);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
return result;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Basic validation functions
|
|
925
|
+
|
|
926
|
+
function validatePort(value: any, path: string): number {
|
|
927
|
+
const num = Number(value);
|
|
928
|
+
if (isNaN(num) || num < 1 || num > 65535) {
|
|
929
|
+
throw new ConfigValidationError(
|
|
930
|
+
path,
|
|
931
|
+
value,
|
|
932
|
+
'number (1-65535)',
|
|
933
|
+
'Must be a number between 1 and 65535'
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
return num;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function validateBoolean(value: any, path: string): boolean {
|
|
940
|
+
if (value === 'true' || value === true) return true;
|
|
941
|
+
if (value === 'false' || value === false) return false;
|
|
942
|
+
if (value === '1' || value === 1) return true;
|
|
943
|
+
if (value === '0' || value === 0) return false;
|
|
944
|
+
throw new ConfigValidationError(
|
|
945
|
+
path,
|
|
946
|
+
value,
|
|
947
|
+
'boolean',
|
|
948
|
+
'Must be a boolean (true/false) or numeric (1/0)'
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function validateNumber(
|
|
953
|
+
value: any,
|
|
954
|
+
path: string,
|
|
955
|
+
options: { min?: number; max?: number } = {}
|
|
956
|
+
): number {
|
|
957
|
+
const num = Number(value);
|
|
958
|
+
if (isNaN(num)) {
|
|
959
|
+
throw new ConfigValidationError(path, value, 'number', 'Must be a valid number');
|
|
960
|
+
}
|
|
961
|
+
if (options.min !== undefined && num < options.min) {
|
|
962
|
+
throw new ConfigValidationError(
|
|
963
|
+
path,
|
|
964
|
+
value,
|
|
965
|
+
`number >= ${options.min}`,
|
|
966
|
+
`Must be at least ${options.min}`
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
if (options.max !== undefined && num > options.max) {
|
|
970
|
+
throw new ConfigValidationError(
|
|
971
|
+
path,
|
|
972
|
+
value,
|
|
973
|
+
`number <= ${options.max}`,
|
|
974
|
+
`Must be at most ${options.max}`
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
return num;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function validateString(value: any, path: string): string {
|
|
981
|
+
if (typeof value !== 'string') {
|
|
982
|
+
throw new ConfigValidationError(path, value, 'string', 'Must be a string');
|
|
983
|
+
}
|
|
984
|
+
return value;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function validateEnum<T extends string>(value: any, validValues: readonly T[], path: string): T {
|
|
988
|
+
const str = validateString(value, path);
|
|
989
|
+
if (!validValues.includes(str as T)) {
|
|
990
|
+
throw new ConfigValidationError(
|
|
991
|
+
path,
|
|
992
|
+
value,
|
|
993
|
+
`one of: ${validValues.join(', ')}`,
|
|
994
|
+
`Must be one of: ${validValues.join(', ')}`
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
return str as T;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function validateStringArray(value: any, path: string): string[] {
|
|
1001
|
+
if (!Array.isArray(value)) {
|
|
1002
|
+
// Try to parse comma-separated string
|
|
1003
|
+
if (typeof value === 'string') {
|
|
1004
|
+
return value
|
|
1005
|
+
.split(',')
|
|
1006
|
+
.map(s => s.trim())
|
|
1007
|
+
.filter(s => s.length > 0);
|
|
1008
|
+
}
|
|
1009
|
+
throw new ConfigValidationError(
|
|
1010
|
+
path,
|
|
1011
|
+
value,
|
|
1012
|
+
'string[]',
|
|
1013
|
+
'Must be an array or comma-separated string'
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
return value.map((item, index) => validateString(item, `${path}[${index}]`));
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function validateObject(value: any, path: string): Record<string, string> {
|
|
1020
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1021
|
+
throw new ConfigValidationError(path, value, 'object', 'Must be an object');
|
|
1022
|
+
}
|
|
1023
|
+
return value;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Provide helpful validation hints based on the error
|
|
1028
|
+
*/
|
|
1029
|
+
function provideValidationHints(error: ConfigValidationError): void {
|
|
1030
|
+
if (error.field.includes('port')) {
|
|
1031
|
+
logger.error(' 💡 Hint: Ports must be numbers between 1 and 65535');
|
|
1032
|
+
}
|
|
1033
|
+
if (error.field.includes('url')) {
|
|
1034
|
+
logger.error(' 💡 Hint: URLs must include protocol (http:// or https://)');
|
|
1035
|
+
}
|
|
1036
|
+
if (error.field.includes('environment')) {
|
|
1037
|
+
logger.error(' 💡 Hint: NODE_ENV must be one of: development, staging, production');
|
|
1038
|
+
}
|
|
1039
|
+
if (error.field.includes('level')) {
|
|
1040
|
+
logger.error(' 💡 Hint: Log level must be one of: debug, info, warn, error, fatal');
|
|
1041
|
+
}
|
|
1042
|
+
}
|