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