UNPKG

jaxon-optimizely-dxp-mcp

Version:

AI-powered automation for Optimizely DXP - deploy, monitor, and manage environments through natural conversations

701 lines (579 loc) โ€ข 29.7 kB
/** * Database Simple Tools - Natural language database operations * Part of Jaxon Digital Optimizely DXP MCP Server */ const DatabaseTools = require('./database-tools'); const ProjectTools = require('./project-tools'); const ResponseBuilder = require('../response-builder'); const ErrorHandler = require('../error-handler'); const OutputLogger = require('../output-logger'); const CapabilityDetector = require('../capability-detector'); class DatabaseSimpleTools { /** * Simple backup command with smart defaults */ static async handleBackup(args) { try { const { environment, project, databaseName, dryRun, autoDownload, downloadPath, forceNew } = args; // Get project configuration const projectConfig = await this.getProjectConfig(project); // Smart defaults - Production is most important for backups const targetEnv = this.parseEnvironment(environment || 'production'); const dbName = databaseName || 'epicms'; // Most common database name // Check for existing available backup (unless forcing new) if (!forceNew) { const existingBackup = await this.findAvailableBackup(projectConfig, targetEnv, dbName); if (existingBackup) { OutputLogger.info('Found existing completed backup'); // If auto-download requested, download the existing backup if (autoDownload) { const downloadDir = downloadPath || './backups'; const capabilityCheck = await CapabilityDetector.checkAutoDownloadCapability(downloadDir, 100 * 1024 * 1024); if (capabilityCheck.canAutoDownload) { try { await this.downloadExistingBackup(existingBackup, projectConfig, downloadDir, targetEnv, dbName); return ResponseBuilder.success( `โœ… **Existing Backup Downloaded**\n\nFound a recent backup from ${existingBackup.startTime} and downloaded it successfully.\n\n๐Ÿ“ **Location**: ${downloadDir}\n๐Ÿ’ก **Tip**: Use \`--force-new\` to create a fresh backup instead.`, 'backup-download', { existing: true, exportId: existingBackup.exportId, environment: targetEnv } ); } catch (error) { OutputLogger.error('Failed to download existing backup:', error.message); // Fall through to create new backup } } else { return ResponseBuilder.success( `โœ… **Existing Backup Available**\n\nFound a recent backup from ${existingBackup.startTime}.\n\n**Export ID**: ${existingBackup.exportId}\n**Status**: Complete\n\nโš ๏ธ **Auto-download not available**:\n${capabilityCheck.issues.join('\n')}\n\n๐Ÿ’ก Use \`claude "backup status"\` to get the download URL.`, 'backup-existing', { existing: true, exportId: existingBackup.exportId, environment: targetEnv } ); } } else { return ResponseBuilder.success( `โœ… **Recent Backup Available**\n\nFound a recent backup from ${existingBackup.startTime}.\n\n**Export ID**: ${existingBackup.exportId}\n**Status**: Complete\n\n๐Ÿ’ก **Next Steps**:\n- Use \`claude "backup status"\` to get download URL\n- Add \`--auto-download\` to download automatically\n- Use \`--force-new\` to create a fresh backup instead`, 'backup-existing', { existing: true, exportId: existingBackup.exportId, environment: targetEnv } ); } } } // Dry run preview if (dryRun) { const preview = `๐Ÿงช **Database Backup Preview** **Project**: ${projectConfig.name} **Environment**: ${targetEnv} **Database**: ${dbName} **What will happen**: 1. Create backup of ${dbName} database from ${targetEnv} 2. Store backup in your DXP storage container 3. Backup will be available for 7 days 4. You'll receive an export ID to track progress ${autoDownload ? '5. Automatically download the backup when complete' : ''} **Storage Location**: Your backup will be stored in: \`${projectConfig.name.toLowerCase()}-${targetEnv.toLowerCase()}/database-backups/\` ${autoDownload ? `\n**Download Location**: ${downloadPath || './backups/'}` : ''} **To execute**: Run the same command without --dry-run`; return ResponseBuilder.success(preview, 'backup', { dryRun: true, project: projectConfig.name, environment: targetEnv }); } // Starting database backup silently to avoid JSON parsing issues // Execute backup with the traditional tool const result = await DatabaseTools.handleExportDatabase({ projectId: projectConfig.projectId || projectConfig.id, projectName: projectConfig.name, environment: targetEnv, databaseName: dbName, apiKey: projectConfig.apiKey, apiSecret: projectConfig.apiSecret }); // Store backup info for easy status checking if (result.isSuccess) { const exportId = this.extractExportId(result); await this.storeBackupInfo(projectConfig.name, { exportId: exportId, environment: targetEnv, databaseName: dbName, startTime: new Date().toISOString() }); // Handle auto-download if requested if (autoDownload && exportId) { const downloadDir = downloadPath || './backups'; // Check if auto-download is possible const capabilityCheck = await CapabilityDetector.checkAutoDownloadCapability(downloadDir, 100 * 1024 * 1024); // Assume 100MB backup if (capabilityCheck.canAutoDownload) { // Auto-download enabled. Monitoring backup progress silently... // Start monitoring in background this.monitorAndDownload({ exportId, projectConfig, downloadPath: downloadDir, targetEnv, dbName }).catch(error => { OutputLogger.error('Auto-download failed:', error.message); }); // Return enhanced success message const enhancedResult = { ...result, content: [{ ...result.content[0], text: result.content[0].text + `\n\n๐Ÿ“ฅ **Auto-Download**: Enabled\nThe backup will be automatically downloaded to ${downloadDir} when complete.\nYou can continue working - we'll notify you when the download finishes.` }] }; return enhancedResult; } else { // Auto-download not possible - provide fallback const fallbackMessage = `\n\nโš ๏ธ **Auto-Download**: Not Available\n${capabilityCheck.issues.join('\n')}\n\n๐Ÿ’ก **Alternative**: Use \`claude "backup status"\` to get the download URL when complete.`; const fallbackResult = { ...result, content: [{ ...result.content[0], text: result.content[0].text + fallbackMessage }] }; return fallbackResult; } } } return result; } catch (error) { return this.handleError(error, 'backup', args); } } /** * Check backup status with intelligent defaults */ static async handleBackupStatus(args) { try { const { exportId, project, latest } = args; // Get project configuration const projectConfig = await this.getProjectConfig(project); // If no export ID provided, get the latest let targetExportId = exportId; let latestBackup = null; if (!targetExportId || latest) { latestBackup = await this.getLatestBackup(projectConfig.name); if (!latestBackup) { return ResponseBuilder.error( 'โŒ No recent backups found. Run `claude "backup database"` to create one.', 'backup-status', { project: projectConfig.name } ); } targetExportId = latestBackup.exportId; } OutputLogger.info(`Checking backup status: ${targetExportId}`); // Get backup info for environment and database details let backupEnvironment = 'Production'; // Default let backupDatabase = 'epicms'; // Default if (latestBackup) { backupEnvironment = latestBackup.environment || 'Production'; backupDatabase = latestBackup.databaseName || 'epicms'; } // Check status using traditional tool const result = await DatabaseTools.handleCheckExportStatus({ projectId: projectConfig.id || projectConfig.projectId, projectName: projectConfig.name, exportId: targetExportId, environment: backupEnvironment, databaseName: backupDatabase, apiKey: projectConfig.apiKey, apiSecret: projectConfig.apiSecret }); // Enhance the response with helpful information if (result.isSuccess) { const status = this.parseExportStatus(result); if (status.isComplete) { const enhancedMessage = `โœ… **Database Backup Complete** **Export ID**: ${targetExportId} **Status**: ${status.status} **Download URL**: ${status.downloadUrl} **Next Steps**: 1. Download your backup from the URL above 2. The backup will be available for 7 days 3. To restore, use the Optimizely DXP Portal ๐Ÿ’ก **Tip**: Save the download URL - it's only available for a limited time`; return ResponseBuilder.success(enhancedMessage, 'backup-status', { exportId: targetExportId, status: status.status }); } } return result; } catch (error) { return this.handleError(error, 'backup-status', args); } } /** * Check auto-download capabilities */ static async handleCheckCapabilities(args) { try { const { downloadPath } = args; const targetPath = downloadPath || './backups'; const capabilityReport = await CapabilityDetector.generateCapabilityReport(targetPath); return ResponseBuilder.success(capabilityReport.report, 'capability-check', { canAutoDownload: capabilityReport.canAutoDownload, downloadPath: targetPath }); } catch (error) { return this.handleError(error, 'capability-check', args); } } /** * List recent backups */ static async handleListBackups(args) { try { const { project, limit } = args; // Get project configuration const projectConfig = await this.getProjectConfig(project); // Get stored backup history const backups = await this.getBackupHistory(projectConfig.name, limit || 5); if (!backups || backups.length === 0) { return ResponseBuilder.success( '๐Ÿ“‹ No recent backups found. Run `claude "backup database"` to create one.', 'list-backups', { project: projectConfig.name } ); } let message = '๐Ÿ“‹ **Recent Database Backups**\n\n'; backups.forEach((backup, index) => { const timeAgo = this.getTimeAgo(backup.startTime); message += `${index + 1}. **${backup.environment}** - ${backup.databaseName}\n`; message += ` Export ID: ${backup.exportId}\n`; message += ` Started: ${timeAgo}\n`; message += ` Status: ${backup.status || 'Unknown'}\n\n`; }); message += '๐Ÿ’ก To check status: `claude "backup status --exportId <id>"`'; return ResponseBuilder.success(message, 'list-backups', { project: projectConfig.name, count: backups.length }); } catch (error) { return this.handleError(error, 'list-backups', args); } } // Helper methods static async getProjectConfig(projectName) { try { const projects = ProjectTools.getConfiguredProjects(); if (!projects || projects.length === 0) { throw new Error('No projects configured. Run "setup_wizard" to configure your first project.'); } if (projectName) { const project = projects.find(p => p.name && p.name.toLowerCase().includes(projectName.toLowerCase()) ); if (!project) { const availableNames = projects.map(p => p.name).filter(Boolean).join(', ') || 'None'; throw new Error(`Project "${projectName}" not found. Available: ${availableNames}`); } return project; } else { const defaultProject = projects.find(p => p.isDefault); if (defaultProject) { return defaultProject; } if (projects.length === 1) { return projects[0]; } const projectNames = projects.map(p => p.name).filter(Boolean).join(', ') || 'None'; throw new Error(`Multiple projects found but no default set. Available: ${projectNames}`); } } catch (error) { if (error.message.includes('No projects configured')) { throw error; } throw new Error(`Failed to get project configuration: ${error.message}`); } } static parseEnvironment(env) { if (!env) return 'Integration'; const envLower = env.toLowerCase(); const aliases = { 'prod': 'Production', 'production': 'Production', 'pre': 'Preproduction', 'prep': 'Preproduction', 'preproduction': 'Preproduction', 'staging': 'Preproduction', 'int': 'Integration', 'integration': 'Integration', 'dev': 'Integration', 'development': 'Integration' }; return aliases[envLower] || env; } static extractExportId(result) { // Extract export ID from the result try { const content = result.content[0].text; const match = content.match(/Export ID: ([a-f0-9-]+)/i) || content.match(/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i); return match ? match[1] : null; } catch (error) { return null; } } static parseExportStatus(result) { try { const content = result.content[0].text; return { isComplete: content.includes('Succeeded') || content.includes('Complete'), status: content.includes('Succeeded') ? 'Complete' : content.includes('InProgress') ? 'In Progress' : content.includes('Failed') ? 'Failed' : 'Unknown', downloadUrl: this.extractDownloadUrl(content) }; } catch (error) { return { isComplete: false, status: 'Unknown' }; } } static extractDownloadUrl(content) { const match = content.match(/https?:\/\/[^\s]+/); return match ? match[0] : null; } // Simple in-memory storage for backup history (could be persisted to file) static backupHistory = {}; static async storeBackupInfo(projectName, backupInfo) { if (!this.backupHistory[projectName]) { this.backupHistory[projectName] = []; } this.backupHistory[projectName].unshift(backupInfo); // Keep only last 10 backups in memory if (this.backupHistory[projectName].length > 10) { this.backupHistory[projectName] = this.backupHistory[projectName].slice(0, 10); } } static async getLatestBackup(projectName) { const history = this.backupHistory[projectName]; return history && history.length > 0 ? history[0] : null; } static async getBackupHistory(projectName, limit = 5) { const history = this.backupHistory[projectName] || []; return history.slice(0, limit); } static getTimeAgo(dateString) { const now = new Date(); const date = new Date(dateString); const diffMs = now - date; const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); if (diffMins < 1) return 'just now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString(); } static handleError(error, operation, context) { const errorMessage = error.message || 'Unknown error'; // Provide helpful guidance based on error let guidance = ''; if (errorMessage.includes('No projects configured')) { guidance = '\n\n๐Ÿ’ก Run `claude "setup_wizard"` to configure your project'; } else if (errorMessage.includes('unauthorized') || errorMessage.includes('401')) { guidance = '\n\n๐Ÿ’ก Check your API credentials are correct'; } else if (errorMessage.includes('not found')) { guidance = '\n\n๐Ÿ’ก Verify the environment and database name are correct'; } return ResponseBuilder.error( `โŒ Database ${operation} failed: ${errorMessage}${guidance}`, `db-${operation}`, context ); } /** * Monitor backup progress and auto-download when complete */ static async monitorAndDownload(options) { const { exportId, projectConfig, downloadPath, targetEnv, dbName } = options; const fs = require('fs').promises; const path = require('path'); const https = require('https'); OutputLogger.progress(`Monitoring backup ${exportId}...`); // Poll for completion (max 30 minutes) const maxAttempts = 60; // 30 minutes with 30-second intervals let attempts = 0; while (attempts < maxAttempts) { attempts++; try { // Check backup status const statusResult = await DatabaseTools.handleCheckExportStatus({ projectId: projectConfig.id || projectConfig.projectId, projectName: projectConfig.name, exportId: exportId, environment: targetEnv, databaseName: dbName, apiKey: projectConfig.apiKey, apiSecret: projectConfig.apiSecret }); if (statusResult.isSuccess) { const status = this.parseExportStatus(statusResult); if (status.isComplete && status.downloadUrl) { OutputLogger.success('Backup complete! Starting download...'); // Ensure download directory exists await fs.mkdir(downloadPath, { recursive: true }); // Generate filename const timestamp = new Date().toISOString().replace(/:/g, '-').split('.')[0]; const filename = `${projectConfig.name}-${targetEnv}-${dbName}-${timestamp}.bacpac`; const filepath = path.join(downloadPath, filename); // Download the backup await this.downloadFile(status.downloadUrl, filepath); OutputLogger.success('Backup downloaded successfully!'); OutputLogger.log(`๐Ÿ“ Location: ${filepath}`); OutputLogger.log(`๐Ÿ“Š Size: ${await this.getFileSize(filepath)}`); return { success: true, filepath }; } if (status.status === 'Failed') { throw new Error('Backup export failed'); } } // Wait before next check OutputLogger.progress(`Backup still in progress... (check ${attempts}/${maxAttempts})`); await new Promise(resolve => setTimeout(resolve, 30000)); // 30 seconds } catch (error) { OutputLogger.error(`Error checking backup status: ${error.message}`); throw error; } } throw new Error('Backup monitoring timed out after 30 minutes'); } /** * Download file from URL to local path */ static async downloadFile(url, filepath) { const fs = require('fs'); const https = require('https'); return new Promise((resolve, reject) => { const file = fs.createWriteStream(filepath); let downloadedBytes = 0; let totalBytes = 0; https.get(url, (response) => { totalBytes = parseInt(response.headers['content-length'], 10); response.on('data', (chunk) => { downloadedBytes += chunk.length; file.write(chunk); // Show progress if (totalBytes) { const percent = Math.round((downloadedBytes / totalBytes) * 100); process.stdout.write(`\r๐Ÿ“ฅ Downloading: ${percent}% (${this.formatBytes(downloadedBytes)}/${this.formatBytes(totalBytes)})`); } }); response.on('end', () => { file.end(); OutputLogger.log(''); resolve(); }); }).on('error', (error) => { fs.unlinkSync(filepath); reject(error); }); }); } /** * Get file size in human-readable format */ static async getFileSize(filepath) { const fs = require('fs').promises; const stats = await fs.stat(filepath); return this.formatBytes(stats.size); } /** * Format bytes to human-readable size */ static formatBytes(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; } /** * Find an available completed backup for environment and database */ static async findAvailableBackup(projectConfig, environment, databaseName) { try { const backups = await this.getBackupHistory(projectConfig.name, 10); // Check last 10 backups // Look for a completed backup for the same environment and database for (const backup of backups) { if (backup.environment === environment && backup.databaseName === databaseName) { // Check if this backup is still available (not older than 24 hours for safety) const backupTime = new Date(backup.startTime); const now = new Date(); const hoursSinceBackup = (now - backupTime) / (1000 * 60 * 60); if (hoursSinceBackup < 24) { // Verify the backup is actually complete by checking its status try { const statusResult = await DatabaseTools.handleCheckExportStatus({ projectId: projectConfig.id || projectConfig.projectId, projectName: projectConfig.name, exportId: backup.exportId, environment: environment, databaseName: databaseName, apiKey: projectConfig.apiKey, apiSecret: projectConfig.apiSecret }); if (statusResult.isSuccess) { const status = this.parseExportStatus(statusResult); if (status.isComplete && status.downloadUrl) { // Found a valid, downloadable backup return { ...backup, downloadUrl: status.downloadUrl, status: status.status }; } } } catch (error) { // Skip this backup if we can't check its status continue; } } } } return null; } catch (error) { // If we can't check for existing backups, just return null return null; } } /** * Download an existing backup */ static async downloadExistingBackup(backup, projectConfig, downloadPath, targetEnv, dbName) { const fs = require('fs').promises; const path = require('path'); OutputLogger.success('Downloading existing backup...'); // Ensure download directory exists await fs.mkdir(downloadPath, { recursive: true }); // Generate filename const backupDate = new Date(backup.startTime); const timestamp = backupDate.toISOString().replace(/:/g, '-').split('.')[0]; const filename = `${projectConfig.name}-${targetEnv}-${dbName}-${timestamp}.bacpac`; const filepath = path.join(downloadPath, filename); // Download the backup await this.downloadFile(backup.downloadUrl, filepath); OutputLogger.success('Existing backup downloaded successfully!'); OutputLogger.log(`๐Ÿ“ Location: ${filepath}`); OutputLogger.log(`๐Ÿ“Š Size: ${await this.getFileSize(filepath)}`); return { success: true, filepath }; } } module.exports = DatabaseSimpleTools;