UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

399 lines 15.3 kB
/** * Reference Resolver * * Handles entity reference resolution with multiple strategies: * - By ID (direct lookup) * - By name (search and match) * - By key (for entities that support keys) * - Auto-create (if not found and configured) * - Step references (${step_id.field}) */ import { LRUCache } from 'lru-cache'; export class ReferenceResolver { entityCache; lookupStrategies; apiClient; constructor(apiClient) { // Initialize cache with 5-minute TTL this.entityCache = new LRUCache({ max: 1000, ttl: 1000 * 60 * 5, // 5 minutes }); // Mock API client for now this.apiClient = apiClient || this.createMockApiClient(); this.lookupStrategies = new Map(); this.initializeStrategies(); } /** * Resolves an entity reference with multiple fallback strategies */ async resolveReference(ref, context) { // Handle step references first if (typeof ref === 'string' && ref.startsWith('${') && ref.endsWith('}')) { return this.resolveStepReference(ref, context); } // Convert to EntityReference if needed const entityRef = typeof ref === 'string' ? { ref: { id: ref } } : ref; // Check cache first const cacheKey = this.getCacheKey(entityRef, context); const cached = this.entityCache.get(cacheKey); if (cached) return cached; try { // Try resolution strategies in order const strategies = this.getStrategiesFor(context.entityType); for (const strategy of strategies) { const result = await strategy.resolve(entityRef, context); if (result.found) { this.entityCache.set(cacheKey, result); return result; } } // Not found - handle based on ref configuration if (entityRef.ref.auto_create && entityRef.ref.create_template) { return { method: 'will_create', template: entityRef.ref.create_template, reason: `${context.entityType} not found, will be created`, found: false }; } const error = `Unable to resolve ${context.entityType} reference: ${JSON.stringify(entityRef)}`; return { method: 'error', error, suggestion: this.getSuggestion(error, context.entityType), found: false }; } catch (error) { return { method: 'error', error: error instanceof Error ? error.message : String(error), suggestion: this.getSuggestion(error instanceof Error ? error.message : String(error), context.entityType), found: false }; } } /** * Resolve a step reference like ${step_id.field} */ resolveStepReference(ref, context) { const refPattern = /^\$\{([^.]+)\.([^}]+)\}$/; const match = ref.match(refPattern); if (!match) { return { method: 'error', error: `Invalid step reference format: ${ref}`, suggestion: 'Use format: ${step_id.field_name}', found: false }; } const [, stepId, fieldName] = match; if (!context.availableSteps?.has(stepId)) { return { method: 'error', error: `Referenced step "${stepId}" not found`, suggestion: `Available steps: ${Array.from(context.availableSteps?.keys() || []).join(', ')}`, found: false }; } const step = context.availableSteps.get(stepId); const value = step.outputs?.[fieldName] || step.template?.outputs?.[fieldName]; if (value === undefined) { return { method: 'error', error: `Field "${fieldName}" not found in step "${stepId}" outputs`, suggestion: `Available fields: ${Object.keys(step.outputs || {}).join(', ')}`, found: false }; } return { method: 'step_reference', id: value, matchedBy: `${stepId}.${fieldName}`, found: true }; } /** * Initialize lookup strategies */ initializeStrategies() { // ID lookup strategy this.lookupStrategies.set('id', { name: 'ID Lookup', resolve: async (ref, context) => { if (!ref.ref.id) return { found: false, method: 'id' }; try { const entity = await this.apiClient.getEntity(context.entityType, ref.ref.id, context.projectId); return { method: 'id', entity, id: ref.ref.id, found: true }; } catch (error) { return { found: false, method: 'id' }; } } }); // Name lookup strategy this.lookupStrategies.set('name', { name: 'Name Lookup', resolve: async (ref, context) => { if (!ref.ref.name) return { found: false, method: 'name' }; const entities = await this.apiClient.listEntities(context.entityType, context.projectId, { name: ref.ref.name }); if (entities.length === 1) { return { method: 'name', entity: entities[0], id: entities[0].id, matchedBy: 'exact_name', found: true }; } if (entities.length > 1) { // Multiple matches - try exact match const exactMatch = entities.find(e => e.name === ref.ref.name); if (exactMatch) { return { method: 'name', entity: exactMatch, id: exactMatch.id, matchedBy: 'exact_name_multiple', found: true }; } } // Try fuzzy match const fuzzyMatch = await this.fuzzyMatchByName(entities, ref.ref.name, context); if (fuzzyMatch) { return { method: 'name_fuzzy', entity: fuzzyMatch, id: fuzzyMatch.id, matchedBy: 'fuzzy_name', confidence: 0.8, found: true }; } return { found: false, method: 'name' }; } }); // Key lookup strategy this.lookupStrategies.set('key', { name: 'Key Lookup', resolve: async (ref, context) => { if (!ref.ref.key) return { found: false, method: 'key' }; // Only certain entity types support keys const keySupported = ['experiment', 'feature', 'flag', 'event', 'attribute', 'page', 'audience'].includes(context.entityType); if (!keySupported) return { found: false, method: 'key' }; const entities = await this.apiClient.listEntities(context.entityType, context.projectId, { key: ref.ref.key }); if (entities.length > 0) { return { method: 'key', entity: entities[0], id: entities[0].id, matchedBy: 'key', found: true }; } return { found: false, method: 'name' }; } }); } /** * Get strategies for a specific entity type */ getStrategiesFor(entityType) { // Order matters - try most specific first const order = ['id', 'key', 'name']; return order .map(name => this.lookupStrategies.get(name)) .filter((s) => s !== undefined); } /** * Fuzzy match by name */ async fuzzyMatchByName(entities, targetName, context) { if (entities.length === 0) return null; const normalized = targetName.toLowerCase().trim(); // Try various matching strategies const matches = entities.filter(e => { const entityName = (e.name || '').toLowerCase().trim(); // Exact match (case-insensitive) if (entityName === normalized) return true; // Contains match if (entityName.includes(normalized) || normalized.includes(entityName)) return true; // Remove common prefixes/suffixes const cleanTarget = normalized.replace(/^(test|prod|dev|staging)[-_\s]*/i, ''); const cleanEntity = entityName.replace(/^(test|prod|dev|staging)[-_\s]*/i, ''); if (cleanEntity === cleanTarget) return true; return false; }); // Return best match (shortest name that matches) if (matches.length > 0) { return matches.sort((a, b) => a.name.length - b.name.length)[0]; } return null; } /** * Get cache key for a reference */ getCacheKey(ref, context) { const parts = [ context.projectId, context.entityType, ref.ref.id || '', ref.ref.name || '', ref.ref.key || '' ]; return parts.join(':'); } /** * Get helpful suggestion for resolution errors */ getSuggestion(error, entityType) { if (error.includes('not found')) { return `Try using 'auto_create: true' to create the ${entityType} automatically`; } if (error.includes('multiple')) { return `Multiple ${entityType}s found. Try using a more specific name or use ID instead`; } if (error.includes('Invalid')) { return `Check the reference format. Use { ref: { id: "123" } } or { ref: { name: "My ${entityType}" } }`; } return `Verify the ${entityType} exists in the project`; } /** * Resolve all references in a template */ async resolveTemplateReferences(template, projectId, platform = 'web') { const resolutions = new Map(); const availableSteps = new Map(); // First pass - collect all steps for (const step of template.steps || []) { availableSteps.set(step.id, step); } // Second pass - resolve references for (const step of template.steps || []) { if (step.type !== 'template') continue; const entityType = this.extractEntityType(step.template?.system_template_id); if (!entityType) continue; const context = { projectId, entityType, platform, availableSteps }; // Check all fields that might contain references const inputs = step.template?.inputs || {}; const referenceFields = this.getReferenceFieldsForEntity(entityType); for (const field of referenceFields) { const value = inputs[field]; if (!value) continue; const key = `${step.id}.${field}`; if (typeof value === 'string' && value.startsWith('${')) { // Step reference resolutions.set(key, await this.resolveReference(value, context)); } else if (typeof value === 'object' && value.ref) { // Entity reference resolutions.set(key, await this.resolveReference(value, context)); } else if (Array.isArray(value)) { // Array of references for (let i = 0; i < value.length; i++) { const item = value[i]; if (typeof item === 'object' && item.ref) { resolutions.set(`${key}[${i}]`, await this.resolveReference(item, context)); } } } } } return resolutions; } /** * Get fields that typically contain references for an entity type */ getReferenceFieldsForEntity(entityType) { const referenceFields = { experiment: ['campaign_id', 'audience', 'audiences', 'page_ids', 'event_id', 'metrics'], campaign: ['page_ids', 'metrics', 'audiences'], page: [], audience: [], event: ['page_id'], flag: ['environment_id'], ruleset: ['flag_id', 'environment_id'], rule: ['ruleset_id', 'flag_id'] }; return referenceFields[entityType] || []; } /** * Extract entity type from system template ID */ extractEntityType(systemTemplateId) { if (!systemTemplateId) return null; const patterns = [ { regex: /optimizely_experiment_/, type: 'experiment' }, { regex: /optimizely_campaign_/, type: 'campaign' }, { regex: /optimizely_audience_/, type: 'audience' }, { regex: /optimizely_event_/, type: 'event' }, { regex: /optimizely_flag_/, type: 'flag' }, { regex: /optimizely_ruleset_/, type: 'ruleset' }, { regex: /optimizely_rule_/, type: 'rule' }, { regex: /optimizely_page_/, type: 'page' } ]; for (const pattern of patterns) { if (pattern.regex.test(systemTemplateId)) { return pattern.type; } } return null; } /** * Create a mock API client for testing */ createMockApiClient() { return { getEntity: async (entityType, id, projectId) => { // Mock implementation return { id, name: `Mock ${entityType} ${id}`, type: entityType, project_id: projectId }; }, listEntities: async (entityType, projectId, filters) => { // Mock implementation if (filters?.name) { return [{ id: Math.floor(Math.random() * 10000), name: filters.name, type: entityType, project_id: projectId }]; } return []; } }; } } //# sourceMappingURL=ReferenceResolver.js.map