UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

738 lines 29.8 kB
import { getLogger } from '../logging/Logger.js'; import { MappingConfidence } from './types.js'; /** * COMPLETE IntelligentFieldMapper Implementation * * STATUS: 100% COMPLETE - All 26 database entities mapped * DATE: July 2, 2025 * * This is the COMPLETE implementation with ALL entity schemas from the database. * Replaces the incomplete version that only had 3 entities. */ export class IntelligentFieldMapper { sqliteEngine; schemaMap; fieldCache; joinPathCache; constructor(sqliteEngine) { this.sqliteEngine = sqliteEngine; this.schemaMap = this.initializeSchemaMap(); this.fieldCache = new Map(); this.joinPathCache = new Map(); getLogger().info({ totalSchemas: this.schemaMap.size, entities: Array.from(this.schemaMap.keys()) }, 'IntelligentFieldMapper initialized with COMPLETE schema definitions'); } /** * Map a field reference to its database location */ mapField(entity, field) { const cacheKey = `${entity}.${field}`; // Check cache first if (this.fieldCache.has(cacheKey)) { const cached = this.fieldCache.get(cacheKey); return { success: true, table: cached.table, column: cached.column, isJsonPath: cached.isJsonPath, jsonPath: cached.jsonPath, confidence: MappingConfidence.HIGH }; } // Get schema for entity const schema = this.schemaMap.get(entity); if (!schema) { return { success: false, error: `Unknown entity: ${entity}. Available entities: ${Array.from(this.schemaMap.keys()).join(', ')}`, confidence: MappingConfidence.NONE }; } // Try direct column mapping if (schema.columns.includes(field)) { const fieldInfo = { table: schema.table, column: field, isJsonPath: false, dataType: this.inferDataType(field) }; this.fieldCache.set(cacheKey, fieldInfo); return { success: true, table: schema.table, column: field, isJsonPath: false, confidence: MappingConfidence.HIGH }; } // Try JSON path mapping const jsonMapping = this.tryJsonMapping(schema, field); if (jsonMapping.success) { this.fieldCache.set(cacheKey, { table: schema.table, column: jsonMapping.column, isJsonPath: true, jsonPath: jsonMapping.jsonPath, dataType: 'json' }); return jsonMapping; } // Try fuzzy matching const fuzzyMatch = this.findSimilarField(field, schema.columns); if (fuzzyMatch) { return { success: false, error: `Field '${field}' not found. Did you mean '${fuzzyMatch}'?`, suggestion: fuzzyMatch, confidence: MappingConfidence.LOW }; } return { success: false, error: `Field '${field}' not found in entity '${entity}'`, confidence: MappingConfidence.NONE }; } /** * Get join path between two entities */ getJoinPath(fromEntity, toEntity) { const cacheKey = `${fromEntity}->${toEntity}`; if (this.joinPathCache.has(cacheKey)) { return this.joinPathCache.get(cacheKey); } const fromSchema = this.schemaMap.get(fromEntity); const toSchema = this.schemaMap.get(toEntity); if (!fromSchema || !toSchema) { return []; } // Direct join? if (fromSchema.joins && fromSchema.joins[toEntity]) { const joinInfo = { table: toSchema.table, on: fromSchema.joins[toEntity].on, type: fromSchema.joins[toEntity].type || 'INNER', fromTable: fromSchema.table, toTable: toSchema.table, joinType: fromSchema.joins[toEntity].type || 'INNER', joinCondition: fromSchema.joins[toEntity].on }; this.joinPathCache.set(cacheKey, [joinInfo]); return [joinInfo]; } // Reverse join? if (toSchema.joins && toSchema.joins[fromEntity]) { const reverseJoin = toSchema.joins[fromEntity]; const joinInfo = { table: toSchema.table, on: reverseJoin.on, type: 'LEFT', // Reverse joins are typically LEFT fromTable: fromSchema.table, toTable: toSchema.table, joinType: 'LEFT', joinCondition: reverseJoin.on }; this.joinPathCache.set(cacheKey, [joinInfo]); return [joinInfo]; } // TODO: Implement multi-hop join discovery return []; } /** * Try to map field as JSON path */ tryJsonMapping(schema, field) { // Fields that should never be JSON-mapped because they exist in related entities const relatedEntityFields = [ 'environment_key', // This should come from flag_environments table, not JSON 'environment' // This should map to environment_key from flag_environments table ]; if (relatedEntityFields.includes(field)) { return { success: false, confidence: MappingConfidence.NONE }; } // Check if field looks like a JSON path if (field.includes('.') || field.includes('[')) { // For now, assume data_json column if (schema.jsonColumns && schema.jsonColumns.includes('data_json')) { return { success: true, table: schema.table, column: 'data_json', isJsonPath: true, jsonPath: `$.${field}`, confidence: MappingConfidence.MEDIUM }; } } // Try common JSON field patterns const jsonPatterns = [ { column: 'data_json', prefix: '' }, { column: 'conditions', prefix: '' }, { column: 'variables', prefix: '' }, { column: 'results_json', prefix: 'results' }, { column: 'permissions_json', prefix: 'permissions' } ]; for (const pattern of jsonPatterns) { if (schema.jsonColumns && schema.jsonColumns.includes(pattern.column)) { const jsonPath = pattern.prefix ? `$.${pattern.prefix}.${field}` : `$.${field}`; return { success: true, table: schema.table, column: pattern.column, isJsonPath: true, jsonPath, confidence: MappingConfidence.MEDIUM }; } } return { success: false, confidence: MappingConfidence.NONE }; } /** * Find similar field name using fuzzy matching */ findSimilarField(field, candidates) { const normalized = field.toLowerCase().replace(/_/g, ''); for (const candidate of candidates) { const candidateNorm = candidate.toLowerCase().replace(/_/g, ''); // Exact match after normalization if (normalized === candidateNorm) { return candidate; } // Substring match if (candidateNorm.includes(normalized) || normalized.includes(candidateNorm)) { return candidate; } } return null; } /** * Infer data type from field name */ inferDataType(field) { const patterns = [ { pattern: /_id$|^id$/, type: 'string' }, { pattern: /_at$|_time$|created|modified|updated/, type: 'datetime' }, { pattern: /count|total|amount|percentage/, type: 'number' }, { pattern: /^is_|_enabled$|archived|active/, type: 'boolean' }, { pattern: /name|description|key|type|status/, type: 'string' }, { pattern: /_json$|conditions|variables/, type: 'json' } ]; for (const { pattern, type } of patterns) { if (pattern.test(field)) { return type; } } return 'string'; } /** * Initialize the COMPLETE schema map with ALL database entities * STATUS: 100% COMPLETE - All 26 entities mapped */ initializeSchemaMap() { const schemaMap = new Map(); // 1. Projects schema schemaMap.set('projects', { table: 'projects', columns: ['id', 'name', 'description', 'platform', 'status', 'account_id', 'is_flags_enabled', 'archived', 'created_at', 'last_modified', 'data_json'], jsonColumns: ['data_json'], joins: { flags: { table: 'flags', on: 'projects.id = flags.project_id', type: 'LEFT' }, experiments: { table: 'experiments', on: 'projects.id = experiments.project_id', type: 'LEFT' }, environments: { table: 'environments', on: 'projects.id = environments.project_id', type: 'LEFT' } } }); // 2. Environments schema schemaMap.set('environments', { table: 'environments', columns: ['project_id', 'key', 'name', 'is_primary', 'priority', 'archived', 'data_json'], jsonColumns: ['data_json'], joins: { projects: { table: 'projects', on: 'environments.project_id = projects.id', type: 'INNER' }, flag_environments: { table: 'flag_environments', on: 'environments.project_id = flag_environments.project_id AND environments.key = flag_environments.environment_key', type: 'LEFT' } } }); // 3. Flags schema (already exists - keeping for completeness) schemaMap.set('flags', { table: 'flags', columns: ['project_id', 'key', 'id', 'name', 'description', 'archived', 'created_time', 'updated_time', 'data_json'], jsonColumns: ['data_json'], joins: { flag_environments: { table: 'flag_environments', on: 'flags.project_id = flag_environments.project_id AND flags.key = flag_environments.flag_key', type: 'LEFT' }, variations: { table: 'variations', on: 'flags.project_id = variations.project_id AND flags.key = variations.flag_key', type: 'LEFT' }, rules: { table: 'rules', on: 'flags.project_id = rules.project_id AND flags.key = rules.flag_key', type: 'LEFT' }, rulesets: { table: 'rulesets', on: 'flags.project_id = rulesets.project_id AND flags.key = rulesets.flag_key', type: 'LEFT' }, variable_definitions: { table: 'variable_definitions', on: 'flags.project_id = variable_definitions.project_id AND flags.key = variable_definitions.flag_key', type: 'LEFT' } } }); // 4. Flag Environments schema schemaMap.set('flag_environments', { table: 'flag_environments', columns: ['project_id', 'flag_key', 'environment_key', 'enabled', 'archived', 'rules_summary', 'data_json'], jsonColumns: ['rules_summary', 'data_json'], joins: { flags: { table: 'flags', on: 'flag_environments.project_id = flags.project_id AND flag_environments.flag_key = flags.key', type: 'INNER' }, environments: { table: 'environments', on: 'flag_environments.project_id = environments.project_id AND flag_environments.environment_key = environments.key', type: 'INNER' } } }); // 5. Variations schema schemaMap.set('variations', { table: 'variations', columns: ['id', 'key', 'project_id', 'flag_key', 'name', 'description', 'enabled', 'archived', 'variables', 'created_time', 'updated_time', 'data_json'], jsonColumns: ['variables', 'data_json'], joins: { flags: { table: 'flags', on: 'variations.project_id = flags.project_id AND variations.flag_key = flags.key', type: 'INNER' } } }); // 6. Variable Definitions schema schemaMap.set('variable_definitions', { table: 'variable_definitions', columns: ['key', 'project_id', 'flag_key', 'description', 'type', 'default_value', 'created_time', 'updated_time', 'data_json'], jsonColumns: ['data_json'], joins: { flags: { table: 'flags', on: 'variable_definitions.project_id = flags.project_id AND variable_definitions.flag_key = flags.key', type: 'INNER' } } }); // 7. Rulesets schema schemaMap.set('rulesets', { table: 'rulesets', columns: ['id', 'project_id', 'flag_key', 'environment_key', 'enabled', 'rules_count', 'revision', 'created_time', 'updated_time', 'data_json'], jsonColumns: ['data_json'], joins: { flags: { table: 'flags', on: 'rulesets.project_id = flags.project_id AND rulesets.flag_key = flags.key', type: 'INNER' }, rules: { table: 'rules', on: 'rulesets.project_id = rules.project_id AND rulesets.flag_key = rules.flag_key AND rulesets.environment_key = rules.environment_key', type: 'LEFT' } } }); // 8. Rules schema schemaMap.set('rules', { table: 'rules', columns: ['id', 'key', 'project_id', 'flag_key', 'environment_key', 'type', 'name', 'audience_conditions', 'percentage_included', 'variations', 'metrics', 'enabled', 'created_time', 'updated_time', 'data_json'], jsonColumns: ['audience_conditions', 'variations', 'metrics', 'data_json'], joins: { rulesets: { table: 'rulesets', on: 'rules.project_id = rulesets.project_id AND rules.flag_key = rulesets.flag_key AND rules.environment_key = rulesets.environment_key', type: 'INNER' }, flags: { table: 'flags', on: 'rules.project_id = flags.project_id AND rules.flag_key = flags.key', type: 'INNER' } } }); // 9. Changes schema schemaMap.set('changes', { table: 'changes', columns: ['id', 'project_id', 'entity_type', 'entity_id', 'entity_key', 'action', 'user_id', 'user_email', 'timestamp', 'details', 'data_json'], jsonColumns: ['details', 'data_json'], joins: { projects: { table: 'projects', on: 'changes.project_id = projects.id', type: 'INNER' } } }); // 10. Reports schema schemaMap.set('reports', { table: 'reports', columns: ['id', 'project_id', 'flag_key', 'rule_key', 'environment_key', 'report_type', 'start_time', 'end_time', 'data_json'], jsonColumns: ['data_json'], joins: { flags: { table: 'flags', on: 'reports.project_id = flags.project_id AND reports.flag_key = flags.key', type: 'LEFT' } } }); // 11. Features schema schemaMap.set('features', { table: 'features', columns: ['id', 'project_id', 'key', 'name', 'description', 'archived', 'created', 'last_modified', 'variable_definitions', 'data_json'], jsonColumns: ['variable_definitions', 'data_json'], joins: { projects: { table: 'projects', on: 'features.project_id = projects.id', type: 'INNER' } } }); // 12. Experiments schema (already exists - keeping for completeness) schemaMap.set('experiments', { table: 'experiments', columns: ['id', 'project_id', 'name', 'description', 'status', 'type', 'flag_key', 'environment', 'archived', 'created_time', 'updated_time', 'data_json'], jsonColumns: ['data_json'], joins: { results: { table: 'experiment_results', on: 'experiments.id = experiment_results.experiment_id', type: 'LEFT' }, campaigns: { table: 'campaigns', on: 'JSON_EXTRACT(experiments.data_json, "$.campaign_id") = campaigns.id', type: 'LEFT' }, audiences: { table: 'audiences', on: 'EXISTS (SELECT 1 FROM json_each(experiments.data_json, \'$.audience_conditions\') WHERE value = audiences.id)', type: 'LEFT' }, projects: { table: 'projects', on: 'experiments.project_id = projects.id', type: 'INNER' }, flags: { table: 'flags', on: 'experiments.flag_key = flags.key AND experiments.project_id = flags.project_id', type: 'LEFT' } } }); // 13. Experiment Results schema schemaMap.set('experiment_results', { table: 'experiment_results', columns: ['id', 'experiment_id', 'project_id', 'confidence_level', 'use_stats_engine', 'stats_engine_version', 'baseline_count', 'treatment_count', 'total_count', 'start_time', 'last_update', 'results_json', 'reach_json', 'stats_config_json', 'data_json'], jsonColumns: ['results_json', 'reach_json', 'stats_config_json', 'data_json'], joins: { experiments: { table: 'experiments', on: 'experiment_results.experiment_id = experiments.id', type: 'INNER' }, projects: { table: 'projects', on: 'experiment_results.project_id = projects.id', type: 'INNER' } } }); // 14. Campaigns schema schemaMap.set('campaigns', { table: 'campaigns', columns: ['id', 'project_id', 'name', 'description', 'holdback', 'archived', 'created_time', 'updated_time', 'data_json'], jsonColumns: ['data_json'], joins: { projects: { table: 'projects', on: 'campaigns.project_id = projects.id', type: 'INNER' }, experiments: { table: 'experiments', on: 'JSON_EXTRACT(experiments.data_json, "$.campaign_id") = campaigns.id', type: 'LEFT' } } }); // 15. Pages schema schemaMap.set('pages', { table: 'pages', columns: ['id', 'project_id', 'name', 'edit_url', 'activation_mode', 'activation_code', 'conditions', 'archived', 'created_time', 'updated_time', 'data_json'], jsonColumns: ['conditions', 'data_json'], joins: { projects: { table: 'projects', on: 'pages.project_id = projects.id', type: 'INNER' } } }); // 16. Audiences schema (already exists - keeping for completeness) schemaMap.set('audiences', { table: 'audiences', columns: ['id', 'project_id', 'name', 'description', 'conditions', 'archived', 'created_time', 'last_modified', 'data_json'], jsonColumns: ['conditions', 'data_json'], joins: { experiments: { table: 'experiments', on: 'EXISTS (SELECT 1 FROM json_each(experiments.data_json, \'$.audience_conditions\') WHERE value = audiences.id)', type: 'LEFT' }, projects: { table: 'projects', on: 'audiences.project_id = projects.id', type: 'INNER' } } }); // 17. Attributes schema schemaMap.set('attributes', { table: 'attributes', columns: ['id', 'project_id', 'key', 'name', 'condition_type', 'archived', 'last_modified', 'data_json'], jsonColumns: ['data_json'], joins: { projects: { table: 'projects', on: 'attributes.project_id = projects.id', type: 'INNER' } } }); // 18. Events schema schemaMap.set('events', { table: 'events', columns: ['id', 'project_id', 'key', 'name', 'description', 'event_type', 'category', 'archived', 'created_time', 'data_json'], jsonColumns: ['data_json'], joins: { projects: { table: 'projects', on: 'events.project_id = projects.id', type: 'INNER' } } }); // 19. Collaborators schema schemaMap.set('collaborators', { table: 'collaborators', columns: ['user_id', 'project_id', 'email', 'name', 'role', 'permissions_json', 'invited_at', 'last_seen_at', 'data_json'], jsonColumns: ['permissions_json', 'data_json'], joins: { projects: { table: 'projects', on: 'collaborators.project_id = projects.id', type: 'INNER' } } }); // 20. Groups schema schemaMap.set('groups', { table: 'groups', columns: ['id', 'project_id', 'name', 'description', 'type', 'policy', 'traffic_allocation', 'archived', 'created_time', 'updated_time', 'data_json'], jsonColumns: ['data_json'], joins: { projects: { table: 'projects', on: 'groups.project_id = projects.id', type: 'INNER' } } }); // 21. Extensions schema schemaMap.set('extensions', { table: 'extensions', columns: ['id', 'project_id', 'name', 'description', 'extension_type', 'implementation', 'enabled', 'created_time', 'updated_time', 'data_json'], jsonColumns: ['implementation', 'data_json'], joins: { projects: { table: 'projects', on: 'extensions.project_id = projects.id', type: 'INNER' } } }); // 22. Webhooks schema schemaMap.set('webhooks', { table: 'webhooks', columns: ['id', 'project_id', 'name', 'url', 'event_types', 'enabled', 'headers', 'secret', 'created_time', 'updated_time', 'data_json'], jsonColumns: ['event_types', 'headers', 'data_json'], joins: { projects: { table: 'projects', on: 'webhooks.project_id = projects.id', type: 'INNER' } } }); // 23. List Attributes schema schemaMap.set('list_attributes', { table: 'list_attributes', columns: ['id', 'project_id', 'name', 'description', 'key_field', 'list_type', 'list_content', 'created_time', 'updated_time', 'archived', 'data_json'], jsonColumns: ['list_content', 'data_json'], joins: { projects: { table: 'projects', on: 'list_attributes.project_id = projects.id', type: 'INNER' } } }); // 24. Change History schema schemaMap.set('change_history', { table: 'change_history', columns: ['id', 'project_id', 'entity_type', 'entity_id', 'entity_name', 'action', 'timestamp', 'changed_by', 'change_summary', 'archived'], jsonColumns: [], joins: { projects: { table: 'projects', on: 'change_history.project_id = projects.id', type: 'INNER' } } }); // 25. Sync State schema schemaMap.set('sync_state', { table: 'sync_state', columns: ['project_id', 'last_sync_time', 'last_successful_sync', 'sync_in_progress', 'error_count', 'last_error'], jsonColumns: [], joins: { projects: { table: 'projects', on: 'sync_state.project_id = projects.id', type: 'INNER' } } }); // 26. Sync Metadata schema schemaMap.set('sync_metadata', { table: 'sync_metadata', columns: ['key', 'value', 'updated_at'], jsonColumns: [], joins: {} // No direct joins }); getLogger().info({ totalSchemas: schemaMap.size, implementation: 'COMPLETE', coverage: '100%', missingSchemas: 0 }, 'IntelligentFieldMapper schema initialization COMPLETE'); return schemaMap; } /** * Get list of all available entities */ getAvailableEntities() { return Array.from(this.schemaMap.keys()).sort(); } /** * Get schema info for an entity */ getEntitySchema(entity) { return this.schemaMap.get(entity); } /** * Validate that all required schemas are present */ validateCompleteness() { const requiredEntities = [ 'projects', 'environments', 'flags', 'flag_environments', 'variations', 'variable_definitions', 'rulesets', 'rules', 'changes', 'reports', 'features', 'experiments', 'experiment_results', 'campaigns', 'pages', 'audiences', 'attributes', 'events', 'collaborators', 'groups', 'extensions', 'webhooks', 'list_attributes', 'change_history', 'sync_state', 'sync_metadata' ]; const missing = requiredEntities.filter(entity => !this.schemaMap.has(entity)); return { isComplete: missing.length === 0, missing }; } /** * Legacy method for HybridQueryBuilder compatibility * Resolves a field reference to SQL mapping */ resolveField(entityType, fieldPath) { const result = this.mapField(entityType, fieldPath); if (result.success) { return { sqlField: result.isJsonPath ? `JSON_EXTRACT(${result.table}.${result.column}, '${result.jsonPath}')` : `${result.table}.${result.column}`, requiresJsonProcessing: result.isJsonPath }; } else { throw new Error(result.error || `Field ${fieldPath} not found in ${entityType}`); } } /** * Legacy method for HybridQueryBuilder compatibility * Gets required joins for an entity */ getRequiredJoins(entityType, fields) { const schema = this.schemaMap.get(entityType); if (!schema || !schema.joins) { return []; } const joins = []; // For now, return all available joins for the entity // In a more sophisticated implementation, this would analyze which fields actually need which joins for (const [joinEntity, joinInfo] of Object.entries(schema.joins)) { joins.push({ table: joinInfo.table, condition: joinInfo.on, type: joinInfo.type }); } return joins; } } //# sourceMappingURL=IntelligentFieldMapper.js.map