UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

272 lines 10.7 kB
export class EntityAdoptionService { entityRouter; cache; logger; constructor(entityRouter, cache, logger) { this.entityRouter = entityRouter; this.cache = cache; this.logger = logger; } /** * Main entry point - check if entity should be adopted */ async checkForAdoption(entityType, entityData, projectId, options = {}) { // Skip if adoption is disabled if (options.adoptionStrategy === 'never') { return { found: false }; } // Skip if not an adoptable entity type if (!this.shouldCheckForExistence(entityType)) { return { found: false }; } // 1. Extract identifying information based on entity type const reference = this.extractReference(entityType, entityData); // Only proceed if we have something to search for if (Object.keys(reference).length === 0) { return { found: false }; } // 2. Search for existing entity const existing = await this.findExisting(entityType, reference, projectId, options); // 3. Return adoption result return existing; } /** * Determine if entity type should be checked for existence * Copied from EntityOrchestrator line 4110 */ shouldCheckForExistence(entityType) { const adoptableEntities = [ 'event', 'page', 'attribute', 'audience', 'extension', 'webhook' // NOTE: 'variation' and 'variable_definition' removed - these are flag-specific ]; return adoptableEntities.includes(entityType); } /** * Extract identifying fields based on entity type * Copied from EntityOrchestrator line 4151 */ extractReference(entityType, data) { const reference = {}; // Always extract ID if present if (data.id) reference.id = data.id; // Extract different identifier fields based on entity type if (entityType === 'event') { // Events use 'name' field for identification if (data.name) reference.name = data.name; if (data.key) reference.key = data.key; } else if (entityType === 'audience') { // Audiences use 'name' field as primary identifier if (data.name) reference.name = data.name; } else if (entityType === 'page') { // Pages use 'key' field primarily if (data.key) reference.key = data.key; if (data.name) reference.name = data.name; } else { // Most other entities use 'key' field if (data.key) reference.key = data.key; // Also include name for entities that might use it if (data.name) reference.name = data.name; } return reference; } /** * Find existing entity by reference */ async findExisting(entityType, reference, projectId, options) { // 1. Check local cache first const cacheResult = await this.searchCache(entityType, reference, projectId); if (cacheResult.found) { this.logger.debug(`🔍 EntityAdoptionService.findExisting: Found in cache`, { entityType, reference, projectId }); return cacheResult; } // Check if API search is enabled this.logger.debug(`🔍 EntityAdoptionService.findExisting: Not found in cache, checkApi=${options.checkApi}`, { entityType, reference, projectId, checkApi: options.checkApi }); // 2. Check API if enabled and not found in cache if (options.checkApi !== false) { this.logger.warn(`⚠️ EntityAdoptionService.findExisting: API search enabled - this may cause Spanish audience bug!`, { entityType, reference, projectId }); return await this.searchApi(entityType, reference, projectId); } return { found: false }; } /** * Search cache for existing entity */ async searchCache(entityType, reference, projectId) { try { // Use EntityRouter to search in cache const searchResults = await this.entityRouter.searchEntities({ entityType: entityType, projectId, searchCriteria: reference }); if (searchResults && searchResults.length > 0) { // Find best match based on reference const bestMatch = this.findBestMatch(entityType, searchResults, reference); if (bestMatch) { return { found: true, entity: bestMatch, adoptionReason: `Found in cache by ${Object.keys(reference).join(', ')}`, searchCriteria: reference }; } } } catch (error) { this.logger.warn(`Cache search failed for ${entityType}`, { error }); } return { found: false }; } /** * Search API for existing entity */ async searchApi(entityType, reference, projectId) { try { this.logger.debug(`🔍 EntityAdoptionService.searchApi: Starting API search for ${entityType}`, { entityType, reference, projectId }); // Build filters for API search const filters = {}; // Map reference fields to API filter fields if (reference.id) filters.id = reference.id; if (reference.key) filters.key = reference.key; if (reference.name) filters.name = reference.name; this.logger.debug(`🔍 EntityAdoptionService.searchApi: Built filters for API search`, { filters, filtersCount: Object.keys(filters).length }); // Use EntityRouter to find individual entity by ID, key, or name let foundEntity = null; let searchIdentifier = null; let searchType = null; // Determine which identifier to use (priority: ID > key > name) if (reference.id) { searchIdentifier = reference.id; searchType = 'id'; } else if (reference.key) { searchIdentifier = reference.key; searchType = 'key'; } else if (reference.name) { searchIdentifier = reference.name; searchType = 'name'; } if (searchIdentifier) { this.logger.debug(`🔍 EntityAdoptionService.searchApi: Searching by ${searchType}: ${searchIdentifier}`); foundEntity = await this.entityRouter.findEntityByIdOrKey(entityType, searchIdentifier, projectId); } this.logger.debug(`🔍 EntityAdoptionService.searchApi: Individual lookup result`, { entityType, foundEntity: !!foundEntity, searchedBy: searchType || 'none', foundName: foundEntity?.name, foundId: foundEntity?.id }); // Convert to array format for consistency with findBestMatch expectations const resultArray = foundEntity ? [foundEntity] : []; if (Array.isArray(resultArray) && resultArray.length > 0) { this.logger.debug(`🔍 EntityAdoptionService.searchApi: Found ${resultArray.length} results, looking for best match`, { entityType, reference, resultCount: resultArray.length, firstResultName: resultArray[0]?.name, searchingForName: reference.name }); const bestMatch = this.findBestMatch(entityType, resultArray, reference); this.logger.debug(`🔍 EntityAdoptionService.searchApi: Best match result`, { entityType, hasBestMatch: !!bestMatch, bestMatchName: bestMatch?.name, bestMatchId: bestMatch?.id, searchedForName: reference.name }); if (bestMatch) { return { found: true, entity: bestMatch, adoptionReason: `Found in API by ${Object.keys(reference).join(', ')}`, searchCriteria: reference }; } } else { this.logger.debug(`🔍 EntityAdoptionService.searchApi: No results found or results not an array`, { entityType, reference, resultArrayLength: Array.isArray(resultArray) ? resultArray.length : 'not array' }); } } catch (error) { this.logger.warn(`API search failed for ${entityType}`, { error: error instanceof Error ? error.message : String(error) }); } return { found: false }; } /** * Find best match from search results */ findBestMatch(entityType, results, reference) { // For audiences, exact name match is required if (entityType === 'audience' && reference.name) { return results.find(r => r.name === reference.name); } // For events, match by key or name if (entityType === 'event') { if (reference.key) { return results.find(r => r.key === reference.key); } if (reference.name) { return results.find(r => r.name === reference.name); } } // For pages, prefer key match if (entityType === 'page' && reference.key) { return results.find(r => r.key === reference.key); } // Generic ID match if (reference.id) { return results.find(r => String(r.id) === String(reference.id)); } // Generic key match if (reference.key) { return results.find(r => r.key === reference.key); } // Generic name match if (reference.name) { return results.find(r => r.name === reference.name); } // No match found return null; } } //# sourceMappingURL=EntityAdoptionService.js.map