@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
586 lines • 22.3 kB
JavaScript
/**
* 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