@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
536 lines • 19.1 kB
JavaScript
/**
* Optimizely Adapter for Intelligent Query Engine
*
* This adapter provides discovery and execution capabilities for the Optimizely
* data model stored in SQLite. It auto-discovers entities, fields, and relationships
* from the database schema and cached data.
*/
import { getLogger } from '../../../logging/Logger.js';
import { FIELDS } from '../../../generated/fields.generated.js';
const logger = getLogger();
/**
* Map SQLite types to our universal field types
*/
const SQLITE_TYPE_MAP = {
'INTEGER': 'number',
'REAL': 'number',
'TEXT': 'string',
'BLOB': 'binary',
'BOOLEAN': 'boolean',
'JSON': 'json',
'DATETIME': 'date'
};
/**
* Known entity type mappings
*/
const ENTITY_TABLE_MAP = {
'project': 'projects',
'projects': 'projects',
'flag': 'flags',
'flags': 'flags',
'flag_environment': 'flag_environments',
'flag_environments': 'flag_environments',
'environment': 'flag_environments',
'environments': 'flag_environments',
'experiment': 'experiments',
'experiments': 'experiments',
'audience': 'audiences',
'audiences': 'audiences',
'event': 'events',
'events': 'events',
'attribute': 'attributes',
'attributes': 'attributes',
'feature': 'features',
'features': 'features',
'variation': 'variations',
'variations': 'variations',
'campaign': 'campaigns',
'campaigns': 'campaigns',
'page': 'pages',
'pages': 'pages',
'extension': 'extensions',
'extensions': 'extensions',
'group': 'groups',
'groups': 'groups',
'webhook': 'webhooks',
'webhooks': 'webhooks',
'collaborator': 'collaborators',
'collaborators': 'collaborators'
};
/**
* Optimizely Adapter implementation
*/
export class OptimizelyAdapter {
name = 'optimizely';
version = '1.0.0';
description = 'Adapter for Optimizely Feature & Web Experimentation data';
db;
projectFilter;
schemaCache = new Map();
constructor(config) {
this.db = config.database;
this.projectFilter = config.projectFilter;
logger.info(`OptimizelyAdapter initialized with ${config.projectFilter?.length || 'all'} projects`);
}
/**
* Discover all entities in the Optimizely data model
*/
async discoverEntities() {
logger.debug('Discovering Optimizely entities');
const entities = [];
try {
// Get all tables from SQLite
const tables = this.db.prepare(`
SELECT name FROM sqlite_master
WHERE type='table'
AND name NOT LIKE 'sqlite_%'
AND name NOT LIKE 'meta%'
ORDER BY name
`).all();
for (const table of tables) {
// Skip sync state tables
if (table.name.includes('_sync_state'))
continue;
// Get row count
const countResult = this.db.prepare(`SELECT COUNT(*) as count FROM ${table.name}`).get();
// Map table name to entity name
const entityName = this.tableToEntityName(table.name);
entities.push({
name: entityName,
type: 'table',
description: `Optimizely ${entityName} data`,
primaryKey: 'id',
estimatedRowCount: countResult.count,
metadata: {
tableName: table.name,
hasDataJson: this.hasDataJsonColumn(table.name)
}
});
}
// Add virtual entities for relationships
entities.push(...this.getVirtualEntities());
logger.info(`Discovered ${entities.length} Optimizely entities`);
return entities;
}
catch (error) {
logger.error(`Failed to discover entities: ${error}`);
throw error;
}
}
/**
* Discover fields for a specific entity
*/
async discoverFields(entity) {
logger.debug(`Discovering fields for entity: ${entity}`);
const fields = [];
const tableName = ENTITY_TABLE_MAP[entity] || entity;
try {
// Get table schema from SQLite
const columns = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
for (const column of columns) {
const fieldDef = {
name: column.name,
type: this.mapSQLiteType(column.type),
nullable: column.notnull === 0,
metadata: {
sqlType: column.type,
isPrimaryKey: column.pk === 1,
defaultValue: column.dflt_value
}
};
// Special handling for JSON columns
if (column.name === 'data_json' || column.name.endsWith('_json')) {
fieldDef.type = 'json';
fieldDef.jsonSchema = this.getJsonSchemaForEntity(entity);
// Add nested fields from JSON
const jsonFields = await this.discoverJsonFields(entity, column.name);
fields.push(...jsonFields);
}
fields.push(fieldDef);
}
// Add special virtual fields for flags/experiments
if (entity === 'flag' || entity === 'flags') {
// Add environment field that maps to flag_environments table
fields.push({
name: 'environment',
type: 'string',
nullable: true,
computed: false,
metadata: {
isVirtual: true,
requiresJoin: 'flag_environments',
joinField: 'environment_key',
description: 'Environment key from flag_environments relationship'
}
});
// Add enabled field - DO NOT mark as requiresJoin to prevent 700x inflation
// The field resolution strategy will handle this intelligently
fields.push({
name: 'enabled',
type: 'boolean',
nullable: true,
computed: false,
metadata: {
isVirtual: false, // Changed to false to prevent auto-JOIN
description: 'Enabled status - handled by field resolution strategy'
}
});
// Add environment_key field - DO NOT mark as requiresJoin to prevent 700x inflation
fields.push({
name: 'environment_key',
type: 'string',
nullable: true,
computed: false,
metadata: {
isVirtual: false, // Changed to false to prevent auto-JOIN
description: 'Environment key - handled by field resolution strategy'
}
});
// Add status field mapping to archived
fields.push({
name: 'status',
type: 'string',
nullable: true,
computed: true,
metadata: {
isVirtual: true,
mappedField: 'archived',
description: 'Status computed from archived field (archived=true -> inactive, archived=false -> active)'
}
});
}
// Add fields from FIELDS definition if available
const schemaFields = this.getFieldsFromSchema(entity);
for (const schemaField of schemaFields) {
if (!fields.find(f => f.name === schemaField.name)) {
fields.push(schemaField);
}
}
logger.debug(`Discovered ${fields.length} fields for ${entity}`);
return fields;
}
catch (error) {
logger.error(`Failed to discover fields for ${entity}: ${error}`);
return [];
}
}
/**
* Discover relationships between entities
*/
async discoverRelationships() {
logger.debug('Discovering Optimizely relationships');
const relationships = {};
// Define known relationships in Optimizely data model
const knownRelationships = [
// Flag relationships
{ from: 'flags', to: 'flag_environments', via: 'key', type: 'one-to-many' },
{ from: 'flag_environments', to: 'flags', via: 'flag_key', type: 'one-to-one' },
// Experiment relationships
{ from: 'experiments', to: 'campaigns', via: 'campaign_id', type: 'one-to-one' },
{ from: 'experiments', to: 'features', via: 'feature_id', type: 'one-to-one' },
// Project relationships
{ from: 'projects', to: 'flags', via: 'id', type: 'one-to-many' },
{ from: 'projects', to: 'experiments', via: 'id', type: 'one-to-many' },
{ from: 'projects', to: 'audiences', via: 'id', type: 'one-to-many' },
{ from: 'projects', to: 'events', via: 'id', type: 'one-to-many' },
// Environment relationships
{ from: 'flags', to: 'environments', via: 'environments', type: 'many-to-many' },
{ from: 'experiments', to: 'environments', via: 'environments', type: 'many-to-many' }
];
// Build relationship map
for (const rel of knownRelationships) {
const fromEntity = this.tableToEntityName(rel.from);
const toEntity = this.tableToEntityName(rel.to);
if (!relationships[fromEntity]) {
relationships[fromEntity] = [];
}
relationships[fromEntity].push({
from: {
entity: fromEntity,
field: rel.via
},
to: {
entity: toEntity,
field: rel.via === 'id' ? `${fromEntity}_id` : rel.via
},
type: rel.type,
nullable: true
});
}
logger.info('Discovered relationships for Optimizely data model');
return relationships;
}
/**
* Execute a native SQL query
*/
async executeNativeQuery(query) {
logger.debug('Executing native query');
try {
let results;
if (typeof query === 'string') {
// Direct SQL execution
logger.debug('EXECUTING SQL STRING:', query);
// Track GROUP BY queries
if (query.includes('GROUP BY')) {
logger.debug('Executing GROUP BY query in OptimizelyAdapter');
logger.debug(`SQL: ${query}`);
}
results = this.db.prepare(query).all();
// Track GROUP BY results
if (query.includes('GROUP BY')) {
logger.debug(`GROUP BY query returned ${results.length} rows`);
if (results.length > 0) {
logger.debug(`Sample GROUP BY result: ${JSON.stringify(results[0])}`);
}
}
}
else if (query.sql) {
// Parameterized query
logger.debug('EXECUTING PARAMETERIZED SQL:', query.sql);
logger.debug('WITH PARAMETERS:', query.params);
results = this.db.prepare(query.sql).all(...(query.params || []));
}
else {
throw new Error('Invalid query format');
}
return results;
}
catch (error) {
logger.error(`Query execution failed: ${error}`);
logger.error('SQL EXECUTION ERROR:', error.message);
throw error;
}
}
/**
* Get the database connection
*/
getConnectionPool() {
return this.db;
}
/**
* Get adapter capabilities
*/
getCapabilities() {
return {
supportsSQL: true,
supportsJSONPath: true, // Via JSON_EXTRACT
supportsJSONata: false, // Would need external processor
supportsAggregations: true,
supportsJoins: true,
supportsTransactions: true,
maxQueryComplexity: 100,
optimizedOperations: [
'COUNT',
'SUM',
'AVG',
'GROUP BY',
'ORDER BY',
'JSON_EXTRACT'
]
};
}
/**
* Convert table name to entity name
*/
tableToEntityName(tableName) {
// Remove plural 's' for entity name
if (tableName.endsWith('ies')) {
return tableName.slice(0, -3) + 'y';
}
else if (tableName.endsWith('s') && !tableName.endsWith('ss')) {
return tableName.slice(0, -1);
}
return tableName;
}
/**
* Check if table has data_json column
*/
hasDataJsonColumn(tableName) {
try {
const columns = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
return columns.some(col => col.name === 'data_json');
}
catch {
return false;
}
}
/**
* Map SQLite type to universal field type
*/
mapSQLiteType(sqliteType) {
const upperType = sqliteType.toUpperCase();
// Check for exact match first
if (SQLITE_TYPE_MAP[upperType]) {
return SQLITE_TYPE_MAP[upperType];
}
// Check for partial matches
for (const [key, value] of Object.entries(SQLITE_TYPE_MAP)) {
if (upperType.includes(key)) {
return value;
}
}
return 'unknown';
}
/**
* Get JSON schema for entity from FIELDS
*/
getJsonSchemaForEntity(entity) {
const entitySchema = FIELDS[entity];
if (!entitySchema)
return null;
// New FIELDS structure uses required/optional arrays
const properties = {};
// Add required fields
if (entitySchema.required) {
for (const field of entitySchema.required) {
properties[field] = { required: true };
}
}
// Add optional fields
if (entitySchema.optional) {
for (const field of entitySchema.optional) {
properties[field] = { required: false };
}
}
return {
type: 'object',
properties,
required: entitySchema.required || []
};
}
/**
* Discover fields within JSON columns
*/
async discoverJsonFields(entity, jsonColumn) {
const fields = [];
const entitySchema = FIELDS[entity];
if (!entitySchema)
return fields;
// Combine required and optional fields
const allFields = [
...(entitySchema.required || []),
...(entitySchema.optional || [])
];
// Add fields that might be in JSON
for (const fieldName of allFields) {
// Skip if it's a direct column
if (this.isDirectColumn(entity, fieldName))
continue;
fields.push({
name: fieldName,
type: 'unknown', // We don't have type info in new structure
nullable: !(entitySchema.required?.includes(fieldName)),
jsonPath: `$.${fieldName}`,
metadata: {
jsonColumn,
fromSchema: true
}
});
}
return fields;
}
/**
* Get fields from FIELDS schema
*/
getFieldsFromSchema(entity) {
const fields = [];
const entitySchema = FIELDS[entity];
if (!entitySchema)
return fields;
// Process required fields
if (entitySchema.required) {
for (const fieldName of entitySchema.required) {
fields.push({
name: fieldName,
type: 'unknown',
nullable: false,
computed: false,
metadata: {
required: true,
fromSchema: true
}
});
}
}
// Process optional fields
if (entitySchema.optional) {
for (const fieldName of entitySchema.optional) {
fields.push({
name: fieldName,
type: 'unknown',
nullable: true,
computed: false,
metadata: {
required: false,
fromSchema: true
}
});
}
}
return fields;
}
/**
* Check if field is a direct column
*/
isDirectColumn(entity, fieldName) {
const tableName = ENTITY_TABLE_MAP[entity] || entity;
const cacheKey = `${tableName}_columns`;
if (!this.schemaCache.has(cacheKey)) {
try {
const columns = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
this.schemaCache.set(cacheKey, new Set(columns.map(c => c.name)));
}
catch {
return false;
}
}
const columnSet = this.schemaCache.get(cacheKey);
return columnSet?.has(fieldName) || false;
}
/**
* Map schema field type to universal type
*/
mapFieldType(schemaType) {
switch (schemaType) {
case 'string':
return 'string';
case 'number':
case 'integer':
return 'number';
case 'boolean':
return 'boolean';
case 'array':
return 'array';
case 'object':
return 'object';
default:
return 'unknown';
}
}
/**
* Get virtual entities (for special relationships)
*/
getVirtualEntities() {
return [
{
name: 'flag_environment',
type: 'virtual',
description: 'Flag-environment relationship data',
metadata: {
sourceTable: 'flag_environments',
joinRequired: true
}
},
{
name: 'environment',
type: 'virtual',
description: 'Environment data extracted from entities',
metadata: {
jsonPath: '$.environments',
availableIn: ['flags', 'experiments']
}
}
];
}
}
//# sourceMappingURL=OptimizelyAdapter.js.map