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