UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

455 lines 18.6 kB
/** * Reference Transformer * @description Transforms entity payloads by updating references to use destination IDs/keys * with deep understanding of Optimizely entity structures */ import { getLogger } from '../logging/Logger.js'; import { REFERENCE_FIELDS } from './constants.js'; export class ReferenceTransformer { logger = getLogger(); mappingResolver; options; constructor(mappingResolver, options = {}) { this.mappingResolver = mappingResolver; this.options = { removeSourceFields: true, preserveTimestamps: false, validateReferences: true, transformCustomFields: true, ...options }; } /** * Transform entity payload for destination */ async transform(entity, entityType, sourceProjectId, destProjectId) { // Deep clone to avoid modifying original const transformed = JSON.parse(JSON.stringify(entity)); // Transform standard reference fields await this.transformStandardReferences(transformed, entityType, sourceProjectId, destProjectId); // Transform entity-specific fields await this.transformEntitySpecificFields(transformed, entityType, sourceProjectId, destProjectId); // Transform custom fields if enabled if (this.options.transformCustomFields) { await this.transformCustomFields(transformed, entityType, sourceProjectId, destProjectId); } // Clean up source-specific fields if (this.options.removeSourceFields) { this.removeSourceFields(transformed, entityType); } return transformed; } /** * Transform standard reference fields */ async transformStandardReferences(entity, entityType, sourceProjectId, destProjectId) { const referenceFields = REFERENCE_FIELDS[entityType] || []; for (const field of referenceFields) { if (!entity[field]) continue; const targetType = this.getTargetTypeFromField(field); if (!targetType) continue; if (Array.isArray(entity[field])) { const transformed = []; for (const ref of entity[field]) { const resolved = await this.mappingResolver.resolveReference(ref, targetType, sourceProjectId, destProjectId); if (resolved) { transformed.push(resolved); } else if (this.options.validateReferences) { throw new Error(`Failed to resolve ${targetType} reference: ${ref} in field ${field}`); } } entity[field] = transformed; } else { const resolved = await this.mappingResolver.resolveReference(entity[field], targetType, sourceProjectId, destProjectId); if (resolved) { entity[field] = resolved; } else if (this.options.validateReferences) { throw new Error(`Failed to resolve ${targetType} reference: ${entity[field]} in field ${field}`); } } } } /** * Transform entity-specific fields based on entity type */ async transformEntitySpecificFields(entity, entityType, sourceProjectId, destProjectId) { switch (entityType) { case 'flag': await this.transformFlagSpecificFields(entity, sourceProjectId, destProjectId); break; case 'experiment': await this.transformExperimentSpecificFields(entity, sourceProjectId, destProjectId); break; case 'campaign': await this.transformCampaignSpecificFields(entity, sourceProjectId, destProjectId); break; case 'audience': await this.transformAudienceConditions(entity, sourceProjectId, destProjectId); break; case 'webhook': await this.transformWebhookFields(entity, sourceProjectId, destProjectId); break; } } /** * Transform flag-specific fields */ async transformFlagSpecificFields(flag, sourceProjectId, destProjectId) { // Transform variable definitions if they reference other entities if (flag.variable_definitions) { for (const varDef of flag.variable_definitions) { if (varDef.default_value && typeof varDef.default_value === 'object') { // Check if default value contains entity references await this.transformObjectReferences(varDef.default_value, sourceProjectId, destProjectId); } } } // Transform rollout rules if (flag.rollout_rules) { for (const rule of flag.rollout_rules) { if (rule.audience_id) { const resolved = await this.mappingResolver.resolveReference(rule.audience_id, 'audience', sourceProjectId, destProjectId); if (resolved) rule.audience_id = resolved; } } } } /** * Transform experiment-specific fields */ async transformExperimentSpecificFields(experiment, sourceProjectId, destProjectId) { // Transform metrics if (experiment.metrics) { for (const metric of experiment.metrics) { if (metric.event_id) { const resolved = await this.mappingResolver.resolveReference(metric.event_id, 'event', sourceProjectId, destProjectId); if (resolved) metric.event_id = resolved; } } } // Transform variations if (experiment.variations) { for (const variation of experiment.variations) { // Transform page references in variations if (variation.page_ids) { const transformed = []; for (const pageId of variation.page_ids) { const resolved = await this.mappingResolver.resolveReference(pageId, 'page', sourceProjectId, destProjectId); if (resolved) transformed.push(resolved); } variation.page_ids = transformed; } // Transform extension references if (variation.extensions) { await this.transformExtensionReferences(variation.extensions, sourceProjectId, destProjectId); } } } } /** * Transform campaign-specific fields */ async transformCampaignSpecificFields(campaign, sourceProjectId, destProjectId) { // Transform experiment references if (campaign.experiment_ids) { const transformed = []; for (const expId of campaign.experiment_ids) { const resolved = await this.mappingResolver.resolveReference(expId, 'experiment', sourceProjectId, destProjectId); if (resolved) transformed.push(resolved); } campaign.experiment_ids = transformed; } // Transform page references in campaign pages if (campaign.campaign_pages) { for (const campaignPage of campaign.campaign_pages) { if (campaignPage.page_ids) { const transformed = []; for (const pageId of campaignPage.page_ids) { const resolved = await this.mappingResolver.resolveReference(pageId, 'page', sourceProjectId, destProjectId); if (resolved) transformed.push(resolved); } campaignPage.page_ids = transformed; } } } } /** * Transform audience conditions */ async transformAudienceConditions(audience, sourceProjectId, destProjectId) { if (!audience.conditions) return; // Parse conditions (they're stored as JSON string) let conditions; try { conditions = typeof audience.conditions === 'string' ? JSON.parse(audience.conditions) : audience.conditions; } catch (error) { this.logger.warn(`Failed to parse audience conditions for ${audience.name}`); return; } // Transform conditions recursively await this.transformConditionTree(conditions, sourceProjectId, destProjectId); // Stringify back audience.conditions = JSON.stringify(conditions); } /** * Transform condition tree recursively */ async transformConditionTree(conditions, sourceProjectId, destProjectId) { if (Array.isArray(conditions)) { for (const condition of conditions) { if (typeof condition === 'object') { await this.transformConditionTree(condition, sourceProjectId, destProjectId); } } } else if (conditions && typeof conditions === 'object') { // Transform attribute references in conditions if (conditions.name && conditions.type === 'custom_attribute') { const resolved = await this.mappingResolver.resolveReference(conditions.name, 'attribute', sourceProjectId, destProjectId); if (resolved) conditions.name = resolved; } // Transform event references if (conditions.event_id) { const resolved = await this.mappingResolver.resolveReference(conditions.event_id, 'event', sourceProjectId, destProjectId); if (resolved) conditions.event_id = resolved; } } } /** * Transform webhook fields */ async transformWebhookFields(webhook, sourceProjectId, destProjectId) { // Transform event filter if (webhook.event && webhook.event !== '*') { const resolved = await this.mappingResolver.resolveReference(webhook.event, 'event', sourceProjectId, destProjectId); if (resolved) webhook.event = resolved; } // Transform entity filter if (webhook.entity_id) { // Need to determine entity type from webhook config const entityType = this.getEntityTypeFromWebhook(webhook); if (entityType) { const resolved = await this.mappingResolver.resolveReference(webhook.entity_id, entityType, sourceProjectId, destProjectId); if (resolved) webhook.entity_id = resolved; } } } /** * Transform extension references */ async transformExtensionReferences(extensions, sourceProjectId, destProjectId) { for (const [extId, extConfig] of Object.entries(extensions)) { // Transform extension ID const resolved = await this.mappingResolver.resolveReference(extId, 'extension', sourceProjectId, destProjectId); if (resolved && resolved !== extId) { // Replace with new ID extensions[resolved] = extConfig; delete extensions[extId]; } } } /** * Transform custom fields that might contain references */ async transformCustomFields(entity, entityType, sourceProjectId, destProjectId) { // Look for fields that might contain entity references const customFieldPatterns = [ /_id$/, /_ids$/, /_key$/, /_keys$/, /^related_/, /^linked_/ ]; for (const [field, value] of Object.entries(entity)) { // Skip known fields if (REFERENCE_FIELDS[entityType]?.includes(field)) continue; // Check if field name suggests it contains references const mightContainRefs = customFieldPatterns.some(pattern => pattern.test(field)); if (mightContainRefs && value) { const targetType = this.guessEntityTypeFromField(field); if (targetType) { if (Array.isArray(value)) { const transformed = []; for (const ref of value) { const resolved = await this.mappingResolver.resolveReference(ref, targetType, sourceProjectId, destProjectId); if (resolved) transformed.push(resolved); else transformed.push(ref); // Keep original if not resolved } entity[field] = transformed; } else if (typeof value === 'string') { const resolved = await this.mappingResolver.resolveReference(value, targetType, sourceProjectId, destProjectId); if (resolved) entity[field] = resolved; } } } } } /** * Transform object references recursively */ async transformObjectReferences(obj, sourceProjectId, destProjectId) { for (const [key, value] of Object.entries(obj)) { if (typeof value === 'object' && value !== null) { if (Array.isArray(value)) { // Handle arrays for (let i = 0; i < value.length; i++) { if (typeof value[i] === 'object') { await this.transformObjectReferences(value[i], sourceProjectId, destProjectId); } } } else { // Recursively transform nested objects await this.transformObjectReferences(value, sourceProjectId, destProjectId); } } else if (typeof value === 'string' && this.looksLikeEntityReference(key, value)) { // Try to resolve if it looks like an entity reference const targetType = this.guessEntityTypeFromField(key); if (targetType) { const resolved = await this.mappingResolver.resolveReference(value, targetType, sourceProjectId, destProjectId); if (resolved) obj[key] = resolved; } } } } /** * Remove source-specific fields */ removeSourceFields(entity, entityType) { const fieldsToRemove = [ 'id', 'project_id', 'account_id', 'created', 'last_modified', 'created_by', 'last_modified_by' ]; if (!this.options.preserveTimestamps) { fieldsToRemove.push('created', 'last_modified'); } // Remove fields fieldsToRemove.forEach(field => delete entity[field]); // Entity-specific removals switch (entityType) { case 'experiment': delete entity.results; delete entity.stats; break; case 'flag': delete entity.environments; // Will be recreated break; case 'campaign': delete entity.experiments; // Will use experiment_ids break; } } /** * 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', 'flag_keys': 'flag', 'experiment_id': 'experiment', 'experiment_ids': 'experiment', 'campaign_id': 'campaign', 'campaign_ids': 'campaign', 'extension_id': 'extension', 'extension_ids': 'extension', 'group_id': 'group', 'group_ids': 'group', 'webhook_id': 'webhook', 'webhook_ids': 'webhook', 'environment_id': 'environment', 'environment_ids': 'environment' }; return mappings[field] || null; } /** * Guess entity type from field name patterns */ guessEntityTypeFromField(field) { const patterns = [ [/audience/i, 'audience'], [/event/i, 'event'], [/page/i, 'page'], [/attribute/i, 'attribute'], [/flag/i, 'flag'], [/experiment/i, 'experiment'], [/campaign/i, 'campaign'], [/extension/i, 'extension'], [/group/i, 'group'], [/webhook/i, 'webhook'], [/environment/i, 'environment'] ]; for (const [pattern, type] of patterns) { if (pattern.test(field)) return type; } return null; } /** * Check if a value looks like an entity reference */ looksLikeEntityReference(key, value) { // Check if key suggests it's a reference const refPatterns = [/_id$/, /_ids$/, /_key$/, /_keys$/, /^linked_/, /^related_/]; const keyLooksLikeRef = refPatterns.some(p => p.test(key)); // Check if value looks like an ID const valueLooksLikeId = /^[0-9a-zA-Z_-]+$/.test(value) && value.length < 100; return keyLooksLikeRef && valueLooksLikeId; } /** * Get entity type from webhook configuration */ getEntityTypeFromWebhook(webhook) { // Try to determine from webhook type or other fields if (webhook.entity_type) { return webhook.entity_type; } // Guess from URL or other fields if (webhook.url?.includes('flag')) return 'flag'; if (webhook.url?.includes('experiment')) return 'experiment'; if (webhook.url?.includes('campaign')) return 'campaign'; return null; } } //# sourceMappingURL=ReferenceTransformer.js.map