UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

246 lines (245 loc) 9.58 kB
import fs from 'fs'; import path from 'path'; import glob from 'glob'; import { getLogger } from '../logging/Logger.js'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * ViewManager handles creation, validation, and maintenance of database views * This ensures views are always created during database initialization */ export class ViewManager { db; viewsDir; constructor(db) { this.db = db; // Ensure cross-platform compatibility and handle potential path issues this.viewsDir = path.resolve(path.join(__dirname, '..', 'analytics', 'views')); } /** * Create all views from SQL files * @param includeDiscovered - Whether to include views from discovered/ subdirectory * @param progressCallback - Optional callback to report progress on each view created * @returns Array of created view names */ async createAllViews(includeDiscovered = true, progressCallback) { const logger = getLogger(); logger.info('Starting view creation process...'); logger.info(`ViewManager looking for views in: ${this.viewsDir}`); // Get all SQL files const patterns = [ path.join(this.viewsDir, 'create-*.sql'), ]; if (includeDiscovered) { patterns.push(path.join(this.viewsDir, 'discovered', 'create-*.sql')); } const viewFiles = []; // First, let's check if the directory exists and list files directly logger.info(`Checking views directory: ${this.viewsDir}`); if (!fs.existsSync(this.viewsDir)) { logger.error(`Views directory does not exist: ${this.viewsDir}`); logger.error(`Current working directory: ${process.cwd()}`); logger.error(`__dirname: ${__dirname}`); } else { // List files directly to debug try { const dirFiles = fs.readdirSync(this.viewsDir); logger.info(`Files in views directory: ${dirFiles.length} files found`); const sqlFiles = dirFiles.filter(f => f.endsWith('.sql')); logger.info(`SQL files found by direct read: ${sqlFiles.length}`); } catch (err) { logger.error(`Error reading views directory: ${err}`); } } for (const pattern of patterns) { logger.info(`Searching pattern: ${pattern}`); // Normalize the pattern for glob const normalizedPattern = pattern.split(path.sep).join('/'); logger.info(`Normalized pattern: ${normalizedPattern}`); const files = glob.sync(normalizedPattern); logger.info(`Pattern ${normalizedPattern} found ${files.length} files`); // If glob fails, try direct file reading as fallback if (files.length === 0 && fs.existsSync(path.dirname(pattern))) { logger.warn(`Glob found no files, trying direct file reading`); const dir = path.dirname(pattern); const filePattern = path.basename(pattern); const regex = new RegExp(filePattern.replace('*', '.*')); try { const dirFiles = fs.readdirSync(dir); const matchingFiles = dirFiles .filter(f => regex.test(f)) .map(f => path.join(dir, f)); logger.info(`Direct read found ${matchingFiles.length} matching files`); viewFiles.push(...matchingFiles); } catch (err) { logger.error(`Error in direct file reading: ${err}`); } } else { viewFiles.push(...files); } } logger.info(`Found ${viewFiles.length} view files to process`); let created = 0; let failed = 0; const createdViews = []; for (const sqlFile of viewFiles) { try { const sql = fs.readFileSync(sqlFile, 'utf8'); const viewName = this.extractViewName(sql); // Drop existing view if it exists if (viewName) { logger.info(`Dropping existing view: ${viewName}`); this.db.prepare(`DROP VIEW IF EXISTS ${viewName}`).run(); } // Create the view logger.info(`Creating view: ${viewName || 'unknown'} from ${path.basename(sqlFile)}`); this.db.exec(sql); logger.info(`✅ Created view from ${path.basename(sqlFile)}`); created++; if (viewName) { createdViews.push(viewName); // Call progress callback if provided if (progressCallback) { progressCallback(viewName); } } } catch (error) { logger.error(`❌ Failed to create view from ${path.basename(sqlFile)}: ${error}`); failed++; } } logger.info(`View creation complete: ${created} created, ${failed} failed`); return createdViews; } /** * Extract view name from CREATE VIEW statement */ extractViewName(sql) { const match = sql.match(/CREATE\s+VIEW\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)/i); return match ? match[1] : null; } /** * Validate that all expected views exist */ validateViews() { const logger = getLogger(); // Expected views based on tool definitions const expectedViews = [ 'flags_unified_view', 'flag_variations_flat', 'flag_variation_variables', 'flag_variables_summary', 'experiments_unified_view', 'audiences_flat', 'pages_flat', 'experiment_audiences_flat', 'experiment_events_flat', 'experiment_pages_flat', 'entity_usage_view', 'flag_state_history_view', 'analytics_summary_view', 'change_history_flat', 'experiment_code_analysis_view', 'experiment_code_snippets_flat', 'project_code_security_view', 'code_search_patterns_view' ]; // Get actual views from database const actualViews = this.db.prepare(` SELECT name FROM sqlite_master WHERE type = 'view' ORDER BY name `).all().map((row) => row.name); // Find missing and extra views const missing = expectedViews.filter(v => !actualViews.includes(v)); const extra = actualViews.filter(v => !expectedViews.includes(v)); const valid = missing.length === 0; if (!valid) { logger.warn(`View validation failed: ${missing.length} missing, ${extra.length} extra`); logger.warn(`Missing views: ${missing.join(', ')}`); if (extra.length > 0) { logger.info(`Extra views: ${extra.join(', ')}`); } } else { logger.info(`View validation passed: all ${expectedViews.length} expected views exist`); } return { valid, missing, extra }; } /** * Backup all existing views to SQL files */ backupViews() { const logger = getLogger(); const backupDir = path.join(this.viewsDir, 'backups', new Date().toISOString().split('T')[0]); if (!fs.existsSync(backupDir)) { fs.mkdirSync(backupDir, { recursive: true }); } const views = this.db.prepare(` SELECT name, sql FROM sqlite_master WHERE type = 'view' ORDER BY name `).all(); views.forEach((view) => { const filename = `create-${view.name.replace(/_/g, '-')}.sql`; const filepath = path.join(backupDir, filename); const content = `-- Backup of view: ${view.name} -- Backed up at: ${new Date().toISOString()} ${view.sql}; `; fs.writeFileSync(filepath, content); }); logger.info(`Backed up ${views.length} views to ${backupDir}`); } /** * Get statistics about views */ getViewStats() { const views = this.db.prepare(` SELECT name FROM sqlite_master WHERE type = 'view' ORDER BY name `).all(); const stats = { total: views.length, views: views.map((v) => v.name), byPrefix: {} }; // Group by prefix views.forEach((view) => { const prefix = view.name.split('_')[0]; if (!stats.byPrefix[prefix]) { stats.byPrefix[prefix] = []; } stats.byPrefix[prefix].push(view.name); }); return stats; } /** * Drop all views (useful for clean recreation) */ dropAllViews() { const logger = getLogger(); const views = this.db.prepare(` SELECT name FROM sqlite_master WHERE type = 'view' ORDER BY name `).all(); views.forEach((view) => { try { this.db.prepare(`DROP VIEW ${view.name}`).run(); logger.info(`Dropped view: ${view.name}`); } catch (error) { logger.error(`Failed to drop view ${view.name}: ${error}`); } }); } } //# sourceMappingURL=ViewManager.js.map