@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
package/dist/types/config.d.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
export interface ServerConfig {
|
|
2
2
|
port: number;
|
|
3
3
|
host: string;
|
|
4
|
-
environment: 'development' | 'staging' | 'production';
|
|
5
4
|
maxConnections: number;
|
|
6
5
|
timeout: number;
|
|
6
|
+
bodySizeLimit: string;
|
|
7
|
+
requestTracking: {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
};
|
|
10
|
+
errorBoundary: {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
};
|
|
7
13
|
}
|
|
8
14
|
export interface ServiceDiscoveryConfig {
|
|
9
15
|
enabled: boolean;
|
|
@@ -15,7 +21,7 @@ export interface ServiceDiscoveryConfig {
|
|
|
15
21
|
}
|
|
16
22
|
export interface DatabaseConfig {
|
|
17
23
|
url?: string;
|
|
18
|
-
redis
|
|
24
|
+
redis?: {
|
|
19
25
|
url: string;
|
|
20
26
|
maxRetries: number;
|
|
21
27
|
retryDelay: number;
|
|
@@ -31,6 +37,28 @@ export interface DatabaseConfig {
|
|
|
31
37
|
acquireTimeout: number;
|
|
32
38
|
timeout: number;
|
|
33
39
|
};
|
|
40
|
+
postgresql?: {
|
|
41
|
+
host: string;
|
|
42
|
+
port: number;
|
|
43
|
+
database?: string;
|
|
44
|
+
user?: string;
|
|
45
|
+
password?: string;
|
|
46
|
+
connectionLimit: number;
|
|
47
|
+
ssl?: boolean;
|
|
48
|
+
};
|
|
49
|
+
sqlite?: {
|
|
50
|
+
filename: string;
|
|
51
|
+
memory?: boolean;
|
|
52
|
+
verbose?: boolean;
|
|
53
|
+
};
|
|
54
|
+
mongodb?: {
|
|
55
|
+
url?: string;
|
|
56
|
+
host?: string;
|
|
57
|
+
port?: number;
|
|
58
|
+
database?: string;
|
|
59
|
+
username?: string;
|
|
60
|
+
password?: string;
|
|
61
|
+
};
|
|
34
62
|
}
|
|
35
63
|
export interface ModuleDefaultsConfig {
|
|
36
64
|
cache: {
|
|
@@ -132,6 +160,21 @@ export interface PerformanceConfig {
|
|
|
132
160
|
clustering: {
|
|
133
161
|
enabled: boolean;
|
|
134
162
|
workers: number | 'auto';
|
|
163
|
+
memoryPerWorkerGB?: number;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
export interface WebSocketConfig {
|
|
167
|
+
enabled: boolean;
|
|
168
|
+
adapter?: string | 'socket.io' | 'ws';
|
|
169
|
+
compression?: boolean;
|
|
170
|
+
customIdGenerator?: () => string;
|
|
171
|
+
options?: {
|
|
172
|
+
cors?: {
|
|
173
|
+
origin?: string | string[];
|
|
174
|
+
credentials?: boolean;
|
|
175
|
+
};
|
|
176
|
+
path?: string;
|
|
177
|
+
maxPayloadLength?: number;
|
|
135
178
|
};
|
|
136
179
|
}
|
|
137
180
|
export interface AppConfig {
|
|
@@ -143,4 +186,5 @@ export interface AppConfig {
|
|
|
143
186
|
security: SecurityConfig;
|
|
144
187
|
external: ExternalServicesConfig;
|
|
145
188
|
performance: PerformanceConfig;
|
|
189
|
+
websocket: WebSocketConfig;
|
|
146
190
|
}
|
package/dist/types/core.d.ts
CHANGED
|
@@ -1,50 +1,33 @@
|
|
|
1
1
|
import { RuntimeConfig } from './runtime';
|
|
2
2
|
import { LoggerOptions } from './logger';
|
|
3
|
+
import { AppConfig } from './config';
|
|
3
4
|
export interface MoroOptions {
|
|
4
5
|
autoDiscover?: boolean;
|
|
5
6
|
modulesPath?: string;
|
|
6
7
|
middleware?: any[];
|
|
7
|
-
|
|
8
|
+
runtime?: RuntimeConfig;
|
|
9
|
+
http2?: boolean;
|
|
10
|
+
https?: {
|
|
11
|
+
key: string | Buffer;
|
|
12
|
+
cert: string | Buffer;
|
|
13
|
+
ca?: string | Buffer;
|
|
14
|
+
};
|
|
15
|
+
websocket?: {
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
adapter?: any;
|
|
18
|
+
compression?: boolean;
|
|
19
|
+
customIdGenerator?: () => string;
|
|
20
|
+
options?: any;
|
|
21
|
+
} | false;
|
|
8
22
|
cors?: boolean | object;
|
|
9
23
|
compression?: boolean | object;
|
|
10
24
|
helmet?: boolean | object;
|
|
11
|
-
runtime?: RuntimeConfig;
|
|
12
25
|
logger?: LoggerOptions | boolean;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
rateLimit?: {
|
|
21
|
-
enabled?: boolean;
|
|
22
|
-
defaultRequests?: number;
|
|
23
|
-
defaultWindow?: number;
|
|
24
|
-
skipSuccessfulRequests?: boolean;
|
|
25
|
-
skipFailedRequests?: boolean;
|
|
26
|
-
};
|
|
27
|
-
validation?: {
|
|
28
|
-
enabled?: boolean;
|
|
29
|
-
stripUnknown?: boolean;
|
|
30
|
-
abortEarly?: boolean;
|
|
31
|
-
};
|
|
32
|
-
};
|
|
33
|
-
performance?: {
|
|
34
|
-
clustering?: {
|
|
35
|
-
enabled?: boolean;
|
|
36
|
-
workers?: number | 'auto';
|
|
37
|
-
};
|
|
38
|
-
compression?: {
|
|
39
|
-
enabled?: boolean;
|
|
40
|
-
level?: number;
|
|
41
|
-
threshold?: number;
|
|
42
|
-
};
|
|
43
|
-
circuitBreaker?: {
|
|
44
|
-
enabled?: boolean;
|
|
45
|
-
failureThreshold?: number;
|
|
46
|
-
resetTimeout?: number;
|
|
47
|
-
monitoringPeriod?: number;
|
|
48
|
-
};
|
|
49
|
-
};
|
|
26
|
+
server?: Partial<AppConfig['server']>;
|
|
27
|
+
database?: Partial<AppConfig['database']>;
|
|
28
|
+
modules?: Partial<AppConfig['modules']>;
|
|
29
|
+
logging?: Partial<AppConfig['logging']>;
|
|
30
|
+
security?: Partial<AppConfig['security']>;
|
|
31
|
+
external?: Partial<AppConfig['external']>;
|
|
32
|
+
performance?: Partial<AppConfig['performance']>;
|
|
50
33
|
}
|
package/dist/types/logger.d.ts
CHANGED
|
@@ -49,10 +49,14 @@ export interface Logger {
|
|
|
49
49
|
timeEnd(label: string, context?: string, metadata?: Record<string, any>): void;
|
|
50
50
|
child(context: string, metadata?: Record<string, any>): Logger;
|
|
51
51
|
setLevel(level: LogLevel): void;
|
|
52
|
+
getLevel(): LogLevel;
|
|
52
53
|
addOutput(output: LogOutput): void;
|
|
53
54
|
removeOutput(name: string): void;
|
|
54
55
|
addFilter(filter: LogFilter): void;
|
|
55
56
|
removeFilter(name: string): void;
|
|
57
|
+
flush(): void;
|
|
58
|
+
flushBuffer(): void;
|
|
59
|
+
destroy(): void;
|
|
56
60
|
getHistory(count?: number): LogEntry[];
|
|
57
61
|
getMetrics(): LogMetrics;
|
|
58
62
|
clear(): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morojs/moro",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.7",
|
|
4
4
|
"description": "High-performance Node.js framework with intelligent routing, automatic middleware ordering, enterprise authentication (Auth.js), type-safe Zod validation, and functional architecture",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Manager - Immutable Single Source of Truth
|
|
3
|
+
*
|
|
4
|
+
* This module provides centralized, immutable configuration state management.
|
|
5
|
+
* Configuration is locked at createApp() initialization and cannot be changed afterward.
|
|
6
|
+
*
|
|
7
|
+
* Precedence: Environment Variables > createApp Options > Config File > Defaults
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AppConfig } from '../../types/config';
|
|
11
|
+
import { createFrameworkLogger } from '../logger';
|
|
12
|
+
|
|
13
|
+
const logger = createFrameworkLogger('ConfigManager');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Global configuration state - immutable after initialization
|
|
17
|
+
*/
|
|
18
|
+
let globalConfig: Readonly<AppConfig> | null = null;
|
|
19
|
+
let isLocked = false;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Initialize and lock global configuration state
|
|
23
|
+
* This should only be called once during createApp() initialization
|
|
24
|
+
*/
|
|
25
|
+
export function initializeAndLockConfig(config: AppConfig): void {
|
|
26
|
+
if (isLocked) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
'Configuration is already locked and cannot be changed. ' +
|
|
29
|
+
'Configuration can only be set once during createApp() initialization.'
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Deep freeze the configuration to make it truly immutable
|
|
34
|
+
globalConfig = deepFreeze(config);
|
|
35
|
+
isLocked = true;
|
|
36
|
+
|
|
37
|
+
logger.info(
|
|
38
|
+
`Configuration locked and initialized: ${process.env.NODE_ENV || 'development'}:${config.server.port}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the current global configuration
|
|
44
|
+
* Throws if configuration hasn't been initialized
|
|
45
|
+
*/
|
|
46
|
+
export function getGlobalConfig(): Readonly<AppConfig> {
|
|
47
|
+
if (!globalConfig || !isLocked) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
'Configuration not initialized. Call createApp() to initialize the configuration system.'
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return globalConfig;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if configuration has been initialized and locked
|
|
57
|
+
*/
|
|
58
|
+
export function isConfigLocked(): boolean {
|
|
59
|
+
return isLocked && globalConfig !== null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reset configuration state (for testing only)
|
|
64
|
+
* @internal - This should only be used in tests
|
|
65
|
+
*/
|
|
66
|
+
export function resetConfigForTesting(): void {
|
|
67
|
+
if (
|
|
68
|
+
process.env.NODE_ENV !== 'test' &&
|
|
69
|
+
!process.env.MORO_ALLOW_CONFIG_RESET &&
|
|
70
|
+
!process.env.JEST_WORKER_ID
|
|
71
|
+
) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
'Configuration reset is only allowed in test environments. ' +
|
|
74
|
+
'Set MORO_ALLOW_CONFIG_RESET=true to override this check.'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
globalConfig = null;
|
|
79
|
+
isLocked = false;
|
|
80
|
+
logger.debug('Configuration state reset for testing');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Deep freeze an object to make it truly immutable
|
|
85
|
+
* This prevents any accidental mutations to the configuration
|
|
86
|
+
*/
|
|
87
|
+
function deepFreeze<T>(obj: T): Readonly<T> {
|
|
88
|
+
// Get property names
|
|
89
|
+
const propNames = Object.getOwnPropertyNames(obj);
|
|
90
|
+
|
|
91
|
+
// Freeze properties before freezing self
|
|
92
|
+
for (const name of propNames) {
|
|
93
|
+
const value = (obj as any)[name];
|
|
94
|
+
|
|
95
|
+
if (value && typeof value === 'object') {
|
|
96
|
+
deepFreeze(value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return Object.freeze(obj);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get a specific configuration value using dot notation
|
|
105
|
+
* This provides a safe way to access nested config values
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* getConfigValue('server.port') // Returns the server port
|
|
109
|
+
* getConfigValue('database.redis.url') // Returns the Redis URL
|
|
110
|
+
*/
|
|
111
|
+
export function getConfigValue<T = any>(path: string): T | undefined {
|
|
112
|
+
const config = getGlobalConfig();
|
|
113
|
+
|
|
114
|
+
return path.split('.').reduce((obj: any, key: string) => {
|
|
115
|
+
return obj && obj[key] !== undefined ? obj[key] : undefined;
|
|
116
|
+
}, config);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Utility functions for common environment checks
|
|
121
|
+
* These now read NODE_ENV directly for consistency with Node.js ecosystem
|
|
122
|
+
*/
|
|
123
|
+
export function isDevelopment(): boolean {
|
|
124
|
+
return process.env.NODE_ENV === 'development';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function isProduction(): boolean {
|
|
128
|
+
return process.env.NODE_ENV === 'production';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function isStaging(): boolean {
|
|
132
|
+
return process.env.NODE_ENV === 'staging';
|
|
133
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Sources - Load from Environment, Files, and Options
|
|
3
|
+
*
|
|
4
|
+
* This module handles loading configuration from different sources with clear precedence:
|
|
5
|
+
* Environment Variables > createApp Options > Config File > Schema Defaults
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { AppConfig } from '../../types/config';
|
|
9
|
+
import { MoroOptions } from '../../types/core';
|
|
10
|
+
import { DEFAULT_CONFIG } from './schema';
|
|
11
|
+
import { loadConfigFileSync } from './file-loader';
|
|
12
|
+
import { createFrameworkLogger } from '../logger';
|
|
13
|
+
import { validateConfig } from './config-validator';
|
|
14
|
+
|
|
15
|
+
const logger = createFrameworkLogger('ConfigSources');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Configuration source metadata for debugging
|
|
19
|
+
*/
|
|
20
|
+
export interface ConfigSourceInfo {
|
|
21
|
+
source: 'environment' | 'createApp' | 'configFile' | 'default';
|
|
22
|
+
path: string;
|
|
23
|
+
value: any;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Load configuration from all sources with proper precedence
|
|
28
|
+
* Returns a validated, complete configuration object
|
|
29
|
+
*/
|
|
30
|
+
export function loadConfigFromAllSources(createAppOptions?: MoroOptions): AppConfig {
|
|
31
|
+
logger.debug('Loading configuration from all sources');
|
|
32
|
+
|
|
33
|
+
// 1. Start with schema defaults
|
|
34
|
+
let config = JSON.parse(JSON.stringify(DEFAULT_CONFIG)) as AppConfig;
|
|
35
|
+
const sourceMap = new Map<string, ConfigSourceInfo>();
|
|
36
|
+
|
|
37
|
+
// Track default values
|
|
38
|
+
trackConfigSource(config, sourceMap, 'default', 'schema');
|
|
39
|
+
|
|
40
|
+
// 2. Load and merge config file (if exists)
|
|
41
|
+
try {
|
|
42
|
+
const fileConfig = loadConfigFileSync();
|
|
43
|
+
if (fileConfig) {
|
|
44
|
+
config = deepMerge(config, fileConfig);
|
|
45
|
+
trackConfigSource(fileConfig, sourceMap, 'configFile', 'moro.config.js/ts');
|
|
46
|
+
logger.debug('Config file loaded and merged');
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
logger.warn('Config file loading failed, continuing without it:', String(error));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 3. Load and merge environment variables
|
|
53
|
+
const envConfig = loadEnvironmentConfig();
|
|
54
|
+
config = deepMerge(config, envConfig);
|
|
55
|
+
trackConfigSource(envConfig, sourceMap, 'environment', 'process.env');
|
|
56
|
+
|
|
57
|
+
// 4. Load and merge createApp options (highest precedence)
|
|
58
|
+
if (createAppOptions) {
|
|
59
|
+
const normalizedOptions = normalizeCreateAppOptions(createAppOptions);
|
|
60
|
+
config = deepMerge(config, normalizedOptions);
|
|
61
|
+
trackConfigSource(normalizedOptions, sourceMap, 'createApp', 'createApp()');
|
|
62
|
+
logger.debug('createApp options merged');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 5. Validate the final configuration
|
|
66
|
+
const validatedConfig = validateConfig(config);
|
|
67
|
+
|
|
68
|
+
// Log configuration sources for debugging
|
|
69
|
+
logConfigurationSources(sourceMap);
|
|
70
|
+
|
|
71
|
+
return validatedConfig;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Load configuration from environment variables
|
|
76
|
+
* Handles both standard and MORO_ prefixed variables
|
|
77
|
+
*/
|
|
78
|
+
function loadEnvironmentConfig(): Partial<AppConfig> {
|
|
79
|
+
const config: Partial<AppConfig> = {};
|
|
80
|
+
|
|
81
|
+
// Server configuration
|
|
82
|
+
if (process.env.PORT || process.env.MORO_PORT) {
|
|
83
|
+
if (!config.server) config.server = {} as any;
|
|
84
|
+
config.server!.port = parseInt(process.env.PORT || process.env.MORO_PORT || '3001', 10);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (process.env.HOST || process.env.MORO_HOST) {
|
|
88
|
+
if (!config.server) config.server = {} as any;
|
|
89
|
+
config.server!.host = process.env.HOST || process.env.MORO_HOST || 'localhost';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (process.env.MAX_CONNECTIONS || process.env.MORO_MAX_CONNECTIONS) {
|
|
93
|
+
if (!config.server) config.server = {} as any;
|
|
94
|
+
config.server!.maxConnections = parseInt(
|
|
95
|
+
process.env.MAX_CONNECTIONS || process.env.MORO_MAX_CONNECTIONS || '1000',
|
|
96
|
+
10
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (process.env.REQUEST_TIMEOUT || process.env.MORO_TIMEOUT) {
|
|
101
|
+
if (!config.server) config.server = {} as any;
|
|
102
|
+
config.server!.timeout = parseInt(
|
|
103
|
+
process.env.REQUEST_TIMEOUT || process.env.MORO_TIMEOUT || '30000',
|
|
104
|
+
10
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Database configuration
|
|
109
|
+
if (process.env.DATABASE_URL || process.env.MORO_DATABASE_URL) {
|
|
110
|
+
if (!config.database) config.database = {} as any;
|
|
111
|
+
config.database!.url = process.env.DATABASE_URL || process.env.MORO_DATABASE_URL;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Redis configuration
|
|
115
|
+
if (process.env.REDIS_URL || process.env.MORO_REDIS_URL) {
|
|
116
|
+
if (!config.database) config.database = {} as any;
|
|
117
|
+
if (!config.database!.redis) config.database!.redis = {} as any;
|
|
118
|
+
config.database!.redis!.url =
|
|
119
|
+
process.env.REDIS_URL || process.env.MORO_REDIS_URL || 'redis://localhost:6379';
|
|
120
|
+
config.database!.redis!.maxRetries = parseInt(
|
|
121
|
+
process.env.REDIS_MAX_RETRIES || process.env.MORO_REDIS_MAX_RETRIES || '3',
|
|
122
|
+
10
|
|
123
|
+
);
|
|
124
|
+
config.database!.redis!.retryDelay = parseInt(
|
|
125
|
+
process.env.REDIS_RETRY_DELAY || process.env.MORO_REDIS_RETRY_DELAY || '1000',
|
|
126
|
+
10
|
|
127
|
+
);
|
|
128
|
+
config.database!.redis!.keyPrefix =
|
|
129
|
+
process.env.REDIS_KEY_PREFIX || process.env.MORO_REDIS_KEY_PREFIX || 'moro:';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// MySQL configuration - only include if MYSQL_HOST is set
|
|
133
|
+
if (process.env.MYSQL_HOST || process.env.MORO_MYSQL_HOST) {
|
|
134
|
+
if (!config.database) config.database = {} as any;
|
|
135
|
+
config.database!.mysql = {
|
|
136
|
+
host: process.env.MYSQL_HOST || process.env.MORO_MYSQL_HOST || 'localhost',
|
|
137
|
+
port: parseInt(process.env.MYSQL_PORT || process.env.MORO_MYSQL_PORT || '3306', 10),
|
|
138
|
+
database: process.env.MYSQL_DATABASE || process.env.MORO_MYSQL_DB,
|
|
139
|
+
username: process.env.MYSQL_USERNAME || process.env.MORO_MYSQL_USER,
|
|
140
|
+
password: process.env.MYSQL_PASSWORD || process.env.MORO_MYSQL_PASS,
|
|
141
|
+
connectionLimit: parseInt(
|
|
142
|
+
process.env.MYSQL_CONNECTION_LIMIT || process.env.MORO_MYSQL_CONNECTIONS || '10',
|
|
143
|
+
10
|
|
144
|
+
),
|
|
145
|
+
acquireTimeout: parseInt(
|
|
146
|
+
process.env.MYSQL_ACQUIRE_TIMEOUT || process.env.MORO_MYSQL_ACQUIRE || '60000',
|
|
147
|
+
10
|
|
148
|
+
),
|
|
149
|
+
timeout: parseInt(process.env.MYSQL_TIMEOUT || process.env.MORO_MYSQL_TIMEOUT || '60000', 10),
|
|
150
|
+
} as any;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Logging configuration
|
|
154
|
+
if (process.env.LOG_LEVEL || process.env.MORO_LOG_LEVEL) {
|
|
155
|
+
const level = process.env.LOG_LEVEL || process.env.MORO_LOG_LEVEL;
|
|
156
|
+
if (
|
|
157
|
+
level === 'debug' ||
|
|
158
|
+
level === 'info' ||
|
|
159
|
+
level === 'warn' ||
|
|
160
|
+
level === 'error' ||
|
|
161
|
+
level === 'fatal'
|
|
162
|
+
) {
|
|
163
|
+
if (!config.logging) config.logging = {} as any;
|
|
164
|
+
config.logging!.level = level;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// External services - only include if configured
|
|
169
|
+
const externalConfig: Partial<AppConfig['external']> = {};
|
|
170
|
+
|
|
171
|
+
// Stripe
|
|
172
|
+
if (process.env.STRIPE_SECRET_KEY || process.env.MORO_STRIPE_SECRET) {
|
|
173
|
+
externalConfig.stripe = {
|
|
174
|
+
secretKey: process.env.STRIPE_SECRET_KEY || process.env.MORO_STRIPE_SECRET,
|
|
175
|
+
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY || process.env.MORO_STRIPE_PUBLIC,
|
|
176
|
+
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET || process.env.MORO_STRIPE_WEBHOOK,
|
|
177
|
+
apiVersion: process.env.STRIPE_API_VERSION || process.env.MORO_STRIPE_VERSION || '2023-10-16',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// PayPal
|
|
182
|
+
if (process.env.PAYPAL_CLIENT_ID || process.env.MORO_PAYPAL_CLIENT) {
|
|
183
|
+
externalConfig.paypal = {
|
|
184
|
+
clientId: process.env.PAYPAL_CLIENT_ID || process.env.MORO_PAYPAL_CLIENT,
|
|
185
|
+
clientSecret: process.env.PAYPAL_CLIENT_SECRET || process.env.MORO_PAYPAL_SECRET,
|
|
186
|
+
webhookId: process.env.PAYPAL_WEBHOOK_ID || process.env.MORO_PAYPAL_WEBHOOK,
|
|
187
|
+
environment:
|
|
188
|
+
(process.env.PAYPAL_ENVIRONMENT || process.env.MORO_PAYPAL_ENV) === 'production'
|
|
189
|
+
? 'production'
|
|
190
|
+
: 'sandbox',
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// SMTP
|
|
195
|
+
if (process.env.SMTP_HOST || process.env.MORO_SMTP_HOST) {
|
|
196
|
+
externalConfig.smtp = {
|
|
197
|
+
host: process.env.SMTP_HOST || process.env.MORO_SMTP_HOST,
|
|
198
|
+
port: parseInt(process.env.SMTP_PORT || process.env.MORO_SMTP_PORT || '587', 10),
|
|
199
|
+
secure: (process.env.SMTP_SECURE || 'false').toLowerCase() === 'true',
|
|
200
|
+
username: process.env.SMTP_USERNAME || process.env.MORO_SMTP_USER,
|
|
201
|
+
password: process.env.SMTP_PASSWORD || process.env.MORO_SMTP_PASS,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (Object.keys(externalConfig).length > 0) {
|
|
206
|
+
config.external = externalConfig;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return config;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Normalize createApp options to match AppConfig structure
|
|
214
|
+
* This handles the flexible createApp API while converting to structured config
|
|
215
|
+
*/
|
|
216
|
+
function normalizeCreateAppOptions(options: MoroOptions): Partial<AppConfig> {
|
|
217
|
+
const config: Partial<AppConfig> = {};
|
|
218
|
+
|
|
219
|
+
// Direct config section overrides - merge with existing config
|
|
220
|
+
if (options.server) {
|
|
221
|
+
config.server = { ...config.server, ...options.server } as any;
|
|
222
|
+
}
|
|
223
|
+
if (options.database) {
|
|
224
|
+
config.database = { ...config.database, ...options.database } as any;
|
|
225
|
+
}
|
|
226
|
+
if (options.modules) {
|
|
227
|
+
config.modules = { ...config.modules, ...options.modules } as any;
|
|
228
|
+
}
|
|
229
|
+
if (options.logging) {
|
|
230
|
+
config.logging = { ...config.logging, ...options.logging } as any;
|
|
231
|
+
}
|
|
232
|
+
if (options.security) {
|
|
233
|
+
config.security = { ...config.security, ...options.security } as any;
|
|
234
|
+
}
|
|
235
|
+
if (options.external) {
|
|
236
|
+
config.external = { ...config.external, ...options.external } as any;
|
|
237
|
+
}
|
|
238
|
+
if (options.performance) {
|
|
239
|
+
config.performance = { ...config.performance, ...options.performance } as any;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Handle shorthand boolean/object options
|
|
243
|
+
if (options.cors !== undefined) {
|
|
244
|
+
config.security = {
|
|
245
|
+
...config.security,
|
|
246
|
+
cors:
|
|
247
|
+
typeof options.cors === 'boolean'
|
|
248
|
+
? { ...DEFAULT_CONFIG.security.cors, enabled: options.cors }
|
|
249
|
+
: { ...DEFAULT_CONFIG.security.cors, ...options.cors },
|
|
250
|
+
} as any;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (options.compression !== undefined) {
|
|
254
|
+
config.performance = {
|
|
255
|
+
...config.performance,
|
|
256
|
+
compression:
|
|
257
|
+
typeof options.compression === 'boolean'
|
|
258
|
+
? { ...DEFAULT_CONFIG.performance.compression, enabled: options.compression }
|
|
259
|
+
: { ...DEFAULT_CONFIG.performance.compression, ...options.compression },
|
|
260
|
+
} as any;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (options.helmet !== undefined) {
|
|
264
|
+
config.security = {
|
|
265
|
+
...config.security,
|
|
266
|
+
helmet:
|
|
267
|
+
typeof options.helmet === 'boolean'
|
|
268
|
+
? { ...DEFAULT_CONFIG.security.helmet, enabled: options.helmet }
|
|
269
|
+
: { ...DEFAULT_CONFIG.security.helmet, ...options.helmet },
|
|
270
|
+
} as any;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return config;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Check if a config field contains sensitive information
|
|
278
|
+
*/
|
|
279
|
+
function isSensitiveField(path: string): boolean {
|
|
280
|
+
const sensitivePatterns = [
|
|
281
|
+
'password',
|
|
282
|
+
'secret',
|
|
283
|
+
'key',
|
|
284
|
+
'token',
|
|
285
|
+
'auth',
|
|
286
|
+
'stripe',
|
|
287
|
+
'paypal',
|
|
288
|
+
'smtp.password',
|
|
289
|
+
'smtp.username',
|
|
290
|
+
'database.url',
|
|
291
|
+
'redis.url',
|
|
292
|
+
'mysql.password',
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
return sensitivePatterns.some(pattern => path.toLowerCase().includes(pattern.toLowerCase()));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Deep merge two configuration objects
|
|
300
|
+
* Later object properties override earlier ones
|
|
301
|
+
*/
|
|
302
|
+
function deepMerge<T>(target: T, source: Partial<T>): T {
|
|
303
|
+
const result = { ...target };
|
|
304
|
+
|
|
305
|
+
for (const key in source) {
|
|
306
|
+
const sourceValue = source[key];
|
|
307
|
+
const targetValue = result[key];
|
|
308
|
+
|
|
309
|
+
if (
|
|
310
|
+
sourceValue &&
|
|
311
|
+
typeof sourceValue === 'object' &&
|
|
312
|
+
!Array.isArray(sourceValue) &&
|
|
313
|
+
targetValue &&
|
|
314
|
+
typeof targetValue === 'object' &&
|
|
315
|
+
!Array.isArray(targetValue)
|
|
316
|
+
) {
|
|
317
|
+
(result as any)[key] = deepMerge(targetValue, sourceValue);
|
|
318
|
+
} else if (sourceValue !== undefined) {
|
|
319
|
+
(result as any)[key] = sourceValue;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Track configuration sources for debugging
|
|
328
|
+
*/
|
|
329
|
+
function trackConfigSource(
|
|
330
|
+
config: any,
|
|
331
|
+
sourceMap: Map<string, ConfigSourceInfo>,
|
|
332
|
+
source: ConfigSourceInfo['source'],
|
|
333
|
+
path: string
|
|
334
|
+
): void {
|
|
335
|
+
function traverse(obj: any, currentPath: string): void {
|
|
336
|
+
for (const key in obj) {
|
|
337
|
+
const value = obj[key];
|
|
338
|
+
const fullPath = currentPath ? `${currentPath}.${key}` : key;
|
|
339
|
+
|
|
340
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
341
|
+
traverse(value, fullPath);
|
|
342
|
+
} else {
|
|
343
|
+
sourceMap.set(fullPath, { source, path, value });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
traverse(config, '');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Log configuration sources for debugging
|
|
353
|
+
*/
|
|
354
|
+
function logConfigurationSources(sourceMap: Map<string, ConfigSourceInfo>): void {
|
|
355
|
+
const allSources = Array.from(sourceMap.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
356
|
+
const nonDefaultSources = allSources.filter(([_, info]) => info.source !== 'default');
|
|
357
|
+
|
|
358
|
+
if (process.env.NODE_ENV === 'production') {
|
|
359
|
+
// In production, only show non-default values with sensitive data obfuscated
|
|
360
|
+
if (nonDefaultSources.length > 0) {
|
|
361
|
+
logger.debug(`Configuration overrides loaded (${nonDefaultSources.length} total)`);
|
|
362
|
+
|
|
363
|
+
nonDefaultSources.forEach(([path, info]) => {
|
|
364
|
+
const valueStr = isSensitiveField(path)
|
|
365
|
+
? '***'
|
|
366
|
+
: typeof info.value === 'object'
|
|
367
|
+
? JSON.stringify(info.value)
|
|
368
|
+
: String(info.value);
|
|
369
|
+
logger.debug(` ${path}: ${valueStr} (from ${info.source})`);
|
|
370
|
+
});
|
|
371
|
+
} else {
|
|
372
|
+
logger.debug('Using default configuration (no overrides)');
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
// In development, show all sources for debugging
|
|
376
|
+
logger.debug(`Configuration sources loaded (${allSources.length} total)`);
|
|
377
|
+
|
|
378
|
+
allSources.forEach(([path, info]) => {
|
|
379
|
+
const valueStr =
|
|
380
|
+
typeof info.value === 'object' ? JSON.stringify(info.value) : String(info.value);
|
|
381
|
+
logger.debug(` ${path}: ${valueStr} (from ${info.source})`);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|