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