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