@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
272 lines • 10.7 kB
JavaScript
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