@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
421 lines • 19.7 kB
JavaScript
import { getLogger } from '../logging/Logger.js';
/**
* Centralized reference resolver for all entity types in template mode.
* Handles conversion of reference objects like {ref: {name: "Entity Name"}} to actual IDs.
*
* Usage:
* const resolver = new ReferenceResolver(entityRouter);
* const resolved = await resolver.resolveEntityReferences(templateData, entityType, projectId);
*
* Supported reference patterns:
* - {ref: {id: "123"}} - Direct ID reference
* - {ref: {name: "Entity Name"}} - Name-based reference (converted to ID)
* - {ref: {key: "entity_key"}} - Key-based reference (converted to ID)
* - Direct values: "123" or 123 - Treated as direct ID references
*/
export class ReferenceResolver {
entityRouter;
constructor(entityRouter) {
this.entityRouter = entityRouter;
}
/**
* PUBLIC API: Resolves event references specifically
* @param templateData Raw template data containing event reference objects
* @param projectId Project ID for scoped searches
* @returns Object containing resolved event reference fields
*/
async resolveEventReferences(templateData, projectId) {
const resolved = {};
await this.resolveEventReferencesInternal(templateData, 'event', resolved, projectId);
return resolved;
}
/**
* PUBLIC API: Resolves attribute references specifically
* @param templateData Raw template data containing attribute reference objects
* @param projectId Project ID for scoped searches
* @returns Object containing resolved attribute reference fields
*/
async resolveAttributeReferences(templateData, projectId) {
const resolved = {};
await this.resolveAttributeReferencesInternal(templateData, 'attribute', resolved, projectId);
return resolved;
}
/**
* PUBLIC API: Resolves extension references specifically
* @param templateData Raw template data containing extension reference objects
* @param projectId Project ID for scoped searches
* @returns Object containing resolved extension reference fields
*/
async resolveExtensionReferences(templateData, projectId) {
const resolved = {};
await this.resolveExtensionReferencesInternal(templateData, 'extension', resolved, projectId);
return resolved;
}
/**
* PUBLIC API: Resolves variable references specifically
* @param templateData Raw template data containing variable reference objects
* @param projectId Project ID for scoped searches
* @returns Object containing resolved variable reference fields
*/
async resolveVariableReferences(templateData, projectId) {
const resolved = {};
await this.resolveVariableReferencesInternal(templateData, 'variable', resolved, projectId);
return resolved;
}
/**
* PUBLIC API: Resolves environment references specifically
* @param templateData Raw template data containing environment reference objects
* @param projectId Project ID for scoped searches
* @returns Object containing resolved environment reference fields
*/
async resolveEnvironmentReferences(templateData, projectId) {
const resolved = {};
await this.resolveEnvironmentReferencesInternal(templateData, 'environment', resolved, projectId);
return resolved;
}
/**
* PUBLIC API: Resolves group references specifically
* @param templateData Raw template data containing group reference objects
* @param projectId Project ID for scoped searches
* @returns Object containing resolved group reference fields
*/
async resolveGroupReferences(templateData, projectId) {
const resolved = {};
await this.resolveGroupReferencesInternal(templateData, 'group', resolved, projectId);
return resolved;
}
/**
* PUBLIC API: Resolves page references specifically
* @param templateData Raw template data containing page reference objects
* @param projectId Project ID for scoped searches
* @returns Object containing resolved page reference fields
*/
async resolvePageReferences(templateData, projectId) {
const resolved = {};
await this.resolvePageReferencesInternal(templateData, 'page', resolved, projectId);
return resolved;
}
/**
* PUBLIC API: Resolves audience references specifically
* @param templateData Raw template data containing audience reference objects
* @param projectId Project ID for scoped searches
* @returns Object containing resolved audience reference fields
*/
async resolveAudienceReferences(templateData, projectId) {
const resolved = {};
await this.resolveAudienceReferencesInternal(templateData, 'audience', resolved, projectId);
return resolved;
}
/**
* Resolves all entity references in template data for a specific entity type
* @param templateData Raw template data containing reference objects
* @param entityType The type of entity being created (for context/logging)
* @param projectId Project ID for scoped searches
* @returns Object containing resolved reference fields to merge into processed template
*/
async resolveEntityReferences(templateData, entityType, projectId) {
const resolved = {};
getLogger().info({
method: 'resolveEntityReferences',
entityType,
projectId,
templateDataKeys: Object.keys(templateData),
stage: 'start'
}, 'Starting centralized reference resolution');
// Page references (campaigns and experiments)
await this.resolvePageReferencesInternal(templateData, entityType, resolved, projectId);
// Audience references (experiments and campaigns)
await this.resolveAudienceReferencesInternal(templateData, entityType, resolved, projectId);
// Event references (metrics, goals)
await this.resolveEventReferencesInternal(templateData, entityType, resolved, projectId);
// Attribute references (audiences, targeting)
await this.resolveAttributeReferencesInternal(templateData, entityType, resolved, projectId);
// Extension references (integrations)
await this.resolveExtensionReferencesInternal(templateData, entityType, resolved, projectId);
// Variable references (feature flags)
await this.resolveVariableReferencesInternal(templateData, entityType, resolved, projectId);
// Environment references (deployments, rollouts)
await this.resolveEnvironmentReferencesInternal(templateData, entityType, resolved, projectId);
// Group references (audience segments)
await this.resolveGroupReferencesInternal(templateData, entityType, resolved, projectId);
getLogger().info({
method: 'resolveEntityReferences',
entityType,
projectId,
resolvedFields: Object.keys(resolved),
stage: 'complete'
}, 'Completed centralized reference resolution');
return resolved;
}
/**
* Resolves page references for campaigns and experiments
* Handles both pages (array) and page (singular) patterns
*/
async resolvePageReferencesInternal(templateData, entityType, resolved, projectId) {
// Handle pages array (campaigns and some experiments)
if (templateData.pages && Array.isArray(templateData.pages)) {
getLogger().info({
method: 'resolvePageReferences',
entityType,
arrayPattern: true,
pagesCount: templateData.pages.length,
stage: 'pages_array_start'
}, 'Resolving page references (array pattern)');
const resolvedPageIds = await this.resolveReferenceArray(templateData.pages, 'page', projectId);
if (resolvedPageIds.length > 0) {
resolved.page_ids = resolvedPageIds;
// Handle nested structures (orchestration compatibility)
if (entityType === 'campaign') {
resolved.campaign = resolved.campaign || {};
resolved.campaign.page_ids = resolvedPageIds;
}
}
}
// Handle singular page reference (experiments per documentation)
if (templateData.page && templateData.page.ref) {
getLogger().info({
method: 'resolvePageReferences',
entityType,
singularPattern: true,
pageRef: templateData.page.ref,
stage: 'page_singular_start'
}, 'Resolving page reference (singular pattern)');
const resolvedPageId = await this.resolveSingleReference(templateData.page, 'page', projectId);
if (resolvedPageId) {
resolved.page = { id: resolvedPageId };
resolved.page_ids = [resolvedPageId];
}
}
}
/**
* Resolves audience references for experiments and campaigns
*/
async resolveAudienceReferencesInternal(templateData, entityType, resolved, projectId) {
if (templateData.audiences && Array.isArray(templateData.audiences)) {
getLogger().info({
method: 'resolveAudienceReferences',
entityType,
audiencesCount: templateData.audiences.length,
stage: 'audiences_array_start'
}, 'Resolving audience references');
const resolvedAudienceIds = await this.resolveReferenceArray(templateData.audiences, 'audience', projectId);
if (resolvedAudienceIds.length > 0) {
resolved.audiences = resolvedAudienceIds;
// Preserve audience operator if specified
if (templateData.audiences_operator) {
resolved.audiences_operator = templateData.audiences_operator;
}
}
}
}
/**
* Resolves event references for metrics and goals
*/
async resolveEventReferencesInternal(templateData, entityType, resolved, projectId) {
// Handle events array
if (templateData.events && Array.isArray(templateData.events)) {
const resolvedEventIds = await this.resolveReferenceArray(templateData.events, 'event', projectId);
if (resolvedEventIds.length > 0) {
resolved.event_ids = resolvedEventIds;
}
}
// Handle singular event reference
if (templateData.event && templateData.event.ref) {
const resolvedEventId = await this.resolveSingleReference(templateData.event, 'event', projectId);
if (resolvedEventId) {
resolved.event_id = resolvedEventId;
}
}
// Handle event references nested inside metrics array (experiments)
if (templateData.metrics && Array.isArray(templateData.metrics)) {
getLogger().info({
method: 'resolveEventReferences',
entityType,
metricsPattern: true,
metricsCount: templateData.metrics.length,
stage: 'metrics_array_start'
}, 'Resolving event references inside metrics array');
const resolvedMetrics = [];
for (let i = 0; i < templateData.metrics.length; i++) {
const metric = templateData.metrics[i];
if (metric.event && metric.event.ref) {
const resolvedEventId = await this.resolveSingleReference(metric.event, 'event', projectId);
if (resolvedEventId) {
// Create resolved metric with event_id instead of event.ref (per swagger spec)
const resolvedMetric = { ...metric };
delete resolvedMetric.event; // Remove the entire event object
resolvedMetric.event_id = parseInt(resolvedEventId); // Set event_id as integer
resolvedMetrics[i] = resolvedMetric;
getLogger().info({
method: 'resolveEventReferences',
entityType,
metricIndex: i,
resolvedEventId,
stage: 'metric_event_resolved'
}, 'Resolved event reference in metric');
}
else {
resolvedMetrics[i] = metric; // Keep original if resolution failed
}
}
else {
resolvedMetrics[i] = metric; // Keep original if no event ref
}
}
if (resolvedMetrics.length > 0) {
resolved.metrics = resolvedMetrics;
}
}
}
/**
* Resolves attribute references for audience conditions
*/
async resolveAttributeReferencesInternal(templateData, entityType, resolved, projectId) {
if (templateData.attributes && Array.isArray(templateData.attributes)) {
const resolvedAttributeIds = await this.resolveReferenceArray(templateData.attributes, 'attribute', projectId);
if (resolvedAttributeIds.length > 0) {
resolved.attribute_ids = resolvedAttributeIds;
}
}
}
/**
* Resolves extension references for integrations
*/
async resolveExtensionReferencesInternal(templateData, entityType, resolved, projectId) {
if (templateData.extensions && Array.isArray(templateData.extensions)) {
const resolvedExtensionIds = await this.resolveReferenceArray(templateData.extensions, 'extension', projectId);
if (resolvedExtensionIds.length > 0) {
resolved.extension_ids = resolvedExtensionIds;
}
}
}
/**
* Resolves variable references for feature flags
*/
async resolveVariableReferencesInternal(templateData, entityType, resolved, projectId) {
if (templateData.variables && Array.isArray(templateData.variables)) {
const resolvedVariableIds = await this.resolveReferenceArray(templateData.variables, 'variable', projectId);
if (resolvedVariableIds.length > 0) {
resolved.variable_ids = resolvedVariableIds;
}
}
}
/**
* Resolves environment references for deployments
*/
async resolveEnvironmentReferencesInternal(templateData, entityType, resolved, projectId) {
if (templateData.environments && Array.isArray(templateData.environments)) {
const resolvedEnvironmentIds = await this.resolveReferenceArray(templateData.environments, 'environment', projectId);
if (resolvedEnvironmentIds.length > 0) {
resolved.environment_ids = resolvedEnvironmentIds;
}
}
}
/**
* Resolves group references for audience segments
*/
async resolveGroupReferencesInternal(templateData, entityType, resolved, projectId) {
if (templateData.groups && Array.isArray(templateData.groups)) {
const resolvedGroupIds = await this.resolveReferenceArray(templateData.groups, 'group', projectId);
if (resolvedGroupIds.length > 0) {
resolved.group_ids = resolvedGroupIds;
}
}
}
/**
* Resolves an array of reference objects to IDs
* @param refArray Array of reference objects like [{ref: {name: "Name"}}, {ref: {id: "123"}}]
* @param entityType Type of entity being referenced
* @param projectId Project ID for scoped searches
* @returns Array of resolved IDs
*/
async resolveReferenceArray(refArray, entityType, projectId) {
const resolvedIds = [];
for (const item of refArray) {
if (item && typeof item === 'object' && item.ref) {
// Handle reference object
if (item.ref.id) {
// Direct ID reference
resolvedIds.push(item.ref.id);
getLogger().debug({
entityType,
resolvedId: item.ref.id,
method: 'direct_id'
}, 'Resolved reference by direct ID');
}
else if ((item.ref.name || item.ref.key) && this.entityRouter) {
// Name or key reference - need to resolve
const identifier = item.ref.name || item.ref.key;
const foundEntity = await this.entityRouter.findEntityByIdOrKey(entityType, identifier, projectId);
if (foundEntity && foundEntity.id) {
resolvedIds.push(foundEntity.id);
getLogger().info({
entityType,
identifier,
resolvedId: foundEntity.id,
method: item.ref.name ? 'name_lookup' : 'key_lookup'
}, 'Resolved reference by identifier');
}
else {
getLogger().warn({
entityType,
identifier,
projectId,
method: 'lookup_failed'
}, 'Could not resolve entity reference - entity not found');
}
}
}
else if (typeof item === 'string' || typeof item === 'number') {
// Direct ID value
resolvedIds.push(String(item));
getLogger().debug({
entityType,
resolvedId: String(item),
method: 'direct_value'
}, 'Resolved reference by direct value');
}
}
return resolvedIds;
}
/**
* Resolves a single reference object to an ID
* @param refObject Reference object like {ref: {name: "Name"}} or {ref: {id: "123"}}
* @param entityType Type of entity being referenced
* @param projectId Project ID for scoped searches
* @returns Resolved ID or null if not found
*/
async resolveSingleReference(refObject, entityType, projectId) {
if (!refObject || !refObject.ref) {
return null;
}
if (refObject.ref.id) {
// Direct ID reference
return refObject.ref.id;
}
if ((refObject.ref.name || refObject.ref.key) && this.entityRouter) {
// Name or key reference - need to resolve
const identifier = refObject.ref.name || refObject.ref.key;
const foundEntity = await this.entityRouter.findEntityByIdOrKey(entityType, identifier, projectId);
if (foundEntity && foundEntity.id) {
getLogger().info({
entityType,
identifier,
resolvedId: foundEntity.id,
method: refObject.ref.name ? 'name_lookup' : 'key_lookup'
}, 'Resolved single reference by identifier');
return foundEntity.id;
}
else {
getLogger().warn({
entityType,
identifier,
projectId,
method: 'single_lookup_failed'
}, 'Could not resolve single entity reference - entity not found');
}
}
return null;
}
}
//# sourceMappingURL=ReferenceResolver.js.map