@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
246 lines (245 loc) • 9.58 kB
JavaScript
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