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
JavaScript
;
/**
* 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