UNPKG

supa-seed

Version:

A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support

479 lines 19.4 kB
"use strict"; /** * Configuration Validation System * Phase 6, Checkpoint F1 - Comprehensive validation for all configuration inputs */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ConfigValidator = void 0; const logger_1 = require("./logger"); const error_handler_1 = require("./error-handler"); class ConfigValidator { /** * Initialize configuration validator with built-in rules */ static initialize() { this.registerBuiltInRules(); logger_1.Logger.info('✅ Configuration validator initialized'); } /** * Register a validation rule for a specific config path */ static registerRule(configPath, rule) { if (!this.validationRules.has(configPath)) { this.validationRules.set(configPath, []); } this.validationRules.get(configPath).push(rule); logger_1.Logger.debug(`📝 Registered validation rule: ${rule.name} for ${configPath}`); } /** * Register a global validation rule */ static registerGlobalRule(rule) { this.globalRules.push(rule); logger_1.Logger.debug(`🌐 Registered global validation rule: ${rule.name}`); } /** * Validate a configuration object */ static validateConfig(config, environment = 'development') { const report = { valid: true, errors: [], warnings: [], info: [], summary: { totalRules: 0, passed: 0, failed: 0, warnings: 0, criticalIssues: [], recommendations: [] } }; logger_1.Logger.info('🔍 Starting configuration validation'); try { // Validate with global rules first this.validateWithRules(config, [], this.globalRules, environment, report); // Validate specific paths this.validateObjectRecursively(config, [], config, environment, report); // Generate summary this.generateValidationSummary(report); logger_1.Logger.info('✅ Configuration validation completed:', { valid: report.valid, errors: report.errors.length, warnings: report.warnings.length, rulesChecked: report.summary.totalRules }); } catch (error) { logger_1.Logger.error('🚨 Configuration validation failed:', error); throw error_handler_1.ErrorHandler.createValidationError(`Configuration validation process failed: ${error.message}`, { component: 'config_validator', operation: 'validateConfig' }); } return report; } /** * Validate specific configuration value */ static validateValue(value, configPath, context) { const rules = this.validationRules.get(configPath) || []; if (rules.length === 0) { return { valid: true, message: 'No validation rules defined for this path' }; } const fullContext = { configPath: configPath.split('.'), fullConfig: context?.fullConfig || {}, environment: context?.environment || 'development', metadata: context?.metadata }; // Run first applicable rule for (const rule of rules) { const result = rule.validator(value, fullContext); if (!result.valid) { return result; } } return { valid: true, message: 'All validation rules passed' }; } /** * Get validation rules for a path */ static getRulesForPath(configPath) { return this.validationRules.get(configPath) || []; } /** * Validate configuration schema structure */ static validateSchema(config, expectedSchema) { const missingFields = []; const extraFields = []; const typeErrors = []; // Check for missing required fields this.checkRequiredFields(config, expectedSchema, '', missingFields); // Check for extra fields (only if strict mode) if (expectedSchema._strict) { this.checkExtraFields(config, expectedSchema, '', extraFields); } // Check field types this.checkFieldTypes(config, expectedSchema, '', typeErrors); const hasErrors = missingFields.length > 0 || typeErrors.length > 0; const hasWarnings = extraFields.length > 0; let message = 'Schema validation passed'; if (hasErrors) { message = `Schema validation failed: ${missingFields.length} missing fields, ${typeErrors.length} type errors`; } else if (hasWarnings) { message = `Schema validation passed with warnings: ${extraFields.length} extra fields`; } return { valid: !hasErrors, message, details: { missingFields, extraFields, typeErrors } }; } /** * Register built-in validation rules */ static registerBuiltInRules() { // Database configuration validation this.registerRule('database.url', { name: 'database_url_format', description: 'Validate Supabase database URL format', validator: (value) => { if (!value || typeof value !== 'string') { return { valid: false, message: 'Database URL is required and must be a string', suggestion: 'Provide a valid Supabase URL (https://xxx.supabase.co)' }; } if (!value.includes('supabase.co') && !value.includes('localhost')) { return { valid: false, message: 'Database URL should be a valid Supabase URL', suggestion: 'Use format: https://xxx.supabase.co or localhost for development' }; } return { valid: true, message: 'Database URL format is valid' }; }, severity: 'error', category: 'compatibility' }); this.registerRule('database.key', { name: 'database_key_security', description: 'Validate Supabase API key security', validator: (value, context) => { if (!value || typeof value !== 'string') { return { valid: false, message: 'Database API key is required', suggestion: 'Provide a valid Supabase API key' }; } if (value.length < 20) { return { valid: false, message: 'API key appears to be too short', suggestion: 'Ensure you are using the correct Supabase API key' }; } if (context?.environment === 'production' && value.includes('anon')) { return { valid: false, message: 'Using anonymous key in production is not recommended', suggestion: 'Use service role key for production seeding' }; } return { valid: true, message: 'API key format is valid' }; }, severity: 'error', category: 'security' }); // AI configuration validation this.registerRule('ai.ollamaUrl', { name: 'ollama_url_format', description: 'Validate Ollama service URL', validator: (value) => { if (!value) { return { valid: true, message: 'Ollama URL is optional' }; } if (typeof value !== 'string') { return { valid: false, message: 'Ollama URL must be a string' }; } if (!value.match(/^https?:\/\/.+/)) { return { valid: false, message: 'Ollama URL must be a valid HTTP/HTTPS URL', suggestion: 'Use format: http://localhost:11434' }; } return { valid: true, message: 'Ollama URL format is valid' }; }, severity: 'warning', category: 'compatibility' }); // Performance configuration validation this.registerRule('performance.batchSize', { name: 'batch_size_optimization', description: 'Validate batch size for performance', validator: (value) => { if (typeof value !== 'number' || value <= 0) { return { valid: false, message: 'Batch size must be a positive number', suggestion: 'Use a value between 10 and 1000' }; } if (value > 1000) { return { valid: false, message: 'Batch size too large, may cause memory issues', suggestion: 'Consider using a batch size of 100-500 for optimal performance' }; } if (value < 10) { return { valid: false, message: 'Batch size too small, may be inefficient', suggestion: 'Consider using a batch size of at least 10' }; } return { valid: true, message: 'Batch size is optimal' }; }, severity: 'warning', category: 'performance' }); // Global configuration rules this.registerGlobalRule({ name: 'environment_consistency', description: 'Validate environment-specific configurations', validator: (config, context) => { const env = context?.environment; if (env === 'production') { // Production-specific validations if (!config.database?.url?.includes('https://')) { return { valid: false, message: 'Production environment must use HTTPS database URL', suggestion: 'Ensure database URL uses HTTPS in production' }; } if (config.debug === true) { return { valid: false, message: 'Debug mode should be disabled in production', suggestion: 'Set debug: false for production environment' }; } } if (env === 'development') { // Development-specific validations if (config.ai?.enabled && !config.ai?.ollamaUrl) { return { valid: false, message: 'AI features enabled but no Ollama URL configured', suggestion: 'Configure ai.ollamaUrl or disable AI features' }; } } return { valid: true, message: 'Environment configuration is consistent' }; }, severity: 'error', category: 'logic' }); this.registerGlobalRule({ name: 'required_fields_check', description: 'Validate presence of required configuration fields', validator: (config) => { const requiredFields = ['database.url', 'database.key']; const missingFields = []; for (const field of requiredFields) { const value = this.getNestedValue(config, field); if (value === undefined || value === null || value === '') { missingFields.push(field); } } if (missingFields.length > 0) { return { valid: false, message: `Missing required configuration fields: ${missingFields.join(', ')}`, suggestion: 'Provide all required configuration values' }; } return { valid: true, message: 'All required fields are present' }; }, severity: 'error', category: 'syntax' }); } /** * Validate object recursively */ static validateObjectRecursively(obj, currentPath, fullConfig, environment, report) { if (typeof obj !== 'object' || obj === null) { return; } for (const [key, value] of Object.entries(obj)) { const path = [...currentPath, key]; const pathString = path.join('.'); // Get rules for this path const rules = this.validationRules.get(pathString) || []; if (rules.length > 0) { this.validateWithRules(value, path, rules, environment, report, fullConfig); } // Recurse for nested objects if (typeof value === 'object' && value !== null && !Array.isArray(value)) { this.validateObjectRecursively(value, path, fullConfig, environment, report); } } } /** * Validate with specific rules */ static validateWithRules(value, path, rules, environment, report, fullConfig) { const context = { configPath: path, fullConfig: fullConfig || {}, environment }; for (const rule of rules) { report.summary.totalRules++; try { const result = rule.validator(value, context); if (result.valid) { report.summary.passed++; } else { const pathString = path.join('.'); switch (rule.severity) { case 'error': report.errors.push({ path: pathString, rule: rule.name, message: result.message, severity: 'error', category: rule.category, suggestion: result.suggestion }); report.summary.failed++; report.valid = false; break; case 'warning': report.warnings.push({ path: pathString, rule: rule.name, message: result.message, severity: 'warning', category: rule.category, suggestion: result.suggestion }); report.summary.warnings++; break; case 'info': report.info.push({ path: pathString, rule: rule.name, message: result.message, severity: 'info', category: rule.category }); break; } } } catch (error) { logger_1.Logger.warn(`⚠️ Validation rule failed: ${rule.name}`, error); report.summary.failed++; } } } /** * Generate validation summary */ static generateValidationSummary(report) { // Identify critical issues report.summary.criticalIssues = report.errors .filter(e => e.category === 'security' || e.category === 'syntax') .map(e => `${e.path}: ${e.message}`); // Generate recommendations const recommendations = new Set(); for (const error of report.errors) { if (error.suggestion) { recommendations.add(error.suggestion); } } for (const warning of report.warnings) { if (warning.suggestion) { recommendations.add(warning.suggestion); } } report.summary.recommendations = Array.from(recommendations); } /** * Helper methods for schema validation */ static checkRequiredFields(config, schema, prefix, missingFields) { for (const [key, value] of Object.entries(schema)) { if (key.startsWith('_')) continue; // Skip meta fields const fieldPath = prefix ? `${prefix}.${key}` : key; if (typeof value === 'object' && value !== null) { const schemaField = value; if (schemaField.required && (config[key] === undefined || config[key] === null)) { missingFields.push(fieldPath); } else if (typeof config[key] === 'object') { this.checkRequiredFields(config[key], value, fieldPath, missingFields); } } } } static checkExtraFields(config, schema, prefix, extraFields) { for (const key of Object.keys(config)) { if (!schema.hasOwnProperty(key)) { const fieldPath = prefix ? `${prefix}.${key}` : key; extraFields.push(fieldPath); } } } static checkFieldTypes(config, schema, prefix, typeErrors) { for (const [key, schemaValue] of Object.entries(schema)) { if (key.startsWith('_')) continue; const fieldPath = prefix ? `${prefix}.${key}` : key; const configValue = config[key]; if (configValue !== undefined && typeof schemaValue === 'object' && schemaValue !== null) { const expectedType = schemaValue.type; if (expectedType && typeof configValue !== expectedType) { typeErrors.push({ field: fieldPath, expected: expectedType, actual: typeof configValue }); } } } } static getNestedValue(obj, path) { return path.split('.').reduce((current, key) => { return current && typeof current === 'object' ? current[key] : undefined; }, obj); } } exports.ConfigValidator = ConfigValidator; ConfigValidator.validationRules = new Map(); ConfigValidator.globalRules = []; exports.default = ConfigValidator; //# sourceMappingURL=config-validator.js.map