UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

586 lines 22.3 kB
/** * Template Store * @description SQLite storage for orchestration templates and execution history * @author Optimizely MCP Server * @version 1.0.0 */ import Database from '../../database/better-sqlite3-loader.js'; import { getLogger } from '../../logging/Logger.js'; import * as path from 'path'; import * as fs from 'fs/promises'; import { ValidationMiddleware } from '../core/ValidationMiddleware.js'; export class TemplateStore { logger = getLogger(); db; dbPath; initialized = false; validationMiddleware = new ValidationMiddleware(); constructor() { // Get database path from environment or use default const customPath = process.env.ORCHESTRATION_DB_PATH; this.dbPath = customPath || path.join(process.cwd(), 'data', 'orchestration-templates.db'); this.logger.info({ dbPath: this.dbPath, isCustomPath: !!customPath }, 'Initializing template store'); } /** * Initialize the store - must be called before any operations */ async initialize() { if (this.initialized) return; await this.initializeDatabase(); this.initialized = true; } /** * Initialize database connection and schema */ async initializeDatabase() { try { // Ensure data directory exists const dataDir = path.dirname(this.dbPath); await fs.mkdir(dataDir, { recursive: true }); // Open database connection this.db = new Database(this.dbPath); // Enable foreign keys this.db.pragma('foreign_keys = ON'); // Initialize schema this.initializeSchema(); // Create indexes this.createIndexes(); this.logger.info('Template store initialized successfully'); } catch (error) { this.logger.error({ error: error instanceof Error ? error.message : String(error), dbPath: this.dbPath }, 'Failed to initialize template store'); throw error; } } /** * Initialize database schema */ initializeSchema() { this.logger.debug('Creating database schema'); // Orchestration templates table this.db.exec(` CREATE TABLE IF NOT EXISTS orchestration_templates ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, version TEXT NOT NULL, type TEXT CHECK(type IN ('user', 'system')) NOT NULL, platform TEXT CHECK(platform IN ('feature', 'web', 'both')), author TEXT, template_data TEXT NOT NULL, -- JSON tags TEXT, -- JSON array parameters TEXT, -- JSON - template parameters definition steps TEXT, -- JSON - template steps outputs TEXT, -- JSON - template outputs config TEXT, -- JSON - template configuration template_format_version INTEGER DEFAULT 2, -- Format version for Direct Template Architecture is_active INTEGER DEFAULT 1, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP, UNIQUE(name, version) ) `); // Run migrations to handle existing databases this.runMigrations(); // Template execution history this.db.exec(` CREATE TABLE IF NOT EXISTS orchestration_executions ( id TEXT PRIMARY KEY, template_id TEXT NOT NULL, template_version TEXT, started_at TEXT DEFAULT CURRENT_TIMESTAMP, completed_at TEXT, status TEXT CHECK(status IN ('running', 'completed', 'failed', 'cancelled')) NOT NULL, parameters TEXT, -- JSON state TEXT, -- JSON result TEXT, -- JSON error TEXT, execution_time_ms INTEGER, user_id TEXT, project_id TEXT, environment TEXT, FOREIGN KEY (template_id) REFERENCES orchestration_templates(id) ) `); // Template catalog (for system templates) this.db.exec(` CREATE TABLE IF NOT EXISTS template_catalog ( id TEXT PRIMARY KEY, template_type TEXT CHECK(template_type IN ('system', 'orchestration')) NOT NULL, entity_type TEXT, operation TEXT, platform TEXT CHECK(platform IN ('feature', 'web', 'both')), metadata TEXT NOT NULL, -- JSON is_active INTEGER DEFAULT 1, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP ) `); // Template dependencies (for tracking relationships) this.db.exec(` CREATE TABLE IF NOT EXISTS template_dependencies ( id INTEGER PRIMARY KEY AUTOINCREMENT, template_id TEXT NOT NULL, depends_on_template_id TEXT NOT NULL, dependency_type TEXT CHECK(dependency_type IN ('uses', 'extends', 'requires')), created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (template_id) REFERENCES orchestration_templates(id), FOREIGN KEY (depends_on_template_id) REFERENCES orchestration_templates(id), UNIQUE(template_id, depends_on_template_id) ) `); // Execution metrics (for analytics) this.db.exec(` CREATE TABLE IF NOT EXISTS execution_metrics ( id INTEGER PRIMARY KEY AUTOINCREMENT, execution_id TEXT NOT NULL, metric_name TEXT NOT NULL, metric_value REAL NOT NULL, recorded_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (execution_id) REFERENCES orchestration_executions(id) ) `); } /** * Run database migrations for existing databases */ runMigrations() { this.logger.debug('Running database migrations'); try { // Check if we need to add missing columns to existing table const tableInfo = this.db.prepare("PRAGMA table_info(orchestration_templates)").all(); const existingColumns = new Set(tableInfo.map(col => col.name)); // Add missing columns one by one const requiredColumns = [ { name: 'parameters', type: 'TEXT', default: '{}' }, { name: 'steps', type: 'TEXT', default: '[]' }, { name: 'outputs', type: 'TEXT', default: '{}' }, { name: 'config', type: 'TEXT', default: '{}' }, { name: 'template_format_version', type: 'INTEGER', default: '2' } ]; for (const column of requiredColumns) { if (!existingColumns.has(column.name)) { this.logger.info(`Adding missing column: ${column.name}`); this.db.exec(`ALTER TABLE orchestration_templates ADD COLUMN ${column.name} ${column.type} DEFAULT '${column.default}'`); } } this.logger.debug('Database migrations completed'); } catch (error) { this.logger.error({ error: error instanceof Error ? error.message : String(error) }, 'Failed to run database migrations'); // Don't throw - continue with initialization } } /** * Create database indexes */ createIndexes() { this.logger.debug('Creating database indexes'); // Template lookup indexes this.db.exec(` CREATE INDEX IF NOT EXISTS idx_templates_name ON orchestration_templates(name); CREATE INDEX IF NOT EXISTS idx_templates_type ON orchestration_templates(type); CREATE INDEX IF NOT EXISTS idx_templates_platform ON orchestration_templates(platform); CREATE INDEX IF NOT EXISTS idx_templates_active ON orchestration_templates(is_active); `); // Execution lookup indexes this.db.exec(` CREATE INDEX IF NOT EXISTS idx_executions_template ON orchestration_executions(template_id); CREATE INDEX IF NOT EXISTS idx_executions_status ON orchestration_executions(status); CREATE INDEX IF NOT EXISTS idx_executions_project ON orchestration_executions(project_id); CREATE INDEX IF NOT EXISTS idx_executions_started ON orchestration_executions(started_at); `); // Catalog indexes this.db.exec(` CREATE INDEX IF NOT EXISTS idx_catalog_entity ON template_catalog(entity_type); CREATE INDEX IF NOT EXISTS idx_catalog_operation ON template_catalog(operation); `); } // ============================================================================ // Template Operations // ============================================================================ /** * Create orchestration template with validation */ async createTemplate(template, options) { await this.initialize(); const fullTemplate = { ...template, created_at: new Date(), updated_at: new Date() }; // Validate template before saving unless explicitly skipped if (!options?.skipValidation) { const validationReport = await this.validationMiddleware.validateTemplateCreation(fullTemplate, { validateStructure: true, validateEntities: true, autoFix: options?.autoFix || false, platform: fullTemplate.platform }); if (!validationReport.valid) { const errorMessage = this.validationMiddleware.formatErrorsForDisplay(validationReport); const suggestedFixes = this.validationMiddleware.getSuggestedFixes(validationReport); throw new Error(`Template validation failed:\n\n${errorMessage}\n\nSuggested fixes:\n${suggestedFixes.map((fix, i) => `${i + 1}. ${fix}`).join('\n')}`); } this.logger.info({ templateId: fullTemplate.id, validationSummary: validationReport.summary }, 'Template passed validation'); } await this.saveTemplate(fullTemplate); return fullTemplate; } /** * Save orchestration template */ async saveTemplate(template) { await this.initialize(); this.logger.info({ templateId: template.id, templateName: template.name, version: template.version }, 'Saving orchestration template'); const stmt = this.db.prepare(` INSERT OR REPLACE INTO orchestration_templates (id, name, description, version, type, platform, author, template_data, tags, is_active, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) `); try { stmt.run(template.id, template.name, template.description, template.version, template.type, template.platform, template.author, JSON.stringify(template), JSON.stringify(template.tags || []), 1); this.logger.info({ templateId: template.id }, 'Template saved successfully'); } catch (error) { this.logger.error({ error: error instanceof Error ? error.message : String(error), templateId: template.id }, 'Failed to save template'); throw error; } } /** * Get template by ID */ async getTemplate(templateId) { await this.initialize(); const stmt = this.db.prepare(` SELECT * FROM orchestration_templates WHERE id = ? AND is_active = 1 `); const row = stmt.get(templateId); if (!row) { return null; } return JSON.parse(row.template_data); } /** * Get template by name and version */ async getTemplateByName(name, version) { await this.initialize(); let stmt; let row; if (version) { stmt = this.db.prepare(` SELECT * FROM orchestration_templates WHERE name = ? AND version = ? AND is_active = 1 `); row = stmt.get(name, version); } else { // Get latest version stmt = this.db.prepare(` SELECT * FROM orchestration_templates WHERE name = ? AND is_active = 1 ORDER BY created_at DESC LIMIT 1 `); row = stmt.get(name); } if (!row) { return null; } return JSON.parse(row.template_data); } /** * List templates with filters */ async listTemplates(filters) { await this.initialize(); let query = 'SELECT * FROM orchestration_templates WHERE is_active = 1'; const params = []; if (filters) { if (filters.type) { query += ' AND type = ?'; params.push(filters.type); } if (filters.platform) { query += ' AND platform = ?'; params.push(filters.platform); } if (filters.author) { query += ' AND author = ?'; params.push(filters.author); } if (filters.tags && filters.tags.length > 0) { // Check if any of the tags match const tagConditions = filters.tags.map(() => 'tags LIKE ?').join(' OR '); query += ` AND (${tagConditions})`; filters.tags.forEach(tag => params.push(`%"${tag}"%`)); } } query += ' ORDER BY created_at DESC'; const stmt = this.db.prepare(query); const rows = stmt.all(...params); return rows.map(row => JSON.parse(row.template_data)); } /** * Update template */ async updateTemplate(templateId, template) { const stmt = this.db.prepare(` UPDATE orchestration_templates SET name = ?, description = ?, version = ?, platform = ?, type = ?, tags = ?, parameters = ?, steps = ?, outputs = ?, config = ?, author = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `); stmt.run(template.name, template.description, template.version, template.platform, template.type || 'user', JSON.stringify(template.tags || []), JSON.stringify(template.parameters || {}), JSON.stringify(template.steps || []), JSON.stringify(template.outputs || {}), JSON.stringify(template.config || {}), template.author, templateId); this.logger.info({ templateId, name: template.name, version: template.version }, 'Template updated'); } /** * Delete template (soft delete) */ async deleteTemplate(templateId) { const stmt = this.db.prepare(` UPDATE orchestration_templates SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ? `); stmt.run(templateId); this.logger.info({ templateId }, 'Template soft deleted'); } // ============================================================================ // Execution Operations // ============================================================================ /** * Save execution record */ async saveExecution(execution) { const stmt = this.db.prepare(` INSERT INTO orchestration_executions (id, template_id, template_version, started_at, completed_at, status, parameters, state, result, error, execution_time_ms, user_id, project_id, environment) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); try { // Extract template version from state if available const templateVersion = execution.state ? JSON.parse(execution.state).template_version : null; // Calculate execution time let executionTime = null; if (execution.started_at && execution.completed_at) { const start = new Date(execution.started_at).getTime(); const end = new Date(execution.completed_at).getTime(); executionTime = end - start; } stmt.run(execution.id, execution.template_id, templateVersion, execution.started_at, execution.completed_at, execution.status, execution.parameters, execution.state, execution.result, execution.error, executionTime, null, // user_id - to be implemented null, // project_id - to be extracted from parameters null // environment - to be extracted from parameters ); this.logger.info({ executionId: execution.id, templateId: execution.template_id, status: execution.status }, 'Execution saved'); } catch (error) { this.logger.error({ error: error instanceof Error ? error.message : String(error), executionId: execution.id }, 'Failed to save execution'); throw error; } } /** * Update execution status */ async updateExecutionStatus(executionId, status, error) { const stmt = this.db.prepare(` UPDATE orchestration_executions SET status = ?, error = ?, completed_at = CURRENT_TIMESTAMP WHERE id = ? `); stmt.run(status, error, executionId); } /** * Get execution by ID */ async getExecution(executionId) { const stmt = this.db.prepare(` SELECT * FROM orchestration_executions WHERE id = ? `); const row = stmt.get(executionId); return row || null; } /** * List executions with filters */ async listExecutions(filters) { let query = 'SELECT * FROM orchestration_executions WHERE 1=1'; const params = []; if (filters) { if (filters.template_id) { query += ' AND template_id = ?'; params.push(filters.template_id); } if (filters.status) { query += ' AND status = ?'; params.push(filters.status); } if (filters.project_id) { query += ' AND project_id = ?'; params.push(filters.project_id); } if (filters.start_date) { query += ' AND started_at >= ?'; params.push(filters.start_date); } if (filters.end_date) { query += ' AND started_at <= ?'; params.push(filters.end_date); } } query += ' ORDER BY started_at DESC'; if (filters?.limit) { query += ' LIMIT ?'; params.push(filters.limit); } const stmt = this.db.prepare(query); return stmt.all(...params); } // ============================================================================ // System Template Catalog // ============================================================================ /** * Register system template */ async registerSystemTemplate(template) { const stmt = this.db.prepare(` INSERT OR REPLACE INTO template_catalog (id, template_type, entity_type, operation, platform, metadata, updated_at) VALUES (?, 'system', ?, ?, ?, ?, CURRENT_TIMESTAMP) `); stmt.run(template.id, template.entity_type, template.operation, template.platform, JSON.stringify(template.orchestration_metadata)); this.logger.info({ templateId: template.id, entityType: template.entity_type, operation: template.operation }, 'System template registered'); } /** * Get system template catalog */ async getSystemTemplates() { const stmt = this.db.prepare(` SELECT * FROM template_catalog WHERE template_type = 'system' AND is_active = 1 `); const rows = stmt.all(); return rows.map(row => ({ id: row.id, entity_type: row.entity_type, operation: row.operation, platform: row.platform, schema: {}, // Would need to load from actual templates orchestration_metadata: JSON.parse(row.metadata) })); } // ============================================================================ // Analytics & Metrics // ============================================================================ /** * Record execution metric */ async recordMetric(executionId, metricName, metricValue) { const stmt = this.db.prepare(` INSERT INTO execution_metrics (execution_id, metric_name, metric_value) VALUES (?, ?, ?) `); stmt.run(executionId, metricName, metricValue); } /** * Get execution metrics */ async getExecutionMetrics(executionId) { const stmt = this.db.prepare(` SELECT metric_name, metric_value FROM execution_metrics WHERE execution_id = ? `); const rows = stmt.all(executionId); const metrics = {}; rows.forEach(row => { metrics[row.metric_name] = row.metric_value; }); return metrics; } /** * Get template usage statistics */ async getTemplateStats(templateId) { const stmt = this.db.prepare(` SELECT COUNT(*) as total_executions, SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as successful_executions, SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed_executions, AVG(execution_time_ms) as avg_execution_time_ms, MAX(started_at) as last_execution_date FROM orchestration_executions WHERE template_id = ? `); const row = stmt.get(templateId); return { total_executions: row.total_executions || 0, successful_executions: row.successful_executions || 0, failed_executions: row.failed_executions || 0, avg_execution_time_ms: row.avg_execution_time_ms || 0, last_execution_date: row.last_execution_date }; } /** * Close database connection */ close() { this.logger.info('Closing template store database'); this.db.close(); } } //# sourceMappingURL=TemplateStore.js.map