UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

267 lines 9.86 kB
/** * Mapping Resolver * @description Handles ID and key translation between source and destination projects * with intelligent lookup and caching capabilities */ import { getLogger } from '../logging/Logger.js'; import { ENTITY_KEY_FIELDS, REFERENCE_FIELDS, MIGRATION_ERROR_CODES } from './constants.js'; export class MappingResolver { logger = getLogger(); sourceTools; destinationTools; mappings; resolvedMappings = new Map(); options; constructor(sourceTools, destinationTools, initialMappings = {}, options = {}) { this.sourceTools = sourceTools; this.destinationTools = destinationTools; this.mappings = initialMappings; this.options = { autoResolve: true, cacheResults: true, strictMode: false, ...options }; // Initialize resolved mappings cache this.initializeCache(); } /** * Initialize the resolved mappings cache */ initializeCache() { // Pre-populate cache with provided mappings Object.entries(this.mappings).forEach(([entityType, mappingDict]) => { if (mappingDict) { const cache = new Map(Object.entries(mappingDict)); this.resolvedMappings.set(entityType, cache); } }); } /** * Resolve a single reference */ async resolveReference(sourceRef, targetEntityType, sourceProjectId, destProjectId) { const cacheKey = `${targetEntityType}s`; // Check cache first const cached = this.resolvedMappings.get(cacheKey)?.get(sourceRef); if (cached) { this.logger.debug(`Cache hit for ${targetEntityType} ${sourceRef} → ${cached}`); return cached; } // Check provided mappings const provided = this.mappings[cacheKey]?.[sourceRef]; if (provided) { this.cacheMapping(cacheKey, sourceRef, provided); return provided; } // Auto-resolve if enabled if (this.options.autoResolve) { const resolved = await this.autoResolve(sourceRef, targetEntityType, sourceProjectId, destProjectId); if (resolved) { this.cacheMapping(cacheKey, sourceRef, resolved); return resolved; } } // Handle missing mapping if (this.options.strictMode) { throw new Error(`${MIGRATION_ERROR_CODES.MISSING_MAPPING}: No mapping found for ${targetEntityType} ${sourceRef}`); } this.logger.warn(`No mapping found for ${targetEntityType} ${sourceRef}`); return null; } /** * Resolve all references in an entity */ async resolveEntityReferences(entity, entityType, sourceProjectId, destProjectId) { const resolved = { ...entity }; const referenceFields = REFERENCE_FIELDS[entityType] || []; for (const field of referenceFields) { if (!resolved[field]) continue; const targetType = this.getTargetTypeFromField(field); if (!targetType) continue; if (Array.isArray(resolved[field])) { const resolvedRefs = await Promise.all(resolved[field].map((ref) => this.resolveReference(ref, targetType, sourceProjectId, destProjectId))); // Filter out null values (unresolved references) resolved[field] = resolvedRefs.filter(ref => ref !== null); } else { const resolvedRef = await this.resolveReference(resolved[field], targetType, sourceProjectId, destProjectId); if (resolvedRef) { resolved[field] = resolvedRef; } else if (this.options.strictMode) { throw new Error(`${MIGRATION_ERROR_CODES.MISSING_MAPPING}: Required reference ${field} could not be resolved`); } } } return resolved; } /** * Auto-resolve by looking up entity in destination */ async autoResolve(sourceRef, targetEntityType, sourceProjectId, destProjectId) { try { // First, get the entity from source to find its name const sourceEntity = await this.fetchEntity(this.sourceTools, targetEntityType, sourceRef, sourceProjectId); if (!sourceEntity) { this.logger.warn(`Source entity not found: ${targetEntityType} ${sourceRef}`); return null; } // Try to find by name in destination const entityName = sourceEntity.name || sourceEntity.key || sourceRef; const destEntities = await this.searchByName(this.destinationTools, targetEntityType, entityName, destProjectId); if (destEntities.length === 1) { // Exact match found const keyField = ENTITY_KEY_FIELDS[targetEntityType]; const destRef = destEntities[0][keyField]; this.logger.info(`Auto-resolved ${targetEntityType} "${entityName}" → ${destRef}`); return destRef; } else if (destEntities.length > 1) { this.logger.warn(`Multiple matches found for ${targetEntityType} "${entityName}" in destination, cannot auto-resolve`); } else { this.logger.debug(`No match found for ${targetEntityType} "${entityName}" in destination`); } return null; } catch (error) { this.logger.error(`Error during auto-resolve for ${targetEntityType} ${sourceRef}: ${error instanceof Error ? error.message : String(error)}`); return null; } } /** * Fetch entity from project */ async fetchEntity(tools, entityType, entityId, projectId) { try { const result = await tools.getEntityDetails(entityType, entityId, projectId); return result.success ? result.data : null; } catch (error) { return null; } } /** * Search for entities by name */ async searchByName(tools, entityType, name, projectId) { try { const result = await tools.listEntities(entityType, projectId, { filter: { name } }); if (!result.success || !result.data) { return []; } // Filter for exact name match return result.data.filter((entity) => entity.name === name || entity.key === name); } catch (error) { this.logger.error(`Error searching for ${entityType} by name "${name}": ${error instanceof Error ? error.message : String(error)}`); return []; } } /** * Get target entity type from field name */ getTargetTypeFromField(field) { const mappings = { 'audience_ids': 'audience', 'audience_id': 'audience', 'event_ids': 'event', 'event_id': 'event', 'page_ids': 'page', 'page_id': 'page', 'attribute_ids': 'attribute', 'attribute_id': 'attribute', 'flag_key': 'flag', 'experiment_id': 'experiment', 'campaign_id': 'campaign', 'extension_id': 'extension', 'group_id': 'group', 'webhook_id': 'webhook', 'environment_id': 'environment' }; return mappings[field] || null; } /** * Cache a resolved mapping */ cacheMapping(entityType, sourceRef, destRef) { if (!this.options.cacheResults) return; if (!this.resolvedMappings.has(entityType)) { this.resolvedMappings.set(entityType, new Map()); } this.resolvedMappings.get(entityType).set(sourceRef, destRef); // Also update the main mappings object if (!this.mappings[entityType]) { this.mappings[entityType] = {}; } this.mappings[entityType][sourceRef] = destRef; } /** * Get all resolved mappings */ getMappings() { return this.mappings; } /** * Add manual mapping */ addMapping(entityType, sourceRef, destRef) { const cacheKey = `${entityType}s`; this.cacheMapping(cacheKey, sourceRef, destRef); this.logger.info(`Added manual mapping: ${entityType} ${sourceRef} → ${destRef}`); } /** * Bulk add mappings */ addMappings(mappings) { Object.entries(mappings).forEach(([entityType, mappingDict]) => { if (mappingDict) { Object.entries(mappingDict).forEach(([sourceRef, destRef]) => { this.cacheMapping(entityType, sourceRef, destRef); }); } }); this.logger.info(`Added ${Object.keys(mappings).length} mapping categories`); } /** * Clear cache */ clearCache() { this.resolvedMappings.clear(); this.initializeCache(); this.logger.info('Mapping cache cleared'); } /** * Get cache statistics */ getCacheStats() { const stats = {}; this.resolvedMappings.forEach((cache, entityType) => { stats[entityType] = cache.size; }); return stats; } /** * Export mappings to JSON */ exportMappings() { return JSON.stringify(this.mappings, null, 2); } /** * Import mappings from JSON */ importMappings(json) { try { const imported = JSON.parse(json); this.addMappings(imported); } catch (error) { throw new Error(`${MIGRATION_ERROR_CODES.INVALID_MAPPING}: Invalid mapping JSON`); } } } //# sourceMappingURL=MappingResolver.js.map