Spaces:
Paused
Paused
| const fs = require('fs').promises; | |
| const path = require('path'); | |
| const Joi = require('joi'); | |
| const configSchema = require('./schema'); | |
| class ConfigManager { | |
| constructor() { | |
| this.config = null; | |
| this.environment = process.env.NODE_ENV || 'development'; | |
| this.configCache = new Map(); | |
| this.watchers = new Map(); | |
| this.validationErrors = []; | |
| this.isInitialized = false; | |
| } | |
| /** | |
| * Initialize the configuration manager | |
| */ | |
| async initialize() { | |
| if (this.isInitialized) { | |
| return this.config; | |
| } | |
| try { | |
| // Load base configuration | |
| const baseConfig = await this.loadBaseConfig(); | |
| // Load environment-specific configuration | |
| const envConfig = await this.loadEnvironmentConfig(); | |
| // Merge configurations (environment overrides base) | |
| const mergedConfig = this.mergeConfigs(baseConfig, envConfig); | |
| // Load environment variables | |
| const envVarsConfig = this.loadEnvironmentVariables(); | |
| // Final merge (env vars override everything) | |
| const finalConfig = this.mergeConfigs(mergedConfig, envVarsConfig); | |
| // Validate configuration | |
| await this.validateConfig(finalConfig); | |
| this.config = finalConfig; | |
| this.isInitialized = true; | |
| console.log(`Configuration loaded successfully for environment: ${this.environment}`); | |
| return this.config; | |
| } catch (error) { | |
| console.error('Failed to initialize configuration:', error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Load base configuration | |
| */ | |
| async loadBaseConfig() { | |
| const configPath = path.join(__dirname, 'default.json'); | |
| try { | |
| const configFile = await fs.readFile(configPath, 'utf8'); | |
| return JSON.parse(configFile); | |
| } catch (error) { | |
| if (error.code === 'ENOENT') { | |
| console.warn('Default configuration file not found, using schema defaults'); | |
| return {}; | |
| } | |
| throw new Error(`Failed to load base configuration: ${error.message}`); | |
| } | |
| } | |
| /** | |
| * Load environment-specific configuration | |
| */ | |
| async loadEnvironmentConfig() { | |
| const configPath = path.join(__dirname, `${this.environment}.json`); | |
| try { | |
| const configFile = await fs.readFile(configPath, 'utf8'); | |
| return JSON.parse(configFile); | |
| } catch (error) { | |
| if (error.code === 'ENOENT') { | |
| console.warn(`Environment configuration file not found: ${configPath}`); | |
| return {}; | |
| } | |
| throw new Error(`Failed to load environment configuration: ${error.message}`); | |
| } | |
| } | |
| /** | |
| * Load configuration from environment variables | |
| */ | |
| loadEnvironmentVariables() { | |
| const envConfig = {}; | |
| // Map environment variables to configuration structure | |
| const envMappings = { | |
| // Server | |
| 'PORT': 'server.port', | |
| 'HOST': 'server.host', | |
| 'NODE_ENV': 'server.environment', | |
| 'CORS_ENABLED': 'server.corsEnabled', | |
| 'RATE_LIMIT_ENABLED': 'server.rateLimitEnabled', | |
| 'RATE_LIMIT_WINDOW_MS': 'server.rateLimitWindowMs', | |
| 'RATE_LIMIT_MAX_REQUESTS': 'server.rateLimitMaxRequests', | |
| // Database | |
| 'DATABASE_TYPE': 'database.type', | |
| 'DATABASE_PATH': 'database.path', | |
| 'DATABASE_HOST': 'database.host', | |
| 'DATABASE_PORT': 'database.port', | |
| 'DATABASE_NAME': 'database.name', | |
| 'DATABASE_USERNAME': 'database.username', | |
| 'DATABASE_PASSWORD': 'database.password', | |
| // AI Services | |
| 'GEMINI_API_KEY': 'ai.gemini.apiKey', | |
| 'GEMINI_MODEL': 'ai.gemini.model', | |
| 'GEMINI_MAX_RETRIES': 'ai.gemini.maxRetries', | |
| 'GEMINI_TIMEOUT_MS': 'ai.gemini.timeoutMs', | |
| // Analysis | |
| 'MAX_CONCURRENT_ANALYSES': 'analysis.maxConcurrentAnalyses', | |
| 'ANALYSIS_TIMEOUT_MS': 'analysis.timeoutMs', | |
| 'MAX_STUCK_TIME_MS': 'analysis.maxStuckTimeMs', | |
| 'CLEANUP_INTERVAL_MS': 'analysis.cleanupIntervalMs', | |
| 'ENABLE_ACCESSIBILITY': 'analysis.enableAccessibility', | |
| 'ENABLE_VISUAL_ANALYSIS': 'analysis.enableVisualAnalysis', | |
| 'ENABLE_AI_CRITIQUE': 'analysis.enableAICritique', | |
| // Screenshots | |
| 'SCREENSHOT_STORAGE_PATH': 'screenshots.storagePath', | |
| 'SCREENSHOT_TIMEOUT_MS': 'screenshots.timeoutMs', | |
| 'SCREENSHOT_WAIT_FOR_MS': 'screenshots.waitForMs', | |
| // Browser | |
| 'BROWSER_POOL_SIZE': 'browser.poolSize', | |
| 'BROWSER_MAX_IDLE_TIME_MS': 'browser.maxIdleTimeMs', | |
| 'BROWSER_MAX_LIFETIME_MS': 'browser.maxLifetimeMs', | |
| 'BROWSER_HEADLESS': 'browser.headless', | |
| // Logging | |
| 'LOG_LEVEL': 'logging.level', | |
| 'LOG_ENABLE_CONSOLE': 'logging.enableConsole', | |
| 'LOG_ENABLE_FILE': 'logging.enableFile', | |
| 'LOG_FILE_PATH': 'logging.filePath', | |
| // Features | |
| 'FEATURE_ACCESSIBILITY_ANALYSIS': 'features.accessibilityAnalysis', | |
| 'FEATURE_VISUAL_DESIGN_ANALYSIS': 'features.visualDesignAnalysis', | |
| 'FEATURE_AI_CRITIQUE': 'features.aiCritique', | |
| 'FEATURE_QUEUE_SYSTEM': 'features.queueSystem', | |
| 'FEATURE_CACHING': 'features.caching', | |
| // Redis/Queue | |
| 'REDIS_HOST': 'queue.redis.host', | |
| 'REDIS_PORT': 'queue.redis.port', | |
| 'REDIS_PASSWORD': 'queue.redis.password', | |
| 'REDIS_DB': 'queue.redis.db' | |
| }; | |
| // Process environment variables | |
| for (const [envVar, configPath] of Object.entries(envMappings)) { | |
| const value = process.env[envVar]; | |
| if (value !== undefined) { | |
| this.setNestedValue(envConfig, configPath, this.parseEnvValue(value)); | |
| } | |
| } | |
| return envConfig; | |
| } | |
| /** | |
| * Parse environment variable value to appropriate type | |
| */ | |
| parseEnvValue(value) { | |
| // Boolean | |
| if (value.toLowerCase() === 'true') return true; | |
| if (value.toLowerCase() === 'false') return false; | |
| // Number | |
| if (/^\d+$/.test(value)) return parseInt(value, 10); | |
| if (/^\d+\.\d+$/.test(value)) return parseFloat(value); | |
| // Array (comma-separated) | |
| if (value.includes(',')) { | |
| return value.split(',').map(item => item.trim()); | |
| } | |
| // String | |
| return value; | |
| } | |
| /** | |
| * Set nested object value using dot notation | |
| */ | |
| setNestedValue(obj, path, value) { | |
| const keys = path.split('.'); | |
| let current = obj; | |
| for (let i = 0; i < keys.length - 1; i++) { | |
| const key = keys[i]; | |
| if (!(key in current) || typeof current[key] !== 'object') { | |
| current[key] = {}; | |
| } | |
| current = current[key]; | |
| } | |
| current[keys[keys.length - 1]] = value; | |
| } | |
| /** | |
| * Merge two configuration objects | |
| */ | |
| mergeConfigs(base, override) { | |
| const result = JSON.parse(JSON.stringify(base)); | |
| function merge(target, source) { | |
| for (const key in source) { | |
| if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { | |
| if (!target[key] || typeof target[key] !== 'object') { | |
| target[key] = {}; | |
| } | |
| merge(target[key], source[key]); | |
| } else { | |
| target[key] = source[key]; | |
| } | |
| } | |
| } | |
| merge(result, override); | |
| return result; | |
| } | |
| /** | |
| * Validate configuration against schema | |
| */ | |
| async validateConfig(config) { | |
| try { | |
| const { error, value } = configSchema.validate(config, { | |
| allowUnknown: false, | |
| abortEarly: false, | |
| stripUnknown: true | |
| }); | |
| if (error) { | |
| this.validationErrors = error.details.map(detail => ({ | |
| path: detail.path.join('.'), | |
| message: detail.message, | |
| value: detail.context?.value | |
| })); | |
| const errorMsg = `Configuration validation failed:\n${this.validationErrors | |
| .map(err => ` - ${err.path}: ${err.message}`) | |
| .join('\n')}`; | |
| throw new Error(errorMsg); | |
| } | |
| // Use validated and default-filled configuration | |
| Object.assign(config, value); | |
| } catch (error) { | |
| console.error('Configuration validation error:', error.message); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Get configuration value by path | |
| */ | |
| get(path, defaultValue = undefined) { | |
| if (!this.isInitialized) { | |
| throw new Error('Configuration not initialized. Call initialize() first.'); | |
| } | |
| if (!path) { | |
| return this.config; | |
| } | |
| const keys = path.split('.'); | |
| let current = this.config; | |
| for (const key of keys) { | |
| if (current && typeof current === 'object' && key in current) { | |
| current = current[key]; | |
| } else { | |
| return defaultValue; | |
| } | |
| } | |
| return current; | |
| } | |
| /** | |
| * Check if a feature is enabled | |
| */ | |
| isFeatureEnabled(featureName) { | |
| return this.get(`features.${featureName}`, false); | |
| } | |
| /** | |
| * Get database configuration | |
| */ | |
| getDatabaseConfig() { | |
| return this.get('database'); | |
| } | |
| /** | |
| * Get server configuration | |
| */ | |
| getServerConfig() { | |
| return this.get('server'); | |
| } | |
| /** | |
| * Get AI service configuration | |
| */ | |
| getAIConfig() { | |
| return this.get('ai'); | |
| } | |
| /** | |
| * Get analysis configuration | |
| */ | |
| getAnalysisConfig() { | |
| return this.get('analysis'); | |
| } | |
| /** | |
| * Get browser configuration | |
| */ | |
| getBrowserConfig() { | |
| return this.get('browser'); | |
| } | |
| /** | |
| * Get logging configuration | |
| */ | |
| getLoggingConfig() { | |
| return this.get('logging'); | |
| } | |
| /** | |
| * Get health check configuration | |
| */ | |
| getHealthConfig() { | |
| return this.get('health'); | |
| } | |
| /** | |
| * Get security configuration | |
| */ | |
| getSecurityConfig() { | |
| return this.get('security'); | |
| } | |
| /** | |
| * Watch configuration file for changes | |
| */ | |
| async watchConfig(callback) { | |
| if (!this.isFeatureEnabled('configReloading')) { | |
| return; | |
| } | |
| const configFiles = [ | |
| path.join(__dirname, 'default.json'), | |
| path.join(__dirname, `${this.environment}.json`) | |
| ]; | |
| for (const filePath of configFiles) { | |
| try { | |
| const watcher = fs.watch(filePath, async (eventType) => { | |
| if (eventType === 'change') { | |
| console.log(`Configuration file changed: ${filePath}`); | |
| try { | |
| await this.reload(); | |
| if (callback) callback(this.config); | |
| } catch (error) { | |
| console.error('Failed to reload configuration:', error); | |
| } | |
| } | |
| }); | |
| this.watchers.set(filePath, watcher); | |
| } catch (error) { | |
| console.warn(`Cannot watch configuration file: ${filePath}`); | |
| } | |
| } | |
| } | |
| /** | |
| * Reload configuration | |
| */ | |
| async reload() { | |
| this.isInitialized = false; | |
| this.config = null; | |
| this.configCache.clear(); | |
| await this.initialize(); | |
| } | |
| /** | |
| * Stop watching configuration files | |
| */ | |
| stopWatching() { | |
| for (const [filePath, watcher] of this.watchers) { | |
| try { | |
| watcher.close(); | |
| console.log(`Stopped watching: ${filePath}`); | |
| } catch (error) { | |
| console.warn(`Error closing watcher for ${filePath}:`, error); | |
| } | |
| } | |
| this.watchers.clear(); | |
| } | |
| /** | |
| * Validate required environment variables | |
| */ | |
| validateRequiredEnvVars() { | |
| const required = [ | |
| 'GEMINI_API_KEY' | |
| ]; | |
| const missing = required.filter(envVar => !process.env[envVar]); | |
| if (missing.length > 0) { | |
| throw new Error(`Missing required environment variables: ${missing.join(', ')}`); | |
| } | |
| } | |
| /** | |
| * Get current environment | |
| */ | |
| getEnvironment() { | |
| return this.environment; | |
| } | |
| /** | |
| * Check if running in development mode | |
| */ | |
| isDevelopment() { | |
| return this.environment === 'development'; | |
| } | |
| /** | |
| * Check if running in production mode | |
| */ | |
| isProduction() { | |
| return this.environment === 'production'; | |
| } | |
| /** | |
| * Get configuration summary for logging | |
| */ | |
| getSummary() { | |
| if (!this.isInitialized) { | |
| return { status: 'not_initialized' }; | |
| } | |
| return { | |
| environment: this.environment, | |
| server: { | |
| port: this.get('server.port'), | |
| host: this.get('server.host') | |
| }, | |
| features: this.get('features'), | |
| database: { | |
| type: this.get('database.type'), | |
| path: this.get('database.path') | |
| }, | |
| analysis: { | |
| maxConcurrent: this.get('analysis.maxConcurrentAnalyses'), | |
| timeoutMs: this.get('analysis.timeoutMs') | |
| } | |
| }; | |
| } | |
| } | |
| // Create singleton instance | |
| const configManager = new ConfigManager(); | |
| module.exports = configManager; |