@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
670 lines • 28.9 kB
JavaScript
/**
* Template Processor Utility
* @description Handles processing of entity creation templates including placeholder validation,
* type checking, and data transformation for Optimizely API calls.
*
* Core Functions:
* - Validate template completion (all {FILL:} placeholders replaced)
* - Type validation against expected data types
* - Extract placeholder guidance for AI agents
* - Transform filled templates into API-ready payloads
*
* @author Optimizely MCP Server
* @version 1.0.0
*/
import { getLogger } from "../logging/Logger.js";
/**
* Template Processor Class
* Handles all template processing operations including validation and transformation
*/
export class TemplateProcessor {
static FILL_PATTERN = /\{FILL:\s*([^}]+)\}/g;
static OPTIONAL_PATTERN = /\{OPTIONAL:\s*([^}]+)\}/g;
static EXISTING_PATTERN = /\{EXISTING:\s*([^}]+)\}/g;
static FIND_OR_CREATE_PATTERN = /\{FIND_OR_CREATE:\s*([^}]+)\}/g;
static CREATE_NEW_PATTERN = /\{CREATE_NEW:\s*([^}]+)\}/g;
static PLACEHOLDER_PATTERN = /\{(FILL|OPTIONAL|EXISTING|FIND_OR_CREATE|CREATE_NEW):\s*([^}]+)\}/g;
/**
* Process a filled template and validate its completion
* @param filledTemplate - Template with user-provided values
* @param originalTemplate - Original template structure for reference
* @returns Processed template with validation results
*/
static processTemplate(filledTemplate, originalTemplate) {
const logger = getLogger();
logger.info("TemplateProcessor: Starting template processing");
// CRITICAL DEBUG: Track description field at entry
if (filledTemplate &&
typeof filledTemplate === "object" &&
"description" in filledTemplate) {
logger.info({
method: "processTemplate",
debugPoint: "ENTRY",
descriptionValue: filledTemplate.description,
descriptionType: typeof filledTemplate.description,
descriptionIsArray: Array.isArray(filledTemplate.description),
templateKeys: Object.keys(filledTemplate),
}, "DESCRIPTION BUG TRACE: TemplateProcessor.processTemplate ENTRY");
}
// Extract placeholder information
const placeholdersFound = this.extractPlaceholderInfo(filledTemplate);
// Validate template completion
const validationResult = this.validateTemplate(filledTemplate, placeholdersFound);
// Process the data if validation passes
const processedData = validationResult.isValid
? this.transformToAPIPayload(filledTemplate)
: filledTemplate;
// CRITICAL DEBUG: Track description field after transformation
if (processedData &&
typeof processedData === "object" &&
"description" in processedData) {
logger.info({
method: "processTemplate",
debugPoint: "AFTER_TRANSFORM",
descriptionValue: processedData.description,
descriptionType: typeof processedData.description,
descriptionIsArray: Array.isArray(processedData.description),
processedKeys: Object.keys(processedData),
}, "DESCRIPTION BUG TRACE: TemplateProcessor.processTemplate AFTER_TRANSFORM");
}
const result = {
processedData,
metadata: {
originalTemplate: originalTemplate || null,
placeholdersFound,
validationResult,
},
};
logger.info(`TemplateProcessor: Processing complete. Valid: ${validationResult.isValid}, Completion: ${validationResult.completionStatus.completionPercentage}%`);
return result;
}
/**
* Extract all placeholders from a template structure
* @param template - Template object to analyze
* @returns Array of placeholder information
*/
static extractPlaceholderInfo(template) {
const placeholders = [];
function extractRecursive(obj, path = "") {
if (typeof obj === "string") {
// Check for FILL placeholders
const fillMatches = Array.from(obj.matchAll(TemplateProcessor.FILL_PATTERN));
fillMatches.forEach((match) => {
placeholders.push({
path: path,
type: "FILL",
description: match[1].trim(),
currentValue: obj,
expectedType: TemplateProcessor.inferTypeFromDescription(match[1]),
});
});
// Check for OPTIONAL placeholders
const optionalMatches = Array.from(obj.matchAll(TemplateProcessor.OPTIONAL_PATTERN));
optionalMatches.forEach((match) => {
placeholders.push({
path: path,
type: "OPTIONAL",
description: match[1].trim(),
currentValue: obj,
expectedType: TemplateProcessor.inferTypeFromDescription(match[1]),
});
});
// Check for EXISTING placeholders (entity must exist)
const existingMatches = Array.from(obj.matchAll(TemplateProcessor.EXISTING_PATTERN));
existingMatches.forEach((match) => {
const info = TemplateProcessor.parseEntityReference(match[1]);
placeholders.push({
path: path,
type: "EXISTING",
description: match[1].trim(),
currentValue: obj,
entityType: info.entityType,
entityIdentifier: info.identifier,
});
});
// Check for FIND_OR_CREATE placeholders
const findOrCreateMatches = Array.from(obj.matchAll(TemplateProcessor.FIND_OR_CREATE_PATTERN));
findOrCreateMatches.forEach((match) => {
const info = TemplateProcessor.parseEntityReference(match[1]);
placeholders.push({
path: path,
type: "FIND_OR_CREATE",
description: match[1].trim(),
currentValue: obj,
entityType: info.entityType,
entityIdentifier: info.identifier,
});
});
// Check for CREATE_NEW placeholders
const createNewMatches = Array.from(obj.matchAll(TemplateProcessor.CREATE_NEW_PATTERN));
createNewMatches.forEach((match) => {
const info = TemplateProcessor.parseEntityReference(match[1]);
placeholders.push({
path: path,
type: "CREATE_NEW",
description: match[1].trim(),
currentValue: obj,
entityType: info.entityType,
entityIdentifier: info.identifier,
});
});
}
else if (Array.isArray(obj)) {
obj.forEach((item, index) => {
extractRecursive(item, path ? `${path}[${index}]` : `[${index}]`);
});
}
else if (obj && typeof obj === "object") {
Object.entries(obj).forEach(([key, value]) => {
const newPath = path ? `${path}.${key}` : key;
extractRecursive(value, newPath);
});
}
}
extractRecursive(template);
return placeholders;
}
/**
* Validate that all required placeholders have been filled
* @param template - Template to validate
* @param placeholders - Extracted placeholder information
* @returns Validation result with detailed feedback
*/
static validateTemplate(template, placeholders) {
const errors = [];
const warnings = [];
const missingRequired = [];
let totalPlaceholders = 0;
let filledPlaceholders = 0;
placeholders.forEach((placeholder) => {
totalPlaceholders++;
const hasPlaceholderSyntax = this.hasPlaceholderSyntax(placeholder.currentValue);
if (hasPlaceholderSyntax) {
if (placeholder.type === "FILL") {
missingRequired.push(placeholder.path);
errors.push(`Required field '${placeholder.path}' not filled. Expected: ${placeholder.description}`);
}
else {
warnings.push(`Optional field '${placeholder.path}' not filled: ${placeholder.description}`);
}
}
else {
filledPlaceholders++;
// Validate data type if specified
const typeValidation = this.validateDataType(placeholder.currentValue, placeholder.expectedType);
if (!typeValidation.isValid) {
errors.push(`Invalid type for '${placeholder.path}': ${typeValidation.error}`);
}
}
});
const completionPercentage = totalPlaceholders > 0
? Math.round((filledPlaceholders / totalPlaceholders) * 100)
: 100;
const isValid = errors.length === 0 && missingRequired.length === 0;
return {
isValid,
errors,
warnings,
missingRequired,
completionStatus: {
totalPlaceholders,
filledPlaceholders,
completionPercentage,
},
};
}
/**
* Check if a value still contains placeholder syntax
* @param value - Value to check
* @returns True if placeholder syntax found
*/
static hasPlaceholderSyntax(value) {
if (typeof value !== "string")
return false;
return this.PLACEHOLDER_PATTERN.test(value);
}
/**
* Parse entity reference from placeholder content
* Examples: "event:purchase_completed", "page:12345", "audience:mobile_users"
* @param reference - Entity reference string
* @returns Parsed entity type and identifier
*/
static parseEntityReference(reference) {
const trimmed = reference.trim();
// Check for entity_type:identifier format
const match = trimmed.match(/^(\w+):(.+)$/);
if (match) {
return {
entityType: match[1],
identifier: match[2].trim(),
};
}
// No colon format - just return as description
return {};
}
/**
* Infer expected data type from placeholder description
* @param description - Placeholder description
* @returns Expected data type
*/
static inferTypeFromDescription(description) {
const desc = description.toLowerCase();
if (desc.includes("percentage") ||
desc.includes("0_to_100") ||
desc.includes("0_to_10000")) {
return "number";
}
else if (desc.includes("boolean") || desc.includes("true|false")) {
return "boolean";
}
else if (desc.includes("array") ||
desc.includes("json") ||
desc.includes("[]")) {
return "array";
}
else if (desc.includes("object") || desc.includes("{}")) {
return "object";
}
else if (desc.includes("integer") || desc.includes("number")) {
return "number";
}
else {
return "string";
}
}
/**
* Validate data type against expected type
* @param value - Value to validate
* @param expectedType - Expected data type
* @returns Validation result
*/
static validateDataType(value, expectedType) {
if (!expectedType)
return { isValid: true };
switch (expectedType) {
case "number":
const num = Number(value);
if (isNaN(num)) {
return {
isValid: false,
error: `Expected number, got: ${typeof value}`,
};
}
return { isValid: true };
case "boolean":
if (typeof value === "boolean")
return { isValid: true };
if (typeof value === "string" &&
(value === "true" || value === "false")) {
return { isValid: true };
}
return {
isValid: false,
error: `Expected boolean, got: ${typeof value}`,
};
case "array":
if (Array.isArray(value))
return { isValid: true };
return {
isValid: false,
error: `Expected array, got: ${typeof value}`,
};
case "object":
if (typeof value === "object" &&
value !== null &&
!Array.isArray(value)) {
return { isValid: true };
}
return {
isValid: false,
error: `Expected object, got: ${typeof value}`,
};
case "string":
if (typeof value === "string")
return { isValid: true };
return {
isValid: false,
error: `Expected string, got: ${typeof value}`,
};
default:
return { isValid: true };
}
}
/**
* Transform filled template into API-ready payload
* @param template - Filled template
* @returns API-ready data structure
*/
static transformToAPIPayload(template) {
// CRITICAL DEBUG: Track description field at entry
if (template && typeof template === "object" && "description" in template) {
getLogger().info({
method: "transformToAPIPayload",
debugPoint: "ENTRY",
descriptionValue: template.description,
descriptionType: typeof template.description,
descriptionIsArray: Array.isArray(template.description),
templateKeys: Object.keys(template),
}, "DESCRIPTION BUG TRACE: transformToAPIPayload ENTRY");
}
// Deep clone to avoid modifying original
const payload = JSON.parse(JSON.stringify(template));
// CRITICAL DEBUG: Track description field after clone
if (payload && typeof payload === "object" && "description" in payload) {
getLogger().info({
method: "transformToAPIPayload",
debugPoint: "AFTER_CLONE",
descriptionValue: payload.description,
descriptionType: typeof payload.description,
descriptionIsArray: Array.isArray(payload.description),
payloadKeys: Object.keys(payload),
}, "DESCRIPTION BUG TRACE: transformToAPIPayload AFTER_CLONE");
}
// Clean up unfilled optional sections
this.cleanUnfilledSections(payload);
// CRITICAL DEBUG: Track description field after clean
if (payload && typeof payload === "object" && "description" in payload) {
getLogger().info({
method: "transformToAPIPayload",
debugPoint: "AFTER_CLEAN",
descriptionValue: payload.description,
descriptionType: typeof payload.description,
descriptionIsArray: Array.isArray(payload.description),
payloadKeys: Object.keys(payload),
}, "DESCRIPTION BUG TRACE: transformToAPIPayload AFTER_CLEAN");
}
// Transform specific field formats for API compatibility
this.transformAPIFields(payload);
// CRITICAL DEBUG: Track description field after transform
if (payload && typeof payload === "object" && "description" in payload) {
getLogger().info({
method: "transformToAPIPayload",
debugPoint: "AFTER_TRANSFORM_FIELDS",
descriptionValue: payload.description,
descriptionType: typeof payload.description,
descriptionIsArray: Array.isArray(payload.description),
payloadKeys: Object.keys(payload),
}, "DESCRIPTION BUG TRACE: transformToAPIPayload AFTER_TRANSFORM_FIELDS");
}
return payload;
}
/**
* Remove sections that still contain unfilled placeholders
* @param obj - Object to clean
*/
static cleanUnfilledSections(obj) {
if (!obj || typeof obj !== "object")
return;
// Process arrays
if (Array.isArray(obj)) {
// Filter out array items that contain placeholder syntax
for (let i = obj.length - 1; i >= 0; i--) {
if (this.containsPlaceholderSyntax(obj[i])) {
obj.splice(i, 1);
}
else {
this.cleanUnfilledSections(obj[i]);
}
}
return;
}
// Process objects
const keysToDelete = [];
Object.entries(obj).forEach(([key, value]) => {
if (this.containsPlaceholderSyntax(value)) {
keysToDelete.push(key);
}
else if (value && typeof value === "object") {
this.cleanUnfilledSections(value);
// After cleaning, check if object/array is now empty
if (Array.isArray(value) && value.length === 0) {
keysToDelete.push(key);
}
else if (!Array.isArray(value) && Object.keys(value).length === 0) {
keysToDelete.push(key);
}
}
});
// Remove marked keys
keysToDelete.forEach((key) => delete obj[key]);
}
/**
* Check if a value contains placeholder syntax (recursively)
* @param value - Value to check
* @returns True if placeholder syntax is found
*/
static containsPlaceholderSyntax(value) {
if (typeof value === "string") {
return this.PLACEHOLDER_PATTERN.test(value);
}
else if (Array.isArray(value)) {
return value.some((item) => this.containsPlaceholderSyntax(item));
}
else if (value && typeof value === "object") {
return Object.values(value).some((v) => this.containsPlaceholderSyntax(v));
}
return false;
}
/**
* Transform fields to match API expectations
* @param payload - Payload to transform
*/
static transformAPIFields(payload) {
function transformRecursive(obj, path = "") {
if (Array.isArray(obj)) {
obj.forEach((item, index) => transformRecursive(item, `${path}[${index}]`));
}
else if (obj && typeof obj === "object") {
Object.entries(obj).forEach(([key, value]) => {
const currentPath = path ? `${path}.${key}` : key;
// Convert percentage strings to numbers
if (typeof value === "string" &&
/^\d+$/.test(value) &&
(key.includes("percentage") ||
key.includes("weight") ||
key.includes("allocation"))) {
obj[key] = parseInt(value, 10);
}
// Convert boolean strings to booleans
if (typeof value === "string" &&
(value === "true" || value === "false")) {
obj[key] = value === "true";
}
// Special handling for page conditions - ensure it's a string
if (key === "conditions" && currentPath.includes("page")) {
if (value && typeof value !== "string") {
obj[key] = JSON.stringify(value);
getLogger().info({
path: currentPath,
original: value,
stringified: obj[key],
}, "TemplateProcessor: Stringified page conditions field");
}
}
// Recursively process nested objects
if (typeof value === "object") {
transformRecursive(value, currentPath);
}
});
}
}
transformRecursive(payload);
}
/**
* Get guidance for AI agents on what information to collect
* @param template - Original template structure
* @returns Array of guidance instructions for collecting user input
*/
static getCollectionGuidance(template) {
const placeholders = this.extractPlaceholderInfo(template);
const guidance = [];
// Group by requirement type
const required = placeholders.filter((p) => p.type === "FILL");
const optional = placeholders.filter((p) => p.type === "OPTIONAL");
if (required.length > 0) {
guidance.push("Required information to collect from user:");
required.forEach((p) => {
guidance.push(` • ${p.path}: ${p.description}`);
});
}
if (optional.length > 0) {
guidance.push("Optional information you can ask for:");
optional.forEach((p) => {
guidance.push(` • ${p.path}: ${p.description}`);
});
}
guidance.push("");
guidance.push("Instructions:");
guidance.push("1. Ask user for required information first");
guidance.push("2. Offer to collect optional information");
guidance.push("3. Replace {FILL: ...} placeholders with user values");
guidance.push("4. Leave {OPTIONAL: ...} placeholders if user skips them");
guidance.push('5. Call manage_entity_lifecycle with mode="template"');
return guidance;
}
/**
* Generate example filled template for AI agent reference
* @param template - Original template
* @param entityType - Type of entity being created
* @returns Example with realistic values
*/
static generateExampleTemplate(template, entityType) {
const example = JSON.parse(JSON.stringify(template));
function fillExample(obj, path = "") {
if (typeof obj === "string") {
// Replace FILL placeholders with example values
obj = obj.replace(TemplateProcessor.FILL_PATTERN, (match, description) => {
return TemplateProcessor.getExampleValue(description.trim(), path, entityType);
});
// Remove OPTIONAL placeholders (leave them unfilled)
obj = obj.replace(TemplateProcessor.OPTIONAL_PATTERN, "");
return obj;
}
else if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
obj[i] = fillExample(obj[i], `${path}[${i}]`);
}
}
else if (obj && typeof obj === "object") {
Object.keys(obj).forEach((key) => {
const newPath = path ? `${path}.${key}` : key;
obj[key] = fillExample(obj[key], newPath);
});
}
return obj;
}
return fillExample(example);
}
/**
* Process entity reference placeholders in template
* This transforms entity references into their resolved IDs/keys
* @param template - Template with entity references
* @param entityResolver - Function to resolve entity references
* @returns Template with resolved references
*/
static async resolveEntityReferences(template, entityResolver) {
const errors = [];
const resolved = JSON.parse(JSON.stringify(template)); // Deep clone
async function resolveRecursive(obj, path = "") {
if (typeof obj === "string") {
// Check for EXISTING references
const existingMatch = obj.match(/^\{EXISTING:\s*([^}]+)\}$/);
if (existingMatch) {
const info = TemplateProcessor.parseEntityReference(existingMatch[1]);
if (info.entityType && info.identifier) {
const resolvedId = await entityResolver(info.entityType, info.identifier, "existing");
if (resolvedId) {
return resolvedId;
}
else {
errors.push(`${path}: Required entity not found: ${existingMatch[1]}`);
return obj; // Keep original for error tracking
}
}
}
// Check for FIND_OR_CREATE references
const findOrCreateMatch = obj.match(/^\{FIND_OR_CREATE:\s*([^}]+)\}$/);
if (findOrCreateMatch) {
const info = TemplateProcessor.parseEntityReference(findOrCreateMatch[1]);
if (info.entityType && info.identifier) {
const resolvedId = await entityResolver(info.entityType, info.identifier, "find_or_create");
return resolvedId || obj;
}
}
// Check for CREATE_NEW references
const createNewMatch = obj.match(/^\{CREATE_NEW:\s*([^}]+)\}$/);
if (createNewMatch) {
const info = TemplateProcessor.parseEntityReference(createNewMatch[1]);
if (info.entityType && info.identifier) {
const resolvedId = await entityResolver(info.entityType, info.identifier, "create_new");
return resolvedId || obj;
}
}
return obj;
}
else if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
obj[i] = await resolveRecursive(obj[i], `${path}[${i}]`);
}
return obj;
}
else if (obj && typeof obj === "object") {
for (const key of Object.keys(obj)) {
const newPath = path ? `${path}.${key}` : key;
obj[key] = await resolveRecursive(obj[key], newPath);
}
return obj;
}
return obj;
}
await resolveRecursive(resolved);
return { resolved, errors };
}
/**
* Generate example values for placeholders
* @param description - Placeholder description
* @param path - Field path
* @param entityType - Entity type for context
* @returns Example value
*/
static getExampleValue(description, path, entityType) {
const desc = description.toLowerCase();
// Type-specific examples
if (desc.includes("percentage") || desc.includes("0_to_100")) {
return "50";
}
else if (desc.includes("0_to_10000")) {
return "5000";
}
else if (desc.includes("true|false") || desc.includes("boolean")) {
return "true";
}
else if (desc.includes("unique") && desc.includes("key")) {
return `${entityType}_test_${Date.now()}`;
}
else if (path.includes("name") || desc.includes("display_name")) {
return `Test ${entityType.charAt(0).toUpperCase() + entityType.slice(1)}`;
}
else if (desc.includes("url")) {
return "https://example.com";
}
else if (desc.includes("email")) {
return "user@example.com";
}
else if (desc.includes("event_key")) {
return "custom_event";
}
else if (desc.includes("environment")) {
return "development";
}
else if (desc.includes("variation") && desc.includes("key")) {
return "control";
}
else if (desc.includes("variation") && desc.includes("name")) {
return "Control";
}
// Default examples based on common patterns
return `example_${path.split(".").pop() || "value"}`;
}
}
//# sourceMappingURL=TemplateProcessor.js.map