UNPKG

supa-seed

Version:

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

673 lines 27.2 kB
"use strict"; /** * Configuration File Updater System * Phase 4, Checkpoint D2 - Safe configuration file modification with rollback support */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ConfigurationFileUpdater = void 0; const logger_1 = require("../core/utils/logger"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const crypto = __importStar(require("crypto")); class ConfigurationFileUpdater { constructor(backupDirectory) { this.fileProcessors = new Map(); this.templates = new Map(); this.updateHistory = []; this.backupDirectory = backupDirectory || path.join(process.cwd(), '.supa-seed', 'backups'); this.initializeFileProcessors(); this.initializeTemplates(); this.ensureBackupDirectory(); } /** * Apply a batch of configuration updates based on user choices */ async applyUpdates(choices, prompts, schemaChanges) { logger_1.Logger.info(`🔧 Applying configuration updates: ${choices.length} choices`); const updates = []; const errors = []; const warnings = []; const rollbackActions = []; const updatedFiles = []; let totalChanges = 0; const startTime = Date.now(); try { // Generate updates from choices for (const choice of choices) { const prompt = prompts.find(p => p.id === choice.promptId); if (!prompt) { errors.push({ code: 'PROMPT_NOT_FOUND', message: `Prompt not found: ${choice.promptId}`, filePath: '', severity: 'error' }); continue; } const selectedOption = prompt.options.find(o => o.id === choice.optionId); if (!selectedOption) { errors.push({ code: 'OPTION_NOT_FOUND', message: `Option not found: ${choice.optionId}`, filePath: '', severity: 'error' }); continue; } // Process each action in the selected option for (const action of selectedOption.actions) { if (action.type === 'file_update' || action.type === 'file_create') { try { const update = await this.generateUpdate(action, choice, schemaChanges); if (update) { updates.push(update); } } catch (error) { errors.push({ code: 'UPDATE_GENERATION_FAILED', message: `Failed to generate update: ${error.message}`, filePath: action.targetPath, severity: 'error', context: { choice: choice.promptId, action: action.type } }); } } } } // Apply updates in order of risk (low risk first) const sortedUpdates = updates.sort((a, b) => { const riskOrder = { LOW: 1, MEDIUM: 2, HIGH: 3 }; return riskOrder[a.metadata.estimatedRisk] - riskOrder[b.metadata.estimatedRisk]; }); for (const update of sortedUpdates) { try { const result = await this.applyUpdate(update); if (result.success) { updatedFiles.push(update.filePath); totalChanges += update.changes.length; rollbackActions.push(...result.rollbackActions); this.updateHistory.push(update); } else { errors.push(...result.errors); warnings.push(...result.warnings); } } catch (error) { errors.push({ code: 'UPDATE_APPLICATION_FAILED', message: `Failed to apply update: ${error.message}`, filePath: update.filePath, severity: 'critical', context: { updateId: update.id } }); } } } catch (error) { errors.push({ code: 'BATCH_UPDATE_FAILED', message: `Batch update failed: ${error.message}`, filePath: '', severity: 'critical' }); } const executionTime = Date.now() - startTime; return { success: errors.filter(e => e.severity === 'critical').length === 0, updatedFiles: [...new Set(updatedFiles)], errors, warnings, rollbackPlan: rollbackActions.sort((a, b) => b.order - a.order), // Reverse order for rollback metadata: { totalChanges, executionTime, filesModified: updatedFiles.length, backupsCreated: rollbackActions.filter(a => a.type === 'restore_file').length } }; } /** * Apply a single configuration update */ async applyUpdate(update) { logger_1.Logger.debug(`🔧 Applying update: ${update.id} to ${update.filePath}`); const errors = []; const warnings = []; const rollbackActions = []; try { // Determine file processor const processor = this.getFileProcessor(update.filePath); if (!processor) { errors.push({ code: 'NO_PROCESSOR', message: `No processor available for file: ${update.filePath}`, filePath: update.filePath, severity: 'error' }); return { success: false, errors, warnings, rollbackActions }; } // Create backup if file exists if (fs.existsSync(update.filePath)) { const backupPath = await this.createBackup(update.filePath); update.metadata.backupPath = backupPath; rollbackActions.push({ type: 'restore_file', filePath: update.filePath, backupPath, order: rollbackActions.length }); } // Read and parse existing file (or create new) let currentData = {}; if (fs.existsSync(update.filePath)) { const currentContent = fs.readFileSync(update.filePath, 'utf8'); try { currentData = processor.parse(currentContent); } catch (error) { errors.push({ code: 'PARSE_ERROR', message: `Failed to parse file: ${error.message}`, filePath: update.filePath, severity: 'error' }); return { success: false, errors, warnings, rollbackActions }; } } // Apply changes let updatedData; try { updatedData = processor.applyChanges(currentData, update.changes); } catch (error) { errors.push({ code: 'APPLY_CHANGES_ERROR', message: `Failed to apply changes: ${error.message}`, filePath: update.filePath, severity: 'error' }); return { success: false, errors, warnings, rollbackActions }; } // Validate updated data const validation = processor.validate(updatedData); if (!validation.isValid) { errors.push({ code: 'VALIDATION_ERROR', message: `Updated file failed validation: ${validation.errors.join(', ')}`, filePath: update.filePath, severity: 'error' }); return { success: false, errors, warnings, rollbackActions }; } // Write updated file try { const updatedContent = processor.stringify(updatedData, { indent: 2 }); // Ensure directory exists const dirPath = path.dirname(update.filePath); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } fs.writeFileSync(update.filePath, updatedContent, 'utf8'); // Update checksum update.metadata.checksum = this.calculateChecksum(updatedContent); logger_1.Logger.info(`✅ Successfully updated: ${update.filePath}`); } catch (error) { errors.push({ code: 'WRITE_ERROR', message: `Failed to write file: ${error.message}`, filePath: update.filePath, severity: 'critical' }); return { success: false, errors, warnings, rollbackActions }; } } catch (error) { errors.push({ code: 'UNEXPECTED_ERROR', message: `Unexpected error: ${error.message}`, filePath: update.filePath, severity: 'critical' }); } return { success: errors.length === 0, errors, warnings, rollbackActions }; } /** * Rollback configuration changes */ async rollbackUpdates(rollbackPlan) { logger_1.Logger.info(`🔄 Rolling back configuration changes: ${rollbackPlan.length} actions`); const errors = []; const restoredFiles = []; // Execute rollback actions in order for (const action of rollbackPlan) { try { switch (action.type) { case 'restore_file': if (action.backupPath && fs.existsSync(action.backupPath)) { fs.copyFileSync(action.backupPath, action.filePath); restoredFiles.push(action.filePath); logger_1.Logger.debug(`Restored: ${action.filePath} from ${action.backupPath}`); } break; case 'delete_file': if (fs.existsSync(action.filePath)) { fs.unlinkSync(action.filePath); logger_1.Logger.debug(`Deleted: ${action.filePath}`); } break; case 'undo_changes': // This would reverse specific changes - complex implementation logger_1.Logger.debug(`Undo changes: ${action.filePath}`); break; case 'recreate_backup': if (action.backupPath) { const backupDir = path.dirname(action.backupPath); if (!fs.existsSync(backupDir)) { fs.mkdirSync(backupDir, { recursive: true }); } fs.copyFileSync(action.filePath, action.backupPath); logger_1.Logger.debug(`Recreated backup: ${action.backupPath}`); } break; } } catch (error) { errors.push(`Failed to execute rollback action ${action.type}: ${error.message}`); } } return { success: errors.length === 0, errors, restoredFiles }; } /** * Create configuration from template */ async createFromTemplate(templateId, variables, targetDirectory) { logger_1.Logger.info(`📄 Creating configuration from template: ${templateId}`); const template = this.templates.get(templateId); if (!template) { return { success: false, createdFiles: [], errors: [`Template not found: ${templateId}`] }; } const errors = []; const createdFiles = []; try { // Validate variables const validationErrors = this.validateTemplateVariables(template, variables); if (validationErrors.length > 0) { errors.push(...validationErrors); return { success: false, createdFiles, errors }; } // Generate configuration data const configData = template.generate(variables); // Create files for (const targetFile of template.targetFiles) { const filePath = targetDirectory ? path.join(targetDirectory, targetFile) : targetFile; try { const processor = this.getFileProcessor(filePath); if (!processor) { errors.push(`No processor for file: ${filePath}`); continue; } const content = processor.stringify(configData[targetFile] || {}, { indent: 2 }); // Ensure directory exists const dirPath = path.dirname(filePath); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } fs.writeFileSync(filePath, content, 'utf8'); createdFiles.push(filePath); logger_1.Logger.debug(`Created: ${filePath}`); } catch (error) { errors.push(`Failed to create ${filePath}: ${error.message}`); } } } catch (error) { errors.push(`Template generation failed: ${error.message}`); } return { success: errors.length === 0, createdFiles, errors }; } /** * Get update history */ getUpdateHistory() { return [...this.updateHistory]; } /** * Clear update history */ clearUpdateHistory() { this.updateHistory = []; } /** * Add custom file processor */ addFileProcessor(processor) { this.fileProcessors.set(processor.name, processor); logger_1.Logger.debug(`Added file processor: ${processor.name}`); } /** * Add configuration template */ addTemplate(template) { this.templates.set(template.id, template); logger_1.Logger.debug(`Added template: ${template.name}`); } /** * Private: Initialize built-in file processors */ initializeFileProcessors() { // JSON processor this.addFileProcessor({ name: 'json', supportedExtensions: ['.json'], canProcess: (filePath) => path.extname(filePath) === '.json', parse: (content) => JSON.parse(content), stringify: (data, options) => JSON.stringify(data, null, options?.indent || 2), applyChanges: (data, changes) => { const result = JSON.parse(JSON.stringify(data)); // Deep clone for (const change of changes) { const pathParts = change.path.split('.'); let current = result; // Navigate to parent for (let i = 0; i < pathParts.length - 1; i++) { const part = pathParts[i]; if (!(part in current)) { current[part] = {}; } current = current[part]; } const finalKey = pathParts[pathParts.length - 1]; switch (change.operation) { case 'set': current[finalKey] = change.newValue; break; case 'delete': delete current[finalKey]; break; case 'push': if (!Array.isArray(current[finalKey])) { current[finalKey] = []; } current[finalKey].push(change.newValue); break; case 'merge': if (typeof current[finalKey] !== 'object') { current[finalKey] = {}; } Object.assign(current[finalKey], change.newValue); break; } } return result; }, validate: (data) => { try { JSON.stringify(data); return { isValid: true, errors: [] }; } catch (error) { return { isValid: false, errors: [error.message] }; } } }); // JavaScript processor (simplified) this.addFileProcessor({ name: 'javascript', supportedExtensions: ['.js', '.mjs'], canProcess: (filePath) => ['.js', '.mjs'].includes(path.extname(filePath)), parse: (content) => { // This is a simplified implementation // In practice, you'd use a proper JS parser like @babel/parser return { content }; }, stringify: (data) => data.content || '', applyChanges: (data, changes) => { // Simplified - would need sophisticated JS AST manipulation let content = data.content || ''; for (const change of changes) { if (change.operation === 'set' && change.context) { // Simple text replacement content = content.replace(change.context.before || '', change.context.after || String(change.newValue)); } } return { content }; }, validate: (data) => { // Basic validation - could use ESLint or similar return { isValid: true, errors: [] }; } }); } /** * Private: Initialize configuration templates */ initializeTemplates() { // Basic supa-seed configuration template this.addTemplate({ id: 'supa-seed-basic', name: 'Basic Supa-Seed Configuration', description: 'Creates a basic supa-seed configuration with common settings', targetFiles: ['supa-seed.config.js'], variables: [ { name: 'supabaseUrl', type: 'string', description: 'Supabase project URL', required: true, validation: { pattern: '^https://.*\\.supabase\\.co$' } }, { name: 'supabaseAnonKey', type: 'string', description: 'Supabase anonymous key', required: true }, { name: 'enableSync', type: 'boolean', description: 'Enable automatic synchronization', defaultValue: true, required: false } ], generate: (variables) => ({ 'supa-seed.config.js': { supabase: { url: variables.supabaseUrl, anonKey: variables.supabaseAnonKey }, sync: { enabled: variables.enableSync || true, interval: 30000 }, seeding: { batchSize: 100, parallelUploads: 5 } } }) }); } /** * Private: Generate update from configuration action */ async generateUpdate(action, choice, schemaChanges) { if (!action.targetPath) { return null; } // Generate changes based on action parameters const changes = []; if (action.parameters.changes) { // Schema-driven changes for (const schemaChange of action.parameters.changes) { if (schemaChange.tableName) { changes.push({ path: `seeders.${schemaChange.tableName}.enabled`, operation: 'set', newValue: true }); } } } if (action.parameters.configChanges) { // Direct configuration changes for (const [key, value] of Object.entries(action.parameters.configChanges)) { changes.push({ path: key, operation: 'set', newValue: value }); } } const update = { id: `update-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, filePath: action.targetPath, updateType: action.type === 'file_create' ? 'create' : 'modify', targetSection: 'root', changes, metadata: { triggeredBy: choice.promptId, timestamp: new Date(), checksum: '', estimatedRisk: changes.length > 5 ? 'HIGH' : changes.length > 2 ? 'MEDIUM' : 'LOW' } }; return update; } /** * Private: Get appropriate file processor */ getFileProcessor(filePath) { for (const processor of this.fileProcessors.values()) { if (processor.canProcess(filePath)) { return processor; } } return null; } /** * Private: Create backup of file */ async createBackup(filePath) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const basename = path.basename(filePath); const backupName = `${timestamp}-${basename}`; const backupPath = path.join(this.backupDirectory, backupName); if (!fs.existsSync(this.backupDirectory)) { fs.mkdirSync(this.backupDirectory, { recursive: true }); } fs.copyFileSync(filePath, backupPath); logger_1.Logger.debug(`Created backup: ${backupPath}`); return backupPath; } /** * Private: Calculate file checksum */ calculateChecksum(content) { return crypto.createHash('sha256').update(content).digest('hex'); } /** * Private: Ensure backup directory exists */ ensureBackupDirectory() { if (!fs.existsSync(this.backupDirectory)) { fs.mkdirSync(this.backupDirectory, { recursive: true }); } } /** * Private: Validate template variables */ validateTemplateVariables(template, variables) { const errors = []; for (const variable of template.variables) { const value = variables[variable.name]; // Check required variables if (variable.required && (value === undefined || value === null)) { errors.push(`Required variable missing: ${variable.name}`); continue; } // Type validation if (value !== undefined && typeof value !== variable.type) { errors.push(`Variable ${variable.name} must be of type ${variable.type}`); } // Pattern validation if (variable.validation?.pattern && typeof value === 'string') { const regex = new RegExp(variable.validation.pattern); if (!regex.test(value)) { errors.push(`Variable ${variable.name} does not match required pattern`); } } // Range validation if (typeof value === 'number') { if (variable.validation?.min !== undefined && value < variable.validation.min) { errors.push(`Variable ${variable.name} must be at least ${variable.validation.min}`); } if (variable.validation?.max !== undefined && value > variable.validation.max) { errors.push(`Variable ${variable.name} must be at most ${variable.validation.max}`); } } // Options validation if (variable.validation?.options && !variable.validation.options.includes(value)) { errors.push(`Variable ${variable.name} must be one of: ${variable.validation.options.join(', ')}`); } } return errors; } } exports.ConfigurationFileUpdater = ConfigurationFileUpdater; //# sourceMappingURL=config-file-updater.js.map