@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
288 lines • 11.2 kB
JavaScript
/**
* Template Entity Validator
*
* Extracts all entities from orchestration templates and validates them
* against API schemas before template storage or execution.
*/
import { ComprehensiveSchemaValidator } from './ComprehensiveSchemaValidator.js';
import { getLogger } from '../logging/Logger.js';
export class TemplateEntityValidator {
logger = getLogger();
schemaValidator;
constructor(schemaCache) {
// Pass schema cache to comprehensive validator
this.schemaValidator = new ComprehensiveSchemaValidator(schemaCache);
}
/**
* Validate all entities in a template
*/
async validateTemplate(template, parameters) {
const errors = [];
const warnings = [];
this.logger.info({
templateId: template.id,
templateName: template.name,
stepCount: template.steps.length
}, 'Validating template entities');
// Extract all entities from template
const entities = this.extractEntitiesFromTemplate(template);
// Resolve parameters if provided
if (parameters) {
for (const entity of entities) {
entity.resolvedData = this.resolveParameters(entity.data, parameters);
}
}
// Validate each entity
for (const entity of entities) {
const dataToValidate = entity.resolvedData || entity.data;
// Skip validation for non-create operations (they may have partial data)
if (entity.operation !== 'create') {
this.logger.debug({
stepId: entity.stepId,
operation: entity.operation
}, 'Skipping validation for non-create operation');
continue;
}
const result = this.schemaValidator.validateEntity(entity.entityType, dataToValidate, {
platform: template.platform,
autoFix: false,
operation: entity.operation
});
if (!result.valid) {
errors.push({
stepId: entity.stepId,
entityType: entity.entityType,
errors: result.errors
});
}
if (result.warnings.length > 0) {
warnings.push({
stepId: entity.stepId,
entityType: entity.entityType,
warnings: result.warnings
});
}
}
const valid = errors.length === 0;
this.logger.info({
templateId: template.id,
valid,
errorCount: errors.length,
warningCount: warnings.length,
entityCount: entities.length
}, 'Template entity validation complete');
return {
valid,
errors,
warnings,
entities
};
}
/**
* Extract all entities from template steps
*/
extractEntitiesFromTemplate(template) {
const entities = [];
const processSteps = (steps) => {
for (const step of steps) {
if (step.type === 'template' && step.template) {
const entityType = step.template.entity_type || this.extractEntityType(step.template.system_template_id || '');
if (entityType) {
entities.push({
stepId: step.id,
entityType: entityType,
operation: step.template.operation || 'create',
data: step.template.template_data || step.template.entity_data || step.template.inputs || {}
});
}
}
// Process nested steps
if (step.type === 'conditional' && step.condition) {
if (step.condition.then)
processSteps(step.condition.then);
if (step.condition.else)
processSteps(step.condition.else);
}
if (step.type === 'loop' && step.loop?.steps) {
processSteps(step.loop.steps);
}
if (step.type === 'parallel' && step.parallel?.steps) {
processSteps(step.parallel.steps);
}
}
};
processSteps(template.steps);
return entities;
}
/**
* Extract entity type from system template ID
*/
extractEntityType(systemTemplateId) {
// Pattern 1: optimizely/{entity_type}/{operation}
let match = systemTemplateId.match(/optimizely\/([^\/]+)\//);
// Pattern 2: {entity_type}_{operation} (e.g., event_create, flag_update)
if (!match) {
match = systemTemplateId.match(/^([^_]+)_/);
}
if (match) {
const entityType = match[1];
// Map common variations to standard entity names
const entityMap = {
'experiments': 'experiment',
'experiment': 'experiment',
'campaigns': 'campaign',
'campaign': 'campaign',
'audiences': 'audience',
'audience': 'audience',
'events': 'event',
'event': 'event',
'pages': 'page',
'page': 'page',
'flags': 'flag',
'flag': 'flag',
'rulesets': 'ruleset',
'ruleset': 'ruleset',
'rules': 'rule',
'rule': 'rule',
'variations': 'variation',
'variation': 'variation',
'attributes': 'attribute',
'attribute': 'attribute',
'extensions': 'extension',
'extension': 'extension',
'webhooks': 'webhook',
'webhook': 'webhook',
'projects': 'project',
'project': 'project',
'environments': 'environment',
'environment': 'environment',
'groups': 'group',
'group': 'group',
'variable_definitions': 'variable_definition',
'variable_definition': 'variable_definition',
'variables': 'variable',
'variable': 'variable'
};
return entityMap[entityType] || null;
}
return null;
}
/**
* Resolve template parameters in entity data
*/
resolveParameters(data, parameters) {
if (!data)
return data;
// Handle strings with ${param} syntax
if (typeof data === 'string') {
// CRITICAL FIX: If the entire string is just a parameter reference, preserve type
const singleParamMatch = data.match(/^\$\{([^}]+)\}$/);
if (singleParamMatch) {
const value = this.getNestedValue(parameters, singleParamMatch[1]);
return value !== undefined ? value : data; // Return with original type
}
// Otherwise do string interpolation
return data.replace(/\$\{([^}]+)\}/g, (match, param) => {
const value = this.getNestedValue(parameters, param);
return value !== undefined ? String(value) : match;
});
}
// Handle arrays
if (Array.isArray(data)) {
return data.map(item => this.resolveParameters(item, parameters));
}
// Handle objects
if (typeof data === 'object' && data !== null) {
const resolved = {};
for (const [key, value] of Object.entries(data)) {
resolved[key] = this.resolveParameters(value, parameters);
}
return resolved;
}
return data;
}
/**
* Get nested value from object using dot notation
*/
getNestedValue(obj, path) {
const parts = path.split('.');
let current = obj;
for (const part of parts) {
if (current === null || current === undefined) {
return undefined;
}
current = current[part];
}
return current;
}
/**
* Pre-execution validation with full parameter resolution
*/
async validateBeforeExecution(template, parameters) {
// First validate parameter types match template expectations
if (template.parameters) {
const paramErrors = [];
for (const [paramName, paramDef] of Object.entries(template.parameters)) {
if (paramDef.required && !(paramName in parameters)) {
paramErrors.push(`Required parameter '${paramName}' is missing`);
}
if (paramName in parameters) {
const expectedType = paramDef.type;
const actualType = Array.isArray(parameters[paramName]) ? 'array' : typeof parameters[paramName];
if (actualType !== expectedType) {
paramErrors.push(`Parameter '${paramName}' has wrong type: expected ${expectedType}, got ${actualType}`);
}
}
}
if (paramErrors.length > 0) {
return {
valid: false,
errors: [{
stepId: 'parameters',
entityType: 'parameters',
errors: paramErrors
}],
warnings: [],
entities: []
};
}
}
// Validate all entities with resolved parameters
return this.validateTemplate(template, parameters);
}
/**
* Get a detailed validation report
*/
generateValidationReport(result) {
let report = `Template Validation Report\n`;
report += `========================\n\n`;
if (result.valid) {
report += `✅ Template is valid!\n\n`;
report += `Validated ${result.entities.length} entities successfully.\n`;
}
else {
report += `❌ Template validation failed!\n\n`;
report += `Found ${result.errors.length} error(s) in ${result.entities.length} entities.\n\n`;
report += `Errors:\n`;
report += `-------\n`;
for (const error of result.errors) {
report += `\nStep: ${error.stepId} (${error.entityType})\n`;
for (const err of error.errors) {
report += ` - ${err}\n`;
}
}
}
if (result.warnings.length > 0) {
report += `\nWarnings:\n`;
report += `---------\n`;
for (const warning of result.warnings) {
report += `\nStep: ${warning.stepId} (${warning.entityType})\n`;
for (const warn of warning.warnings) {
report += ` ⚠️ ${warn}\n`;
}
}
}
return report;
}
}
//# sourceMappingURL=TemplateEntityValidator.js.map